From 82bab0f24a4eeccf03322c243d9d181edbdd8638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Tue, 26 Dec 2023 19:48:33 +0800 Subject: [PATCH] feat!: custom preloader & previewer (#401) --- Cargo.lock | 69 ++++- README.md | 7 +- cspell.json | 2 +- yazi-adaptor/Cargo.toml | 2 + yazi-adaptor/src/adaptor.rs | 65 ++--- yazi-adaptor/src/image.rs | 88 +++--- yazi-adaptor/src/iterm2.rs | 17 +- yazi-adaptor/src/kitty.rs | 14 +- yazi-adaptor/src/kitty_old.rs | 17 +- yazi-adaptor/src/lib.rs | 5 + yazi-adaptor/src/sixel.rs | 17 +- yazi-adaptor/src/ueberzug.rs | 47 ++- yazi-config/Cargo.toml | 1 + yazi-config/build.rs | 6 +- yazi-config/preset/keymap.toml | 8 +- yazi-config/preset/theme.toml | 4 - yazi-config/preset/yazi.toml | 41 ++- yazi-config/src/boot/{cli.rs => args.rs} | 2 +- yazi-config/src/boot/boot.rs | 66 ++--- yazi-config/src/boot/mod.rs | 3 +- yazi-config/src/keymap/exec.rs | 5 +- yazi-config/src/keymap/mod.rs | 2 +- yazi-config/src/layout.rs | 12 + yazi-config/src/lib.rs | 20 +- yazi-config/src/manager/layout.rs | 83 ------ yazi-config/src/manager/manager.rs | 5 +- yazi-config/src/manager/mod.rs | 4 +- yazi-config/src/manager/ratio.rs | 31 ++ yazi-config/src/open/open.rs | 2 +- yazi-config/src/pattern.rs | 4 +- yazi-config/src/plugin/exec.rs | 39 +++ yazi-config/src/plugin/mod.rs | 7 + yazi-config/src/plugin/plugin.rs | 78 +++++ yazi-config/src/plugins/mod.rs | 3 - yazi-config/src/plugins/plugins.rs | 29 -- yazi-config/src/preset.rs | 4 +- yazi-config/src/preview/preview.rs | 34 ++- yazi-config/src/theme/filetype.rs | 13 +- yazi-config/src/theme/theme.rs | 4 - yazi-config/src/xdg.rs | 3 + yazi-core/Cargo.toml | 3 +- yazi-core/src/clipboard.rs | 15 +- yazi-core/src/completion/mod.rs | 2 +- yazi-core/src/{files => folder}/files.rs | 267 +++++++++++------- yazi-core/src/{tab => folder}/folder.rs | 39 +-- yazi-core/src/{files => folder}/mod.rs | 2 + yazi-core/src/{files => folder}/sorter.rs | 6 +- yazi-core/src/help/commands/escape.rs | 8 +- yazi-core/src/help/commands/filter.rs | 8 +- yazi-core/src/input/commands/backward.rs | 8 +- yazi-core/src/input/commands/redo.rs | 8 +- yazi-core/src/input/commands/undo.rs | 8 +- yazi-core/src/input/commands/visual.rs | 8 +- yazi-core/src/input/commands/yank.rs | 8 +- yazi-core/src/input/shell.rs | 2 +- yazi-core/src/lib.rs | 7 +- yazi-core/src/manager/commands/close.rs | 8 +- yazi-core/src/manager/commands/create.rs | 4 +- yazi-core/src/manager/commands/hover.rs | 5 +- yazi-core/src/manager/commands/mod.rs | 3 + yazi-core/src/manager/commands/open.rs | 124 +++++--- yazi-core/src/manager/commands/peek.rs | 76 +++-- yazi-core/src/manager/commands/refresh.rs | 15 +- yazi-core/src/manager/commands/rename.rs | 13 +- yazi-core/src/manager/commands/seek.rs | 40 +++ yazi-core/src/manager/commands/suspend.rs | 12 +- .../src/manager/commands/update_files.rs | 82 ++++++ .../src/manager/commands/update_mimetype.rs | 58 ++++ yazi-core/src/manager/linked.rs | 44 +++ yazi-core/src/manager/manager.rs | 62 +--- yazi-core/src/manager/mod.rs | 2 + yazi-core/src/manager/tabs.rs | 15 +- yazi-core/src/manager/watcher.rs | 238 ++++++---------- yazi-core/src/preview/mod.rs | 7 - yazi-core/src/preview/preview.rs | 140 --------- yazi-core/src/preview/provider.rs | 91 ------ yazi-core/src/step.rs | 4 +- yazi-core/src/tab/commands/cd.rs | 8 +- yazi-core/src/tab/commands/jump.rs | 9 +- yazi-core/src/tab/commands/mod.rs | 1 + yazi-core/src/tab/commands/preview.rs | 36 +++ yazi-core/src/tab/commands/reveal.rs | 7 +- yazi-core/src/tab/commands/search.rs | 8 +- yazi-core/src/tab/commands/shell.rs | 28 +- yazi-core/src/tab/config.rs | 2 +- yazi-core/src/tab/finder.rs | 14 +- yazi-core/src/tab/mod.rs | 4 +- yazi-core/src/tab/preview.rs | 111 ++++++++ yazi-core/src/tab/tab.rs | 27 +- yazi-core/src/tasks/commands/cancel.rs | 8 +- yazi-core/src/tasks/commands/inspect.rs | 16 +- yazi-core/src/tasks/commands/open.rs | 34 +-- yazi-core/src/tasks/tasks.rs | 140 +++++---- yazi-fm/Cargo.toml | 17 +- yazi-fm/src/app/app.rs | 62 ++-- yazi-fm/src/app/commands/mod.rs | 1 + yazi-fm/src/app/commands/plugin.rs | 66 +++++ yazi-fm/src/app/commands/stop.rs | 4 +- yazi-fm/src/completion/completion.rs | 3 +- yazi-fm/src/components/header.rs | 20 ++ yazi-fm/src/components/manager.rs | 20 ++ yazi-fm/src/components/mod.rs | 11 + yazi-fm/src/components/preview.rs | 29 ++ yazi-fm/src/components/status.rs | 20 ++ {yazi-core => yazi-fm}/src/context.rs | 9 +- yazi-fm/src/executor.rs | 11 +- yazi-fm/src/help/bindings.rs | 3 +- yazi-fm/src/help/layout.rs | 3 +- yazi-fm/src/input/input.rs | 4 +- yazi-fm/src/lives/active.rs | 78 +++++ yazi-fm/src/lives/folder.rs | 200 +++++++++++++ yazi-fm/src/lives/lives.rs | 56 ++++ yazi-fm/src/lives/mod.rs | 16 ++ .../bindings => yazi-fm/src/lives}/tabs.rs | 59 ++-- .../bindings => yazi-fm/src/lives}/tasks.rs | 8 +- yazi-fm/src/main.rs | 6 + yazi-fm/src/root.rs | 11 +- yazi-fm/src/select/select.rs | 3 +- yazi-fm/src/signals.rs | 4 +- yazi-fm/src/tasks/layout.rs | 4 +- yazi-fm/src/which/layout.rs | 3 +- yazi-fm/src/widgets/clear.rs | 8 +- yazi-plugin/Cargo.toml | 25 +- yazi-plugin/preset/components/current.lua | 31 ++ yazi-plugin/preset/components/folder.lua | 83 +----- yazi-plugin/preset/components/header.lua | 12 +- yazi-plugin/preset/components/manager.lua | 14 +- yazi-plugin/preset/components/parent.lua | 26 ++ yazi-plugin/preset/components/preview.lua | 8 + yazi-plugin/preset/components/status.lua | 10 +- yazi-plugin/preset/inspect/inspect.lua | 6 +- yazi-plugin/preset/plugins/archive.lua | 21 ++ yazi-plugin/preset/plugins/code.lua | 21 ++ yazi-plugin/preset/plugins/folder.lua | 39 +++ yazi-plugin/preset/plugins/image.lua | 22 ++ yazi-plugin/preset/plugins/json.lua | 49 ++++ yazi-plugin/preset/plugins/mime.lua | 35 +++ yazi-plugin/preset/plugins/noop.lua | 9 + yazi-plugin/preset/plugins/pdf.lua | 43 +++ yazi-plugin/preset/plugins/video.lua | 55 ++++ yazi-plugin/preset/ui.lua | 28 +- yazi-plugin/preset/{utils.lua => ya.lua} | 28 +- yazi-plugin/src/bindings/active.rs | 134 --------- yazi-plugin/src/bindings/bindings.rs | 9 +- yazi-plugin/src/bindings/cha.rs | 51 ++++ yazi-plugin/src/bindings/file.rs | 33 +++ yazi-plugin/src/bindings/files.rs | 196 ------------- yazi-plugin/src/bindings/mod.rs | 25 +- yazi-plugin/src/bindings/range.rs | 17 ++ yazi-plugin/src/bindings/shared.rs | 43 --- yazi-plugin/src/bindings/url.rs | 38 +++ yazi-plugin/src/bindings/window.rs | 26 ++ yazi-plugin/src/cast.rs | 133 +++++++++ yazi-plugin/src/components/base.rs | 43 --- yazi-plugin/src/components/components.rs | 41 --- yazi-plugin/src/components/folder.rs | 35 --- yazi-plugin/src/components/header.rs | 24 -- yazi-plugin/src/components/manager.rs | 23 -- yazi-plugin/src/components/mod.rs | 17 -- yazi-plugin/src/components/preview.rs | 33 --- yazi-plugin/src/components/status.rs | 24 -- yazi-plugin/src/config.rs | 53 ++-- yazi-plugin/src/{layout => elements}/bar.rs | 71 ++--- yazi-plugin/src/elements/border.rs | 99 +++++++ yazi-plugin/src/elements/constraint.rs | 38 +++ yazi-plugin/src/elements/elements.rs | 43 +++ yazi-plugin/src/{layout => elements}/gauge.rs | 71 ++--- .../src/{layout => elements}/layout.rs | 33 +-- yazi-plugin/src/{layout => elements}/line.rs | 31 +- yazi-plugin/src/elements/list.rs | 92 ++++++ yazi-plugin/src/elements/mod.rs | 29 ++ yazi-plugin/src/elements/padding.rs | 86 ++++++ yazi-plugin/src/elements/paragraph.rs | 83 ++++++ yazi-plugin/src/elements/rect.rs | 64 +++++ yazi-plugin/src/{layout => elements}/span.rs | 27 +- yazi-plugin/src/{layout => elements}/style.rs | 29 +- .../src/external/fd.rs | 0 .../src/external/fzf.rs | 0 .../src/external}/highlighter.rs | 2 +- .../src/external/lsar.rs | 0 yazi-plugin/src/external/mod.rs | 15 + .../src/external/rg.rs | 0 .../src/external/shell.rs | 0 .../src/external/zoxide.rs | 0 yazi-plugin/src/fs/fs.rs | 30 ++ yazi-plugin/src/fs/mod.rs | 5 + yazi-plugin/src/isolate/entry.rs | 23 ++ yazi-plugin/src/isolate/isolate.rs | 25 ++ yazi-plugin/src/isolate/mod.rs | 13 + yazi-plugin/src/isolate/peek.rs | 72 +++++ yazi-plugin/src/isolate/preload.rs | 40 +++ yazi-plugin/src/isolate/seek.rs | 21 ++ yazi-plugin/src/layout/border.rs | 84 ------ yazi-plugin/src/layout/constraint.rs | 54 ---- yazi-plugin/src/layout/list.rs | 117 -------- yazi-plugin/src/layout/mod.rs | 27 -- yazi-plugin/src/layout/padding.rs | 41 --- yazi-plugin/src/layout/paragraph.rs | 80 ------ yazi-plugin/src/layout/rect.rs | 63 ----- yazi-plugin/src/lib.rs | 21 +- yazi-plugin/src/loader.rs | 50 ++++ yazi-plugin/src/opt.rs | 35 +++ yazi-plugin/src/plugin.rs | 68 ++--- yazi-plugin/src/process/child.rs | 68 +++++ yazi-plugin/src/process/command.rs | 80 ++++++ yazi-plugin/src/process/mod.rs | 13 + yazi-plugin/src/process/output.rs | 19 ++ yazi-plugin/src/process/process.rs | 7 + yazi-plugin/src/process/status.rs | 16 ++ yazi-plugin/src/scope.rs | 16 -- yazi-plugin/src/utils.rs | 33 --- yazi-plugin/src/utils/cache.rs | 22 ++ yazi-plugin/src/utils/call.rs | 51 ++++ yazi-plugin/src/utils/image.rs | 29 ++ yazi-plugin/src/utils/mod.rs | 13 + yazi-plugin/src/utils/plugin.rs | 16 ++ yazi-plugin/src/utils/preview.rs | 94 ++++++ yazi-plugin/src/utils/target.rs | 27 ++ yazi-plugin/src/utils/text.rs | 36 +++ yazi-plugin/src/utils/utils.rs | 17 ++ yazi-scheduler/Cargo.toml | 5 +- .../src/external/ffmpegthumbnailer.rs | 28 -- yazi-scheduler/src/external/file.rs | 40 --- yazi-scheduler/src/external/jq.rs | 45 --- yazi-scheduler/src/external/mod.rs | 23 -- yazi-scheduler/src/external/pdftoppm.rs | 29 -- yazi-scheduler/src/external/unar.rs | 23 -- yazi-scheduler/src/lib.rs | 3 +- yazi-scheduler/src/running.rs | 10 +- yazi-scheduler/src/scheduler.rs | 139 +++++---- yazi-scheduler/src/workers/file.rs | 26 +- yazi-scheduler/src/workers/mod.rs | 10 +- yazi-scheduler/src/workers/plugin.rs | 75 +++++ yazi-scheduler/src/workers/precache.rs | 162 ----------- yazi-scheduler/src/workers/preload.rs | 93 ++++++ yazi-scheduler/src/workers/process.rs | 11 +- yazi-shared/Cargo.toml | 1 + yazi-shared/src/condition.rs | 140 +++++++++ yazi-shared/src/event/event.rs | 49 +--- yazi-shared/src/fs/cha.rs | 2 + yazi-shared/src/fs/file.rs | 40 +-- yazi-shared/src/fs/op.rs | 65 ++++- yazi-shared/src/fs/url.rs | 2 +- yazi-shared/src/lib.rs | 7 + yazi-shared/src/mime.rs | 78 ++--- yazi-shared/src/natsort.rs | 101 +------ yazi-shared/src/number.rs | 24 ++ yazi-shared/src/term/cursor.rs | 4 +- 248 files changed, 4898 insertions(+), 3636 deletions(-) rename yazi-config/src/boot/{cli.rs => args.rs} (95%) create mode 100644 yazi-config/src/layout.rs delete mode 100644 yazi-config/src/manager/layout.rs create mode 100644 yazi-config/src/manager/ratio.rs create mode 100644 yazi-config/src/plugin/exec.rs create mode 100644 yazi-config/src/plugin/mod.rs create mode 100644 yazi-config/src/plugin/plugin.rs delete mode 100644 yazi-config/src/plugins/mod.rs delete mode 100644 yazi-config/src/plugins/plugins.rs rename yazi-core/src/{files => folder}/files.rs (54%) rename yazi-core/src/{tab => folder}/folder.rs (73%) rename yazi-core/src/{files => folder}/mod.rs (66%) rename yazi-core/src/{files => folder}/sorter.rs (94%) create mode 100644 yazi-core/src/manager/commands/seek.rs create mode 100644 yazi-core/src/manager/commands/update_files.rs create mode 100644 yazi-core/src/manager/commands/update_mimetype.rs create mode 100644 yazi-core/src/manager/linked.rs delete mode 100644 yazi-core/src/preview/mod.rs delete mode 100644 yazi-core/src/preview/preview.rs delete mode 100644 yazi-core/src/preview/provider.rs create mode 100644 yazi-core/src/tab/commands/preview.rs create mode 100644 yazi-core/src/tab/preview.rs create mode 100644 yazi-fm/src/app/commands/plugin.rs create mode 100644 yazi-fm/src/components/header.rs create mode 100644 yazi-fm/src/components/manager.rs create mode 100644 yazi-fm/src/components/mod.rs create mode 100644 yazi-fm/src/components/preview.rs create mode 100644 yazi-fm/src/components/status.rs rename {yazi-core => yazi-fm}/src/context.rs (81%) create mode 100644 yazi-fm/src/lives/active.rs create mode 100644 yazi-fm/src/lives/folder.rs create mode 100644 yazi-fm/src/lives/lives.rs create mode 100644 yazi-fm/src/lives/mod.rs rename {yazi-plugin/src/bindings => yazi-fm/src/lives}/tabs.rs (58%) rename {yazi-plugin/src/bindings => yazi-fm/src/lives}/tasks.rs (72%) create mode 100644 yazi-plugin/preset/components/current.lua create mode 100644 yazi-plugin/preset/components/parent.lua create mode 100644 yazi-plugin/preset/components/preview.lua create mode 100644 yazi-plugin/preset/plugins/archive.lua create mode 100644 yazi-plugin/preset/plugins/code.lua create mode 100644 yazi-plugin/preset/plugins/folder.lua create mode 100644 yazi-plugin/preset/plugins/image.lua create mode 100644 yazi-plugin/preset/plugins/json.lua create mode 100644 yazi-plugin/preset/plugins/mime.lua create mode 100644 yazi-plugin/preset/plugins/noop.lua create mode 100644 yazi-plugin/preset/plugins/pdf.lua create mode 100644 yazi-plugin/preset/plugins/video.lua rename yazi-plugin/preset/{utils.lua => ya.lua} (57%) delete mode 100644 yazi-plugin/src/bindings/active.rs create mode 100644 yazi-plugin/src/bindings/cha.rs create mode 100644 yazi-plugin/src/bindings/file.rs delete mode 100644 yazi-plugin/src/bindings/files.rs create mode 100644 yazi-plugin/src/bindings/range.rs delete mode 100644 yazi-plugin/src/bindings/shared.rs create mode 100644 yazi-plugin/src/bindings/url.rs create mode 100644 yazi-plugin/src/bindings/window.rs create mode 100644 yazi-plugin/src/cast.rs delete mode 100644 yazi-plugin/src/components/base.rs delete mode 100644 yazi-plugin/src/components/components.rs delete mode 100644 yazi-plugin/src/components/folder.rs delete mode 100644 yazi-plugin/src/components/header.rs delete mode 100644 yazi-plugin/src/components/manager.rs delete mode 100644 yazi-plugin/src/components/mod.rs delete mode 100644 yazi-plugin/src/components/preview.rs delete mode 100644 yazi-plugin/src/components/status.rs rename yazi-plugin/src/{layout => elements}/bar.rs (76%) create mode 100644 yazi-plugin/src/elements/border.rs create mode 100644 yazi-plugin/src/elements/constraint.rs create mode 100644 yazi-plugin/src/elements/elements.rs rename yazi-plugin/src/{layout => elements}/gauge.rs (62%) rename yazi-plugin/src/{layout => elements}/layout.rs (68%) rename yazi-plugin/src/{layout => elements}/line.rs (61%) create mode 100644 yazi-plugin/src/elements/list.rs create mode 100644 yazi-plugin/src/elements/mod.rs create mode 100644 yazi-plugin/src/elements/padding.rs create mode 100644 yazi-plugin/src/elements/paragraph.rs create mode 100644 yazi-plugin/src/elements/rect.rs rename yazi-plugin/src/{layout => elements}/span.rs (74%) rename yazi-plugin/src/{layout => elements}/style.rs (75%) rename {yazi-scheduler => yazi-plugin}/src/external/fd.rs (100%) rename {yazi-scheduler => yazi-plugin}/src/external/fzf.rs (100%) rename {yazi-core/src => yazi-plugin/src/external}/highlighter.rs (98%) rename {yazi-scheduler => yazi-plugin}/src/external/lsar.rs (100%) create mode 100644 yazi-plugin/src/external/mod.rs rename {yazi-scheduler => yazi-plugin}/src/external/rg.rs (100%) rename {yazi-scheduler => yazi-plugin}/src/external/shell.rs (100%) rename {yazi-scheduler => yazi-plugin}/src/external/zoxide.rs (100%) create mode 100644 yazi-plugin/src/fs/fs.rs create mode 100644 yazi-plugin/src/fs/mod.rs create mode 100644 yazi-plugin/src/isolate/entry.rs create mode 100644 yazi-plugin/src/isolate/isolate.rs create mode 100644 yazi-plugin/src/isolate/mod.rs create mode 100644 yazi-plugin/src/isolate/peek.rs create mode 100644 yazi-plugin/src/isolate/preload.rs create mode 100644 yazi-plugin/src/isolate/seek.rs delete mode 100644 yazi-plugin/src/layout/border.rs delete mode 100644 yazi-plugin/src/layout/constraint.rs delete mode 100644 yazi-plugin/src/layout/list.rs delete mode 100644 yazi-plugin/src/layout/mod.rs delete mode 100644 yazi-plugin/src/layout/padding.rs delete mode 100644 yazi-plugin/src/layout/paragraph.rs delete mode 100644 yazi-plugin/src/layout/rect.rs create mode 100644 yazi-plugin/src/loader.rs create mode 100644 yazi-plugin/src/opt.rs create mode 100644 yazi-plugin/src/process/child.rs create mode 100644 yazi-plugin/src/process/command.rs create mode 100644 yazi-plugin/src/process/mod.rs create mode 100644 yazi-plugin/src/process/output.rs create mode 100644 yazi-plugin/src/process/process.rs create mode 100644 yazi-plugin/src/process/status.rs delete mode 100644 yazi-plugin/src/scope.rs delete mode 100644 yazi-plugin/src/utils.rs create mode 100644 yazi-plugin/src/utils/cache.rs create mode 100644 yazi-plugin/src/utils/call.rs create mode 100644 yazi-plugin/src/utils/image.rs create mode 100644 yazi-plugin/src/utils/mod.rs create mode 100644 yazi-plugin/src/utils/plugin.rs create mode 100644 yazi-plugin/src/utils/preview.rs create mode 100644 yazi-plugin/src/utils/target.rs create mode 100644 yazi-plugin/src/utils/text.rs create mode 100644 yazi-plugin/src/utils/utils.rs delete mode 100644 yazi-scheduler/src/external/ffmpegthumbnailer.rs delete mode 100644 yazi-scheduler/src/external/file.rs delete mode 100644 yazi-scheduler/src/external/jq.rs delete mode 100644 yazi-scheduler/src/external/mod.rs delete mode 100644 yazi-scheduler/src/external/pdftoppm.rs delete mode 100644 yazi-scheduler/src/external/unar.rs create mode 100644 yazi-scheduler/src/workers/plugin.rs delete mode 100644 yazi-scheduler/src/workers/precache.rs create mode 100644 yazi-scheduler/src/workers/preload.rs create mode 100644 yazi-shared/src/condition.rs create mode 100644 yazi-shared/src/number.rs diff --git a/Cargo.lock b/Cargo.lock index c544e00d9..2a5739cf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,12 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "async-channel" version = "1.9.0" @@ -911,6 +917,12 @@ dependencies = [ "tiff", ] +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + [[package]] name = "indexmap" version = "2.1.0" @@ -1158,7 +1170,9 @@ checksum = "7c81f8ac20188feb5461a73eabb22a34dd09d6d58513535eb587e46bff6ba250" dependencies = [ "bstr", "erased-serde", + "futures-util", "mlua-sys", + "mlua_derive", "num-traits", "once_cell", "rustc-hash", @@ -1179,6 +1193,21 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "mlua_derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f359220f24e6452dd82a3f50d7242d4aab822b5594798048e953d7a9e0314c6" +dependencies = [ + "itertools", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.40", +] + [[package]] name = "nom" version = "7.1.3" @@ -1981,6 +2010,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.8" @@ -2572,9 +2614,11 @@ name = "yazi-adaptor" version = "0.1.5" dependencies = [ "anyhow", + "arc-swap", "base64", "color_quant", "image", + "imagesize", "ratatui", "tokio", "tracing", @@ -2587,6 +2631,7 @@ name = "yazi-config" version = "0.1.5" dependencies = [ "anyhow", + "arc-swap", "clap", "clap_complete", "clap_complete_fig", @@ -2626,11 +2671,12 @@ dependencies = [ "syntect", "tokio", "tokio-stream", + "tokio-util", "tracing", "unicode-width", "yazi-adaptor", "yazi-config", - "yazi-prebuild", + "yazi-plugin", "yazi-scheduler", "yazi-shared", ] @@ -2646,6 +2692,7 @@ dependencies = [ "fdlimit", "futures", "libc", + "mlua", "ratatui", "signal-hook-tokio", "tokio", @@ -2657,6 +2704,7 @@ dependencies = [ "yazi-config", "yazi-core", "yazi-plugin", + "yazi-scheduler", "yazi-shared", ] @@ -2666,12 +2714,23 @@ version = "0.1.5" dependencies = [ "ansi-to-tui", "anyhow", + "crossterm", + "futures", + "libc", + "md-5", "mlua", + "parking_lot", "ratatui", + "serde", + "serde_json", + "syntect", + "tokio", + "tokio-util", "tracing", "unicode-width", + "yazi-adaptor", "yazi-config", - "yazi-core", + "yazi-prebuild", "yazi-shared", ] @@ -2690,16 +2749,15 @@ dependencies = [ "base64", "crossterm", "futures", - "libc", "parking_lot", "regex", - "serde", - "serde_json", "tokio", + "tokio-stream", "tracing", "trash", "yazi-adaptor", "yazi-config", + "yazi-plugin", "yazi-shared", ] @@ -2716,6 +2774,7 @@ dependencies = [ "percent-encoding", "ratatui", "regex", + "serde", "tokio", ] diff --git a/README.md b/README.md index 0ef451735..0d1adc409 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ ## Yazi - ⚡️ Blazing Fast Terminal File Manager -Yazi ("duck" in Chinese) is a terminal file manager written in Rust, based on non-blocking async I/O. It aims to provide an efficient, user-friendly, and customizable file management experience. +Yazi (means "duck") is a terminal file manager written in Rust, based on non-blocking async I/O. It aims to provide an efficient, user-friendly, and customizable file management experience. 💡 A new article explaining its internal workings: [Why is Yazi Fast?](https://yazi-rs.github.io/blog/why-is-yazi-fast) - 🚀 **Full Asynchronous Support**: All I/O operations are asynchronous, CPU tasks are spread across multiple threads, making the most of available resources. - 💪 **Powerful Async Task Scheduling and Management**: Provides real-time progress updates, task cancellation, and internal task priority assignment. - 🖼️ **Built-in Support for Multiple Image Protocols**: Also integrated with Überzug++, covering almost all terminals. -- 🌟 **Built-in Code Highlighting and Image Encoding**: Combined with the pre-caching mechanism, greatly accelerates image and normal file loading. +- 🌟 **Built-in Code Highlighting and Image Decoding**: Combined with the pre-loading mechanism, greatly accelerates image and normal file loading. +- 🔌 **Concurrent Plugin System**: UI plugins (rewriting most of the UI), functional plugins (coming soon), custom previewer, and custom preloader; Just some pieces of Lua. - 🧰 Integration with fd, rg, fzf, zoxide -- 💫 Vim-like Input component, and Select component +- 💫 Vim-like input/select component, auto-completion for cd paths - 🏷️ Multi-Tab Support, Scrollable Preview (for videos, PDFs, archives, directories, code, etc.) - 🔄 Batch Renaming, Visual Mode, File Chooser - 🎨 Theme System, Custom Layouts, Trash Bin, CSI u diff --git a/cspell.json b/cspell.json index 9c481edcf..07e982c0a 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","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","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","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"],"version":"0.2"} +{"language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","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","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","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","imagesize","Upserting"],"version":"0.2"} diff --git a/yazi-adaptor/Cargo.toml b/yazi-adaptor/Cargo.toml index 46d9057dc..687d7b3ae 100644 --- a/yazi-adaptor/Cargo.toml +++ b/yazi-adaptor/Cargo.toml @@ -14,9 +14,11 @@ yazi-shared = { path = "../yazi-shared", version = "0.1.5" } # External dependencies anyhow = "^1" +arc-swap = "^1" base64 = "^0" color_quant = "^1" image = "^0" +imagesize = "^0" ratatui = "^0" tokio = { version = "^1", features = [ "parking_lot", "io-util", "process" ] } diff --git a/yazi-adaptor/src/adaptor.rs b/yazi-adaptor/src/adaptor.rs index 4d16fa308..1d721641d 100644 --- a/yazi-adaptor/src/adaptor.rs +++ b/yazi-adaptor/src/adaptor.rs @@ -1,19 +1,12 @@ -use std::{env, path::{Path, PathBuf}, sync::atomic::{AtomicBool, Ordering}}; +use std::{env, path::Path, sync::{atomic::Ordering, Arc}}; use anyhow::{anyhow, Result}; use ratatui::prelude::Rect; -use tokio::{fs, sync::mpsc::UnboundedSender}; use tracing::warn; -use yazi_config::PREVIEW; -use yazi_shared::{env_exists, RoCell}; +use yazi_shared::{env_exists, term::Term}; use super::{Iterm2, Kitty, KittyOld}; -use crate::{ueberzug::Ueberzug, Sixel, TMUX}; - -static IMAGE_SHOWN: AtomicBool = AtomicBool::new(false); - -#[allow(clippy::type_complexity)] -static UEBERZUG: RoCell>>> = RoCell::new(); +use crate::{ueberzug::Ueberzug, Sixel, SHOWN, TMUX}; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Adaptor { @@ -164,46 +157,46 @@ impl ToString for Adaptor { } impl Adaptor { - pub(super) fn start(self) { - UEBERZUG.init(if self.needs_ueberzug() { Ueberzug::start(self).ok() } else { None }); - } - - pub async fn image_show(self, mut path: &Path, rect: Rect) -> Result<()> { - let cache = PREVIEW.cache(path, 0); - if fs::symlink_metadata(&cache).await.is_ok() { - path = cache.as_path(); - } - - self.image_hide(rect).ok(); - IMAGE_SHOWN.store(true, Ordering::Relaxed); + pub(super) fn start(self) { Ueberzug::start(self); } + pub async fn image_show(self, path: &Path, rect: Rect) -> Result<(u32, u32)> { match self { Self::Kitty => Kitty::image_show(path, rect).await, Self::KittyOld => KittyOld::image_show(path, rect).await, Self::Iterm2 => Iterm2::image_show(path, rect).await, Self::Sixel => Sixel::image_show(path, rect).await, - _ => Ok(if let Some(tx) = &*UEBERZUG { - tx.send(Some((path.to_path_buf(), rect)))?; - }), + _ => Ueberzug::image_show(path, rect).await, } } - pub fn image_hide(self, rect: Rect) -> Result<()> { - if !IMAGE_SHOWN.swap(false, Ordering::Relaxed) { - return Ok(()); - } + pub fn image_hide(self) -> Result<()> { + if let Some(rect) = SHOWN.swap(None) { self.image_erase(*rect) } else { Ok(()) } + } + pub fn image_erase(self, rect: Rect) -> Result<()> { match self { - Self::Kitty => Kitty::image_hide(rect), - Self::Iterm2 => Iterm2::image_hide(rect), - Self::KittyOld => KittyOld::image_hide(), - Self::Sixel => Sixel::image_hide(rect), - _ => Ok(if let Some(tx) = &*UEBERZUG { - tx.send(None)?; - }), + Self::Kitty => Kitty::image_erase(rect), + Self::Iterm2 => Iterm2::image_erase(rect), + Self::KittyOld => KittyOld::image_erase(), + Self::Sixel => Sixel::image_erase(rect), + _ => Ueberzug::image_erase(rect), } } + #[inline] + pub(super) fn shown_store(rect: Rect, size: (u32, u32)) { + SHOWN.store(Some(Arc::new( + Term::ratio() + .map(|(r1, r2)| Rect { + x: rect.x, + y: rect.y, + width: (size.0 as f64 / r1).ceil() as u16, + height: (size.1 as f64 / r2).ceil() as u16, + }) + .unwrap_or(rect), + ))); + } + #[inline] pub(super) fn needs_ueberzug(self) -> bool { !matches!(self, Self::Kitty | Self::KittyOld | Self::Iterm2 | Self::Sixel) diff --git a/yazi-adaptor/src/image.rs b/yazi-adaptor/src/image.rs index 746cb4711..d5366353f 100644 --- a/yazi-adaptor/src/image.rs +++ b/yazi-adaptor/src/image.rs @@ -2,51 +2,13 @@ use std::{fs::File, io::BufReader, path::{Path, PathBuf}}; use anyhow::Result; use image::{imageops::FilterType, io::Limits, DynamicImage, ImageFormat}; +use ratatui::layout::Rect; use yazi_config::{PREVIEW, TASKS}; use yazi_shared::term::Term; pub struct Image; impl Image { - fn set_limits(mut r: image::io::Reader>) -> image::io::Reader> { - let mut limits = Limits::no_limits(); - if TASKS.image_alloc > 0 { - limits.max_alloc = Some(TASKS.image_alloc as u64); - } - if TASKS.image_bound[0] > 0 { - limits.max_image_width = Some(TASKS.image_bound[0] as u32); - } - if TASKS.image_bound[1] > 0 { - limits.max_image_height = Some(TASKS.image_bound[1] as u32); - } - r.limits(limits); - r - } - - pub(super) async fn downscale(path: &Path, size: (u16, u16)) -> Result { - let (w, h) = Term::ratio() - .map(|(w, h)| { - let (w, h) = ((size.0 as f64 * w) as u32, (size.1 as f64 * h) as u32); - (w.min(PREVIEW.max_width), h.min(PREVIEW.max_height)) - }) - .unwrap_or((PREVIEW.max_width, PREVIEW.max_height)); - - let path = path.to_owned(); - let img = tokio::task::spawn_blocking(move || { - Self::set_limits(image::io::Reader::open(path)?.with_guessed_format()?).decode() - }) - .await??; - - tokio::task::spawn_blocking(move || { - Ok(if img.width() > w || img.height() > h { - img.resize(w, h, FilterType::Triangle) - } else { - img - }) - }) - .await? - } - pub async fn precache(path: &Path, cache: PathBuf) -> Result<()> { let path = path.to_owned(); let mut img = tokio::task::spawn_blocking(move || { @@ -69,21 +31,45 @@ impl Image { .await? } - pub async fn precache_vec(bin: Vec, cache: PathBuf) -> Result<()> { - let mut img = tokio::task::spawn_blocking(move || image::load_from_memory(&bin)).await??; + pub(super) async fn downscale(path: &Path, rect: Rect) -> Result { + let path = path.to_owned(); + let img = tokio::task::spawn_blocking(move || { + Self::set_limits(image::io::Reader::open(path)?.with_guessed_format()?).decode() + }) + .await??; + let (w, h) = Self::max_size(rect); tokio::task::spawn_blocking(move || { - let (w, h) = (PREVIEW.max_width, PREVIEW.max_height); - if img.width() > w || img.height() > h { - img = img.resize(w, h, FilterType::Triangle); - } - - Ok(match img { - DynamicImage::ImageRgb8(buf) => buf.save_with_format(cache, ImageFormat::Jpeg), - DynamicImage::ImageRgba8(buf) => buf.save_with_format(cache, ImageFormat::Jpeg), - buf => buf.into_rgb8().save_with_format(cache, ImageFormat::Jpeg), - }?) + Ok(if img.width() > w || img.height() > h { + img.resize(w, h, FilterType::Triangle) + } else { + img + }) }) .await? } + + pub(super) fn max_size(rect: Rect) -> (u32, u32) { + Term::ratio() + .map(|(r1, r2)| { + let (w, h) = ((rect.width as f64 * r1) as u32, (rect.height as f64 * r2) as u32); + (w.min(PREVIEW.max_width), h.min(PREVIEW.max_height)) + }) + .unwrap_or((PREVIEW.max_width, PREVIEW.max_height)) + } + + fn set_limits(mut r: image::io::Reader>) -> image::io::Reader> { + let mut limits = Limits::no_limits(); + if TASKS.image_alloc > 0 { + limits.max_alloc = Some(TASKS.image_alloc as u64); + } + if TASKS.image_bound[0] > 0 { + limits.max_image_width = Some(TASKS.image_bound[0] as u32); + } + if TASKS.image_bound[1] > 0 { + limits.max_image_height = Some(TASKS.image_bound[1] as u32); + } + r.limits(limits); + r + } } diff --git a/yazi-adaptor/src/iterm2.rs b/yazi-adaptor/src/iterm2.rs index 4ca13bc4e..9dd60c0a1 100644 --- a/yazi-adaptor/src/iterm2.rs +++ b/yazi-adaptor/src/iterm2.rs @@ -7,20 +7,25 @@ use ratatui::prelude::Rect; use yazi_shared::term::Term; use super::image::Image; -use crate::{CLOSE, START}; +use crate::{adaptor::Adaptor, CLOSE, START}; pub(super) struct Iterm2; impl Iterm2 { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { - let img = Image::downscale(path, (rect.width, rect.height)).await?; + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { + let img = Image::downscale(path, rect).await?; + let size = (img.width(), img.height()); let b = Self::encode(img).await?; - Self::image_hide(rect)?; - Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| Ok(stdout.write_all(&b)?)) + Adaptor::Iterm2.image_hide()?; + Adaptor::shown_store(rect, size); + Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| { + stdout.write_all(&b)?; + Ok(size) + }) } - pub(super) fn image_hide(rect: Rect) -> Result<()> { + pub(super) fn image_erase(rect: Rect) -> Result<()> { let stdout = BufWriter::new(stdout().lock()); let s = " ".repeat(rect.width as usize); Term::move_lock(stdout, (0, 0), |stdout| { diff --git a/yazi-adaptor/src/kitty.rs b/yazi-adaptor/src/kitty.rs index 86a35880a..a799739d4 100644 --- a/yazi-adaptor/src/kitty.rs +++ b/yazi-adaptor/src/kitty.rs @@ -7,7 +7,7 @@ use ratatui::prelude::Rect; use yazi_shared::term::Term; use super::image::Image; -use crate::{CLOSE, ESCAPE, START}; +use crate::{adaptor::Adaptor, CLOSE, ESCAPE, START}; static DIACRITICS: [char; 297] = [ '\u{0305}', @@ -312,11 +312,13 @@ static DIACRITICS: [char; 297] = [ pub(super) struct Kitty; impl Kitty { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { - let img = Image::downscale(path, (rect.width, rect.height)).await?; + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { + let img = Image::downscale(path, rect).await?; + let size = (img.width(), img.height()); let b = Self::encode(img).await?; - Self::image_hide(rect)?; + Adaptor::Kitty.image_hide()?; + Adaptor::shown_store(rect, size); Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| { stdout.write_all(&b)?; @@ -335,11 +337,11 @@ impl Kitty { stdout.write_all(buf.as_bytes())?; } - Ok(()) + Ok(size) }) } - pub(super) fn image_hide(rect: Rect) -> Result<()> { + pub(super) fn image_erase(rect: Rect) -> Result<()> { let stdout = BufWriter::new(stdout().lock()); let s = " ".repeat(rect.width as usize); Term::move_lock(stdout, (0, 0), |stdout| { diff --git a/yazi-adaptor/src/kitty_old.rs b/yazi-adaptor/src/kitty_old.rs index 1dffaaa8d..96e1cf7d9 100644 --- a/yazi-adaptor/src/kitty_old.rs +++ b/yazi-adaptor/src/kitty_old.rs @@ -7,21 +7,26 @@ use ratatui::prelude::Rect; use yazi_shared::term::Term; use super::image::Image; -use crate::{CLOSE, ESCAPE, START}; +use crate::{adaptor::Adaptor, CLOSE, ESCAPE, START}; pub(super) struct KittyOld; impl KittyOld { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { - let img = Image::downscale(path, (rect.width, rect.height)).await?; + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { + let img = Image::downscale(path, rect).await?; + let size = (img.width(), img.height()); let b = Self::encode(img).await?; - Self::image_hide()?; - Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| Ok(stdout.write_all(&b)?)) + Adaptor::KittyOld.image_hide()?; + Adaptor::shown_store(rect, size); + Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| { + stdout.write_all(&b)?; + Ok(size) + }) } #[inline] - pub(super) fn image_hide() -> Result<()> { + pub(super) fn image_erase() -> Result<()> { let mut stdout = stdout().lock(); stdout.write_all(format!("{}_Gq=1,a=d,d=A{}\\{}", START, ESCAPE, CLOSE).as_bytes())?; stdout.flush()?; diff --git a/yazi-adaptor/src/lib.rs b/yazi-adaptor/src/lib.rs index de261e479..b4d52f4a8 100644 --- a/yazi-adaptor/src/lib.rs +++ b/yazi-adaptor/src/lib.rs @@ -25,12 +25,17 @@ static ESCAPE: RoCell<&'static str> = RoCell::new(); static START: RoCell<&'static str> = RoCell::new(); static CLOSE: RoCell<&'static str> = RoCell::new(); +// Image state +static SHOWN: RoCell> = RoCell::new(); + pub fn init() { TMUX.init(env_exists("TMUX")); START.init(if *TMUX { "\x1bPtmux;\x1b\x1b" } else { "\x1b" }); CLOSE.init(if *TMUX { "\x1b\\" } else { "" }); ESCAPE.init(if *TMUX { "\x1b\x1b" } else { "\x1b" }); + SHOWN.with(Default::default); + ADAPTOR.init(Adaptor::detect()); ADAPTOR.start(); diff --git a/yazi-adaptor/src/sixel.rs b/yazi-adaptor/src/sixel.rs index ac899dcdc..86ca0ffd3 100644 --- a/yazi-adaptor/src/sixel.rs +++ b/yazi-adaptor/src/sixel.rs @@ -6,20 +6,25 @@ use image::DynamicImage; use ratatui::prelude::Rect; use yazi_shared::term::Term; -use crate::{Image, CLOSE, ESCAPE, START}; +use crate::{adaptor::Adaptor, Image, CLOSE, ESCAPE, START}; pub(super) struct Sixel; impl Sixel { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { - let img = Image::downscale(path, (rect.width, rect.height)).await?; + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { + let img = Image::downscale(path, rect).await?; + let size = (img.width(), img.height()); let b = Self::encode(img).await?; - Self::image_hide(rect)?; - Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| Ok(stdout.write_all(&b)?)) + Adaptor::Sixel.image_hide()?; + Adaptor::shown_store(rect, size); + Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| { + stdout.write_all(&b)?; + Ok(size) + }) } - pub(super) fn image_hide(rect: Rect) -> Result<()> { + pub(super) fn image_erase(rect: Rect) -> Result<()> { let stdout = BufWriter::new(stdout().lock()); let s = " ".repeat(rect.width as usize); Term::move_lock(stdout, (0, 0), |stdout| { diff --git a/yazi-adaptor/src/ueberzug.rs b/yazi-adaptor/src/ueberzug.rs index dce59f176..c491e4417 100644 --- a/yazi-adaptor/src/ueberzug.rs +++ b/yazi-adaptor/src/ueberzug.rs @@ -1,17 +1,26 @@ -use std::{path::PathBuf, process::Stdio}; +use std::{path::{Path, PathBuf}, process::Stdio}; -use anyhow::Result; +use anyhow::{bail, Result}; +use imagesize::ImageSize; use ratatui::prelude::Rect; use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}}; use tracing::debug; use yazi_config::PREVIEW; +use yazi_shared::RoCell; -use crate::Adaptor; +use crate::{Adaptor, Image}; + +#[allow(clippy::type_complexity)] +static DEMON: RoCell>>> = RoCell::new(); pub(super) struct Ueberzug; impl Ueberzug { - pub(super) fn start(adaptor: Adaptor) -> Result>> { + pub(super) fn start(adaptor: Adaptor) { + if !adaptor.needs_ueberzug() { + return DEMON.init(None); + } + let mut child = Self::create_demon(adaptor).ok(); let (tx, mut rx) = mpsc::unbounded_channel(); @@ -29,8 +38,36 @@ impl Ueberzug { } } }); + DEMON.init(Some(tx)) + } + + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { + if let Some(tx) = &*DEMON { + tx.send(Some((path.to_path_buf(), rect)))?; + Adaptor::shown_store(rect, (0, 0)); + } else { + bail!("uninitialized ueberzug"); + } - Ok(tx) + let path = path.to_owned(); + let ImageSize { width: w, height: h } = + tokio::task::spawn_blocking(move || imagesize::size(path)).await??; + + let (max_w, max_h) = Image::max_size(rect); + if w <= max_w as usize && h <= max_h as usize { + return Ok((w as u32, h as u32)); + } + + let ratio = f64::min(max_w as f64 / w as f64, max_h as f64 / h as f64); + Ok(((w as f64 * ratio).round() as u32, (h as f64 * ratio).round() as u32)) + } + + pub(super) fn image_erase(_: Rect) -> Result<()> { + if let Some(tx) = &*DEMON { + Ok(tx.send(None)?) + } else { + bail!("uninitialized ueberzug"); + } } fn create_demon(adaptor: Adaptor) -> Result { diff --git a/yazi-config/Cargo.toml b/yazi-config/Cargo.toml index 28640a240..f7b465a45 100644 --- a/yazi-config/Cargo.toml +++ b/yazi-config/Cargo.toml @@ -13,6 +13,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.1.5" } # External dependencies anyhow = "^1" +arc-swap = "^1" clap = { version = "^4", features = [ "derive" ] } crossterm = "^0" dirs = "^5" diff --git a/yazi-config/build.rs b/yazi-config/build.rs index 553c001be..5b8e7ef30 100644 --- a/yazi-config/build.rs +++ b/yazi-config/build.rs @@ -1,5 +1,5 @@ -#[path = "src/boot/cli.rs"] -mod cli; +#[path = "src/boot/args.rs"] +mod args; use std::{env, error::Error, fs}; @@ -14,7 +14,7 @@ fn main() -> Result<(), Box> { return Ok(()); } - let cmd = &mut cli::Args::command(); + let cmd = &mut args::Args::command(); let bin = "yazi"; let out = "completions"; diff --git a/yazi-config/preset/keymap.toml b/yazi-config/preset/keymap.toml index 93063d4b6..1dc00d374 100644 --- a/yazi-config/preset/keymap.toml +++ b/yazi-config/preset/keymap.toml @@ -33,10 +33,10 @@ keymap = [ { on = [ "H" ], exec = "back", desc = "Go back to the previous directory" }, { on = [ "L" ], exec = "forward", desc = "Go forward to the next directory" }, - { on = [ "" ], exec = "peek -5", desc = "Peek up 5 units in the preview" }, - { on = [ "" ], exec = "peek 5", desc = "Peek down 5 units in the preview" }, - { on = [ "" ], exec = "peek -5", desc = "Peek up 5 units in the preview" }, - { on = [ "" ], exec = "peek 5", desc = "Peek down 5 units in the preview" }, + { on = [ "" ], exec = "seek -5", desc = "Peek up 5 units in the preview" }, + { on = [ "" ], exec = "seek 5", desc = "Peek down 5 units in the preview" }, + { on = [ "" ], exec = "seek -5", desc = "Peek up 5 units in the preview" }, + { on = [ "" ], exec = "seek 5", desc = "Peek down 5 units in the preview" }, { on = [ "" ], exec = "arrow -1", desc = "Move cursor up" }, { on = [ "" ], exec = "arrow 1", desc = "Move cursor down" }, diff --git a/yazi-config/preset/theme.toml b/yazi-config/preset/theme.toml index ae21d95f2..3ccd7665f 100644 --- a/yazi-config/preset/theme.toml +++ b/yazi-config/preset/theme.toml @@ -27,10 +27,6 @@ tab_width = 1 border_symbol = "│" border_style = { fg = "gray" } -# Offset -folder_offset = [ 1, 0, 1, 0 ] -preview_offset = [ 1, 1, 1, 1 ] - # Highlighting syntect_theme = "" diff --git a/yazi-config/preset/yazi.toml b/yazi-config/preset/yazi.toml index 4240f05b7..4db32843d 100644 --- a/yazi-config/preset/yazi.toml +++ b/yazi-config/preset/yazi.toml @@ -61,6 +61,7 @@ rules = [ { mime = "application/x-bzip2", use = [ "extract", "reveal" ] }, { mime = "application/x-7z-compressed", use = [ "extract", "reveal" ] }, { mime = "application/x-rar", use = [ "extract", "reveal" ] }, + { mime = "application/xz", use = [ "extract", "reveal" ] }, { mime = "*", use = [ "open", "reveal" ] }, ] @@ -73,8 +74,44 @@ image_alloc = 536870912 # 512MB image_bound = [ 0, 0 ] suppress_preload = false -[plugins] -preload = [] +[plugin] + +preloaders = [ + { name = "*", cond = "!mime", exec = "mime.lua", multi = true }, + # Image + { mime = "image/vnd.djvu", exec = "noop.lua" }, + { mime = "image/*", exec = "image.lua" }, + # Video + { mime = "video/*", exec = "video.lua" }, + # PDF + { mime = "application/pdf", exec = "pdf.lua" }, +] +previewers = [ + { name = "*/", exec = "folder.lua", sync = true }, + # Code + { mime = "text/*", exec = "code.lua" }, + { mime = "*/xml", exec = "code.lua" }, + { mime = "*/javascript", exec = "code.lua" }, + { mime = "*/x-wine-extension-ini", exec = "code.lua" }, + # JSON + { mime = "application/json", exec = "json.lua" }, + # Image + { mime = "image/vnd.djvu", exec = "noop.lua" }, + { mime = "image/*", exec = "image.lua" }, + # Video + { mime = "video/*", exec = "video.lua" }, + # PDF + { mime = "application/pdf", exec = "pdf.lua" }, + # Archive + { mime = "application/zip", exec = "archive.lua" }, + { mime = "application/gzip", exec = "archive.lua" }, + { mime = "application/x-tar", exec = "archive.lua" }, + { mime = "application/x-bzip", exec = "archive.lua" }, + { mime = "application/x-bzip2", exec = "archive.lua" }, + { mime = "application/x-7z-compressed", exec = "archive.lua" }, + { mime = "application/x-rar", exec = "archive.lua" }, + { mime = "application/xz", exec = "archive.lua" }, +] [input] # cd diff --git a/yazi-config/src/boot/cli.rs b/yazi-config/src/boot/args.rs similarity index 95% rename from yazi-config/src/boot/cli.rs rename to yazi-config/src/boot/args.rs index 111e13458..08c4a11aa 100644 --- a/yazi-config/src/boot/cli.rs +++ b/yazi-config/src/boot/args.rs @@ -4,7 +4,7 @@ use clap::{command, Parser}; #[derive(Debug, Parser)] #[command(name = "yazi")] -pub(super) struct Args { +pub struct Args { /// Set the current working entry #[arg(index = 1)] pub entry: Option, diff --git a/yazi-config/src/boot/boot.rs b/yazi-config/src/boot/boot.rs index 60c64e0be..47ddbcdc9 100644 --- a/yazi-config/src/boot/boot.rs +++ b/yazi-config/src/boot/boot.rs @@ -1,24 +1,24 @@ -use std::{ffi::OsString, fs, path::PathBuf, process}; +use std::{ffi::OsString, fs, path::{Path, PathBuf}, process}; use clap::Parser; +use serde::Serialize; use yazi_shared::fs::{current_cwd, expand_path}; -use super::cli::Args; -use crate::{Xdg, PREVIEW}; +use super::Args; +use crate::{Xdg, ARGS}; -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct Boot { pub cwd: PathBuf, pub file: Option, - pub state_dir: PathBuf, - - pub cwd_file: Option, - pub chooser_file: Option, + pub config_dir: PathBuf, + pub plugin_dir: PathBuf, + pub state_dir: PathBuf, } impl Boot { - fn parse_entry(entry: Option) -> (PathBuf, Option) { + fn parse_entry(entry: Option<&Path>) -> (PathBuf, Option) { let entry = match entry { Some(p) => expand_path(p), None => return (current_cwd().unwrap(), None), @@ -35,48 +35,38 @@ impl Boot { impl Default for Boot { fn default() -> Self { - let args = Args::parse(); - if args.version { - println!( - "yazi {} ({} {})", - env!("CARGO_PKG_VERSION"), - env!("VERGEN_GIT_SHA"), - env!("VERGEN_BUILD_DATE") - ); - process::exit(0); - } - - let (cwd, file) = Self::parse_entry(args.entry); + let (cwd, file) = Self::parse_entry(ARGS.entry.as_deref()); let boot = Self { cwd, file, + config_dir: Xdg::config_dir().unwrap(), + plugin_dir: Xdg::plugin_dir().unwrap(), state_dir: Xdg::state_dir().unwrap(), - - cwd_file: args.cwd_file, - chooser_file: args.chooser_file, }; if !boot.state_dir.is_dir() { fs::create_dir_all(&boot.state_dir).unwrap(); } - if !PREVIEW.cache_dir.is_dir() { - fs::create_dir(&PREVIEW.cache_dir).unwrap(); - } - if args.clear_cache { - if PREVIEW.cache_dir == Xdg::cache_dir() { - println!("Clearing cache directory: \n{:?}", PREVIEW.cache_dir); - fs::remove_dir_all(&PREVIEW.cache_dir).unwrap(); - } else { - println!( - "You've changed the default cache directory, for your data's safety, please clear it manually: \n{:?}", - PREVIEW.cache_dir - ); - } + boot + } +} + +impl Default for Args { + fn default() -> Self { + let args = Self::parse(); + + if args.version { + println!( + "yazi {} ({} {})", + env!("CARGO_PKG_VERSION"), + env!("VERGEN_GIT_SHA"), + env!("VERGEN_BUILD_DATE") + ); process::exit(0); } - boot + args } } diff --git a/yazi-config/src/boot/mod.rs b/yazi-config/src/boot/mod.rs index 865dc405b..d29db39cf 100644 --- a/yazi-config/src/boot/mod.rs +++ b/yazi-config/src/boot/mod.rs @@ -1,4 +1,5 @@ +mod args; mod boot; -mod cli; +pub use args::*; pub use boot::*; diff --git a/yazi-config/src/keymap/exec.rs b/yazi-config/src/keymap/exec.rs index 7c257c54f..b0efd99e7 100644 --- a/yazi-config/src/keymap/exec.rs +++ b/yazi-config/src/keymap/exec.rs @@ -34,7 +34,7 @@ where type Value = Vec; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a exec string, e.g. `tab_switch 0`") + formatter.write_str("a `exec` string or array of strings within [keymap]") } fn visit_seq(self, mut seq: A) -> Result @@ -45,6 +45,9 @@ where while let Some(value) = &seq.next_element::()? { execs.push(parse(value).map_err(de::Error::custom)?); } + if execs.is_empty() { + return Err(de::Error::custom("`exec` within [keymap] cannot be empty")); + } Ok(execs) } diff --git a/yazi-config/src/keymap/mod.rs b/yazi-config/src/keymap/mod.rs index c531882ad..76a1c2c4a 100644 --- a/yazi-config/src/keymap/mod.rs +++ b/yazi-config/src/keymap/mod.rs @@ -4,7 +4,7 @@ mod key; mod keymap; pub use control::*; -#[allow(unused_imports)] pub use exec::*; +#[allow(unused_imports)] pub use key::*; pub use keymap::*; diff --git a/yazi-config/src/layout.rs b/yazi-config/src/layout.rs new file mode 100644 index 000000000..c63809d54 --- /dev/null +++ b/yazi-config/src/layout.rs @@ -0,0 +1,12 @@ +use ratatui::layout::Rect; + +#[derive(Default)] +pub struct Layout { + pub header: Rect, + + pub parent: Rect, + pub current: Rect, + pub preview: Rect, + + pub status: Rect, +} diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index 0edb2e453..e71004f8b 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -4,11 +4,12 @@ use yazi_shared::RoCell; mod boot; pub mod keymap; +mod layout; mod log; pub mod manager; pub mod open; mod pattern; -pub mod plugins; +pub mod plugin; pub mod popup; mod preset; pub mod preview; @@ -17,10 +18,15 @@ pub mod theme; mod validation; mod xdg; +pub use layout::*; pub(crate) use pattern::*; pub(crate) use preset::*; pub(crate) use xdg::*; +pub static ARGS: RoCell = RoCell::new(); +pub static BOOT: RoCell = RoCell::new(); +pub static LAYOUT: RoCell> = RoCell::new(); + static MERGED_KEYMAP: RoCell = RoCell::new(); static MERGED_THEME: RoCell = RoCell::new(); static MERGED_YAZI: RoCell = RoCell::new(); @@ -29,16 +35,18 @@ pub static KEYMAP: RoCell = RoCell::new(); pub static LOG: RoCell = RoCell::new(); pub static MANAGER: RoCell = RoCell::new(); pub static OPEN: RoCell = RoCell::new(); -pub static PLUGINS: RoCell = RoCell::new(); +pub static PLUGIN: RoCell = RoCell::new(); pub static PREVIEW: RoCell = RoCell::new(); pub static TASKS: RoCell = RoCell::new(); pub static THEME: RoCell = RoCell::new(); pub static INPUT: RoCell = RoCell::new(); pub static SELECT: RoCell = RoCell::new(); -pub static BOOT: RoCell = RoCell::new(); - pub fn init() { + ARGS.with(Default::default); + BOOT.with(Default::default); + LAYOUT.with(Default::default); + MERGED_KEYMAP.with(Preset::keymap); MERGED_THEME.with(Preset::theme); MERGED_YAZI.with(Preset::yazi); @@ -47,12 +55,10 @@ pub fn init() { LOG.with(Default::default); MANAGER.with(Default::default); OPEN.with(Default::default); - PLUGINS.with(Default::default); + PLUGIN.with(Default::default); PREVIEW.with(Default::default); TASKS.with(Default::default); THEME.with(Default::default); INPUT.with(Default::default); SELECT.with(Default::default); - - BOOT.with(Default::default); } diff --git a/yazi-config/src/manager/layout.rs b/yazi-config/src/manager/layout.rs deleted file mode 100644 index 61cbb346b..000000000 --- a/yazi-config/src/manager/layout.rs +++ /dev/null @@ -1,83 +0,0 @@ -use anyhow::bail; -use crossterm::terminal::WindowSize; -use ratatui::{prelude::Rect, widgets::{Block, Padding}}; -use serde::{Deserialize, Serialize}; -use yazi_shared::term::Term; - -use crate::{PREVIEW, THEME}; - -#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -#[serde(try_from = "Vec")] -pub struct ManagerLayout { - pub parent: u16, - pub current: u16, - pub preview: u16, - pub all: u16, -} - -impl TryFrom> for ManagerLayout { - type Error = anyhow::Error; - - fn try_from(ratio: Vec) -> Result { - if ratio.len() != 3 { - bail!("invalid layout ratio: {:?}", ratio); - } - if ratio.iter().all(|&r| r == 0) { - bail!("at least one layout ratio must be non-zero: {:?}", ratio); - } - - Ok(Self { - parent: ratio[0], - current: ratio[1], - preview: ratio[2], - all: ratio[0] + ratio[1] + ratio[2], - }) - } -} - -impl ManagerLayout { - pub fn preview_rect(&self) -> Rect { - let WindowSize { columns, rows, .. } = Term::size(); - let (top, right, bottom, left) = THEME.manager.preview_offset; - - let w = (columns * self.preview) as f64 / self.all as f64; - let w = if w.fract() > 0.5 { w.ceil() as u16 } else { w.floor() as u16 }; - - Rect { - x: left.saturating_add(columns - w), - y: top, - width: w.saturating_sub(left + right), - height: rows.saturating_sub(top + bottom), - } - } - - #[inline] - pub fn preview_height(&self) -> usize { self.preview_rect().height as usize } - - pub fn image_rect(&self) -> Rect { - let mut rect = self.preview_rect(); - if PREVIEW.max_width == 0 || PREVIEW.max_height == 0 { - return rect; - } - if let Some((w, h)) = Term::ratio() { - rect.width = rect.width.min((PREVIEW.max_width as f64 / w).ceil() as u16); - rect.height = rect.height.min((PREVIEW.max_height as f64 / h).ceil() as u16); - } - rect - } - - pub fn folder_rect(&self) -> Rect { - let WindowSize { columns, rows, .. } = Term::size(); - - let offset = THEME.manager.folder_offset; - Block::default().padding(Padding::new(offset.3, offset.1, offset.0, offset.2)).inner(Rect { - x: columns * self.parent / self.all, - y: 0, - width: columns * self.current / self.all, - height: rows, - }) - } - - #[inline] - pub fn folder_height(&self) -> usize { self.folder_rect().height as usize } -} diff --git a/yazi-config/src/manager/manager.rs b/yazi-config/src/manager/manager.rs index 6f8e8eada..be20951c9 100644 --- a/yazi-config/src/manager/manager.rs +++ b/yazi-config/src/manager/manager.rs @@ -1,12 +1,13 @@ use serde::{Deserialize, Serialize}; use validator::Validate; -use super::{ManagerLayout, SortBy}; +use super::{ManagerRatio, SortBy}; use crate::{validation::check_validation, MERGED_YAZI}; #[derive(Debug, Deserialize, Serialize, Validate)] pub struct Manager { - pub layout: ManagerLayout, + // FIXME: rename this to "ratio" + pub layout: ManagerRatio, // Sorting pub sort_by: SortBy, diff --git a/yazi-config/src/manager/mod.rs b/yazi-config/src/manager/mod.rs index 08394cbf1..2ddd65991 100644 --- a/yazi-config/src/manager/mod.rs +++ b/yazi-config/src/manager/mod.rs @@ -1,7 +1,7 @@ -mod layout; mod manager; +mod ratio; mod sorting; -pub use layout::*; pub use manager::*; +pub use ratio::*; pub use sorting::*; diff --git a/yazi-config/src/manager/ratio.rs b/yazi-config/src/manager/ratio.rs new file mode 100644 index 000000000..c264cb922 --- /dev/null +++ b/yazi-config/src/manager/ratio.rs @@ -0,0 +1,31 @@ +use anyhow::bail; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(try_from = "Vec")] +pub struct ManagerRatio { + pub parent: u16, + pub current: u16, + pub preview: u16, + pub all: u16, +} + +impl TryFrom> for ManagerRatio { + type Error = anyhow::Error; + + fn try_from(ratio: Vec) -> Result { + if ratio.len() != 3 { + bail!("invalid layout ratio: {:?}", ratio); + } + if ratio.iter().all(|&r| r == 0) { + bail!("at least one layout ratio must be non-zero: {:?}", ratio); + } + + Ok(Self { + parent: ratio[0], + current: ratio[1], + preview: ratio[2], + all: ratio[0] + ratio[1] + ratio[2], + }) + } +} diff --git a/yazi-config/src/open/open.rs b/yazi-config/src/open/open.rs index 30d04237e..5339314db 100644 --- a/yazi-config/src/open/open.rs +++ b/yazi-config/src/open/open.rs @@ -23,7 +23,7 @@ impl Open { P: AsRef, M: AsRef, { - let is_folder = Some(mime.as_ref() == MIME_DIR); + let is_folder = mime.as_ref() == MIME_DIR; self.rules.iter().find_map(|rule| { if rule.mime.as_ref().is_some_and(|m| m.matches(&mime)) || rule.name.as_ref().is_some_and(|n| n.match_path(&path, is_folder)) diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index 205707e05..10ee044e0 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -23,14 +23,14 @@ impl Pattern { } #[inline] - pub fn match_path(&self, path: impl AsRef, is_folder: Option) -> bool { + pub fn match_path(&self, path: impl AsRef, is_folder: bool) -> bool { let path = path.as_ref(); let s = if self.full_path { path.to_str() } else { path.file_name().and_then(|n| n.to_str()).or_else(|| path.to_str()) }; - is_folder.map_or(true, |f| f == self.is_folder) && s.is_some_and(|s| self.matches(s)) + is_folder == self.is_folder && s.is_some_and(|s| self.matches(s)) } } diff --git a/yazi-config/src/plugin/exec.rs b/yazi-config/src/plugin/exec.rs new file mode 100644 index 000000000..8e7f81bb4 --- /dev/null +++ b/yazi-config/src/plugin/exec.rs @@ -0,0 +1,39 @@ +use std::fmt; + +use anyhow::Result; +use serde::{de::{self, Visitor}, Deserializer}; +use yazi_shared::event::Exec; + +pub(super) fn exec_deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct ExecVisitor; + + impl<'de> Visitor<'de> for ExecVisitor { + type Value = Exec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a `exec` string or array of strings") + } + + fn visit_seq(self, _: A) -> Result + where + A: de::SeqAccess<'de>, + { + Err(de::Error::custom("`exec` within [plugin] must be a string")) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + if value.is_empty() { + return Err(de::Error::custom("`exec` within [plugin] cannot be empty")); + } + Ok(Exec { cmd: value.to_owned(), ..Default::default() }) + } + } + + deserializer.deserialize_any(ExecVisitor) +} diff --git a/yazi-config/src/plugin/mod.rs b/yazi-config/src/plugin/mod.rs new file mode 100644 index 000000000..af03fb09b --- /dev/null +++ b/yazi-config/src/plugin/mod.rs @@ -0,0 +1,7 @@ +mod exec; +mod plugin; + +pub use exec::*; +pub use plugin::*; + +pub const MAX_PRELOADERS: u8 = 32; diff --git a/yazi-config/src/plugin/plugin.rs b/yazi-config/src/plugin/plugin.rs new file mode 100644 index 000000000..83eb47eff --- /dev/null +++ b/yazi-config/src/plugin/plugin.rs @@ -0,0 +1,78 @@ +use std::path::Path; + +use serde::Deserialize; +use yazi_shared::{event::Exec, Condition, MIME_DIR}; + +use crate::{pattern::Pattern, plugin::MAX_PRELOADERS, MERGED_YAZI}; + +#[derive(Deserialize)] +pub struct Plugin { + pub preloaders: Vec, + pub previewers: Vec, +} + +#[derive(Deserialize)] +pub struct PluginRule { + #[serde(default)] + pub id: u8, + pub cond: Option, + pub name: Option, + pub mime: Option, + #[serde(deserialize_with = "super::exec_deserialize")] + pub exec: Exec, + #[serde(default)] + pub sync: bool, + #[serde(default)] + pub multi: bool, +} + +impl Default for Plugin { + fn default() -> Self { + #[derive(Deserialize)] + struct Outer { + plugin: Plugin, + } + + let mut plugin = toml::from_str::(&MERGED_YAZI).unwrap().plugin; + if plugin.preloaders.len() > MAX_PRELOADERS as usize { + panic!("Too many preloaders"); + } + + for (i, preloader) in plugin.preloaders.iter_mut().enumerate() { + if preloader.sync { + panic!("Preloaders cannot be synchronous"); + } + preloader.id = i as u8; + } + + plugin + } +} + +impl Plugin { + pub fn preloaders( + &self, + path: &Path, + mime: Option<&str>, + f: impl Fn(&str) -> bool + Copy, + ) -> Vec<&PluginRule> { + let is_folder = mime == Some(MIME_DIR); + self + .preloaders + .iter() + .filter(|&rule| { + rule.cond.as_ref().and_then(|c| c.eval(f)) != Some(false) + && (rule.name.as_ref().is_some_and(|n| n.match_path(path, is_folder)) + || rule.mime.as_ref().zip(mime).map_or(false, |(m, s)| m.matches(s))) + }) + .collect() + } + + pub fn previewer(&self, path: &Path, mime: &str) -> Option<&PluginRule> { + let is_folder = mime == MIME_DIR; + self.previewers.iter().find(|&rule| { + rule.mime.as_ref().is_some_and(|m| m.matches(mime)) + || rule.name.as_ref().is_some_and(|n| n.match_path(path, is_folder)) + }) + } +} diff --git a/yazi-config/src/plugins/mod.rs b/yazi-config/src/plugins/mod.rs deleted file mode 100644 index 1e08e1acc..000000000 --- a/yazi-config/src/plugins/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod plugins; - -pub use plugins::*; diff --git a/yazi-config/src/plugins/plugins.rs b/yazi-config/src/plugins/plugins.rs deleted file mode 100644 index b2b95a785..000000000 --- a/yazi-config/src/plugins/plugins.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::path::PathBuf; - -use serde::Deserialize; -use validator::Validate; -use yazi_shared::fs::expand_path; - -use crate::MERGED_YAZI; - -#[derive(Debug, Deserialize, Validate)] -pub struct Plugins { - pub preload: Vec, -} - -impl Default for Plugins { - fn default() -> Self { - #[derive(Deserialize)] - struct Outer { - plugins: Plugins, - } - - let mut plugins = toml::from_str::(&MERGED_YAZI).unwrap().plugins; - - plugins.preload.iter_mut().for_each(|p| { - *p = expand_path(&p); - }); - - plugins - } -} diff --git a/yazi-config/src/preset.rs b/yazi-config/src/preset.rs index 37e3bd640..3ad63f794 100644 --- a/yazi-config/src/preset.rs +++ b/yazi-config/src/preset.rs @@ -2,7 +2,7 @@ use std::fs; use toml::Table; -use crate::xdg::Xdg; +use crate::BOOT; pub(crate) struct Preset; @@ -29,7 +29,7 @@ impl Preset { } fn merge_str(user: &str, base: &str) -> String { - let path = Xdg::config_dir().unwrap().join(user); + let path = BOOT.config_dir.join(user); let mut user = fs::read_to_string(path).unwrap_or_default().parse::().unwrap(); let base = base.parse::
().unwrap(); diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index d22aa078e..37d68e267 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -1,12 +1,11 @@ -use std::{path::{Path, PathBuf}, time::{self, SystemTime}}; +use std::{fs, path::PathBuf, process, time::{self, SystemTime}}; -use md5::{Digest, Md5}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use yazi_shared::fs::expand_path; -use crate::{xdg::Xdg, MERGED_YAZI}; +use crate::{xdg::Xdg, ARGS, MERGED_YAZI}; -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct Preview { pub tab_size: u8, pub max_width: u32, @@ -37,10 +36,26 @@ impl Default for Preview { } let preview = toml::from_str::(&MERGED_YAZI).unwrap().preview; - let cache_dir = preview.cache_dir.filter(|p| !p.is_empty()).map_or_else(Xdg::cache_dir, expand_path); + if !cache_dir.is_dir() { + fs::create_dir(&cache_dir).unwrap(); + } + + if ARGS.clear_cache { + if cache_dir == Xdg::cache_dir() { + println!("Clearing cache directory: \n{:?}", cache_dir); + fs::remove_dir_all(&cache_dir).unwrap(); + } else { + println!( + "You've changed the default cache directory, for your data's safety, please clear it manually: \n{:?}", + cache_dir + ); + } + process::exit(0); + } + Preview { tab_size: preview.tab_size, max_width: preview.max_width, @@ -55,13 +70,6 @@ impl Default for Preview { } impl Preview { - #[inline] - pub fn cache(&self, path: &Path, skip: usize) -> PathBuf { - self - .cache_dir - .join(format!("{:x}", Md5::new_with_prefix(format!("{:?}///{}", path, skip)).finalize())) - } - #[inline] pub fn tmpfile(&self, prefix: &str) -> PathBuf { let nanos = SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap().as_nanos(); diff --git a/yazi-config/src/theme/filetype.rs b/yazi-config/src/theme/filetype.rs index e3f95da6e..f5fdb16db 100644 --- a/yazi-config/src/theme/filetype.rs +++ b/yazi-config/src/theme/filetype.rs @@ -1,6 +1,7 @@ use std::path::Path; use serde::{Deserialize, Deserializer}; +use yazi_shared::MIME_DIR; use super::{Color, Style, StyleShadow}; use crate::Pattern; @@ -12,14 +13,10 @@ pub struct Filetype { } impl Filetype { - pub fn matches(&self, path: &Path, mime: Option>, is_dir: bool) -> bool { - if self.name.as_ref().is_some_and(|e| e.match_path(path, Some(is_dir))) { - return true; - } - if let Some(mime) = mime { - return self.mime.as_ref().is_some_and(|m| m.matches(mime)); - } - false + pub fn matches(&self, path: &Path, mime: Option<&str>) -> bool { + let is_dir = mime == Some(MIME_DIR); + self.name.as_ref().is_some_and(|n| n.match_path(path, is_dir)) + || self.mime.as_ref().zip(mime).map_or(false, |(m, s)| m.matches(s)) } } diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index ae5cfa75d..d58796ebc 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -34,10 +34,6 @@ pub struct Manager { pub border_symbol: String, pub border_style: Style, - // Offset - pub(crate) folder_offset: (u16, u16, u16, u16), - pub(crate) preview_offset: (u16, u16, u16, u16), - // Highlighting pub syntect_theme: PathBuf, } diff --git a/yazi-config/src/xdg.rs b/yazi-config/src/xdg.rs index d17e34094..ec44c7b60 100644 --- a/yazi-config/src/xdg.rs +++ b/yazi-config/src/xdg.rs @@ -24,6 +24,9 @@ impl Xdg { } } + #[inline] + pub(super) fn plugin_dir() -> Option { Self::config_dir().map(|p| p.join("plugins")) } + pub(super) fn state_dir() -> Option { #[cfg(windows)] { diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index 8b083ccfa..81ad2873d 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/sxyazi/yazi" [dependencies] yazi-adaptor = { path = "../yazi-adaptor", version = "0.1.5" } yazi-config = { path = "../yazi-config", version = "0.1.5" } +yazi-plugin = { path = "../yazi-plugin", version = "0.1.5" } yazi-scheduler = { path = "../yazi-scheduler", version = "0.1.5" } yazi-shared = { path = "../yazi-shared", version = "0.1.5" } @@ -30,8 +31,8 @@ serde = "^1" syntect = { version = "^5", default-features = false, features = [ "parsing", "default-themes", "plist-load", "regex-onig" ] } tokio = { version = "^1", features = [ "parking_lot", "macros", "rt-multi-thread", "sync", "time", "fs", "process", "io-std", "io-util" ] } tokio-stream = "^0" +tokio-util = "^0" unicode-width = "^0" -yazi-prebuild = "^0" # Logging tracing = { version = "^0", features = [ "max_level_debug", "release_max_level_warn" ] } diff --git a/yazi-core/src/clipboard.rs b/yazi-core/src/clipboard.rs index 418d85a3d..f7e758447 100644 --- a/yazi-core/src/clipboard.rs +++ b/yazi-core/src/clipboard.rs @@ -1,13 +1,12 @@ -use std::ffi::OsString; +use std::{cell::RefCell, ffi::OsString}; -use parking_lot::Mutex; use yazi_shared::RoCell; pub static CLIPBOARD: RoCell = RoCell::new(); #[derive(Default)] pub struct Clipboard { - content: Mutex, + content: RefCell, } impl Clipboard { @@ -19,7 +18,7 @@ impl Clipboard { use yazi_shared::in_ssh_connection; if in_ssh_connection() { - return self.content.lock().clone(); + return self.content.borrow().clone(); } let all = [ @@ -37,7 +36,7 @@ impl Clipboard { return OsString::from_vec(output.stdout); } } - self.content.lock().clone() + self.content.borrow().clone() } #[cfg(windows)] @@ -49,7 +48,7 @@ impl Clipboard { return s.into(); } - self.content.lock().clone() + self.content.borrow().clone() } #[cfg(unix)] @@ -60,7 +59,7 @@ impl Clipboard { use tokio::{io::AsyncWriteExt, process::Command}; use yazi_shared::in_ssh_connection; - *self.content.lock() = s.as_ref().to_owned(); + *self.content.borrow_mut() = s.as_ref().to_owned(); if in_ssh_connection() { execute!(stdout(), osc52::SetClipboard::new(s.as_ref())).ok(); } @@ -102,7 +101,7 @@ impl Clipboard { use clipboard_win::{formats, set_clipboard}; let s = s.as_ref().to_owned(); - *self.content.lock() = s.clone(); + *self.content.borrow_mut() = s.clone(); tokio::task::spawn_blocking(move || set_clipboard(formats::Unicode, s.to_string_lossy())) .await diff --git a/yazi-core/src/completion/mod.rs b/yazi-core/src/completion/mod.rs index daf831ad5..de2ab0039 100644 --- a/yazi-core/src/completion/mod.rs +++ b/yazi-core/src/completion/mod.rs @@ -1,4 +1,4 @@ mod commands; mod completion; -pub(super) use completion::*; +pub use completion::*; diff --git a/yazi-core/src/files/files.rs b/yazi-core/src/folder/files.rs similarity index 54% rename from yazi-core/src/files/files.rs rename to yazi-core/src/folder/files.rs index f5e3b426e..90ad88c28 100644 --- a/yazi-core/src/files/files.rs +++ b/yazi-core/src/folder/files.rs @@ -8,10 +8,11 @@ use yazi_shared::fs::{File, Url, FILES_TICKET}; use super::FilesSorter; pub struct Files { - items: Vec, - hidden: Vec, - ticket: u64, - pub(crate) version: u64, + hidden: Vec, + items: Vec, + ticket: u64, + version: u64, + pub(crate) revision: u64, pub sizes: BTreeMap, selected: BTreeSet, @@ -23,10 +24,11 @@ pub struct Files { impl Default for Files { fn default() -> Self { Self { - items: Default::default(), - hidden: Default::default(), - ticket: Default::default(), - version: Default::default(), + items: Default::default(), + hidden: Default::default(), + ticket: Default::default(), + version: Default::default(), + revision: Default::default(), sizes: Default::default(), selected: Default::default(), @@ -68,7 +70,7 @@ impl Files { #[inline] pub fn select(&mut self, url: &Url, state: Option) -> bool { let old = self.selected.contains(url); - let new = if let Some(new) = state { new } else { !old }; + let new = state.unwrap_or(!old); if new == old { return false; @@ -126,132 +128,184 @@ impl Files { applied } - pub fn update_full(&mut self, mut items: Vec) -> bool { - if !self.show_hidden { - (self.hidden, items) = items.into_iter().partition(|f| f.is_hidden()); + pub fn update_full(&mut self, files: Vec) { + if files.is_empty() { + return; } + self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed); - self.sorter.sort(&mut items, &self.sizes); - self.items = items; - self.version += 1; - true + self.revision += 1; + + (self.hidden, self.items) = if self.show_hidden { + (vec![], files) + } else { + files.into_iter().partition(|f| f.is_hidden()) + }; } - pub fn update_part(&mut self, version: u64, items: Vec) -> bool { - if !items.is_empty() { - if version != self.ticket { - return false; + pub fn update_part(&mut self, files: Vec, ticket: u64) { + if !files.is_empty() { + if ticket != self.ticket { + return; } + self.revision += 1; if self.show_hidden { - self.items.extend(items); + self.hidden.clear(); + self.items.extend(files); } else { - let (hidden, items): (Vec<_>, Vec<_>) = items.into_iter().partition(|f| f.is_hidden()); - self.items.extend(items); + let (hidden, items): (Vec<_>, Vec<_>) = files.into_iter().partition(|f| f.is_hidden()); self.hidden.extend(hidden); + self.items.extend(items); } - - self.sorter.sort(&mut self.items, &self.sizes); - self.version += 1; - return true; + return; } - self.ticket = version; - if self.items.is_empty() && self.hidden.is_empty() { - return false; - } - - self.items.clear(); + self.ticket = ticket; self.hidden.clear(); - self.version += 1; - true + if !self.items.is_empty() { + self.revision += 1; + self.items.clear(); + } } - pub fn update_size(&mut self, items: BTreeMap) -> bool { - self.sizes.extend(items); + pub fn update_size(&mut self, sizes: BTreeMap) { + if sizes.is_empty() { + return; + } + if self.sorter.by == SortBy::Size { - self.sorter.sort(&mut self.items, &self.sizes); - self.version += 1; + self.revision += 1; } - true + self.sizes.extend(sizes); } - pub fn update_creating(&mut self, mut todo: BTreeMap) -> bool { - if !self.show_hidden { - todo.retain(|_, f| !f.is_hidden()); + pub fn update_creating(&mut self, files: Vec) { + if files.is_empty() { + return; } - let b = self.update_replacing(&mut todo); - if todo.is_empty() { - return b; + macro_rules! go { + ($dist:expr, $src:expr) => { + let mut todo: BTreeMap<_, _> = $src.into_iter().map(|f| (f.url(), f)).collect(); + for f in &$dist { + if todo.remove(&f.url).is_some() && todo.is_empty() { + break; + } + } + if !todo.is_empty() { + self.revision += 1; + $dist.extend(todo.into_values()); + } + }; } - self.items.extend(todo.into_values()); - self.sorter.sort(&mut self.items, &self.sizes); - self.version += 1; - true + let (hidden, items) = if self.show_hidden { + (vec![], files) + } else { + files.into_iter().partition(|f| f.is_hidden()) + }; + + if !items.is_empty() { + go!(self.items, items); + } + if !hidden.is_empty() { + go!(self.hidden, hidden); + } } - pub fn update_deleting(&mut self, mut todo: BTreeSet) -> bool { - let mut removed = Vec::with_capacity(todo.len()); + pub fn update_deleting(&mut self, urls: Vec) { + if urls.is_empty() { + return; + } + macro_rules! go { - ($name:expr) => { - removed.clear(); - for i in 0..$name.len() { - if todo.remove(&$name[i].url) { - removed.push(i); - if todo.is_empty() { - break; - } - } - } - for i in (0..removed.len()).rev() { - $name.remove(removed[i]); + ($dist:expr, $src:expr) => { + let mut todo: BTreeSet<_> = $src.into_iter().collect(); + let len = $dist.len(); + + $dist.retain(|f| !todo.remove(&f.url)); + if $dist.len() != len { + self.revision += 1; } }; } - let mut b = false; - if !todo.is_empty() { - go!(self.items); - b |= !removed.is_empty(); - } + let (hidden, items) = + if self.show_hidden { (vec![], urls) } else { urls.into_iter().partition(|u| u.is_hidden()) }; - if !todo.is_empty() { - go!(self.hidden); - b |= !removed.is_empty(); + if !items.is_empty() { + go!(self.items, items); + } + if !hidden.is_empty() { + go!(self.hidden, hidden); } - b } - pub fn update_replacing(&mut self, todo: &mut BTreeMap) -> bool { - if todo.is_empty() { - return false; + pub fn update_updating(&mut self, files: BTreeMap) -> [BTreeMap; 2] { + if files.is_empty() { + return Default::default(); } macro_rules! go { - ($name:expr) => { - for i in 0..$name.len() { - if let Some(f) = todo.remove(&$name[i].url) { - $name[i] = f; - if todo.is_empty() { - self.version += 1; - return true; + ($dist:expr, $src:expr) => { + let len = $src.len(); + for i in 0..$dist.len() { + if let Some(f) = $src.remove(&$dist[i].url) { + $dist[i] = f; + if $src.is_empty() { + break; } } } + if $src.len() != len { + self.revision += 1; + } }; } - let old = todo.len(); - go!(self.items); - go!(self.hidden); + let (mut hidden, mut items) = if self.show_hidden { + (BTreeMap::new(), files) + } else { + files.into_iter().partition(|(_, f)| f.is_hidden()) + }; + + if !items.is_empty() { + go!(self.items, items); + } + if !hidden.is_empty() { + go!(self.hidden, hidden); + } + [hidden, items] + } + + pub fn update_upserting(&mut self, files: BTreeMap) { + if files.is_empty() { + return; + } + + let [hidden, items] = self.update_updating(files); + if hidden.is_empty() && items.is_empty() { + return; + } + + if !hidden.is_empty() { + self.hidden.extend(hidden.into_values()); + } + if !items.is_empty() { + self.revision += 1; + self.items.extend(items.into_values()); + } + } - if old != todo.len() { - self.version += 1; - return true; + pub fn catchup_revision(&mut self) -> bool { + if self.version == self.revision { + return false; } - false + + self.version = self.revision; + self.sorter.sort(&mut self.items, &self.sizes); + true } } @@ -313,33 +367,32 @@ impl Files { #[inline] pub fn sorter(&self) -> &FilesSorter { &self.sorter } - pub fn set_sorter(&mut self, sorter: FilesSorter) -> bool { - if self.sorter == sorter { - return false; + pub fn set_sorter(&mut self, sorter: FilesSorter) { + if self.sorter != sorter { + self.sorter = sorter; + self.revision += 1; } - self.sorter = sorter; - self.version += 1; - self.sorter.sort(&mut self.items, &self.sizes) } // --- Show hidden - pub fn set_show_hidden(&mut self, state: bool) -> bool { - if state == self.show_hidden { - return false; - } else if state && self.hidden.is_empty() { - return false; + pub fn set_show_hidden(&mut self, state: bool) { + if self.show_hidden == state { + return; } - if state { + self.show_hidden = state; + if self.show_hidden && self.hidden.is_empty() { + return; + } else if !self.show_hidden && self.items.is_empty() { + return; + } + + self.revision += 1; + if self.show_hidden { self.items.append(&mut self.hidden); - self.sorter.sort(&mut self.items, &self.sizes); } else { let items = mem::take(&mut self.items); (self.hidden, self.items) = items.into_iter().partition(|f| f.is_hidden()); } - - self.show_hidden = state; - self.version += 1; - true } } diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/folder/folder.rs similarity index 73% rename from yazi-core/src/tab/folder.rs rename to yazi-core/src/folder/folder.rs index 832fb63f1..45292d4fb 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/folder/folder.rs @@ -1,8 +1,8 @@ use ratatui::layout::Rect; -use yazi_config::MANAGER; -use yazi_shared::{emit, fs::{File, FilesOp}, fs::Url}; +use yazi_config::LAYOUT; +use yazi_shared::{emit, fs::{File, FilesOp, Url}}; -use crate::{files::Files, Step}; +use crate::{folder::Files, Step}; #[derive(Default)] pub struct Folder { @@ -25,17 +25,18 @@ impl From<&Url> for Folder { impl Folder { pub fn update(&mut self, op: FilesOp) -> bool { - let b = match op { - FilesOp::Full(_, items) => self.files.update_full(items), - FilesOp::Part(_, ticket, items) => self.files.update_part(ticket, items), - FilesOp::Size(_, items) => self.files.update_size(items), - - FilesOp::Creating(_, items) => self.files.update_creating(items), - FilesOp::Deleting(_, items) => self.files.update_deleting(items), - FilesOp::Replacing(_, mut items) => self.files.update_replacing(&mut items), + match op { + FilesOp::Full(_, files) => self.files.update_full(files), + FilesOp::Part(_, files, ticket) => self.files.update_part(files, ticket), + FilesOp::Size(_, sizes) => self.files.update_size(sizes), + + FilesOp::Creating(_, files) => self.files.update_creating(files), + FilesOp::Deleting(_, urls) => self.files.update_deleting(urls), + FilesOp::Updating(_, files) => _ = self.files.update_updating(files), + FilesOp::Upserting(_, files) => self.files.update_upserting(files), _ => unreachable!(), - }; - if !b { + } + if !self.files.catchup_revision() { return false; } @@ -50,7 +51,7 @@ impl Folder { } pub fn set_page(&mut self, force: bool) { - let limit = MANAGER.layout.folder_height(); + let limit = LAYOUT.load().current.height as usize; if limit == 0 { return; } @@ -81,8 +82,8 @@ impl Folder { let old = (self.cursor, self.offset); let len = self.files.len(); - let limit = MANAGER.layout.folder_height(); - self.cursor = step.add(self.cursor, || limit).min(len.saturating_sub(1)); + let limit = LAYOUT.load().current.height as usize; + self.cursor = step.add(self.cursor, limit).min(len.saturating_sub(1)); self.offset = if self.cursor >= (self.offset + limit).min(len).saturating_sub(5) { len.saturating_sub(limit).min(self.offset + self.cursor - old.0) } else { @@ -97,7 +98,7 @@ impl Folder { let old = (self.cursor, self.offset); let max = self.files.len().saturating_sub(1); - self.cursor = step.add(self.cursor, || MANAGER.layout.folder_height()).min(max); + self.cursor = step.add(self.cursor, LAYOUT.load().current.height as usize).min(max); self.offset = if self.cursor < self.offset + 5 { self.offset.saturating_sub(old.0 - self.cursor) } else { @@ -133,7 +134,7 @@ impl Folder { pub fn paginate(&self, page: usize) -> &[File] { let len = self.files.len(); - let limit = MANAGER.layout.folder_height(); + let limit = LAYOUT.load().current.height as usize; let start = (page * limit).min(len.saturating_sub(1)); let end = (start + limit).min(len); @@ -143,7 +144,7 @@ impl Folder { pub fn rect_current(&self, url: &Url) -> Option { let y = self.files.position(url)? - self.offset; - let mut rect = MANAGER.layout.folder_rect(); + let mut rect = LAYOUT.load().current; rect.y = rect.y.saturating_sub(1) + y as u16; rect.height = 1; Some(rect) diff --git a/yazi-core/src/files/mod.rs b/yazi-core/src/folder/mod.rs similarity index 66% rename from yazi-core/src/files/mod.rs rename to yazi-core/src/folder/mod.rs index f6b8945de..f40a0ab7f 100644 --- a/yazi-core/src/files/mod.rs +++ b/yazi-core/src/folder/mod.rs @@ -1,5 +1,7 @@ mod files; +mod folder; mod sorter; pub use files::*; +pub use folder::*; pub use sorter::*; diff --git a/yazi-core/src/files/sorter.rs b/yazi-core/src/folder/sorter.rs similarity index 94% rename from yazi-core/src/files/sorter.rs rename to yazi-core/src/folder/sorter.rs index 75c0c5fee..4dea60833 100644 --- a/yazi-core/src/files/sorter.rs +++ b/yazi-core/src/folder/sorter.rs @@ -68,16 +68,16 @@ impl FilesSorter { let mut entities = Vec::with_capacity(items.len()); for (i, file) in items.iter().enumerate() { indices.push(i); - entities.push((file.url.to_string_lossy(), file)); + entities.push(file.url.as_os_str().as_encoded_bytes()); } indices.sort_unstable_by(|&a, &b| { - let promote = self.promote(entities[a].1, entities[b].1); + let promote = self.promote(&items[a], &items[b]); if promote != Ordering::Equal { return promote; } - let ordering = natsort(&entities[a].0, &entities[b].0, !self.sensitive); + let ordering = natsort(entities[a], entities[b], !self.sensitive); if self.reverse { ordering.reverse() } else { ordering } }); diff --git a/yazi-core/src/help/commands/escape.rs b/yazi-core/src/help/commands/escape.rs index afde74e28..d05696768 100644 --- a/yazi-core/src/help/commands/escape.rs +++ b/yazi-core/src/help/commands/escape.rs @@ -2,14 +2,8 @@ use yazi_shared::event::Exec; use crate::help::Help; -pub struct Opt; - -impl From<&Exec> for Opt { - fn from(_: &Exec) -> Self { Self } -} - impl Help { - pub fn escape(&mut self, _: impl Into) -> bool { + pub fn escape(&mut self, _: &Exec) -> bool { if self.in_filter.is_some() { self.in_filter = None; self.filter_apply(); diff --git a/yazi-core/src/help/commands/filter.rs b/yazi-core/src/help/commands/filter.rs index b6580b2d8..5a93c07b5 100644 --- a/yazi-core/src/help/commands/filter.rs +++ b/yazi-core/src/help/commands/filter.rs @@ -3,14 +3,8 @@ use yazi_shared::event::Exec; use crate::{help::Help, input::Input}; -pub struct Opt; - -impl From<&Exec> for Opt { - fn from(_: &Exec) -> Self { Self } -} - impl Help { - pub fn filter(&mut self, _: impl Into) -> bool { + pub fn filter(&mut self, _: &Exec) -> bool { let mut input = Input::default(); input.position = Position::new(Origin::BottomLeft, Offset::line()); diff --git a/yazi-core/src/input/commands/backward.rs b/yazi-core/src/input/commands/backward.rs index 3fe3b0b7f..4131ad022 100644 --- a/yazi-core/src/input/commands/backward.rs +++ b/yazi-core/src/input/commands/backward.rs @@ -2,14 +2,8 @@ use yazi_shared::{event::Exec, CharKind}; use crate::input::Input; -pub struct Opt; - -impl From<&Exec> for Opt { - fn from(_: &Exec) -> Self { Self } -} - impl Input { - pub fn backward(&mut self, _: impl Into) -> bool { + pub fn backward(&mut self, _: &Exec) -> bool { let snap = self.snap(); if snap.cursor == 0 { return self.move_(0); diff --git a/yazi-core/src/input/commands/redo.rs b/yazi-core/src/input/commands/redo.rs index 9573f0e1f..135238e39 100644 --- a/yazi-core/src/input/commands/redo.rs +++ b/yazi-core/src/input/commands/redo.rs @@ -2,12 +2,6 @@ use yazi_shared::event::Exec; use crate::input::Input; -pub struct Opt; - -impl From<&Exec> for Opt { - fn from(_: &Exec) -> Self { Self } -} - impl Input { - pub fn redo(&mut self, _: impl Into) -> bool { self.snaps.redo() } + pub fn redo(&mut self, _: &Exec) -> bool { self.snaps.redo() } } diff --git a/yazi-core/src/input/commands/undo.rs b/yazi-core/src/input/commands/undo.rs index 12f63ceb4..8ba4b7290 100644 --- a/yazi-core/src/input/commands/undo.rs +++ b/yazi-core/src/input/commands/undo.rs @@ -2,14 +2,8 @@ use yazi_shared::event::Exec; use crate::input::{Input, InputMode}; -pub struct Opt; - -impl From<&Exec> for Opt { - fn from(_: &Exec) -> Self { Self } -} - impl Input { - pub fn undo(&mut self, _: impl Into) -> bool { + pub fn undo(&mut self, _: &Exec) -> bool { if !self.snaps.undo() { return false; } diff --git a/yazi-core/src/input/commands/visual.rs b/yazi-core/src/input/commands/visual.rs index 0ce945ac8..67ba3fcbc 100644 --- a/yazi-core/src/input/commands/visual.rs +++ b/yazi-core/src/input/commands/visual.rs @@ -2,15 +2,9 @@ use yazi_shared::event::Exec; use crate::input::{op::InputOp, Input, InputMode}; -pub struct Opt; - -impl From<&Exec> for Opt { - fn from(_: &Exec) -> Self { Self } -} - impl Input { #[inline] - pub fn visual(&mut self, _: impl Into) -> bool { + pub fn visual(&mut self, _: &Exec) -> bool { let snap = self.snap_mut(); if snap.mode != InputMode::Normal { return false; diff --git a/yazi-core/src/input/commands/yank.rs b/yazi-core/src/input/commands/yank.rs index c56f653b1..14ee9914f 100644 --- a/yazi-core/src/input/commands/yank.rs +++ b/yazi-core/src/input/commands/yank.rs @@ -2,14 +2,8 @@ use yazi_shared::event::Exec; use crate::input::{op::InputOp, Input}; -pub struct Opt; - -impl From<&Exec> for Opt { - fn from(_: &Exec) -> Self { Self } -} - impl Input { - pub fn yank(&mut self, _: impl Into) -> bool { + pub fn yank(&mut self, _: &Exec) -> bool { match self.snap().op { InputOp::None => { self.snap_mut().op = InputOp::Yank(self.snap().cursor); diff --git a/yazi-core/src/input/shell.rs b/yazi-core/src/input/shell.rs index 16cf0230c..9ff89feee 100644 --- a/yazi-core/src/input/shell.rs +++ b/yazi-core/src/input/shell.rs @@ -1,8 +1,8 @@ use anyhow::{bail, Result}; use syntect::{easy::HighlightLines, util::as_24_bit_terminal_escaped}; +use yazi_plugin::external::Highlighter; use super::Input; -use crate::Highlighter; impl Input { pub fn value_pretty(&self) -> Result { diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index 5552e2acc..c2cefc44a 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -8,13 +8,10 @@ mod clipboard; pub mod completion; -mod context; -pub mod files; +pub mod folder; pub mod help; -mod highlighter; pub mod input; pub mod manager; -pub mod preview; pub mod select; mod step; pub mod tab; @@ -22,8 +19,6 @@ pub mod tasks; pub mod which; pub use clipboard::*; -pub use context::*; -pub use highlighter::*; pub use step::*; pub fn init() { diff --git a/yazi-core/src/manager/commands/close.rs b/yazi-core/src/manager/commands/close.rs index 7249a111f..47e9478eb 100644 --- a/yazi-core/src/manager/commands/close.rs +++ b/yazi-core/src/manager/commands/close.rs @@ -2,14 +2,8 @@ use yazi_shared::event::Exec; use crate::{manager::Manager, tasks::Tasks}; -pub struct Opt; - -impl From<&Exec> for Opt { - fn from(_: &Exec) -> Self { Self } -} - impl Manager { - pub fn close(&mut self, _: impl Into, tasks: &Tasks) -> bool { + pub fn close(&mut self, _: &Exec, tasks: &Tasks) -> bool { if self.tabs.len() > 1 { return self.tabs.close(self.tabs.idx); } diff --git a/yazi-core/src/manager/commands/create.rs b/yazi-core/src/manager/commands/create.rs index 7320fd5dd..fb329ce62 100644 --- a/yazi-core/src/manager/commands/create.rs +++ b/yazi-core/src/manager/commands/create.rs @@ -2,7 +2,7 @@ use std::path::{PathBuf, MAIN_SEPARATOR}; use tokio::fs; use yazi_config::popup::InputCfg; -use yazi_shared::{emit, event::Exec, fs::{File, FilesOp}, fs::Url}; +use yazi_shared::{event::Exec, fs::{File, FilesOp, Url}}; use crate::{input::Input, manager::Manager}; @@ -42,7 +42,7 @@ impl Manager { let child = Url::from(path.components().take(cwd.components().count() + 1).collect::()); if let Ok(f) = File::from(child.clone()).await { - emit!(Files(FilesOp::Creating(cwd, f.into_map()))); + FilesOp::Creating(cwd, vec![f]).emit(); Manager::_hover(Some(child)); } Ok::<(), anyhow::Error>(()) diff --git a/yazi-core/src/manager/commands/hover.rs b/yazi-core/src/manager/commands/hover.rs index 331880937..75144d76f 100644 --- a/yazi-core/src/manager/commands/hover.rs +++ b/yazi-core/src/manager/commands/hover.rs @@ -11,6 +11,9 @@ pub struct Opt { impl From<&Exec> for Opt { fn from(e: &Exec) -> Self { Self { url: e.args.first().map(Url::from) } } } +impl From> for Opt { + fn from(url: Option) -> Self { Self { url } } +} impl Manager { #[inline] @@ -27,7 +30,7 @@ impl Manager { let mut b = self.current_mut().repos(opt.url); // Re-peek - b |= self.peek(0); + b |= self.peek(false); // Refresh watcher let mut to_watch = BTreeSet::new(); diff --git a/yazi-core/src/manager/commands/mod.rs b/yazi-core/src/manager/commands/mod.rs index a1fd71fde..72cef281f 100644 --- a/yazi-core/src/manager/commands/mod.rs +++ b/yazi-core/src/manager/commands/mod.rs @@ -9,9 +9,12 @@ mod quit; mod refresh; mod remove; mod rename; +mod seek; mod suspend; mod tab_close; mod tab_create; mod tab_swap; mod tab_switch; +mod update_files; +mod update_mimetype; mod yank; diff --git a/yazi-core/src/manager/commands/open.rs b/yazi-core/src/manager/commands/open.rs index 029c6eb6e..b8603881b 100644 --- a/yazi-core/src/manager/commands/open.rs +++ b/yazi-core/src/manager/commands/open.rs @@ -1,71 +1,117 @@ use std::ffi::OsString; -use yazi_config::{popup::SelectCfg, OPEN}; -use yazi_scheduler::external; -use yazi_shared::{event::Exec, MIME_DIR}; +use tokio::fs; +use tracing::error; +use yazi_config::{popup::SelectCfg, ARGS, OPEN}; +use yazi_plugin::isolate; +use yazi_shared::{emit, event::Exec, fs::{File, Url}, Layer, MIME_DIR}; use crate::{manager::Manager, select::Select, tasks::Tasks}; pub struct Opt { + targets: Option)>>, interactive: bool, } impl From<&Exec> for Opt { - fn from(e: &Exec) -> Self { Self { interactive: e.named.contains_key("interactive") } } + fn from(e: &Exec) -> Self { + Self { targets: e.take_data(), interactive: e.named.contains_key("interactive") } + } } impl Manager { - async fn open_interactive(files: Vec<(OsString, String)>) { - let openers = OPEN.common_openers(&files); - if openers.is_empty() { - return; + pub fn open(&mut self, opt: impl Into, tasks: &Tasks) -> bool { + let selected = self.selected(); + if selected.is_empty() { + return false; + } else if Self::quit_with_selected(&selected) { + return false; } - let result = Select::_show(SelectCfg::open(openers.iter().map(|o| o.desc.clone()).collect())); - if let Ok(choice) = result.await { - Tasks::_open(files, Some(openers[choice].clone())); + let (mut done, mut todo) = (Vec::with_capacity(selected.len()), vec![]); + for f in selected { + if f.is_dir() { + done.push((f.url(), Some(MIME_DIR.to_owned()))); + } else if self.mimetype.get(&f.url).is_some() { + done.push((f.url(), None)); + } else { + todo.push(f.clone()); + } + } + + let mut opt = opt.into() as Opt; + if todo.is_empty() { + opt.targets = Some(done); + return self.open_do(opt, tasks); } + + tokio::spawn(async move { + done.extend(todo.iter().map(|f| (f.url(), None))); + if let Err(e) = isolate::preload("mime.lua".to_string(), todo, true).await { + error!("preload in watcher failed: {e}"); + } + + Self::_open_do(opt.interactive, done); + }); + false + } + + #[inline] + pub fn _open_do(interactive: bool, targets: Vec<(Url, Option)>) { + emit!(Call( + Exec::call("open_do", vec![]).with_bool("interactive", interactive).with_data(targets).vec(), + Layer::Manager + )); } - pub fn open(&mut self, opt: impl Into) -> bool { - let mut files: Vec<_> = self - .selected() + pub fn open_do(&mut self, opt: impl Into, tasks: &Tasks) -> bool { + let opt = opt.into() as Opt; + let Some(targets) = opt.targets else { + return false; + }; + + let targets: Vec<_> = targets .into_iter() - .map(|f| { - ( - f.url(), - f.is_dir().then(|| MIME_DIR.to_owned()).or_else(|| self.mimetype.get(&f.url).cloned()), - ) - }) + .filter_map(|(u, m)| m.or_else(|| self.mimetype.get(&u).cloned()).map(|m| (u, m))) .collect(); - if files.is_empty() { + if targets.is_empty() { + return false; + } else if !opt.interactive { + tasks.file_open(&targets); return false; } - let opt = opt.into() as Opt; + let openers: Vec<_> = OPEN.common_openers(&targets).into_iter().cloned().collect(); + if openers.is_empty() { + return false; + } + + let urls = targets.into_iter().map(|(u, _)| u).collect(); tokio::spawn(async move { - let todo: Vec<_> = files.iter().filter(|(_, m)| m.is_none()).map(|(u, _)| u).collect(); - if let Ok(mut mimes) = external::file(&todo).await { - files = files - .into_iter() - .map(|(u, m)| { - let mime = m.or_else(|| mimes.remove(&u)); - (u, mime) - }) - .collect(); + let result = Select::_show(SelectCfg::open(openers.iter().map(|o| o.desc.clone()).collect())); + if let Ok(choice) = result.await { + Tasks::_open(urls, openers[choice].clone()); } + }); + false + } - let files: Vec<_> = - files.into_iter().filter_map(|(u, m)| m.map(|m| (u.into_os_string(), m))).collect(); + fn quit_with_selected(selected: &[&File]) -> bool { + let Some(p) = ARGS.chooser_file.clone() else { + return false; + }; - if opt.interactive { - Self::open_interactive(files).await; - return; - } + let paths = selected.iter().fold(OsString::new(), |mut s, &f| { + s.push(f.url.as_os_str()); + s.push("\n"); + s + }); - Tasks::_open(files, None); + tokio::spawn(async move { + fs::write(p, paths.as_encoded_bytes()).await.ok(); + emit!(Quit(false)); }); - false + true } } diff --git a/yazi-core/src/manager/commands/peek.rs b/yazi-core/src/manager/commands/peek.rs index 500ad8d04..dd337d213 100644 --- a/yazi-core/src/manager/commands/peek.rs +++ b/yazi-core/src/manager/commands/peek.rs @@ -1,11 +1,11 @@ -use yazi_config::MANAGER; use yazi_shared::{emit, event::Exec, fs::Url, Layer, MIME_DIR}; use crate::manager::Manager; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Opt { - step: isize, + skip: Option, + force: bool, only_if: Option, upper_bound: bool, } @@ -13,26 +13,21 @@ pub struct Opt { impl From<&Exec> for Opt { fn from(e: &Exec) -> Self { Self { - step: e.args.first().and_then(|s| s.parse().ok()).unwrap_or(0), + skip: e.args.first().and_then(|s| s.parse().ok()), + force: e.named.contains_key("force"), only_if: e.named.get("only-if").map(Url::from), upper_bound: e.named.contains_key("upper-bound"), } } } -impl From for Opt { - fn from(step: isize) -> Self { Self { step, only_if: None, upper_bound: false } } +impl From for Opt { + fn from(force: bool) -> Self { Self { force, ..Default::default() } } } impl Manager { #[inline] - pub fn _peek_upper_bound(bound: usize, only_if: &Url) { - emit!(Call( - Exec::call("peek", vec![bound.to_string()]) - .with("only-if", only_if.to_string()) - .with_bool("upper-bound", true) - .vec(), - Layer::Manager - )); + pub fn _peek(force: bool) { + emit!(Call(Exec::call("peek", vec![]).with_bool("force", force).vec(), Layer::Manager)); } pub fn peek(&mut self, opt: impl Into) -> bool { @@ -45,42 +40,35 @@ impl Manager { return false; } - if hovered.is_dir() { - return self.peek_folder(opt, hovered.url.clone()); - } - - let Some(mime) = self.mimetype.get(&hovered.url).cloned() else { - return self.active_mut().preview.reset(); - }; - - let (url, cha) = (hovered.url.clone(), hovered.cha); - if opt.upper_bound { - self.active_mut().preview.arrow(0, &mime, Some(opt.step as usize)); - } else if self.active().preview.same_url(&url) { - self.active_mut().preview.arrow(opt.step, &mime, None); - } else { - self.active_mut().preview.arrow(0, &mime, Some(0)); + let hovered = hovered.clone(); + if !self.active().preview.same_url(&hovered.url) { + self.active_mut().preview.skip = 0; self.active_mut().preview.reset(); } - self.active_mut().preview.go(&url, cha, &mime); - false - } + if let Some(skip) = opt.skip { + let preview = &mut self.active_mut().preview; + if opt.upper_bound { + preview.skip = preview.skip.min(skip); + } else { + preview.skip = skip; + } + } - fn peek_folder(&mut self, opt: Opt, url: Url) -> bool { - let folder = self.active().history.get(&url); - let (skip, bound) = folder - .map(|f| (f.offset, f.files.len().saturating_sub(MANAGER.layout.folder_height()))) - .unwrap_or_default(); + if hovered.is_dir() { + if self.active().history.contains_key(&hovered.url) { + self.active_mut().preview.go(hovered, MIME_DIR, opt.force); + } else { + self.active_mut().preview.go_folder(hovered, opt.force); + } + return false; + } - let in_chunks = folder.is_none(); - if self.active().preview.same_url(&url) { - self.active_mut().preview.arrow(opt.step, MIME_DIR, Some(bound)); - self.active_mut().preview.sync_skip() + if let Some(s) = self.mimetype.get(&hovered.url).cloned() { + self.active_mut().preview.go(hovered, &s, opt.force); } else { - self.active_mut().preview.arrow(skip as isize, MIME_DIR, Some(skip)); - self.active_mut().preview.go_folder(url, in_chunks); - false + return self.active_mut().preview.reset(); } + false } } diff --git a/yazi-core/src/manager/commands/refresh.rs b/yazi-core/src/manager/commands/refresh.rs index a3691fe56..8b56efd81 100644 --- a/yazi-core/src/manager/commands/refresh.rs +++ b/yazi-core/src/manager/commands/refresh.rs @@ -4,31 +4,24 @@ use yazi_shared::{emit, event::Exec, Layer}; use crate::manager::Manager; -pub struct Opt; - -impl From<&Exec> for Opt { - fn from(_: &Exec) -> Self { Self } -} - impl Manager { #[inline] pub fn _refresh() { emit!(Call(Exec::call("refresh", vec![]).vec(), Layer::Manager)); } - pub fn refresh(&mut self, _: impl Into) -> bool { + pub fn refresh(&mut self, _: &Exec) -> bool { env::set_current_dir(self.cwd()).ok(); env::set_var("PWD", self.cwd()); self.active_mut().apply_files_attrs(false); - if let Some(f) = self.parent() { - self.watcher.trigger_dirs(&[self.cwd(), &f.cwd]); + if let Some(p) = self.parent() { + self.watcher.trigger_dirs(&[self.cwd(), &p.cwd]); } else { self.watcher.trigger_dirs(&[self.cwd()]); } - Self::_hover(None); - false + self.hover(None) } } diff --git a/yazi-core/src/manager/commands/rename.rs b/yazi-core/src/manager/commands/rename.rs index b820754fe..9ad61234c 100644 --- a/yazi-core/src/manager/commands/rename.rs +++ b/yazi-core/src/manager/commands/rename.rs @@ -3,10 +3,11 @@ use std::{collections::BTreeMap, ffi::OsStr, io::{stdout, BufWriter, Write}, pat use anyhow::{anyhow, bail, Result}; use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; use yazi_config::{popup::InputCfg, OPEN, PREVIEW}; -use yazi_scheduler::{external::{self, ShellOpt}, BLOCKER}; -use yazi_shared::{emit, event::Exec, fs::{max_common_root, File, FilesOp, Url}, term::Term, Defer}; +use yazi_plugin::external::{self, ShellOpt}; +use yazi_scheduler::{Scheduler, BLOCKER}; +use yazi_shared::{event::Exec, fs::{max_common_root, File, FilesOp, Url}, term::Term, Defer}; -use crate::{input::Input, manager::Manager, Ctx}; +use crate::{input::Input, manager::Manager}; pub struct Opt { force: bool, @@ -24,7 +25,7 @@ impl Manager { } let file = File::from(new.clone()).await?; - emit!(Files(FilesOp::Replacing(file.parent().unwrap(), BTreeMap::from_iter([(old, file)])))); + FilesOp::Upserting(file.parent().unwrap(), BTreeMap::from_iter([(old, file)])).emit(); Ok(Self::_hover(Some(new))) } @@ -87,10 +88,10 @@ impl Manager { let _guard = BLOCKER.acquire().await.unwrap(); let _defer = Defer::new(|| { - Ctx::resume(); + Scheduler::app_resume(); tokio::spawn(fs::remove_file(tmp.clone())) }); - Ctx::stop().await; + Scheduler::app_stop().await; let mut child = external::shell(ShellOpt { cmd: (*opener.exec).into(), diff --git a/yazi-core/src/manager/commands/seek.rs b/yazi-core/src/manager/commands/seek.rs new file mode 100644 index 000000000..8687e8f53 --- /dev/null +++ b/yazi-core/src/manager/commands/seek.rs @@ -0,0 +1,40 @@ +use yazi_config::PLUGIN; +use yazi_plugin::isolate; +use yazi_shared::{event::Exec, MIME_DIR}; + +use crate::manager::Manager; + +#[derive(Debug)] +pub struct Opt { + units: i16, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + Self { units: e.args.first().and_then(|s| s.parse().ok()).unwrap_or(0) } + } +} + +impl Manager { + pub fn seek(&mut self, opt: impl Into) -> bool { + let Some(hovered) = self.hovered() else { + return self.active_mut().preview.reset(); + }; + + let mime = if hovered.is_dir() { + MIME_DIR + } else if let Some(s) = self.mimetype.get(&hovered.url) { + s + } else { + return self.active_mut().preview.reset(); + }; + + let Some(previewer) = PLUGIN.previewer(&hovered.url, mime) else { + return self.active_mut().preview.reset(); + }; + + let opt = opt.into() as Opt; + isolate::seek_sync(&previewer.exec, hovered.clone(), opt.units); + false + } +} diff --git a/yazi-core/src/manager/commands/suspend.rs b/yazi-core/src/manager/commands/suspend.rs index f3db2bf17..b1865de14 100644 --- a/yazi-core/src/manager/commands/suspend.rs +++ b/yazi-core/src/manager/commands/suspend.rs @@ -1,17 +1,13 @@ +use yazi_scheduler::Scheduler; use yazi_shared::event::Exec; -use crate::{manager::Manager, Ctx}; - -pub struct Opt; -impl From<&Exec> for Opt { - fn from(_: &Exec) -> Self { Self } -} +use crate::manager::Manager; impl Manager { - pub fn suspend(&mut self, _: impl Into) -> bool { + pub fn suspend(&mut self, _: &Exec) -> bool { #[cfg(unix)] tokio::spawn(async move { - Ctx::stop().await; + Scheduler::app_stop().await; unsafe { libc::raise(libc::SIGTSTP) }; }); false diff --git a/yazi-core/src/manager/commands/update_files.rs b/yazi-core/src/manager/commands/update_files.rs new file mode 100644 index 000000000..232891e57 --- /dev/null +++ b/yazi-core/src/manager/commands/update_files.rs @@ -0,0 +1,82 @@ +use yazi_shared::{event::Exec, fs::FilesOp}; + +use crate::{folder::Folder, manager::Manager, tasks::Tasks}; + +pub struct Opt { + op: FilesOp, +} + +impl TryFrom<&Exec> for Opt { + type Error = (); + + fn try_from(e: &Exec) -> Result { Ok(Self { op: e.take_data().ok_or(())? }) } +} + +impl Manager { + fn handle_read(&mut self, op: FilesOp) -> bool { + let url = op.url().clone(); + let cwd = self.cwd().to_owned(); + let hovered = self.hovered().map(|h| h.url()); + + let mut b = if cwd == url { + self.current_mut().update(op) + } else if matches!(self.parent(), Some(p) if p.cwd == url) { + self.active_mut().parent.as_mut().unwrap().update(op) + } else if matches!(self.hovered(), Some(h) if h.url == url) { + self.active_mut().history.entry(url.clone()).or_insert_with(|| Folder::from(&url)); + self.active_mut().apply_files_attrs(true); + self.active_mut().history.get_mut(&url).unwrap().update(op) | self.peek(true) + } else { + self.active_mut().history.entry(url.clone()).or_insert_with(|| Folder::from(&url)).update(op); + false + }; + + b |= self.active_mut().parent.as_mut().is_some_and(|p| p.hover(&cwd)); + b |= hovered.as_ref().is_some_and(|h| self.current_mut().hover(h)); + + if hovered.as_ref() != self.hovered().map(|h| &h.url) { + b |= self.hover(None); + } + b + } + + fn handle_ioerr(&mut self, op: FilesOp) -> bool { + let url = op.url(); + let op = FilesOp::Full(url.clone(), Vec::new()); + + if url == self.cwd() { + self.current_mut().update(op); + self.active_mut().leave(()); + true + } else if matches!(self.parent(), Some(p) if &p.cwd == url) { + self.active_mut().parent.as_mut().unwrap().update(op) + } else { + false + } + } + + pub fn update_files(&mut self, opt: impl TryInto, tasks: &Tasks) -> bool { + let Ok(opt) = opt.try_into() else { + return false; + }; + let calc = !matches!(opt.op, FilesOp::Size(..) | FilesOp::IOErr(_) | FilesOp::Deleting(..)); + + let mut ops = vec![opt.op]; + for u in self.watcher.linked.read().from_dir(ops[0].url()) { + ops.push(ops[0].chroot(u)); + } + + let mut b = false; + for op in ops { + b |= match op { + FilesOp::IOErr(..) => self.handle_ioerr(op), + _ => self.handle_read(op), + }; + } + + if calc { + tasks.preload_sorted(&self.current().files); + } + b + } +} diff --git a/yazi-core/src/manager/commands/update_mimetype.rs b/yazi-core/src/manager/commands/update_mimetype.rs new file mode 100644 index 000000000..ffe15949c --- /dev/null +++ b/yazi-core/src/manager/commands/update_mimetype.rs @@ -0,0 +1,58 @@ +use std::collections::HashMap; + +use yazi_plugin::ValueSendable; +use yazi_shared::{event::Exec, fs::Url}; + +use crate::{manager::Manager, tasks::Tasks}; + +pub struct Opt { + data: ValueSendable, +} + +impl TryFrom<&Exec> for Opt { + type Error = (); + + fn try_from(e: &Exec) -> Result { Ok(Self { data: e.take_data().ok_or(())? }) } +} + +impl Manager { + pub fn update_mimetype(&mut self, opt: impl TryInto, tasks: &Tasks) -> bool { + let Ok(opt) = opt.try_into() else { + return false; + }; + + let linked = self.watcher.linked.read(); + let updates = opt + .data + .into_table_string() + .into_iter() + .map(|(url, mime)| (Url::from(url), mime)) + .filter(|(url, mime)| self.mimetype.get(url) != Some(mime)) + .fold(HashMap::new(), |mut map, (u, m)| { + for u in linked.from_file(&u) { + map.insert(u, m.clone()); + } + map.insert(u, m); + map + }); + + drop(linked); + if updates.is_empty() { + return false; + } + + let affected: Vec<_> = self + .current() + .paginate(self.current().page) + .iter() + .filter(|&f| updates.contains_key(&f.url)) + .cloned() + .collect(); + + self.mimetype.extend(updates); + self.peek(false); + + tasks.preload_affected(&affected, &self.mimetype); + true + } +} diff --git a/yazi-core/src/manager/linked.rs b/yazi-core/src/manager/linked.rs new file mode 100644 index 000000000..9d688febf --- /dev/null +++ b/yazi-core/src/manager/linked.rs @@ -0,0 +1,44 @@ +use std::{collections::BTreeMap, ops::{Deref, DerefMut}}; + +use yazi_shared::fs::Url; + +#[derive(Default)] +pub struct Linked(BTreeMap /* from ==> to */); + +impl Deref for Linked { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DerefMut for Linked { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +impl Linked { + pub fn from_dir(&self, url: &Url) -> Vec<&Url> { + if let Some(to) = self.get(url) { + self.iter().filter(|(k, v)| *v == to && *k != url).map(|(k, _)| k).collect() + } else { + self.iter().filter(|(_, v)| *v == url).map(|(k, _)| k).collect() + } + } + + pub fn from_file(&self, url: &Url) -> Vec { + if self.is_empty() { + return Default::default(); + } + + let Some(p) = url.parent_url() else { + return Default::default(); + }; + + let relatives = self.from_dir(&p); + if relatives.is_empty() { + return Default::default(); + } + + let name = url.file_name().unwrap(); + relatives.into_iter().map(|u| u.join(name)).collect() + } +} diff --git a/yazi-core/src/manager/manager.rs b/yazi-core/src/manager/manager.rs index 4cc0c7a54..813593fdb 100644 --- a/yazi-core/src/manager/manager.rs +++ b/yazi-core/src/manager/manager.rs @@ -1,9 +1,9 @@ -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{HashMap, HashSet}; -use yazi_shared::{fs::{File, FilesOp}, fs::Url}; +use yazi_shared::fs::{File, Url}; use super::{Tabs, Watcher}; -use crate::{tab::{Folder, Tab}, tasks::Tasks}; +use crate::{folder::Folder, tab::Tab}; pub struct Manager { pub tabs: Tabs, @@ -23,62 +23,6 @@ impl Manager { mimetype: Default::default(), } } - - pub fn update_read(&mut self, op: FilesOp) -> bool { - let url = op.url().clone(); - let cwd = self.cwd().to_owned(); - let hovered = self.hovered().map(|h| h.url()); - - let mut b = if cwd == url { - self.current_mut().update(op) - } else if matches!(self.parent(), Some(p) if p.cwd == url) { - self.active_mut().parent.as_mut().unwrap().update(op) - } else if matches!(self.hovered(), Some(h) if h.url == url) { - self.active_mut().history.entry(url.clone()).or_insert_with(|| Folder::from(&url)); - self.active_mut().apply_files_attrs(true); - self.active_mut().history.get_mut(&url).unwrap().update(op) - } else { - self.active_mut().history.entry(url.clone()).or_insert_with(|| Folder::from(&url)).update(op); - false - }; - - b |= self.active_mut().parent.as_mut().is_some_and(|p| p.hover(&cwd)); - b |= hovered.as_ref().is_some_and(|h| self.current_mut().hover(h)); - - if hovered.as_ref() != self.hovered().map(|h| &h.url) { - Self::_hover(None); - } - b - } - - pub fn update_ioerr(&mut self, op: FilesOp) -> bool { - let url = op.url(); - let op = FilesOp::Full(url.clone(), Vec::new()); - - if url == self.cwd() { - self.current_mut().update(op); - self.active_mut().leave(()); - true - } else if matches!(self.parent(), Some(p) if &p.cwd == url) { - self.active_mut().parent.as_mut().unwrap().update(op) - } else { - false - } - } - - pub fn update_mimetype(&mut self, mut mimes: BTreeMap, tasks: &Tasks) -> bool { - mimes.retain(|f, m| self.mimetype.get(f) != Some(m)); - if mimes.is_empty() { - return false; - } - - tasks.precache_image(&mimes); - tasks.precache_video(&mimes); - tasks.precache_pdf(&mimes); - - self.mimetype.extend(mimes); - true - } } impl Manager { diff --git a/yazi-core/src/manager/mod.rs b/yazi-core/src/manager/mod.rs index 8e9223e0f..8016b0e42 100644 --- a/yazi-core/src/manager/mod.rs +++ b/yazi-core/src/manager/mod.rs @@ -1,8 +1,10 @@ mod commands; +mod linked; mod manager; mod tabs; mod watcher; +pub use linked::*; pub use manager::*; pub use tabs::*; pub use watcher::*; diff --git a/yazi-core/src/manager/tabs.rs b/yazi-core/src/manager/tabs.rs index 0f8a882dd..410699ca4 100644 --- a/yazi-core/src/manager/tabs.rs +++ b/yazi-core/src/manager/tabs.rs @@ -10,12 +10,12 @@ pub struct Tabs { impl Tabs { pub fn make() -> Self { - let mut tabs = Self { idx: usize::MAX, items: vec![Tab::from(Url::from(&BOOT.cwd))] }; + let mut tabs = Self { idx: 0, items: vec![Tab::from(Url::from(&BOOT.cwd))] }; if let Some(file) = &BOOT.file { tabs.items[0].reveal(Url::from(BOOT.cwd.join(file))); } - tabs.set_idx(0); + Manager::_refresh(); tabs } @@ -30,9 +30,18 @@ impl Tabs { #[inline] pub(super) fn set_idx(&mut self, idx: usize) { + if self.idx == idx { + return; + } + + // Reset the preview of the previous active tab + if let Some(active) = self.items.get_mut(self.idx) { + active.preview.reset_image(); + } + self.idx = idx; - self.active_mut().preview.reset_image(); Manager::_refresh(); + Manager::_peek(true); } } diff --git a/yazi-core/src/manager/watcher.rs b/yazi-core/src/manager/watcher.rs index ac62a0c92..b24ad702e 100644 --- a/yazi-core/src/manager/watcher.rs +++ b/yazi-core/src/manager/watcher.rs @@ -1,18 +1,20 @@ -use std::{collections::BTreeSet, sync::Arc, time::Duration}; +use std::{collections::{BTreeMap, BTreeSet}, sync::Arc, time::Duration}; -use indexmap::IndexMap; use notify::{event::{MetadataKind, ModifyKind}, EventKind, RecommendedWatcher, RecursiveMode, Watcher as _Watcher}; use parking_lot::RwLock; use tokio::{fs, pin, sync::mpsc::{self, UnboundedReceiver}}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; -use yazi_scheduler::external; -use yazi_shared::{emit, fs::{File, FilesOp, Url}}; +use tracing::error; +use yazi_plugin::isolate; +use yazi_shared::fs::{File, FilesOp, Url}; -use crate::files::Files; +use super::Linked; +use crate::folder::Files; pub struct Watcher { - watcher: RecommendedWatcher, - watched: Arc>>>, + watcher: RecommendedWatcher, + watched: Arc>>, + pub linked: Arc>, } impl Watcher { @@ -26,55 +28,45 @@ impl Watcher { return; }; - let Some(path) = event.paths.first().map(Url::from) else { - return; - }; - - let parent = path.parent_url().unwrap_or_else(|| path.clone()); match event.kind { - EventKind::Create(_) => { - tx.send(parent).ok(); - } - EventKind::Modify(kind) => { - match kind { - ModifyKind::Data(_) => {} - ModifyKind::Metadata(kind) => match kind { - MetadataKind::Permissions => {} - MetadataKind::Ownership => {} - MetadataKind::Extended => {} - _ => return, - }, - ModifyKind::Name(_) => {} + EventKind::Create(_) => {} + EventKind::Modify(kind) => match kind { + ModifyKind::Data(_) => {} + ModifyKind::Metadata(md) => match md { + MetadataKind::WriteTime => {} + MetadataKind::Permissions => {} + MetadataKind::Ownership => {} _ => return, - }; + }, + ModifyKind::Name(_) => {} + _ => return, + }, + EventKind::Remove(_) => {} + _ => return, + } - tx.send(path).ok(); - tx.send(parent).ok(); - } - EventKind::Remove(_) => { - tx.send(path).ok(); - tx.send(parent).ok(); - } - _ => (), + for path in event.paths { + tx.send(Url::from(path)).ok(); } } }, Default::default(), ); - let instance = Self { watcher: watcher.unwrap(), watched: Default::default() }; - tokio::spawn(Self::on_changed(rx, instance.watched.clone())); + let instance = + Self { watcher: watcher.unwrap(), watched: Default::default(), linked: Default::default() }; + tokio::spawn(Self::on_changed(rx)); instance } - pub(super) fn watch(&mut self, mut watched: BTreeSet<&Url>) { - watched.retain(|&u| u.is_regular()); + pub(super) fn watch(&mut self, mut new: BTreeSet<&Url>) { + new.retain(|&u| u.is_regular()); let (to_unwatch, to_watch): (BTreeSet<_>, BTreeSet<_>) = { let guard = self.watched.read(); - let keys = guard.keys().collect::>(); + let old: BTreeSet<_> = guard.iter().collect(); ( - keys.difference(&watched).map(|&x| x.clone()).collect(), - watched.difference(&keys).map(|&x| x.clone()).collect(), + old.difference(&new).map(|&x| x.clone()).collect(), + new.difference(&old).map(|&x| x.clone()).collect(), ) }; @@ -83,140 +75,90 @@ impl Watcher { } for u in to_watch { if self.watcher.watch(&u, RecursiveMode::NonRecursive).is_err() { - watched.remove(&u); + new.remove(&u); } } - let mut to_resolve = Vec::new(); - let mut guard = self.watched.write(); - *guard = watched - .into_iter() - .map(|k| { - if let Some(old) = guard.remove_entry(k) { - old - } else { - to_resolve.push(k.clone()); - (k.clone(), None) - } - }) - .collect(); + *self.watched.write() = new.into_iter().cloned().collect(); + self.sync_linked(); + } + + pub(super) fn trigger_dirs(&self, dirs: &[&Url]) { + let urls: Vec<_> = dirs.iter().filter(|&u| u.is_regular()).map(|&u| u.clone()).collect(); + if urls.is_empty() { + return; + } - let lock = self.watched.clone(); tokio::spawn(async move { - for k in to_resolve { - match fs::canonicalize(&k).await { - Ok(v) if v != *k => { - lock.write().insert(k, Some(Url::from(v))); - } - _ => {} - } + for u in urls { + let Ok(rx) = Files::from_dir(&u).await else { + FilesOp::IOErr(u).emit(); + return; + }; + + let files: Vec<_> = UnboundedReceiverStream::new(rx).collect().await; + FilesOp::Full(u, files).emit(); } }); } - pub(super) fn trigger_dirs(&self, dirs: &[&Url]) { - let dirs: Vec<_> = dirs.iter().filter(|&u| u.is_regular()).map(|&u| u.clone()).collect(); - if dirs.is_empty() { - return; - } + fn sync_linked(&self) { + let mut new = self.watched.read().clone(); + self.linked.write().retain(|k, _| new.remove(k)); let watched = self.watched.clone(); + let linked = self.linked.clone(); + macro_rules! go { + ($todo:expr) => { + for from in $todo { + match fs::canonicalize(&from).await { + Ok(to) if to != *from && watched.read().contains(&from) => { + linked.write().insert(from, Url::from(to)); + } + _ => {} + } + } + }; + } + tokio::spawn(async move { - let watched = watched.read().clone(); - for dir in dirs { - Self::dir_changed(&dir, &watched).await; - } + let old: Vec<_> = linked.read().keys().cloned().collect(); + go!(new); + go!(old); }); } - async fn on_changed( - rx: UnboundedReceiver, - watched: Arc>>>, - ) { + async fn on_changed(rx: UnboundedReceiver) { // TODO: revert this once a new notification is implemented let rx = UnboundedReceiverStream::new(rx).chunks_timeout(10, Duration::from_millis(20)); pin!(rx); while let Some(urls) = rx.next().await { - let (mut files, mut dirs): (Vec<_>, Vec<_>) = Default::default(); - for url in urls.into_iter().collect::>() { - if fs::metadata(&url).await.map(|m| !m.is_dir()).unwrap_or(false) { - files.push(url); - } else { - dirs.push(url); - } - } + let urls: BTreeSet<_> = urls.into_iter().collect(); + let mut reload = Vec::with_capacity(urls.len()); - let watched = watched.read().clone(); + for u in urls { + let Some(parent) = u.parent_url() else { + continue; + }; - Self::files_changed(&files, &watched).await; - for file in files { - for u in Self::linked_urls(&file, &watched) { - emit!(Files(FilesOp::IOErr(u.clone()))); + let Ok(file) = File::from(u.clone()).await else { + FilesOp::Deleting(parent, vec![u]).emit(); + continue; + }; + + if !file.is_dir() { + reload.push(file.clone()); } - emit!(Files(FilesOp::IOErr(file))); + FilesOp::Upserting(parent, BTreeMap::from_iter([(u, file)])).emit(); } - for dir in dirs { - Self::dir_changed(&dir, &watched).await; + if reload.is_empty() { + continue; } - } - } - - async fn files_changed(urls: &[Url], watched: &IndexMap>) { - let Ok(mut mimes) = external::file(urls).await else { - return; - }; - - let linked: Vec<_> = watched.iter().filter_map(|(k, v)| v.as_ref().map(|v| (k, v))).fold( - Vec::new(), - |mut aac, (k, v)| { - mimes - .iter() - .filter(|(u, _)| u.parent().map(|p| p == **v) == Some(true)) - .for_each(|(u, m)| aac.push((k.join(u.file_name().unwrap()), m.clone()))); - aac - }, - ); - - mimes.extend(linked); - emit!(Mimetype(mimes)); - } - - async fn dir_changed(url: &Url, watched: &IndexMap>) { - let linked = Self::linked_urls(url, watched); - let Ok(rx) = Files::from_dir(url).await else { - emit!(Files(FilesOp::IOErr(url.clone()))); - for u in linked { - emit!(Files(FilesOp::IOErr(u.clone()))); + if let Err(e) = isolate::preload("mime.lua".to_string(), reload, true).await { + error!("preload in watcher failed: {e}"); } - return; - }; - - let linked_files = |files: &[File], linked: &Url| -> Vec { - let mut new = Vec::with_capacity(files.len()); - for file in files { - let mut file = file.clone(); - file.url = linked.join(file.url.strip_prefix(url).unwrap()); - new.push(file); - } - new - }; - - let files: Vec<_> = UnboundedReceiverStream::new(rx).collect().await; - for u in linked { - let files = linked_files(&files, u); - emit!(Files(FilesOp::Full(u.clone(), files))); } - emit!(Files(FilesOp::Full(url.clone(), files))); - } - - fn linked_urls<'a>(url: &'a Url, watched: &'a IndexMap>) -> Vec<&'a Url> { - watched - .iter() - .filter_map(|(k, v)| v.as_ref().map(|v| (k, v))) - .filter(|(_, v)| *v == url) - .map(|(k, _)| k) - .collect() } } diff --git a/yazi-core/src/preview/mod.rs b/yazi-core/src/preview/mod.rs deleted file mode 100644 index 2d5ba586b..000000000 --- a/yazi-core/src/preview/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod preview; -mod provider; - -pub use preview::*; -use provider::*; - -pub static COLLISION: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); diff --git a/yazi-core/src/preview/preview.rs b/yazi-core/src/preview/preview.rs deleted file mode 100644 index 81ec6e01f..000000000 --- a/yazi-core/src/preview/preview.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::{mem, time::Duration}; - -use tokio::{pin, task::JoinHandle}; -use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; -use yazi_adaptor::ADAPTOR; -use yazi_config::MANAGER; -use yazi_shared::{emit, event::{PreviewData, PreviewLock}, fs::{Cha, FilesOp, Url}, MimeKind, PeekError}; - -use super::Provider; -use crate::{files::Files, manager::Manager, Highlighter}; - -#[derive(Default)] -pub struct Preview { - pub lock: Option, - skip: usize, - - handle: Option>, -} - -impl Preview { - pub fn go(&mut self, url: &Url, cha: Cha, mime: &str) { - if self.content_unchanged(url, &cha) { - return; - } - - self.abort(); - let (url, kind, skip) = (url.clone(), MimeKind::new(mime), self.skip); - - self.handle = Some(tokio::spawn(async move { - match Provider::auto(kind, &url, skip).await { - Ok(data) => { - emit!(Preview(PreviewLock { url, cha: Some(cha), skip, data })); - } - Err(PeekError::Exceed(max)) => { - Manager::_peek_upper_bound(max, &url); - } - _ => {} - } - })); - } - - pub fn go_folder(&mut self, url: Url, in_chunks: bool) { - self.abort(); - self.lock = Some(PreviewLock { - url: url.clone(), - cha: None, - skip: self.skip, - data: PreviewData::Folder, - }); - - self.handle = Some(tokio::spawn(async move { - let Ok(rx) = Files::from_dir(&url).await else { - emit!(Files(FilesOp::IOErr(url.clone()))); - return; - }; - - if !in_chunks { - emit!(Files(FilesOp::Full(url.clone(), UnboundedReceiverStream::new(rx).collect().await))); - return; - } - - let stream = - UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(500)); - pin!(stream); - - let ticket = FilesOp::prepare(&url); - while let Some(chunk) = stream.next().await { - emit!(Files(FilesOp::Part(url.clone(), ticket, chunk))); - } - })); - } - - pub fn arrow(&mut self, step: isize, mime: &str, upper: Option) { - let size = Provider::step_size(MimeKind::new(mime), step.unsigned_abs()); - self.skip = if step < 0 { self.skip.saturating_sub(size) } else { self.skip + size }; - - if let Some(upper) = upper { - self.skip = self.skip.min(upper); - } - } - - #[inline] - pub fn abort(&mut self) { - self.handle.take().map(|h| h.abort()); - Highlighter::abort(); - ADAPTOR.image_hide(MANAGER.layout.image_rect()).ok(); - } - - #[inline] - pub fn reset(&mut self) -> bool { - self.abort(); - self.lock.take().map(|l| l.is_image()) == Some(false) - } - - pub fn reset_image(&mut self) -> bool { - if matches!(self.lock, Some(ref l) if l.is_image()) { - self.reset(); - true - } else { - false - } - } - - pub fn same_url(&self, url: &Url) -> bool { - matches!(self.lock, Some(ref lock) if lock.url == *url) - } - - pub fn sync_skip(&mut self) -> bool { - if let Some(lock) = &mut self.lock { - mem::replace(&mut lock.skip, self.skip) != self.skip - } else { - false - } - } - - fn content_unchanged(&self, url: &Url, cha: &Cha) -> bool { - let Some(lock) = &self.lock else { - return false; - }; - let Some(cha_) = &lock.cha else { - return false; - }; - - *url == lock.url - && self.skip == lock.skip - && cha.len == cha_.len - && cha.modified == cha_.modified - && cha.kind == cha_.kind - && { - #[cfg(unix)] - { - cha.permissions == cha_.permissions - } - #[cfg(windows)] - { - true - } - } - } -} diff --git a/yazi-core/src/preview/provider.rs b/yazi-core/src/preview/provider.rs deleted file mode 100644 index 09e0df683..000000000 --- a/yazi-core/src/preview/provider.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::path::Path; - -use tokio::fs; -use yazi_adaptor::ADAPTOR; -use yazi_config::{MANAGER, PREVIEW}; -use yazi_scheduler::external; -use yazi_shared::{event::PreviewData, MimeKind, PeekError}; - -use crate::Highlighter; - -pub(super) struct Provider; - -impl Provider { - pub(super) async fn auto( - kind: MimeKind, - path: &Path, - skip: usize, - ) -> Result { - match kind { - MimeKind::Empty => Err("Empty file".into()), - MimeKind::Archive => Provider::archive(path, skip).await.map(PreviewData::Text), - MimeKind::Image => Provider::image(path).await, - MimeKind::Video => Provider::video(path, skip).await, - MimeKind::JSON => Provider::json(path, skip).await.map(PreviewData::Text), - MimeKind::PDF => Provider::pdf(path, skip).await, - MimeKind::Text => Provider::highlight(path, skip).await.map(PreviewData::Text), - MimeKind::Others => Err("Unsupported mimetype".into()), - } - } - - pub(super) fn step_size(kind: MimeKind, step: usize) -> usize { - match kind { - MimeKind::Empty => 0, - MimeKind::Archive => step * MANAGER.layout.preview_height() / 10, - MimeKind::Image => 0, - MimeKind::Video => step, - MimeKind::JSON => step * MANAGER.layout.preview_height() / 10, - MimeKind::PDF => 1, - MimeKind::Text => step * MANAGER.layout.preview_height() / 10, - MimeKind::Others => step * MANAGER.layout.preview_height() / 10, - } - } - - pub(super) async fn image(path: &Path) -> Result { - ADAPTOR.image_show(path, MANAGER.layout.image_rect()).await?; - Ok(PreviewData::Image) - } - - pub(super) async fn video(path: &Path, skip: usize) -> Result { - let cache = PREVIEW.cache(path, skip); - if fs::symlink_metadata(&cache).await.is_err() { - external::ffmpegthumbnailer(path, &cache, skip).await?; - } - - Self::image(&cache).await - } - - pub(super) async fn pdf(path: &Path, skip: usize) -> Result { - let cache = PREVIEW.cache(path, skip); - if fs::symlink_metadata(&cache).await.is_err() { - external::pdftoppm(path, &cache, skip).await?; - } - - Self::image(&cache).await - } - - pub(super) async fn json(path: &Path, skip: usize) -> Result { - let result = external::jq(path, skip, MANAGER.layout.preview_height()).await; - if let Err(PeekError::Unexpected(_)) = result { - return Self::highlight(path, skip).await; - } - result - } - - pub(super) async fn archive(path: &Path, skip: usize) -> Result { - Ok( - external::lsar(path, skip, MANAGER.layout.preview_height()) - .await? - .into_iter() - .map(|f| f.name) - .collect::>() - .join("\n"), - ) - } - - pub(super) async fn highlight(path: &Path, skip: usize) -> Result { - let limit = MANAGER.layout.preview_height(); - let result = Highlighter::new(path.to_owned()).highlight(skip, limit).await?; - Ok(result.replace('\t', &" ".repeat(PREVIEW.tab_size as usize))) - } -} diff --git a/yazi-core/src/step.rs b/yazi-core/src/step.rs index 153d637e3..1379d415c 100644 --- a/yazi-core/src/step.rs +++ b/yazi-core/src/step.rs @@ -36,11 +36,11 @@ impl Step { impl Step { #[inline] - pub fn add usize>(self, pos: usize, f: F) -> usize { + pub fn add(self, pos: usize, limit: usize) -> usize { let fixed = match self { Self::Fixed(n) => n, Self::Percent(0) => 0, - Self::Percent(n) => n as isize * f() as isize / 100, + Self::Percent(n) => n as isize * limit as isize / 100, }; if fixed > 0 { pos + fixed as usize } else { pos.saturating_sub(fixed.unsigned_abs()) } } diff --git a/yazi-core/src/tab/commands/cd.rs b/yazi-core/src/tab/commands/cd.rs index c4e3f40b8..dc173f698 100644 --- a/yazi-core/src/tab/commands/cd.rs +++ b/yazi-core/src/tab/commands/cd.rs @@ -35,7 +35,7 @@ impl Tab { pub fn cd(&mut self, opt: impl Into) -> bool { let opt = opt.into() as Opt; if opt.interactive { - return self.cd_interactive(opt); + return self.cd_interactive(); } if self.current.cwd == opt.target { @@ -68,11 +68,9 @@ impl Tab { true } - fn cd_interactive(&mut self, opt: impl Into) -> bool { - let opt = opt.into() as Opt; - + fn cd_interactive(&mut self) -> bool { tokio::spawn(async move { - let rx = Input::_show(InputCfg::cd().with_value(opt.target.to_string_lossy())); + let rx = Input::_show(InputCfg::cd()); let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50)); pin!(rx); diff --git a/yazi-core/src/tab/commands/jump.rs b/yazi-core/src/tab/commands/jump.rs index 0700daf01..8a2913d9f 100644 --- a/yazi-core/src/tab/commands/jump.rs +++ b/yazi-core/src/tab/commands/jump.rs @@ -1,7 +1,8 @@ -use yazi_scheduler::{external::{self, FzfOpt, ZoxideOpt}, BLOCKER}; +use yazi_plugin::external::{self, FzfOpt, ZoxideOpt}; +use yazi_scheduler::{Scheduler, BLOCKER}; use yazi_shared::{event::Exec, fs::ends_with_slash, Defer}; -use crate::{tab::Tab, Ctx}; +use crate::tab::Tab; pub struct Opt { type_: OptType, @@ -36,8 +37,8 @@ impl Tab { let cwd = self.current.cwd.clone(); tokio::spawn(async move { let _guard = BLOCKER.acquire().await.unwrap(); - let _defer = Defer::new(Ctx::resume); - Ctx::stop().await; + let _defer = Defer::new(Scheduler::app_resume); + Scheduler::app_stop().await; let result = if opt.type_ == OptType::Fzf { external::fzf(FzfOpt { cwd }).await diff --git a/yazi-core/src/tab/commands/mod.rs b/yazi-core/src/tab/commands/mod.rs index fa8d1c39a..868347559 100644 --- a/yazi-core/src/tab/commands/mod.rs +++ b/yazi-core/src/tab/commands/mod.rs @@ -9,6 +9,7 @@ mod hidden; mod jump; mod leave; mod linemode; +mod preview; mod reveal; mod search; mod select; diff --git a/yazi-core/src/tab/commands/preview.rs b/yazi-core/src/tab/commands/preview.rs new file mode 100644 index 000000000..a23ae3c2f --- /dev/null +++ b/yazi-core/src/tab/commands/preview.rs @@ -0,0 +1,36 @@ +use anyhow::anyhow; +use yazi_plugin::utils::PreviewLock; +use yazi_shared::event::Exec; + +use crate::tab::Tab; + +pub struct Opt { + lock: PreviewLock, +} + +impl TryFrom<&Exec> for Opt { + type Error = anyhow::Error; + + fn try_from(e: &Exec) -> Result { + Ok(Self { lock: e.take_data().ok_or_else(|| anyhow!("invalid data"))? }) + } +} + +impl Tab { + pub fn preview(&mut self, opt: impl TryInto) -> bool { + let Some(hovered) = self.current.hovered().map(|h| &h.url) else { + return self.preview.reset(); + }; + + let Ok(opt) = opt.try_into() else { + return false; + }; + + if hovered != &opt.lock.url { + return false; + } + + self.preview.lock = Some(opt.lock); + true + } +} diff --git a/yazi-core/src/tab/commands/reveal.rs b/yazi-core/src/tab/commands/reveal.rs index 129c67a60..f75832a2b 100644 --- a/yazi-core/src/tab/commands/reveal.rs +++ b/yazi-core/src/tab/commands/reveal.rs @@ -1,4 +1,4 @@ -use yazi_shared::{emit, event::Exec, fs::{File, FilesOp}, fs::{expand_path, Url}, Layer}; +use yazi_shared::{emit, event::Exec, fs::{expand_path, File, FilesOp, Url}, Layer}; use crate::{manager::Manager, tab::Tab}; @@ -34,10 +34,7 @@ impl Tab { }; let b = self.cd(parent.clone()); - emit!(Files(FilesOp::Creating( - parent.clone(), - File::from_dummy(opt.target.clone()).into_map() - ))); + FilesOp::Creating(parent, vec![File::from_dummy(opt.target.clone())]).emit(); Manager::_hover(Some(opt.target)); b } diff --git a/yazi-core/src/tab/commands/search.rs b/yazi-core/src/tab/commands/search.rs index 69e2176f3..59635de5d 100644 --- a/yazi-core/src/tab/commands/search.rs +++ b/yazi-core/src/tab/commands/search.rs @@ -4,8 +4,8 @@ use anyhow::bail; use tokio::pin; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use yazi_config::popup::InputCfg; -use yazi_scheduler::external; -use yazi_shared::{emit, event::Exec, fs::FilesOp}; +use yazi_plugin::external; +use yazi_shared::{event::Exec, fs::FilesOp}; use crate::{input::Input, manager::Manager, tab::Tab}; @@ -66,7 +66,7 @@ impl Tab { Tab::_cd(&cwd); first = false; } - emit!(Files(FilesOp::Part(cwd.clone(), ticket, chunk))); + FilesOp::Part(cwd.clone(), chunk, ticket).emit(); } Ok(()) })); @@ -78,8 +78,6 @@ impl Tab { handle.abort(); } if self.current.cwd.is_search() { - self.preview.reset_image(); - let rep = self.history_new(&self.current.cwd.to_regular()); drop(mem::replace(&mut self.current, rep)); Manager::_refresh(); diff --git a/yazi-core/src/tab/commands/shell.rs b/yazi-core/src/tab/commands/shell.rs index f98c4a891..e6454a113 100644 --- a/yazi-core/src/tab/commands/shell.rs +++ b/yazi-core/src/tab/commands/shell.rs @@ -21,13 +21,9 @@ impl<'a> From<&'a Exec> for Opt { impl Tab { pub fn shell(&self, opt: impl Into) -> bool { - let selected: Vec<_> = self - .selected() - .into_iter() - .map(|f| (f.url.as_os_str().to_owned(), Default::default())) - .collect(); - let mut opt = opt.into() as Opt; + let selected: Vec<_> = self.selected().into_iter().map(|f| f.url()).collect(); + tokio::spawn(async move { if !opt.confirm || opt.cmd.is_empty() { let mut result = Input::_show(InputCfg::shell(opt.block).with_value(opt.cmd)); @@ -37,19 +33,15 @@ impl Tab { } } - Tasks::_open( - selected, - Some(Opener { - exec: opt.cmd, - block: opt.block, - orphan: false, - desc: Default::default(), - for_: None, - spread: true, - }), - ); + Tasks::_open(selected, Opener { + exec: opt.cmd, + block: opt.block, + orphan: false, + desc: Default::default(), + for_: None, + spread: true, + }); }); - false } } diff --git a/yazi-core/src/tab/config.rs b/yazi-core/src/tab/config.rs index 5f6f11d23..9ac497e2f 100644 --- a/yazi-core/src/tab/config.rs +++ b/yazi-core/src/tab/config.rs @@ -1,6 +1,6 @@ use yazi_config::{manager::SortBy, MANAGER}; -use crate::files::FilesSorter; +use crate::folder::FilesSorter; #[derive(Clone, PartialEq)] pub struct Config { diff --git a/yazi-core/src/tab/finder.rs b/yazi-core/src/tab/finder.rs index 8ed6101c9..c537a0c82 100644 --- a/yazi-core/src/tab/finder.rs +++ b/yazi-core/src/tab/finder.rs @@ -4,7 +4,7 @@ use anyhow::Result; use regex::bytes::{Regex, RegexBuilder}; use yazi_shared::fs::Url; -use crate::files::Files; +use crate::folder::Files; #[derive(PartialEq, Eq)] pub enum FinderCase { @@ -14,9 +14,9 @@ pub enum FinderCase { } pub struct Finder { - query: Regex, - matched: BTreeMap, - version: u64, + query: Regex, + matched: BTreeMap, + revision: u64, } impl Finder { @@ -29,7 +29,7 @@ impl Finder { FinderCase::Sensitive => Regex::new(s)?, FinderCase::Insensitive => RegexBuilder::new(s).case_insensitive(true).build()?, }; - Ok(Self { query, matched: Default::default(), version: 0 }) + Ok(Self { query, matched: Default::default(), revision: 0 }) } pub(super) fn prev(&self, files: &Files, cursor: usize, include: bool) -> Option { @@ -53,7 +53,7 @@ impl Finder { } pub(super) fn catchup(&mut self, files: &Files) -> bool { - if self.version == files.version { + if self.revision == files.revision { return false; } self.matched.clear(); @@ -72,7 +72,7 @@ impl Finder { i += 1; } - self.version = files.version; + self.revision = files.revision; true } diff --git a/yazi-core/src/tab/mod.rs b/yazi-core/src/tab/mod.rs index ebcd998e6..b002a2faf 100644 --- a/yazi-core/src/tab/mod.rs +++ b/yazi-core/src/tab/mod.rs @@ -2,13 +2,13 @@ mod backstack; mod commands; mod config; mod finder; -mod folder; mod mode; +mod preview; mod tab; pub use backstack::*; pub use config::*; pub use finder::*; -pub use folder::*; pub use mode::*; +pub use preview::*; pub use tab::*; diff --git a/yazi-core/src/tab/preview.rs b/yazi-core/src/tab/preview.rs new file mode 100644 index 000000000..544565923 --- /dev/null +++ b/yazi-core/src/tab/preview.rs @@ -0,0 +1,111 @@ +use std::time::Duration; + +use tokio::{pin, task::JoinHandle}; +use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; +use tokio_util::sync::CancellationToken; +use yazi_adaptor::ADAPTOR; +use yazi_config::PLUGIN; +use yazi_plugin::{external::Highlighter, utils::PreviewLock}; +use yazi_shared::{fs::{Cha, File, FilesOp, Url}, MIME_DIR}; + +use crate::folder::Files; + +#[derive(Default)] +pub struct Preview { + pub lock: Option, + pub skip: usize, + + previewer_ct: Option, + folder_handle: Option>, +} + +impl Preview { + pub fn go(&mut self, file: File, mime: &str, force: bool) { + if !force && self.content_unchanged(&file.url, &file.cha) { + return; + } + + let Some(previewer) = PLUGIN.previewer(&file.url, mime) else { + self.reset(); + return; + }; + + self.abort(); + if previewer.sync { + yazi_plugin::isolate::peek_sync(&previewer.exec, file, self.skip); + } else { + self.previewer_ct = Some(yazi_plugin::isolate::peek(&previewer.exec, file, self.skip)); + } + } + + pub fn go_folder(&mut self, file: File, force: bool) { + if !force && self.content_unchanged(&file.url, &file.cha) { + return; + } + + self.go(file.clone(), MIME_DIR, force); + + self.folder_handle.take().map(|h| h.abort()); + self.folder_handle = Some(tokio::spawn(async move { + let Ok(rx) = Files::from_dir(&file.url).await else { + FilesOp::IOErr(file.url).emit(); + return; + }; + + let stream = + UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(350)); + pin!(stream); + + let ticket = FilesOp::prepare(&file.url); + while let Some(chunk) = stream.next().await { + FilesOp::Part(file.url.clone(), chunk, ticket).emit(); + } + })); + } + + #[inline] + pub fn abort(&mut self) { + self.previewer_ct.take().map(|ct| ct.cancel()); + Highlighter::abort(); + } + + #[inline] + pub fn reset(&mut self) -> bool { + self.abort(); + ADAPTOR.image_hide().ok(); + self.lock.take().is_some() + } + + #[inline] + pub fn reset_image(&mut self) { + self.abort(); + ADAPTOR.image_hide().ok(); + } + + #[inline] + pub fn same_url(&self, url: &Url) -> bool { + matches!(self.lock, Some(ref lock) if lock.url == *url) + } + + fn content_unchanged(&self, url: &Url, cha: &Cha) -> bool { + let Some(lock) = &self.lock else { + return false; + }; + + *url == lock.url + && self.skip == lock.skip + && cha.len == lock.cha.len + && cha.modified == lock.cha.modified + && cha.kind == lock.cha.kind + && { + #[cfg(unix)] + { + cha.permissions == lock.cha.permissions + } + #[cfg(windows)] + { + true + } + } + } +} diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index 72adf9886..c493ca387 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -2,10 +2,10 @@ use std::{borrow::Cow, collections::BTreeMap}; use anyhow::Result; use tokio::task::JoinHandle; -use yazi_shared::{event::PreviewLock, fs::File, fs::Url}; +use yazi_shared::fs::{File, Url}; -use super::{Backstack, Config, Finder, Folder, Mode}; -use crate::preview::Preview; +use super::{Backstack, Config, Finder, Mode, Preview}; +use crate::folder::Folder; pub struct Tab { pub mode: Mode, @@ -46,21 +46,6 @@ impl From<&Url> for Tab { fn from(url: &Url) -> Self { Self::from(url.clone()) } } -impl Tab { - pub fn update_preview(&mut self, lock: PreviewLock) -> bool { - let Some(hovered) = self.current.hovered().map(|h| &h.url) else { - return self.preview.reset(); - }; - - if lock.url != *hovered { - return false; - } - - self.preview.lock = Some(lock); - true - } -} - impl Tab { // --- Mode #[inline] @@ -91,9 +76,9 @@ impl Tab { let apply = |f: &mut Folder| { let hovered = f.hovered().map(|h| h.url()); - let mut b = f.files.set_show_hidden(self.conf.show_hidden); - b |= f.files.set_sorter(self.conf.sorter()); - b | f.repos(hovered) + f.files.set_show_hidden(self.conf.show_hidden); + f.files.set_sorter(self.conf.sorter()); + f.files.catchup_revision() | f.repos(hovered) }; let mut b = false; diff --git a/yazi-core/src/tasks/commands/cancel.rs b/yazi-core/src/tasks/commands/cancel.rs index 013d10edd..6800b1752 100644 --- a/yazi-core/src/tasks/commands/cancel.rs +++ b/yazi-core/src/tasks/commands/cancel.rs @@ -2,14 +2,8 @@ use yazi_shared::event::Exec; use crate::tasks::Tasks; -pub struct Opt; - -impl From<&Exec> for Opt { - fn from(_: &Exec) -> Self { Self } -} - impl Tasks { - pub fn cancel(&mut self, _: impl Into) -> bool { + pub fn cancel(&mut self, _: &Exec) -> bool { let id = self.scheduler.running.read().get_id(self.cursor); if id.map(|id| self.scheduler.cancel(id)) != Some(true) { return false; diff --git a/yazi-core/src/tasks/commands/inspect.rs b/yazi-core/src/tasks/commands/inspect.rs index e93f54794..8c5d17828 100644 --- a/yazi-core/src/tasks/commands/inspect.rs +++ b/yazi-core/src/tasks/commands/inspect.rs @@ -2,19 +2,13 @@ use std::io::{stdout, Write}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use tokio::{io::{stdin, AsyncReadExt}, select, sync::mpsc, time}; -use yazi_scheduler::BLOCKER; +use yazi_scheduler::{Scheduler, BLOCKER}; use yazi_shared::{event::Exec, term::Term, Defer}; -use crate::{tasks::Tasks, Ctx}; - -pub struct Opt; - -impl From<&Exec> for Opt { - fn from(_: &Exec) -> Self { Self } -} +use crate::tasks::Tasks; impl Tasks { - pub fn inspect(&self, _: impl Into) -> bool { + pub fn inspect(&self, _: &Exec) -> bool { let Some(id) = self.scheduler.running.read().get_id(self.cursor) else { return false; }; @@ -32,10 +26,10 @@ impl Tasks { task.logs.clone() }; - Ctx::stop().await; + Scheduler::app_stop().await; let _defer = Defer::new(|| { disable_raw_mode().ok(); - Ctx::resume(); + Scheduler::app_resume(); }); Term::clear(&mut stdout()).ok(); diff --git a/yazi-core/src/tasks/commands/open.rs b/yazi-core/src/tasks/commands/open.rs index 513945f48..5b831e31a 100644 --- a/yazi-core/src/tasks/commands/open.rs +++ b/yazi-core/src/tasks/commands/open.rs @@ -1,14 +1,12 @@ -use std::ffi::OsString; - use anyhow::anyhow; -use yazi_config::{open::Opener, BOOT}; -use yazi_shared::{emit, event::Exec, Layer}; +use yazi_config::open::Opener; +use yazi_shared::{emit, event::Exec, fs::Url, Layer}; use crate::tasks::Tasks; pub struct Opt { - targets: Vec<(OsString, String)>, - opener: Option, + targets: Vec, + opener: Opener, } impl TryFrom<&Exec> for Opt { @@ -20,31 +18,13 @@ impl TryFrom<&Exec> for Opt { } impl Tasks { - pub fn _open(targets: Vec<(OsString, String)>, opener: Option) { + pub fn _open(targets: Vec, opener: Opener) { emit!(Call(Exec::call("open", vec![]).with_data(Opt { targets, opener }).vec(), Layer::Tasks)); } pub fn open(&mut self, opt: impl TryInto) -> bool { - let Ok(opt) = opt.try_into() else { - return false; - }; - - if let Some(p) = &BOOT.chooser_file { - let paths = opt.targets.into_iter().fold(OsString::new(), |mut s, (p, _)| { - s.push(p); - s.push("\n"); - s - }); - - std::fs::write(p, paths.as_encoded_bytes()).ok(); - emit!(Quit(false)); - return false; - } - - if let Some(opener) = opt.opener { - self.file_open_with(&opener, &opt.targets.into_iter().map(|(f, _)| f).collect::>()); - } else { - self.file_open(&opt.targets); + if let Ok(opt) = opt.try_into() { + self.file_open_with(&opt.opener, &opt.targets); } false } diff --git a/yazi-core/src/tasks/tasks.rs b/yazi-core/src/tasks/tasks.rs index d993254cb..40bcfb902 100644 --- a/yazi-core/src/tasks/tasks.rs +++ b/yazi-core/src/tasks/tasks.rs @@ -1,13 +1,13 @@ -use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, path::Path, sync::Arc, time::Duration}; +use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, mem, path::Path, sync::Arc, time::Duration}; use tokio::time::sleep; use tracing::debug; -use yazi_config::{manager::SortBy, open::Opener, popup::InputCfg, OPEN}; +use yazi_config::{manager::SortBy, open::Opener, plugin::{PluginRule, MAX_PRELOADERS}, popup::InputCfg, OPEN, PLUGIN}; use yazi_scheduler::{Scheduler, TaskSummary}; -use yazi_shared::{fs::{File, Url}, term::Term, MimeKind}; +use yazi_shared::{fs::{File, Url}, term::Term, MIME_DIR}; use super::{TasksProgress, TASKS_PADDING, TASKS_PERCENT}; -use crate::{files::Files, input::Input}; +use crate::{folder::Files, input::Input}; pub struct Tasks { pub(super) scheduler: Arc, @@ -149,76 +149,98 @@ impl Tasks { false } - #[inline] - pub fn precache_size(&self, targets: &Files) -> bool { - if targets.sorter().by != SortBy::Size { - return false; - } - - let targets: Vec<_> = targets - .iter() - .filter(|f| f.is_dir() && !targets.sizes.contains_key(&f.url)) - .map(|f| &f.url) - .collect(); - - if !targets.is_empty() { - self.scheduler.precache_size(targets); - } - + pub fn plugin_micro(&self, name: &str) -> bool { + self.scheduler.plugin_micro(name.to_owned()); false } - #[inline] - pub fn precache_mime(&self, targets: &[File], mimetype: &HashMap) -> bool { - let targets: Vec<_> = targets - .iter() - .filter(|f| !f.is_dir() && !mimetype.contains_key(&f.url)) - .map(|f| f.url()) - .collect(); - - if !targets.is_empty() { - self.scheduler.precache_mime(targets); - } + pub fn plugin_macro(&self, name: &str) -> bool { + self.scheduler.plugin_macro(name.to_owned()); false } - pub fn precache_image(&self, mimetype: &BTreeMap) -> bool { - let targets: Vec<_> = mimetype - .iter() - .filter(|(_, m)| MimeKind::new(m) == MimeKind::Image) - .map(|(u, _)| u.clone()) - .collect(); + pub fn preload_paged(&self, paged: &[File], mimetype: &HashMap) { + let mut single_tasks = Vec::with_capacity(paged.len()); + let mut multi_tasks: [Vec<_>; MAX_PRELOADERS as usize] = Default::default(); + + let loaded = self.scheduler.preload.rule_loaded.read(); + for f in paged { + let mime = if f.is_dir() { Some(MIME_DIR) } else { mimetype.get(&f.url).map(|s| &**s) }; + let factors = |s: &str| match s { + "mime" => mime.is_some(), + _ => false, + }; + + for rule in PLUGIN.preloaders(&f.url, mime, factors) { + if loaded.get(&f.url).is_some_and(|x| x & (1 << rule.id) != 0) { + continue; + } + if rule.multi { + multi_tasks[rule.id as usize].push(f); + } else { + single_tasks.push((rule, f)); + } + } + } + + drop(loaded); + let mut loaded = self.scheduler.preload.rule_loaded.write(); - if !targets.is_empty() { - self.scheduler.precache_image(targets); + let mut go = |rule: &PluginRule, targets: Vec<&File>| { + for &f in &targets { + if let Some(n) = loaded.get_mut(&f.url) { + *n |= 1 << rule.id; + } else { + loaded.insert(f.url.clone(), 1 << rule.id); + } + } + self.scheduler.preload_paged(rule, targets); + }; + + for i in 0..PLUGIN.preloaders.len() { + if !multi_tasks[i].is_empty() { + go(&PLUGIN.preloaders[i], mem::take(&mut multi_tasks[i])); + } + } + for (rule, target) in single_tasks { + go(rule, vec![target]); } - false } - pub fn precache_video(&self, mimetype: &BTreeMap) -> bool { - let targets: Vec<_> = mimetype - .iter() - .filter(|(_, m)| MimeKind::new(m) == MimeKind::Video) - .map(|(u, _)| u.clone()) - .collect(); - - if !targets.is_empty() { - self.scheduler.precache_video(targets); + pub fn preload_affected(&self, affected: &[File], mimetype: &HashMap) { + { + let mut loaded = self.scheduler.preload.rule_loaded.write(); + for f in affected { + loaded.remove(&f.url); + } } - false + + self.preload_paged(affected, mimetype); } - pub fn precache_pdf(&self, mimetype: &BTreeMap) -> bool { - let targets: Vec<_> = mimetype - .iter() - .filter(|(_, m)| MimeKind::new(m) == MimeKind::PDF) - .map(|(u, _)| u.clone()) - .collect(); + pub fn preload_sorted(&self, targets: &Files) { + if targets.sorter().by != SortBy::Size { + return; + } + + let targets: Vec<_> = { + let loading = self.scheduler.preload.size_loading.read(); + targets + .iter() + .filter(|f| f.is_dir() && !targets.sizes.contains_key(&f.url) && !loading.contains(&f.url)) + .map(|f| &f.url) + .collect() + }; + if targets.is_empty() { + return; + } - if !targets.is_empty() { - self.scheduler.precache_pdf(targets); + let mut loading = self.scheduler.preload.size_loading.write(); + for &target in &targets { + loading.insert(target.clone()); } - false + + self.scheduler.preload_size(targets); } } diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index 55ab098ce..e33abb021 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -9,11 +9,12 @@ homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" [dependencies] -yazi-adaptor = { path = "../yazi-adaptor", version = "0.1.5" } -yazi-config = { path = "../yazi-config", version = "0.1.5" } -yazi-core = { path = "../yazi-core", version = "0.1.5" } -yazi-plugin = { path = "../yazi-plugin", version = "0.1.5" } -yazi-shared = { path = "../yazi-shared", version = "0.1.5" } +yazi-adaptor = { path = "../yazi-adaptor", version = "0.1.5" } +yazi-config = { path = "../yazi-config", version = "0.1.5" } +yazi-core = { path = "../yazi-core", version = "0.1.5" } +yazi-plugin = { path = "../yazi-plugin", version = "0.1.5" } +yazi-scheduler = { path = "../yazi-scheduler", version = "0.1.5" } +yazi-shared = { path = "../yazi-shared", version = "0.1.5" } # External dependencies ansi-to-tui = "^3" @@ -35,6 +36,12 @@ tracing-subscriber = "^0" libc = "^0" signal-hook-tokio = { version = "^0", features = [ "futures-v0_3" ] } +[target.'cfg(any(target_arch="riscv64", target_arch="loongarch64"))'.dependencies] +mlua = { version = "^0", features = [ "lua52", "vendored" ] } + +[target.'cfg(not(any(target_arch="riscv64", target_arch="loongarch64")))'.dependencies] +mlua = { version = "^0", features = [ "luajit52", "vendored" ] } + [[bin]] name = "yazi" path = "src/main.rs" diff --git a/yazi-fm/src/app/app.rs b/yazi-fm/src/app/app.rs index c8107b038..113076bf8 100644 --- a/yazi-fm/src/app/app.rs +++ b/yazi-fm/src/app/app.rs @@ -2,12 +2,12 @@ use std::sync::atomic::Ordering; use anyhow::{Ok, Result}; use crossterm::event::KeyEvent; -use ratatui::{backend::Backend, prelude::Rect}; -use yazi_config::{keymap::Key, BOOT}; -use yazi_core::{input::InputMode, preview::COLLISION, Ctx}; -use yazi_shared::{emit, event::{Event, Exec}, fs::FilesOp, term::Term, Layer}; +use ratatui::backend::Backend; +use yazi_config::{keymap::Key, ARGS}; +use yazi_core::input::InputMode; +use yazi_shared::{emit, event::{Event, Exec}, term::Term, Layer, COLLISION}; -use crate::{Executor, Logs, Panic, Root, Signals}; +use crate::{lives::Lives, Ctx, Executor, Logs, Panic, Root, Signals}; pub(crate) struct App { pub(crate) cx: Ctx, @@ -19,11 +19,14 @@ impl App { pub(crate) async fn run() -> Result<()> { Panic::install(); let _log = Logs::init()?; - let term = Term::start()?; + let term = Term::start()?; let signals = Signals::start()?; + + Lives::register()?; let mut app = Self { cx: Ctx::make(), term: Some(term), signals }; + app.dispatch_render()?; while let Some(event) = app.signals.recv().await { match event { Event::Quit(no_cwd_file) => { @@ -33,7 +36,7 @@ impl App { Event::Key(key) => app.dispatch_key(key), Event::Paste(str) => app.dispatch_paste(str), Event::Render(_) => app.dispatch_render()?, - Event::Resize(cols, rows) => app.dispatch_resize(cols, rows), + Event::Resize(cols, rows) => app.dispatch_resize(cols, rows)?, Event::Call(exec, layer) => app.dispatch_call(exec, layer), event => app.dispatch_module(event), } @@ -42,7 +45,7 @@ impl App { } fn dispatch_quit(&mut self, no_cwd_file: bool) { - if let Some(p) = BOOT.cwd_file.as_ref().filter(|_| !no_cwd_file) { + if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no_cwd_file) { let cwd = self.cx.manager.cwd().as_os_str(); std::fs::write(p, cwd.as_encoded_bytes()).ok(); } @@ -72,7 +75,7 @@ impl App { let collision = COLLISION.swap(false, Ordering::Relaxed); let frame = term.draw(|f| { - yazi_plugin::scope(&self.cx, |_| { + Lives::scope(&self.cx, |_| { f.render_widget(Root::new(&self.cx), f.size()); }); @@ -83,8 +86,7 @@ impl App { if !COLLISION.load(Ordering::Relaxed) { if collision { // Reload preview if collision is resolved - self.cx.manager.active_mut().preview.reset_image(); - self.cx.manager.peek(0); + self.cx.manager.peek(true); } return Ok(()); } @@ -108,15 +110,13 @@ impl App { Ok(()) } - fn dispatch_resize(&mut self, cols: u16, rows: u16) { - if let Some(term) = &mut self.term { - term.resize(Rect::new(0, 0, cols, rows)).ok(); - } + fn dispatch_resize(&mut self, _: u16, _: u16) -> Result<()> { + self.cx.manager.active_mut().preview.reset(); + self.dispatch_render()?; self.cx.manager.current_mut().set_page(true); - self.cx.manager.active_mut().preview.reset(); - self.cx.manager.peek(0); - emit!(Render); + self.cx.manager.peek(false); + Ok(()) } #[inline] @@ -130,33 +130,9 @@ impl App { let manager = &mut self.cx.manager; let tasks = &mut self.cx.tasks; match event { - Event::Files(op) => { - let calc = !matches!(op, FilesOp::Size(..) | FilesOp::IOErr(_)); - let b = match op { - FilesOp::IOErr(..) => manager.update_ioerr(op), - _ => manager.update_read(op), - }; - if b { - emit!(Render); - } - if calc { - tasks.precache_size(&manager.current().files); - } - } Event::Pages(page) => { let targets = self.cx.manager.current().paginate(page); - tasks.precache_mime(targets, &self.cx.manager.mimetype); - } - Event::Mimetype(mimes) => { - if manager.update_mimetype(mimes, tasks) { - emit!(Render); - manager.peek(0); - } - } - Event::Preview(lock) => { - if manager.active_mut().update_preview(lock) { - emit!(Render); - } + tasks.preload_paged(targets, &self.cx.manager.mimetype); } _ => unreachable!(), } diff --git a/yazi-fm/src/app/commands/mod.rs b/yazi-fm/src/app/commands/mod.rs index eeec0cfbe..4fddf53dd 100644 --- a/yazi-fm/src/app/commands/mod.rs +++ b/yazi-fm/src/app/commands/mod.rs @@ -1 +1,2 @@ +mod plugin; mod stop; diff --git a/yazi-fm/src/app/commands/plugin.rs b/yazi-fm/src/app/commands/plugin.rs new file mode 100644 index 000000000..53a171f29 --- /dev/null +++ b/yazi-fm/src/app/commands/plugin.rs @@ -0,0 +1,66 @@ +use mlua::{ExternalError, ExternalResult, IntoLua, Table, TableExt, Value, Variadic}; +use tracing::error; +use yazi_plugin::{LOADED, LUA}; +use yazi_shared::{emit, event::Exec, Layer}; + +use crate::{app::App, lives::Lives}; + +impl App { + pub(crate) fn plugin(&mut self, opt: impl TryInto) -> bool { + let Ok(opt) = opt.try_into() else { + return false; + }; + + if !opt.sync { + return self.cx.tasks.plugin_micro(&opt.name); + } + + if LOADED.read().contains_key(&opt.name) { + return self.plugin_do(opt); + } + + tokio::spawn(async move { + if LOADED.ensure(&opt.name).await.is_ok() { + emit!(Call(Exec::call("plugin_do", vec![opt.name]).with_data(opt.data).vec(), Layer::App)); + } + }); + false + } + + pub(crate) fn plugin_do(&mut self, opt: impl TryInto) -> bool { + let Ok(opt) = opt.try_into() else { + return false; + }; + + let args = Variadic::from_iter(opt.data.args.into_iter().filter_map(|v| v.into_lua(&LUA).ok())); + let mut ret: mlua::Result = Err("uninitialized plugin".into_lua_err()); + + Lives::scope(&self.cx, |_| { + let mut plugin: Option
= None; + if let Some(b) = LOADED.read().get(&opt.name) { + match LUA.load(b).call(()) { + Ok(t) => plugin = Some(t), + Err(e) => ret = Err(e), + } + } + if let Some(plugin) = plugin { + ret = + if let Some(cb) = opt.data.cb { cb(plugin) } else { plugin.call_method("entry", args) }; + } + }); + + if let Err(e) = ret { + error!("{e}"); + return false; + } + + let Some(tx) = opt.data.tx else { + return false; + }; + + if let Ok(v) = ret.and_then(|v| v.try_into().into_lua_err()) { + tx.send(v).ok(); + } + false + } +} diff --git a/yazi-fm/src/app/commands/stop.rs b/yazi-fm/src/app/commands/stop.rs index 591335cc1..e5c92bd86 100644 --- a/yazi-fm/src/app/commands/stop.rs +++ b/yazi-fm/src/app/commands/stop.rs @@ -1,6 +1,5 @@ use anyhow::Result; use tokio::sync::oneshot; -use yazi_core::manager::Manager; use yazi_shared::{emit, event::Exec, term::Term}; use crate::app::App; @@ -31,8 +30,9 @@ impl App { } else { self.term = Some(Term::start().unwrap()); self.signals.stop_term(false); + self.cx.manager.hover(None); + self.cx.manager.peek(true); emit!(Render); - Manager::_hover(None); } if let Some(tx) = opt.tx { tx.send(()).ok(); diff --git a/yazi-fm/src/completion/completion.rs b/yazi-fm/src/completion/completion.rs index f10bd83b8..909eb6e17 100644 --- a/yazi-fm/src/completion/completion.rs +++ b/yazi-fm/src/completion/completion.rs @@ -2,9 +2,8 @@ use std::path::MAIN_SEPARATOR; use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, List, ListItem, Widget}}; use yazi_config::{popup::{Offset, Position}, THEME}; -use yazi_core::Ctx; -use crate::widgets; +use crate::{widgets, Ctx}; pub(crate) struct Completion<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/components/header.rs b/yazi-fm/src/components/header.rs new file mode 100644 index 000000000..a9d22e2ab --- /dev/null +++ b/yazi-fm/src/components/header.rs @@ -0,0 +1,20 @@ +use mlua::{Table, TableExt}; +use ratatui::{prelude::Buffer, widgets::Widget}; +use tracing::error; +use yazi_plugin::{bindings::Cast, elements::{render_widgets, Rect}, LUA}; + +pub(crate) struct Header; + +impl Widget for Header { + fn render(self, area: ratatui::layout::Rect, buf: &mut Buffer) { + let mut f = || { + let area = Rect::cast(&LUA, area)?; + let comp: Table = LUA.globals().get("Header")?; + render_widgets(comp.call_method("render", area)?, buf); + Ok::<_, anyhow::Error>(()) + }; + if let Err(e) = f() { + error!("{:?}", e); + } + } +} diff --git a/yazi-fm/src/components/manager.rs b/yazi-fm/src/components/manager.rs new file mode 100644 index 000000000..b613196a6 --- /dev/null +++ b/yazi-fm/src/components/manager.rs @@ -0,0 +1,20 @@ +use mlua::{Table, TableExt}; +use ratatui::{prelude::Buffer, widgets::Widget}; +use tracing::error; +use yazi_plugin::{bindings::Cast, elements::{render_widgets, Rect}, LUA}; + +pub(crate) struct Manager; + +impl Widget for Manager { + fn render(self, area: ratatui::layout::Rect, buf: &mut Buffer) { + let mut f = || { + let area = Rect::cast(&LUA, area)?; + let comp: Table = LUA.globals().get("Manager")?; + render_widgets(comp.call_method("render", area)?, buf); + Ok::<_, anyhow::Error>(()) + }; + if let Err(e) = f() { + error!("{:?}", e); + } + } +} diff --git a/yazi-fm/src/components/mod.rs b/yazi-fm/src/components/mod.rs new file mode 100644 index 000000000..95b2d4be1 --- /dev/null +++ b/yazi-fm/src/components/mod.rs @@ -0,0 +1,11 @@ +#![allow(clippy::module_inception)] + +mod header; +mod manager; +mod preview; +mod status; + +pub(super) use header::*; +pub(super) use manager::*; +pub(super) use preview::*; +pub(super) use status::*; diff --git a/yazi-fm/src/components/preview.rs b/yazi-fm/src/components/preview.rs new file mode 100644 index 000000000..5a0067ff3 --- /dev/null +++ b/yazi-fm/src/components/preview.rs @@ -0,0 +1,29 @@ +use ratatui::{prelude::Buffer, widgets::Widget}; + +use crate::Ctx; + +pub(crate) struct Preview<'a> { + cx: &'a Ctx, +} + +impl<'a> Preview<'a> { + #[inline] + pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl Widget for Preview<'_> { + fn render(self, area: ratatui::layout::Rect, buf: &mut Buffer) { + let preview = &self.cx.manager.active().preview; + let Some(lock) = &preview.lock else { + return; + }; + + if (lock.window.rows, lock.window.cols) != (area.height, area.width) { + return; + } + + for w in &lock.data { + w.clone_render(buf); + } + } +} diff --git a/yazi-fm/src/components/status.rs b/yazi-fm/src/components/status.rs new file mode 100644 index 000000000..4452c3e6f --- /dev/null +++ b/yazi-fm/src/components/status.rs @@ -0,0 +1,20 @@ +use mlua::{Table, TableExt}; +use ratatui::widgets::Widget; +use tracing::error; +use yazi_plugin::{bindings::Cast, elements::{render_widgets, Rect}, LUA}; + +pub(crate) struct Status; + +impl Widget for Status { + fn render(self, area: ratatui::layout::Rect, buf: &mut ratatui::prelude::Buffer) { + let mut f = || { + let area = Rect::cast(&LUA, area)?; + let comp: Table = LUA.globals().get("Status")?; + render_widgets(comp.call_method("render", area)?, buf); + Ok::<_, anyhow::Error>(()) + }; + if let Err(e) = f() { + error!("{:?}", e); + } + } +} diff --git a/yazi-core/src/context.rs b/yazi-fm/src/context.rs similarity index 81% rename from yazi-core/src/context.rs rename to yazi-fm/src/context.rs index 373666f6f..39319be7d 100644 --- a/yazi-core/src/context.rs +++ b/yazi-fm/src/context.rs @@ -1,7 +1,6 @@ use ratatui::prelude::Rect; use yazi_config::popup::{Origin, Position}; - -use crate::{completion::Completion, help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which}; +use yazi_core::{completion::Completion, help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which}; pub struct Ctx { pub manager: Manager, @@ -26,12 +25,6 @@ impl Ctx { } } - #[inline] - pub async fn stop() { yazi_scheduler::Scheduler::app_stop().await } - - #[inline] - pub fn resume() { yazi_scheduler::Scheduler::app_resume() } - pub fn area(&self, position: &Position) -> Rect { if position.origin != Origin::Hovered { return position.rect(); diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 8628aec6b..58a8202b3 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -84,6 +84,8 @@ impl<'a> Executor<'a> { }; } + on!(plugin); + on!(plugin_do); on!(stop); false @@ -108,13 +110,17 @@ impl<'a> Executor<'a> { }; } - on!(MANAGER, peek); + on!(MANAGER, update_files, &self.app.cx.tasks); + on!(MANAGER, update_mimetype, &self.app.cx.tasks); on!(MANAGER, hover); + on!(MANAGER, peek); + on!(MANAGER, seek); on!(MANAGER, refresh); on!(MANAGER, quit, &self.app.cx.tasks); on!(MANAGER, close, &self.app.cx.tasks); on!(MANAGER, suspend); on!(ACTIVE, escape); + on!(ACTIVE, preview); // Navigation on!(ACTIVE, arrow); @@ -131,7 +137,8 @@ impl<'a> Executor<'a> { on!(ACTIVE, visual_mode); // Operation - on!(MANAGER, open); + on!(MANAGER, open, &self.app.cx.tasks); + on!(MANAGER, open_do, &self.app.cx.tasks); on!(MANAGER, yank); on!(MANAGER, paste, &self.app.cx.tasks); on!(MANAGER, link, &self.app.cx.tasks); diff --git a/yazi-fm/src/help/bindings.rs b/yazi-fm/src/help/bindings.rs index d990c1439..fbd5dd51b 100644 --- a/yazi-fm/src/help/bindings.rs +++ b/yazi-fm/src/help/bindings.rs @@ -1,6 +1,7 @@ use ratatui::{layout::{self, Constraint}, prelude::{Buffer, Direction, Rect}, widgets::{List, ListItem, Widget}}; use yazi_config::THEME; -use yazi_core::Ctx; + +use crate::Ctx; pub(super) struct Bindings<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/help/layout.rs b/yazi-fm/src/help/layout.rs index 7aa4ab1ad..a34d054f4 100644 --- a/yazi-fm/src/help/layout.rs +++ b/yazi-fm/src/help/layout.rs @@ -1,9 +1,8 @@ use ratatui::{buffer::Buffer, layout::{self, Rect}, prelude::{Constraint, Direction}, widgets::{Paragraph, Widget}}; use yazi_config::THEME; -use yazi_core::Ctx; use super::Bindings; -use crate::widgets; +use crate::{Ctx, widgets}; pub(crate) struct Layout<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/input/input.rs b/yazi-fm/src/input/input.rs index c1858fa73..7e551bdbe 100644 --- a/yazi-fm/src/input/input.rs +++ b/yazi-fm/src/input/input.rs @@ -3,10 +3,10 @@ use std::ops::Range; use ansi_to_tui::IntoText; use ratatui::{buffer::Buffer, layout::Rect, text::{Line, Text}, widgets::{Block, BorderType, Borders, Paragraph, Widget}}; use yazi_config::THEME; -use yazi_core::{input::InputMode, Ctx}; +use yazi_core::input::InputMode; use yazi_shared::term::Term; -use crate::widgets; +use crate::{Ctx, widgets}; pub(crate) struct Input<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/lives/active.rs b/yazi-fm/src/lives/active.rs new file mode 100644 index 000000000..0ba375018 --- /dev/null +++ b/yazi-fm/src/lives/active.rs @@ -0,0 +1,78 @@ +use mlua::{AnyUserData, Lua, MetaMethod, UserDataFields, UserDataMethods, Value}; +use yazi_config::LAYOUT; + +use super::Folder; + +pub struct Active<'a, 'b> { + scope: &'b mlua::Scope<'a, 'a>, + + inner: &'a yazi_core::tab::Tab, +} + +impl<'a, 'b> Active<'a, 'b> { + pub(super) fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("is_select", |_, me| Ok(me.is_select())); + reg.add_field_method_get("is_unset", |_, me| Ok(me.is_unset())); + reg.add_field_method_get("is_visual", |_, me| Ok(me.is_visual())); + reg.add_method("pending", |_, me, (idx, state): (usize, bool)| Ok(me.pending(idx, state))); + + reg.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.to_string())); + })?; + + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("sort_by", |_, me| Ok(me.sort_by.to_string())); + reg.add_field_method_get("sort_sensitive", |_, me| Ok(me.sort_sensitive)); + reg.add_field_method_get("sort_reverse", |_, me| Ok(me.sort_reverse)); + reg.add_field_method_get("sort_dir_first", |_, me| Ok(me.sort_dir_first)); + + reg.add_field_method_get("linemode", |_, me| Ok(me.linemode.to_owned())); + reg.add_field_method_get("show_hidden", |_, me| Ok(me.show_hidden)); + })?; + + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("skip", |_, me| Ok(me.skip)); + reg.add_field_function_get("folder", |_, me| me.named_user_value::("folder")); + })?; + + Ok(()) + } +} + +impl<'a, 'b> Active<'a, 'b> { + pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a yazi_core::tab::Tab) -> Self { + Self { scope, inner } + } + + pub(crate) fn make(&self) -> mlua::Result> { + let ud = self.scope.create_any_userdata_ref(self.inner)?; + ud.set_named_user_value("mode", self.scope.create_any_userdata_ref(&self.inner.mode)?)?; + ud.set_named_user_value("conf", self.scope.create_any_userdata_ref(&self.inner.conf)?)?; + ud.set_named_user_value( + "parent", + self.inner.parent.as_ref().and_then(|p| Folder::new(self.scope, p).make(None).ok()), + )?; + ud.set_named_user_value("current", Folder::new(self.scope, &self.inner.current).make(None)?)?; + ud.set_named_user_value("preview", self.preview(self.inner)?)?; + + Ok(ud) + } + + fn preview(&self, tab: &'a yazi_core::tab::Tab) -> mlua::Result> { + let inner = &tab.preview; + let window = Some((inner.skip, LAYOUT.load().preview.height as usize)); + + let ud = self.scope.create_any_userdata_ref(inner)?; + ud.set_named_user_value( + "folder", + tab + .current + .hovered() + .filter(|&f| f.is_dir()) + .and_then(|f| tab.history(&f.url)) + .and_then(|f| Folder::new(self.scope, f).make(window).ok()), + )?; + + Ok(ud) + } +} diff --git a/yazi-fm/src/lives/folder.rs b/yazi-fm/src/lives/folder.rs new file mode 100644 index 000000000..62478fa4b --- /dev/null +++ b/yazi-fm/src/lives/folder.rs @@ -0,0 +1,200 @@ +use mlua::{AnyUserData, IntoLua, Lua, MetaMethod, UserDataFields, UserDataMethods, Value}; +use yazi_config::{LAYOUT, THEME}; +use yazi_plugin::{bindings::{Cast, File, Range, Url}, elements::Style}; +use yazi_shared::MIME_DIR; + +use super::{CtxRef, FolderRef}; + +pub struct Folder<'a, 'b> { + scope: &'b mlua::Scope<'a, 'a>, + + inner: &'a yazi_core::folder::Folder, +} + +impl<'a, 'b> Folder<'a, 'b> { + pub(super) fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("cwd", |lua, me| Url::cast(lua, me.cwd.clone())); + reg.add_field_method_get("offset", |_, me| Ok(me.offset)); + reg.add_field_method_get("cursor", |_, me| Ok(me.cursor)); + + reg.add_field_function_get("window", |_, me| me.named_user_value::("window")); + reg.add_field_function_get("files", |_, me| me.named_user_value::("files")); + reg.add_field_function_get("hovered", |_, me| me.named_user_value::("hovered")); + })?; + + lua.register_userdata_type::(|reg| { + reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); + + reg.add_meta_function(MetaMethod::Pairs, |lua, me: AnyUserData| { + let iter = lua.create_function(|lua, (me, i): (AnyUserData, usize)| { + let files = me.borrow::()?; + let i = i + 1; + Ok(if i > files.len() { + mlua::Variadic::new() + } else { + mlua::Variadic::from_iter([ + i.into_lua(lua)?, + File::cast(lua, files[i - 1].clone())?.into_lua(lua)?, + ]) + }) + })?; + Ok((iter, me, 0)) + }); + })?; + + File::register(lua, |reg| { + reg.add_function("size", |_, me: AnyUserData| { + let file = me.borrow::()?; + if !file.is_dir() { + return Ok(Some(file.len)); + } + + let folder = me.named_user_value::("folder")?; + Ok(folder.files.sizes.get(&file.url).copied()) + }); + reg.add_function("mime", |lua, me: AnyUserData| { + let cx = lua.named_registry_value::("cx")?; + let file = me.borrow::()?; + Ok(cx.manager.mimetype.get(&file.url).cloned()) + }); + reg.add_function("prefix", |lua, me: AnyUserData| { + let folder = me.named_user_value::("folder")?; + if !folder.cwd.is_search() { + return Ok(None); + } + + let file = me.borrow::()?; + let mut p = file.url.strip_prefix(&folder.cwd).unwrap_or(&file.url).components(); + p.next_back(); + Some(lua.create_string(p.as_path().as_os_str().as_encoded_bytes())).transpose() + }); + reg.add_method("icon", |_, me, ()| { + Ok( + THEME + .icons + .iter() + .find(|&x| x.name.match_path(&me.url, me.is_dir())) + .map(|x| x.display.to_string()), + ) + }); + reg.add_function("style", |lua, me: AnyUserData| { + let cx = lua.named_registry_value::("cx")?; + let file = me.borrow::()?; + + let mime = if file.is_dir() { + Some(MIME_DIR) + } else { + cx.manager.mimetype.get(&file.url).map(|x| &**x) + }; + + Ok( + THEME + .filetypes + .iter() + .find(|&x| x.matches(&file.url, mime)) + .map(|x| Style::from(x.style)), + ) + }); + reg.add_function("is_hovered", |_, me: AnyUserData| { + let folder = me.named_user_value::("folder")?; + let file = me.borrow::()?; + Ok(matches!(folder.hovered(), Some(f) if f.url == file.url)) + }); + reg.add_function("is_yanked", |lua, me: AnyUserData| { + let cx = lua.named_registry_value::("cx")?; + let file = me.borrow::()?; + Ok(if !cx.manager.yanked.1.contains(&file.url) { + 0u8 + } else if cx.manager.yanked.0 { + 2u8 + } else { + 1u8 + }) + }); + reg.add_function("is_selected", |lua, me: AnyUserData| { + let cx = lua.named_registry_value::("cx")?; + let folder = me.named_user_value::("folder")?; + let file = me.borrow::()?; + + let selected = folder.files.is_selected(&file.url); + Ok(if !cx.manager.active().mode.is_visual() { + selected + } else { + let idx: usize = me.named_user_value("idx")?; + cx.manager.active().mode.pending(folder.offset + idx, selected) + }) + }); + reg.add_function("found", |lua, me: AnyUserData| { + let cx = lua.named_registry_value::("cx")?; + let Some(finder) = &cx.manager.active().finder else { + return Ok(None); + }; + + let file = me.borrow::()?; + if let Some(idx) = finder.matched_idx(&file.url) { + return Some( + lua.create_sequence_from([idx.into_lua(lua)?, finder.matched().len().into_lua(lua)?]), + ) + .transpose(); + } + Ok(None) + }); + reg.add_function("highlights", |lua, me: AnyUserData| { + let cx = lua.named_registry_value::("cx")?; + let Some(finder) = &cx.manager.active().finder else { + return Ok(None); + }; + + let file = me.borrow::()?; + let Some(h) = file.name().and_then(|n| finder.highlighted(n)) else { + return Ok(None); + }; + + Ok(Some(h.into_iter().map(Range::from).collect::>())) + }); + })?; + + Ok(()) + } +} + +impl<'a, 'b> Folder<'a, 'b> { + pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a yazi_core::folder::Folder) -> Self { + Self { scope, inner } + } + + pub(crate) fn make(&self, window: Option<(usize, usize)>) -> mlua::Result> { + let window = + window.unwrap_or_else(|| (self.inner.offset, LAYOUT.load().preview.height as usize)); + + let ud = self.scope.create_any_userdata_ref(self.inner)?; + ud.set_named_user_value( + "window", + self + .inner + .files + .iter() + .skip(window.0) + .take(window.1) + .enumerate() + .filter_map(|(i, f)| self.file(i, f).ok()) + .collect::>(), + )?; + ud.set_named_user_value("files", self.scope.create_any_userdata_ref(&self.inner.files)?)?; + ud.set_named_user_value( + "hovered", + self.inner.hovered().and_then(|h| self.file(self.inner.cursor - window.0, h).ok()), + )?; + + Ok(ud) + } + + fn file(&self, idx: usize, inner: &'a yazi_shared::fs::File) -> mlua::Result> { + let ud = self.scope.create_any_userdata_ref(inner)?; + ud.set_named_user_value("idx", idx)?; + ud.set_named_user_value("folder", self.scope.create_any_userdata_ref(self.inner)?)?; + + Ok(ud) + } +} diff --git a/yazi-fm/src/lives/lives.rs b/yazi-fm/src/lives/lives.rs new file mode 100644 index 000000000..ba9038535 --- /dev/null +++ b/yazi-fm/src/lives/lives.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; + +use mlua::{Scope, Table}; +use tracing::error; +use yazi_config::LAYOUT; +use yazi_plugin::{elements::RectRef, LUA}; + +use crate::Ctx; + +pub(crate) struct Lives; + +impl Lives { + pub(crate) fn register() -> mlua::Result<()> { + yazi_plugin::bindings::Cha::register(&LUA)?; + yazi_plugin::bindings::Url::register(&LUA)?; + + super::Active::register(&LUA)?; + super::Folder::register(&LUA)?; + super::Tabs::register(&LUA)?; + super::Tasks::register(&LUA)?; + + Ok(()) + } + + pub(crate) fn scope<'a>(cx: &'a Ctx, f: impl FnOnce(&Scope<'a, 'a>)) { + let result = LUA.scope(|scope| { + LUA.set_named_registry_value("cx", scope.create_any_userdata_ref(cx)?)?; + + let global = LUA.globals(); + global.set( + "cx", + LUA.create_table_from([ + ("active", super::Active::new(scope, cx.manager.active()).make()?), + ("tabs", super::Tabs::new(scope, &cx.manager.tabs).make()?), + ("tasks", super::Tasks::new(scope, &cx.tasks).make()?), + ])?, + )?; + + f(scope); + + LAYOUT.store(Arc::new(yazi_config::Layout { + header: *global.get::<_, Table>("Header")?.get::<_, RectRef>("area")?, + parent: *global.get::<_, Table>("Parent")?.get::<_, RectRef>("area")?, + current: *global.get::<_, Table>("Current")?.get::<_, RectRef>("area")?, + preview: *global.get::<_, Table>("Preview")?.get::<_, RectRef>("area")?, + status: *global.get::<_, Table>("Status")?.get::<_, RectRef>("area")?, + })); + + Ok(()) + }); + + if let Err(e) = result { + error!("{e}"); + } + } +} diff --git a/yazi-fm/src/lives/mod.rs b/yazi-fm/src/lives/mod.rs new file mode 100644 index 000000000..2dcc8e18b --- /dev/null +++ b/yazi-fm/src/lives/mod.rs @@ -0,0 +1,16 @@ +#![allow(clippy::module_inception)] + +mod active; +mod folder; +mod lives; +mod tabs; +mod tasks; + +pub(super) use active::*; +pub(super) use folder::*; +pub(super) use lives::*; +pub(super) use tabs::*; +pub(super) use tasks::*; + +type CtxRef<'lua> = mlua::UserDataRef<'lua, crate::Ctx>; +type FolderRef<'lua> = mlua::UserDataRef<'lua, yazi_core::folder::Folder>; diff --git a/yazi-plugin/src/bindings/tabs.rs b/yazi-fm/src/lives/tabs.rs similarity index 58% rename from yazi-plugin/src/bindings/tabs.rs rename to yazi-fm/src/lives/tabs.rs index 8d31e8b53..c3461ce84 100644 --- a/yazi-plugin/src/bindings/tabs.rs +++ b/yazi-fm/src/lives/tabs.rs @@ -1,6 +1,4 @@ -use mlua::{AnyUserData, MetaMethod, UserDataFields, UserDataMethods, Value}; - -use crate::LUA; +use mlua::{AnyUserData, Lua, MetaMethod, UserDataFields, UserDataMethods, Value}; pub struct Tabs<'a, 'b> { scope: &'b mlua::Scope<'a, 'a>, @@ -9,8 +7,8 @@ pub struct Tabs<'a, 'b> { } impl<'a, 'b> Tabs<'a, 'b> { - pub(crate) fn init() -> mlua::Result<()> { - LUA.register_userdata_type::(|reg| { + pub(super) fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { reg.add_field_method_get("idx", |_, me| Ok(me.idx)); reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); reg.add_meta_function(MetaMethod::Index, |_, (me, index): (AnyUserData, usize)| { @@ -19,17 +17,15 @@ impl<'a, 'b> Tabs<'a, 'b> { }); })?; - LUA.register_userdata_type::(|reg| { - reg.add_method("name", |_, me, ()| { - Ok( - me.current - .cwd - .file_name() - .map(|n| n.to_string_lossy()) - .or_else(|| Some(me.current.cwd.to_string_lossy())) - .unwrap_or_default() - .into_owned(), - ) + lua.register_userdata_type::(|reg| { + reg.add_method("name", |lua, me, ()| { + Some(lua.create_string( + me.current.cwd.file_name().map_or_else( + || me.current.cwd.as_os_str().as_encoded_bytes(), + |n| n.as_encoded_bytes(), + ), + )) + .transpose() }); reg.add_field_function_get("mode", |_, me| me.named_user_value::("mode")); @@ -41,7 +37,9 @@ impl<'a, 'b> Tabs<'a, 'b> { Ok(()) } +} +impl<'a, 'b> Tabs<'a, 'b> { pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a yazi_core::manager::Tabs) -> Self { Self { scope, inner } } @@ -62,35 +60,16 @@ impl<'a, 'b> Tabs<'a, 'b> { ud.set_named_user_value("parent", inner.parent.as_ref().and_then(|p| self.folder(p).ok()))?; ud.set_named_user_value("current", self.folder(&inner.current)?)?; - ud.set_named_user_value("preview", self.preview(inner)?)?; Ok(ud) } - pub(crate) fn folder(&self, inner: &'a yazi_core::tab::Folder) -> mlua::Result> { + pub(crate) fn folder( + &self, + inner: &'a yazi_core::folder::Folder, + ) -> mlua::Result> { let ud = self.scope.create_any_userdata_ref(inner)?; - ud.set_named_user_value("files", self.files(&inner.files)?)?; - - Ok(ud) - } - - fn files(&self, inner: &'a yazi_core::files::Files) -> mlua::Result> { - self.scope.create_any_userdata_ref(inner) - } - - fn preview(&self, tab: &'a yazi_core::tab::Tab) -> mlua::Result> { - let inner = &tab.preview; - - let ud = self.scope.create_any_userdata_ref(inner)?; - ud.set_named_user_value( - "folder", - inner - .lock - .as_ref() - .filter(|l| l.is_folder()) - .and_then(|l| tab.history(&l.url)) - .and_then(|f| self.folder(f).ok()), - )?; + ud.set_named_user_value("files", self.scope.create_any_userdata_ref(&inner.files)?)?; Ok(ud) } diff --git a/yazi-plugin/src/bindings/tasks.rs b/yazi-fm/src/lives/tasks.rs similarity index 72% rename from yazi-plugin/src/bindings/tasks.rs rename to yazi-fm/src/lives/tasks.rs index 079471821..44ed51084 100644 --- a/yazi-plugin/src/bindings/tasks.rs +++ b/yazi-fm/src/lives/tasks.rs @@ -1,6 +1,4 @@ -use mlua::{AnyUserData, LuaSerdeExt, UserDataFields}; - -use crate::LUA; +use mlua::{AnyUserData, Lua, LuaSerdeExt, UserDataFields}; pub struct Tasks<'a, 'b> { scope: &'b mlua::Scope<'a, 'a>, @@ -9,8 +7,8 @@ pub struct Tasks<'a, 'b> { } impl<'a, 'b> Tasks<'a, 'b> { - pub(crate) fn init() -> mlua::Result<()> { - LUA.register_userdata_type::(|reg| { + pub(super) fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { reg.add_field_method_get("progress", |lua, me| lua.to_value(&me.progress)) })?; diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index 8af3c9f45..0ecd9e90e 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -2,9 +2,12 @@ mod app; mod completion; +mod components; +mod context; mod executor; mod help; mod input; +mod lives; mod logs; mod panic; mod root; @@ -14,6 +17,7 @@ mod tasks; mod which; mod widgets; +use context::*; use executor::*; use logs::*; use panic::*; @@ -29,6 +33,8 @@ async fn main() -> anyhow::Result<()> { yazi_core::init(); + yazi_scheduler::init(); + yazi_plugin::init(); yazi_adaptor::init(); diff --git a/yazi-fm/src/root.rs b/yazi-fm/src/root.rs index 4dde5b7ba..d77ef3ae6 100644 --- a/yazi-fm/src/root.rs +++ b/yazi-fm/src/root.rs @@ -1,9 +1,7 @@ use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget}; -use yazi_core::Ctx; -use yazi_plugin::components; use super::{completion, input, select, tasks, which}; -use crate::help; +use crate::{components, help, Ctx}; pub(super) struct Root<'a> { cx: &'a Ctx, @@ -20,9 +18,10 @@ impl<'a> Widget for Root<'a> { .constraints([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)]) .split(area); - components::Header::new(self.cx).render(chunks[0], buf); - components::Manager::new(self.cx).render(chunks[1], buf); - components::Status::new(self.cx).render(chunks[2], buf); + components::Header.render(chunks[0], buf); + components::Manager.render(chunks[1], buf); + components::Status.render(chunks[2], buf); + components::Preview::new(self.cx).render(area, buf); if self.cx.tasks.visible { tasks::Layout::new(self.cx).render(area, buf); diff --git a/yazi-fm/src/select/select.rs b/yazi-fm/src/select/select.rs index a7e94d2bb..19983d0fa 100644 --- a/yazi-fm/src/select/select.rs +++ b/yazi-fm/src/select/select.rs @@ -1,8 +1,7 @@ use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, List, ListItem, Widget}}; use yazi_config::THEME; -use yazi_core::Ctx; -use crate::widgets; +use crate::{Ctx, widgets}; pub(crate) struct Select<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/signals.rs b/yazi-fm/src/signals.rs index 1ce4c9e1c..450eeda4c 100644 --- a/yazi-fm/src/signals.rs +++ b/yazi-fm/src/signals.rs @@ -50,7 +50,7 @@ impl Signals { #[cfg(unix)] fn spawn_system_task(&self) -> Result> { use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGTERM}; - use yazi_core::Ctx; + use yazi_scheduler::Scheduler; let tx = self.tx.clone(); let mut signals = signal_hook_tokio::Signals::new([ @@ -68,7 +68,7 @@ impl Signals { break; } } - SIGCONT => Ctx::resume(), + SIGCONT => Scheduler::app_resume(), _ => {} } } diff --git a/yazi-fm/src/tasks/layout.rs b/yazi-fm/src/tasks/layout.rs index 7c07ab735..82e68ee70 100644 --- a/yazi-fm/src/tasks/layout.rs +++ b/yazi-fm/src/tasks/layout.rs @@ -1,8 +1,8 @@ use ratatui::{buffer::Buffer, layout::{self, Alignment, Constraint, Direction, Rect}, text::Line, widgets::{Block, BorderType, Borders, List, ListItem, Padding, Widget}}; use yazi_config::THEME; -use yazi_core::{tasks::TASKS_PERCENT, Ctx}; +use yazi_core::tasks::TASKS_PERCENT; -use crate::widgets; +use crate::{Ctx, widgets}; pub(crate) struct Layout<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/which/layout.rs b/yazi-fm/src/which/layout.rs index 745570f70..14a20687c 100644 --- a/yazi-fm/src/which/layout.rs +++ b/yazi-fm/src/which/layout.rs @@ -1,9 +1,8 @@ use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, widgets::{Block, Widget}}; use yazi_config::THEME; -use yazi_core::Ctx; use super::Side; -use crate::widgets; +use crate::{Ctx, widgets}; pub(crate) struct Which<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/widgets/clear.rs b/yazi-fm/src/widgets/clear.rs index a7a8e9473..ef68110fa 100644 --- a/yazi-fm/src/widgets/clear.rs +++ b/yazi-fm/src/widgets/clear.rs @@ -2,8 +2,8 @@ use std::sync::atomic::Ordering; use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; use yazi_adaptor::ADAPTOR; -use yazi_config::MANAGER; -use yazi_core::preview::COLLISION; +use yazi_config::LAYOUT; +use yazi_shared::COLLISION; pub(crate) struct Clear; @@ -28,11 +28,11 @@ impl Widget for Clear { fn render(self, area: Rect, buf: &mut Buffer) { ratatui::widgets::Clear.render(area, buf); - let Some(r) = overlap(&area, &MANAGER.layout.image_rect()) else { + let Some(r) = overlap(&area, &LAYOUT.load().preview) else { return; }; - ADAPTOR.image_hide(r).ok(); + ADAPTOR.image_erase(r).ok(); COLLISION.store(true, Ordering::Relaxed); for x in r.left()..r.right() { for y in r.top()..r.bottom() { diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 27b6f0c30..38517200b 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -9,19 +9,30 @@ homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" [dependencies] -yazi-config = { path = "../yazi-config", version = "0.1.5" } -yazi-core = { path = "../yazi-core", version = "0.1.5" } -yazi-shared = { path = "../yazi-shared", version = "0.1.5" } +yazi-adaptor = { path = "../yazi-adaptor", version = "0.1.5" } +yazi-config = { path = "../yazi-config", version = "0.1.5" } +yazi-shared = { path = "../yazi-shared", version = "0.1.5" } # External dependencies ansi-to-tui = "^3" anyhow = "^1" +crossterm = "^0" +futures = "^0" +libc = "^0" +md-5 = "^0" +parking_lot = "^0" ratatui = "^0" +serde = "^1" +serde_json = "^1" +syntect = { version = "^5", default-features = false, features = [ "parsing", "default-themes", "plist-load", "regex-onig" ] } +tokio = { version = "^1", features = [ "parking_lot", "rt-multi-thread" ] } +tokio-util = "^0" tracing = { version = "^0", features = [ "max_level_debug", "release_max_level_warn" ] } unicode-width = "^0" +yazi-prebuild = "^0" -[target.'cfg(any(target_arch = "riscv64", target_arch="loongarch64"))'.dependencies] -mlua = { version = "^0", features = [ "lua52", "vendored", "serialize" ] } +[target.'cfg(any(target_arch="riscv64", target_arch="loongarch64"))'.dependencies] +mlua = { version = "^0", features = [ "lua52", "vendored", "serialize", "macros", "async" ] } -[target.'cfg(not(any(target_arch = "riscv64", target_arch="loongarch64")))'.dependencies] -mlua = { version = "^0", features = [ "luajit52", "vendored", "serialize" ] } +[target.'cfg(not(any(target_arch="riscv64", target_arch="loongarch64")))'.dependencies] +mlua = { version = "^0", features = [ "luajit52", "vendored", "serialize", "macros", "async" ] } diff --git a/yazi-plugin/preset/components/current.lua b/yazi-plugin/preset/components/current.lua new file mode 100644 index 000000000..24fa14d44 --- /dev/null +++ b/yazi-plugin/preset/components/current.lua @@ -0,0 +1,31 @@ +Current = { + area = ui.Rect.default, +} + +function Current:render(area) + self.area = area + + local markers = {} + local items = {} + for i, f in ipairs(Folder:by_kind(Folder.CURRENT).window) do + local name = Folder:highlighted_name(f) + + -- Highlight hovered file + local item = ui.ListItem(ui.Line { Folder:icon(f), table.unpack(name) }) + if f:is_hovered() then + item = item:style(THEME.manager.hovered) + else + item = item:style(f:style()) + end + items[#items + 1] = item + + -- Mark yanked/selected files + local yanked = f:is_yanked() + if yanked ~= 0 then + markers[#markers + 1] = { i, yanked } + elseif f:is_selected() then + markers[#markers + 1] = { i, 3 } + end + end + return ya.flat { ui.List(area, items), Folder:linemode(area), Folder:markers(area, markers) } +end diff --git a/yazi-plugin/preset/components/folder.lua b/yazi-plugin/preset/components/folder.lua index 3cf687ad9..9c74278f1 100644 --- a/yazi-plugin/preset/components/folder.lua +++ b/yazi-plugin/preset/components/folder.lua @@ -55,11 +55,11 @@ function Folder:linemode(area) local spans = { ui.Span(" ") } if mode == "size" then local size = f:size() - spans[#spans + 1] = ui.Span(size and utils.readable_size(size) or "") + spans[#spans + 1] = ui.Span(size and ya.readable_size(size) or "") elseif mode == "mtime" then - spans[#spans + 1] = ui.Span(os.date("%y-%m-%d %H:%M", f.modified)) + spans[#spans + 1] = ui.Span(os.date("%y-%m-%d %H:%M", f.cha.modified)) elseif mode == "permissions" then - spans[#spans + 1] = ui.Span(f:permissions() or "") + spans[#spans + 1] = ui.Span(f.cha:permissions() or "") end spans[#spans + 1] = ui.Span(" ") @@ -109,80 +109,3 @@ function Folder:markers(area, markers) append(last) return elements end - -function Folder:parent(area) - local folder = self:by_kind(self.PARENT) - if folder == nil then - return {} - end - - local items = {} - for _, f in ipairs(folder.window) do - local item = ui.ListItem(ui.Line { self:icon(f), ui.Span(f.name) }) - if f:is_hovered() then - item = item:style(THEME.manager.hovered) - else - item = item:style(f:style()) - end - - items[#items + 1] = item - end - - return { ui.List(area, items) } -end - -function Folder:current(area) - local markers = {} - local items = {} - for i, f in ipairs(self:by_kind(self.CURRENT).window) do - local name = self:highlighted_name(f) - - -- Highlight hovered file - local item = ui.ListItem(ui.Line { self:icon(f), table.unpack(name) }) - if f:is_hovered() then - item = item:style(THEME.manager.hovered) - else - item = item:style(f:style()) - end - items[#items + 1] = item - - -- Mark yanked/selected files - local yanked = f:is_yanked() - if yanked ~= 0 then - markers[#markers + 1] = { i, yanked } - elseif f:is_selected() then - markers[#markers + 1] = { i, 3 } - end - end - return utils.flat { ui.List(area, items), self:linemode(area), self:markers(area, markers) } -end - -function Folder:preview(area) - local folder = self:by_kind(self.PREVIEW) - if folder == nil then - return {} - end - - local items = {} - for _, f in ipairs(folder.window) do - local item = ui.ListItem(ui.Line { self:icon(f), ui.Span(f.name) }) - if f:is_hovered() then - item = item:style(THEME.manager.preview_hovered) - else - item = item:style(f:style()) - end - items[#items + 1] = item - end - - return { ui.List(area, items) } -end - -function Folder:render(area, args) - if args.kind == self.PARENT then - return self:parent(area) - elseif args.kind == self.CURRENT then - return self:current(area) - elseif args.kind == self.PREVIEW then - return self:preview(area) - end -end diff --git a/yazi-plugin/preset/components/header.lua b/yazi-plugin/preset/components/header.lua index 08342425b..c1f082671 100644 --- a/yazi-plugin/preset/components/header.lua +++ b/yazi-plugin/preset/components/header.lua @@ -1,13 +1,15 @@ -Header = {} +Header = { + area = ui.Rect.default, +} function Header:cwd() local cwd = cx.active.current.cwd local span if not cwd.is_search then - span = ui.Span(utils.readable_path(tostring(cwd))) + span = ui.Span(ya.readable_path(tostring(cwd))) else - span = ui.Span(string.format("%s (search: %s)", utils.readable_path(tostring(cwd)), cwd.frag)) + span = ui.Span(string.format("%s (search: %s)", ya.readable_path(tostring(cwd)), cwd.frag)) end return span:style(THEME.manager.cwd) end @@ -17,7 +19,7 @@ function Header:tabs() for i = 1, #cx.tabs do local text = i if THEME.manager.tab_width > 2 then - text = utils.truncate(text .. " " .. cx.tabs[i]:name(), THEME.manager.tab_width) + text = ya.truncate(text .. " " .. cx.tabs[i]:name(), THEME.manager.tab_width) end if i == cx.tabs.idx + 1 then spans[#spans + 1] = ui.Span(" " .. text .. " "):style(THEME.manager.tab_active) @@ -29,6 +31,8 @@ function Header:tabs() end function Header:render(area) + self.area = area + local chunks = ui.Layout() :direction(ui.Direction.HORIZONTAL) :constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) }) diff --git a/yazi-plugin/preset/components/manager.lua b/yazi-plugin/preset/components/manager.lua index c8e11ec1a..2bb298b6f 100644 --- a/yazi-plugin/preset/components/manager.lua +++ b/yazi-plugin/preset/components/manager.lua @@ -1,6 +1,10 @@ -Manager = {} +Manager = { + area = ui.Rect.default, +} function Manager:render(area) + self.area = area + local chunks = ui.Layout() :direction(ui.Direction.HORIZONTAL) :constraints({ @@ -10,16 +14,16 @@ function Manager:render(area) }) :split(area) - return utils.flat { + return ya.flat { -- Borders ui.Bar(chunks[1], ui.Position.RIGHT):symbol(THEME.manager.border_symbol):style(THEME.manager.border_style), ui.Bar(chunks[3], ui.Position.LEFT):symbol(THEME.manager.border_symbol):style(THEME.manager.border_style), -- Parent - Folder:render(chunks[1]:padding(ui.Padding.x(1)), { kind = Folder.PARENT }), + Parent:render(chunks[1]:padding(ui.Padding.x(1))), -- Current - Folder:render(chunks[2], { kind = Folder.CURRENT }), + Current:render(chunks[2]), -- Preview - ui.Base(chunks[3]:padding(ui.Padding.x(1)), ui.Base.PREVIEW), + Preview:render(chunks[3]:padding(ui.Padding.x(1))), } end diff --git a/yazi-plugin/preset/components/parent.lua b/yazi-plugin/preset/components/parent.lua new file mode 100644 index 000000000..6c29bd812 --- /dev/null +++ b/yazi-plugin/preset/components/parent.lua @@ -0,0 +1,26 @@ +Parent = { + area = ui.Rect.default, +} + +function Parent:render(area) + self.area = area + + local folder = Folder:by_kind(Folder.PARENT) + if folder == nil then + return {} + end + + local items = {} + for _, f in ipairs(folder.window) do + local item = ui.ListItem(ui.Line { Folder:icon(f), ui.Span(f.name) }) + if f:is_hovered() then + item = item:style(THEME.manager.hovered) + else + item = item:style(f:style()) + end + + items[#items + 1] = item + end + + return { ui.List(area, items) } +end diff --git a/yazi-plugin/preset/components/preview.lua b/yazi-plugin/preset/components/preview.lua new file mode 100644 index 000000000..dd6b73cce --- /dev/null +++ b/yazi-plugin/preset/components/preview.lua @@ -0,0 +1,8 @@ +Preview = { + area = ui.Rect.default, +} + +function Preview:render(area) + self.area = area + return {} +end diff --git a/yazi-plugin/preset/components/status.lua b/yazi-plugin/preset/components/status.lua index de98ac6dc..0992d6fd3 100644 --- a/yazi-plugin/preset/components/status.lua +++ b/yazi-plugin/preset/components/status.lua @@ -1,4 +1,6 @@ -Status = {} +Status = { + area = ui.Rect.default, +} function Status.style() if cx.active.mode.is_select then @@ -31,7 +33,7 @@ function Status:size() local style = self.style() return ui.Line { - ui.Span(" " .. utils.readable_size(h:size() or h.length) .. " "):fg(style.bg):bg(THEME.status.separator_style.bg), + ui.Span(" " .. ya.readable_size(h:size() or h.cha.length) .. " "):fg(style.bg):bg(THEME.status.separator_style.bg), ui.Span(THEME.status.separator_close):fg(THEME.status.separator_style.fg), } end @@ -51,7 +53,7 @@ function Status:permissions() return ui.Span("") end - local perm = h:permissions() + local perm = h.cha:permissions() if perm == nil then return ui.Span("") end @@ -139,6 +141,8 @@ function Status:progress(area, offset) end function Status:render(area) + self.area = area + local left = ui.Line { self:mode(), self:size(), self:name() } local right = ui.Line { self:permissions(), self:percentage(), self:position() } local progress = self:progress(area, right:width()) diff --git a/yazi-plugin/preset/inspect/inspect.lua b/yazi-plugin/preset/inspect/inspect.lua index 2c80726af..be86016ec 100644 --- a/yazi-plugin/preset/inspect/inspect.lua +++ b/yazi-plugin/preset/inspect/inspect.lua @@ -302,9 +302,9 @@ setmetatable(inspect, { __call = function(_, root, options) return inspect.inspect(root, options) end, }) -yazi = yazi or {} -yazi.inspect = inspect -yazi.print = function(...) +ya = ya or {} +ya.inspect = inspect +ya.print = function(...) local args = { ... } for i = 1, #args do print(inspect(args[i])) diff --git a/yazi-plugin/preset/plugins/archive.lua b/yazi-plugin/preset/plugins/archive.lua new file mode 100644 index 000000000..298c39f3d --- /dev/null +++ b/yazi-plugin/preset/plugins/archive.lua @@ -0,0 +1,21 @@ +local M = {} + +function M:peek() + local _, bound = ya.preview_archive(self) + if bound then + ya.manager_emit("peek", { tostring(bound), only_if = tostring(self.file.url), upper_bound = "" }) + end +end + +function M:seek(units) + local h = cx.active.current.hovered + if h and h.url == self.file.url then + local step = math.floor(units * self.area.h / 10) + ya.manager_emit("peek", { + tostring(math.max(0, cx.active.preview.skip + step)), + only_if = tostring(self.file.url), + }) + end +end + +return M diff --git a/yazi-plugin/preset/plugins/code.lua b/yazi-plugin/preset/plugins/code.lua new file mode 100644 index 000000000..cba031e6d --- /dev/null +++ b/yazi-plugin/preset/plugins/code.lua @@ -0,0 +1,21 @@ +local M = {} + +function M:peek() + local _, bound = ya.preview_code(self) + if bound then + ya.manager_emit("peek", { tostring(bound), only_if = tostring(self.file.url), upper_bound = "" }) + end +end + +function M:seek(units) + local h = cx.active.current.hovered + if h and h.url == self.file.url then + local step = math.floor(units * self.area.h / 10) + ya.manager_emit("peek", { + tostring(math.max(0, cx.active.preview.skip + step)), + only_if = tostring(self.file.url), + }) + end +end + +return M diff --git a/yazi-plugin/preset/plugins/folder.lua b/yazi-plugin/preset/plugins/folder.lua new file mode 100644 index 000000000..6ebf14aa9 --- /dev/null +++ b/yazi-plugin/preset/plugins/folder.lua @@ -0,0 +1,39 @@ +local M = {} + +function M:peek() + local folder = Folder:by_kind(Folder.PREVIEW) + if folder == nil or folder.cwd ~= self.file.url then + return {} + end + + local bound = math.max(0, #folder.files - self.area.h) + if self.skip > bound then + ya.manager_emit("peek", { tostring(bound), only_if = tostring(self.file.url), upper_bound = "" }) + end + + local items = {} + for _, f in ipairs(folder.window) do + local item = ui.ListItem(ui.Line { Folder:icon(f), ui.Span(f.name) }) + if f:is_hovered() then + item = item:style(THEME.manager.preview_hovered) + else + item = item:style(f:style()) + end + items[#items + 1] = item + end + ya.preview_widgets(self, { ui.List(self.area, items) }) +end + +function M:seek(units) + local folder = Folder:by_kind(Folder.PREVIEW) + if folder and folder.cwd == self.file.url then + local step = math.floor(units * self.area.h / 10) + local bound = math.max(0, #folder.files - self.area.h) + ya.manager_emit("peek", { + tostring(ya.clamp(0, cx.active.preview.skip + step, bound)), + only_if = tostring(self.file.url), + }) + end +end + +return M diff --git a/yazi-plugin/preset/plugins/image.lua b/yazi-plugin/preset/plugins/image.lua new file mode 100644 index 000000000..c787acd87 --- /dev/null +++ b/yazi-plugin/preset/plugins/image.lua @@ -0,0 +1,22 @@ +local M = {} + +function M:cache() return ya.cache_file(self.file.url .. tostring(self.file.cha.modified)) end + +function M:peek() + local cache = self:cache() + ya.image_show(fs.symlink_metadata(cache) and cache or self.file.url, self.area) + ya.preview_widgets(self, {}) +end + +function M:seek() end + +function M:preload() + local cache = self:cache() + if fs.symlink_metadata(cache) then + return 1 + end + + return ya.image_precache(self.file.url, cache) and 1 or 2 +end + +return M diff --git a/yazi-plugin/preset/plugins/json.lua b/yazi-plugin/preset/plugins/json.lua new file mode 100644 index 000000000..c6ec9c59a --- /dev/null +++ b/yazi-plugin/preset/plugins/json.lua @@ -0,0 +1,49 @@ +local M = {} + +function M:peek() + local limit = self.area.h + local child = Command.new("jq") + :args({ + "-C", + "--tab", + ".", + tostring(self.file.url), + }) + :stdout(Command.PIPED) + :stderr(Command.PIPED) + :spawn() + + local i, lines = 0, "" + repeat + local code, next = child:read_line() + if code ~= 0 then + break + end + + i = i + 1 + if i > self.skip then + lines = lines .. next + end + until i >= self.skip + limit + + child:start_kill() + if self.skip > 0 and i < self.skip + limit then + ya.manager_emit("peek", { tostring(math.max(0, i - limit)), only_if = tostring(self.file.url), upper_bound = "" }) + else + lines = lines:gsub("\t", string.rep(" ", PREVIEW.tab_size)) + ya.preview_widgets(self, { ui.Paragraph.parse(self.area, lines) }) + end +end + +function M:seek(units) + local h = cx.active.current.hovered + if h and h.url == self.file.url then + local step = math.floor(units * self.area.h / 10) + ya.manager_emit("peek", { + tostring(math.max(0, cx.active.preview.skip + step)), + only_if = tostring(self.file.url), + }) + end +end + +return M diff --git a/yazi-plugin/preset/plugins/mime.lua b/yazi-plugin/preset/plugins/mime.lua new file mode 100644 index 000000000..d7420bf9c --- /dev/null +++ b/yazi-plugin/preset/plugins/mime.lua @@ -0,0 +1,35 @@ +local M = {} + +function M:preload() + local command = Command.new("file"):arg("--mime-type"):stdout(Command.PIPED):stderr(Command.PIPED) + if ya.target_family() == "windows" then + command:arg("-b") + else + command:arg("-bL") + end + + local urls = {} + for _, file in ipairs(self.files) do + urls[#urls + 1] = tostring(file.url) + end + + local i, mimes = 1, {} + local output = command:args(urls):output() + for line in output.stdout:gmatch("[^\r\n]+") do + if i > #urls then + break + end + if ya.mime_valid(line) then + mimes[urls[i]] = line + end + i = i + 1 + end + + if #mimes then + ya.manager_emit("update_mimetype", {}, mimes) + return 3 + end + return 2 +end + +return M diff --git a/yazi-plugin/preset/plugins/noop.lua b/yazi-plugin/preset/plugins/noop.lua new file mode 100644 index 000000000..ad93ad7f5 --- /dev/null +++ b/yazi-plugin/preset/plugins/noop.lua @@ -0,0 +1,9 @@ +local M = {} + +function M:peek() end + +function M:seek() end + +function M:preload() return 1 end + +return M diff --git a/yazi-plugin/preset/plugins/pdf.lua b/yazi-plugin/preset/plugins/pdf.lua new file mode 100644 index 000000000..fd817395a --- /dev/null +++ b/yazi-plugin/preset/plugins/pdf.lua @@ -0,0 +1,43 @@ +local M = {} + +function M:cache() return ya.cache_file(self.file.url .. self.skip .. tostring(self.file.cha.modified)) end + +function M:peek() + if self:preload() == 1 then + ya.image_show(self:cache(), self.area) + ya.preview_widgets(self, {}) + end +end + +function M:seek(units) + local h = cx.active.current.hovered + if h and h.url == self.file.url then + local step = ya.clamp(-1, units, 1) + ya.manager_emit("peek", { tostring(math.max(0, cx.active.preview.skip + step)), only_if = tostring(self.file.url) }) + end +end + +function M:preload() + local cache = self:cache() + if fs.symlink_metadata(cache) then + return 1 + end + + local output = Command.new("pdftoppm") + :args({ "-singlefile", "-jpeg", "-jpegopt", "quality=75", "-f", tostring(self.skip + 1), tostring(self.file.url) }) + :stdout(Command.PIPED) + :stderr(Command.PIPED) + :output() + + if not output.status:success() then + local pages = tonumber(output.stderr:match("the last page %((%d+)%)")) or 0 + if self.skip > 0 and pages > 0 then + ya.manager_emit("peek", { tostring(math.max(0, pages - 1)), only_if = tostring(self.file.url), upper_bound = "" }) + end + return 0 + end + + return fs.write(cache, output.stdout) and 1 or 2 +end + +return M diff --git a/yazi-plugin/preset/plugins/video.lua b/yazi-plugin/preset/plugins/video.lua new file mode 100644 index 000000000..a5a410739 --- /dev/null +++ b/yazi-plugin/preset/plugins/video.lua @@ -0,0 +1,55 @@ +local M = {} + +function M:cache() return ya.cache_file(self.file.url .. self.skip .. tostring(self.file.cha.modified)) end + +function M:peek() + if self:preload() == 1 then + ya.image_show(self:cache(), self.area) + ya.preview_widgets(self, {}) + end +end + +function M:seek(units) + local h = cx.active.current.hovered + if h and h.url == self.file.url then + ya.manager_emit("peek", { + tostring(math.max(0, cx.active.preview.skip + units)), + only_if = tostring(self.file.url), + }) + end +end + +function M:preload() + local percentage = 5 + self.skip + if percentage > 95 then + ya.manager_emit("peek", { "90", only_if = tostring(self.file.url), upper_bound = "" }) + return 2 + end + + local cache = self:cache() + if fs.symlink_metadata(cache) then + return 1 + end + + local status = Command.new("ffmpegthumbnailer") + :args({ + "-q", + "6", + "-c", + "jpeg", + "-i", + tostring(self.file.url), + "-o", + tostring(cache), + "-t", + tostring(percentage), + "-s", + tostring(PREVIEW.max_width), + }) + :spawn() + :wait() + + return status:success() and 1 or 2 +end + +return M diff --git a/yazi-plugin/preset/ui.lua b/yazi-plugin/preset/ui.lua index b408de4b4..0439e87c4 100644 --- a/yazi-plugin/preset/ui.lua +++ b/yazi-plugin/preset/ui.lua @@ -1,4 +1,5 @@ ui = { + -- FIXME: merge those three into their own modules Alignment = { LEFT = 0, CENTER = 1, @@ -16,33 +17,6 @@ ui = { LEFT = 8, ALL = 15, }, - - Base = setmetatable({ - PREVIEW = 0, - }, { - __call = function(_, ...) return ui.Base.new(...) end, - }), - Border = setmetatable({ - PLAIN = 0, - ROUNDED = 1, - DOUBLE = 2, - THICK = 3, - QUADRANT_INSIDE = 4, - QUADRANT_OUTSIDE = 5, - }, { - __call = function(_, ...) return ui.Border.new(...) end, - }), - Padding = setmetatable({ - left = function(left) return ui.Padding.new(left, 0, 0, 0) end, - right = function(right) return ui.Padding.new(0, right, 0, 0) end, - top = function(top) return ui.Padding.new(0, 0, top, 0) end, - bottom = function(bottom) return ui.Padding.new(0, 0, 0, bottom) end, - x = function(x) return ui.Padding.new(x, x, 0, 0) end, - y = function(y) return ui.Padding.new(0, 0, y, y) end, - xy = function(xy) return ui.Padding.new(xy, xy, xy, xy) end, - }, { - __call = function(_, ...) return ui.Padding.new(...) end, - }), } function ui.highlight_ranges(s, ranges) diff --git a/yazi-plugin/preset/utils.lua b/yazi-plugin/preset/ya.lua similarity index 57% rename from yazi-plugin/preset/utils.lua rename to yazi-plugin/preset/ya.lua index 7f5121c0c..f93568c9e 100644 --- a/yazi-plugin/preset/utils.lua +++ b/yazi-plugin/preset/ya.lua @@ -1,8 +1,8 @@ table.unpack = table.unpack or unpack -utils = utils or {} +ya = ya or {} -function utils.clamp(min, x, max) +function ya.clamp(min, x, max) if x < min then return min elseif x > max then @@ -12,11 +12,11 @@ function utils.clamp(min, x, max) end end -function utils.flat(t) +function ya.flat(t) local r = {} for _, v in ipairs(t) do if type(v) == "table" then - for _, v2 in ipairs(utils.flat(v)) do + for _, v2 in ipairs(ya.flat(v)) do r[#r + 1] = v2 end else @@ -26,9 +26,23 @@ function utils.flat(t) return r end -function utils.basename(str) return string.gsub(str, "(.*[/\\])(.*)", "%2") end +function ya.sync(f) + ya.SYNC_CALLS = ya.SYNC_CALLS + 1 + if ya.SYNC_BLOCK == ya.SYNC_CALLS then + ya.SYNC_ENTRY = f + end + + if ya.SYNC_ON then + return f + end + + local calls = ya.SYNC_CALLS + return function(...) return plugin_retrieve(ya.PLUGIN_NAME, calls, ...) end +end + +function ya.basename(str) return string.gsub(str, "(.*[/\\])(.*)", "%2") end -function utils.readable_size(size) +function ya.readable_size(size) local units = { "B", "KB", "MB", "GB", "TB", "PB", "EB" } local i = 1 while size > 1024.0 and i < #units do @@ -38,7 +52,7 @@ function utils.readable_size(size) return string.format("%.1f %s", size, units[i]) end -function utils.readable_path(path) +function ya.readable_path(path) local home = os.getenv("HOME") or os.getenv("USERPROFILE") if home == nil then return path diff --git a/yazi-plugin/src/bindings/active.rs b/yazi-plugin/src/bindings/active.rs deleted file mode 100644 index 9075cf269..000000000 --- a/yazi-plugin/src/bindings/active.rs +++ /dev/null @@ -1,134 +0,0 @@ -use mlua::{AnyUserData, MetaMethod, UserDataFields, UserDataMethods, Value}; -use yazi_config::MANAGER; -use yazi_core::Ctx; - -use super::Url; -use crate::LUA; - -pub struct Active<'a, 'b> { - scope: &'b mlua::Scope<'a, 'a>, - - cx: &'a yazi_core::Ctx, - inner: &'a yazi_core::tab::Tab, -} - -impl<'a, 'b> Active<'a, 'b> { - pub(crate) fn init() -> mlua::Result<()> { - LUA.register_userdata_type::(|reg| { - reg.add_field_method_get("is_select", |_, me| Ok(me.is_select())); - reg.add_field_method_get("is_unset", |_, me| Ok(me.is_unset())); - reg.add_field_method_get("is_visual", |_, me| Ok(me.is_visual())); - reg.add_method("pending", |_, me, (idx, state): (usize, bool)| Ok(me.pending(idx, state))); - - reg.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.to_string())); - })?; - - LUA.register_userdata_type::(|reg| { - reg.add_field_method_get("sort_by", |_, me| Ok(me.sort_by.to_string())); - reg.add_field_method_get("sort_sensitive", |_, me| Ok(me.sort_sensitive)); - reg.add_field_method_get("sort_reverse", |_, me| Ok(me.sort_reverse)); - reg.add_field_method_get("sort_dir_first", |_, me| Ok(me.sort_dir_first)); - - reg.add_field_method_get("linemode", |_, me| Ok(me.linemode.to_owned())); - reg.add_field_method_get("show_hidden", |_, me| Ok(me.show_hidden)); - })?; - - LUA.register_userdata_type::(|reg| { - reg.add_field_method_get("cwd", |_, me| Ok(Url::from(&me.cwd))); - reg.add_field_method_get("offset", |_, me| Ok(me.offset)); - reg.add_field_method_get("cursor", |_, me| Ok(me.cursor)); - - reg.add_field_function_get("window", |_, me| me.named_user_value::("window")); - reg.add_field_function_get("files", |_, me| me.named_user_value::("files")); - reg.add_field_function_get("hovered", |_, me| me.named_user_value::("hovered")); - })?; - - LUA.register_userdata_type::(|reg| { - reg.add_field_function_get("folder", |_, me| me.named_user_value::("folder")); - })?; - - Ok(()) - } - - pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, cx: &'a Ctx) -> Self { - Self { scope, cx, inner: cx.manager.active() } - } - - pub(crate) fn make(&self) -> mlua::Result> { - let ud = self.scope.create_any_userdata_ref(self.inner)?; - ud.set_named_user_value("mode", self.scope.create_any_userdata_ref(&self.inner.mode)?)?; - ud.set_named_user_value("conf", self.scope.create_any_userdata_ref(&self.inner.conf)?)?; - ud.set_named_user_value( - "parent", - self.inner.parent.as_ref().and_then(|p| self.folder(p, None).ok()), - )?; - ud.set_named_user_value("current", self.folder(&self.inner.current, None)?)?; - ud.set_named_user_value("preview", self.preview(self.inner)?)?; - - Ok(ud) - } - - pub(crate) fn folder( - &self, - inner: &'a yazi_core::tab::Folder, - window: Option<(usize, usize)>, - ) -> mlua::Result> { - let window = window.unwrap_or_else(|| (inner.offset, MANAGER.layout.folder_height())); - - let ud = self.scope.create_any_userdata_ref(inner)?; - ud.set_named_user_value( - "window", - inner - .files - .iter() - .skip(window.0) - .take(window.1) - .enumerate() - .filter_map(|(i, f)| self.file(i, f, inner).ok()) - .collect::>(), - )?; - ud.set_named_user_value("files", self.files(&inner.files)?)?; - // TODO: remove this - ud.set_named_user_value( - "hovered", - inner.hovered().and_then(|h| self.file(999, h, inner).ok()), - )?; - - Ok(ud) - } - - fn files(&self, inner: &'a yazi_core::files::Files) -> mlua::Result> { - self.scope.create_any_userdata_ref(inner) - } - - fn file( - &self, - idx: usize, - inner: &'a yazi_shared::fs::File, - folder: &'a yazi_core::tab::Folder, - ) -> mlua::Result> { - let ud = self.scope.create_any_userdata_ref(inner)?; - ud.set_named_user_value("idx", idx)?; - ud.set_named_user_value("folder", self.scope.create_any_userdata_ref(folder)?)?; - ud.set_named_user_value("manager", self.scope.create_any_userdata_ref(&self.cx.manager)?)?; - - Ok(ud) - } - - fn preview(&self, tab: &'a yazi_core::tab::Tab) -> mlua::Result> { - let inner = &tab.preview; - - let ud = self.scope.create_any_userdata_ref(inner)?; - ud.set_named_user_value( - "folder", - inner - .lock - .as_ref() - .filter(|l| l.is_folder()) - .and_then(|l| tab.history(&l.url).map(|f| (l, f))) - .and_then(|(l, f)| self.folder(f, Some((l.skip, MANAGER.layout.preview_height()))).ok()), - )?; - - Ok(ud) - } -} diff --git a/yazi-plugin/src/bindings/bindings.rs b/yazi-plugin/src/bindings/bindings.rs index 76c3d7af8..3b04e52f7 100644 --- a/yazi-plugin/src/bindings/bindings.rs +++ b/yazi-plugin/src/bindings/bindings.rs @@ -1,8 +1,3 @@ -pub fn init() -> mlua::Result<()> { - super::active::Active::init()?; - super::files::Files::init()?; - super::tabs::Tabs::init()?; - super::tasks::Tasks::init()?; - - Ok(()) +pub trait Cast { + fn cast(lua: &mlua::Lua, data: T) -> mlua::Result; } diff --git a/yazi-plugin/src/bindings/cha.rs b/yazi-plugin/src/bindings/cha.rs new file mode 100644 index 000000000..6589466d5 --- /dev/null +++ b/yazi-plugin/src/bindings/cha.rs @@ -0,0 +1,51 @@ +use std::time::UNIX_EPOCH; + +use mlua::{AnyUserData, Lua, UserDataFields, UserDataMethods}; + +use super::Cast; + +pub struct Cha; + +impl Cha { + pub fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("is_link", |_, me| Ok(me.is_link())); + reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden())); + + // Metadata + reg.add_field_method_get("is_dir", |_, me| Ok(me.is_dir())); + reg.add_field_method_get("is_symlink", |_, me| Ok(me.is_link())); + #[cfg(unix)] + { + reg.add_field_method_get("is_block_device", |_, me| Ok(me.is_block_device())); + reg.add_field_method_get("is_char_device", |_, me| Ok(me.is_char_device())); + reg.add_field_method_get("is_fifo", |_, me| Ok(me.is_fifo())); + reg.add_field_method_get("is_socket", |_, me| Ok(me.is_socket())); + } + reg.add_field_method_get("length", |_, me| Ok(me.len)); + reg.add_field_method_get("created", |_, me| { + Ok(me.created.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) + }); + reg.add_field_method_get("modified", |_, me| { + Ok(me.modified.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) + }); + reg.add_field_method_get("accessed", |_, me| { + Ok(me.accessed.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) + }); + reg.add_method("permissions", |_, me, ()| { + Ok( + #[cfg(unix)] + Some(yazi_shared::fs::permissions(me.permissions)), + #[cfg(windows)] + None::, + ) + }); + })?; + + Ok(()) + } +} + +impl> Cast for Cha { + fn cast(lua: &Lua, data: T) -> mlua::Result { lua.create_any_userdata(data.into()) } +} diff --git a/yazi-plugin/src/bindings/file.rs b/yazi-plugin/src/bindings/file.rs new file mode 100644 index 000000000..20dc60952 --- /dev/null +++ b/yazi-plugin/src/bindings/file.rs @@ -0,0 +1,33 @@ +use mlua::{AnyUserData, Lua, UserDataFields, UserDataRef, UserDataRegistry}; + +use super::{Cast, Cha, Url}; + +pub type FileRef<'lua> = UserDataRef<'lua, yazi_shared::fs::File>; + +pub struct File; + +impl File { + pub fn register( + lua: &Lua, + f: impl FnOnce(&mut UserDataRegistry), + ) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("url", |lua, me| Url::cast(lua, me.url.clone())); + reg.add_field_method_get("cha", |lua, me| Cha::cast(lua, me.cha)); + reg.add_field_method_get("link_to", |lua, me| { + me.link_to.as_ref().cloned().map(|u| Url::cast(lua, u)).transpose() + }); + + // Extension + reg.add_field_method_get("name", |lua, me| { + me.url.file_name().map(|n| lua.create_string(n.as_encoded_bytes())).transpose() + }); + + f(reg); + }) + } +} + +impl> Cast for File { + fn cast(lua: &Lua, data: T) -> mlua::Result { lua.create_any_userdata(data.into()) } +} diff --git a/yazi-plugin/src/bindings/files.rs b/yazi-plugin/src/bindings/files.rs deleted file mode 100644 index 18327ffd0..000000000 --- a/yazi-plugin/src/bindings/files.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::time::UNIX_EPOCH; - -use mlua::{AnyUserData, IntoLua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef}; -use yazi_config::THEME; - -use super::{Range, Url}; -use crate::{layout::Style, LUA}; - -pub struct File(yazi_shared::fs::File); - -impl From<&yazi_shared::fs::File> for File { - fn from(value: &yazi_shared::fs::File) -> Self { Self(value.clone()) } -} - -impl UserData for File { - fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("url", |_, me| Ok(Url::from(&me.0.url))); - fields.add_field_method_get("link_to", |_, me| Ok(me.0.link_to().map(Url::from))); - fields.add_field_method_get("is_link", |_, me| Ok(me.0.is_link())); - fields.add_field_method_get("is_hidden", |_, me| Ok(me.0.is_hidden())); - } -} - -pub struct Files; - -impl Files { - pub(crate) fn init() -> mlua::Result<()> { - LUA.register_userdata_type::(|reg| { - reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); - - reg.add_meta_function(MetaMethod::Pairs, |lua, me: AnyUserData| { - let iter = lua.create_function(|lua, (me, i): (AnyUserData, usize)| { - let files = me.borrow::()?; - let i = i + 1; - Ok(if i > files.len() { - mlua::Variadic::new() - } else { - mlua::Variadic::from_iter([i.into_lua(lua)?, File::from(&files[i - 1]).into_lua(lua)?]) - }) - })?; - Ok((iter, me, 0)) - }); - - reg.add_function("slice", |_, (me, skip, take): (AnyUserData, usize, usize)| { - let files = me.borrow::()?; - Ok(files.iter().skip(skip).take(take).map(File::from).collect::>()) - }); - })?; - - LUA.register_userdata_type::(|reg| { - reg.add_field_method_get("url", |_, me| Ok(Url::from(&me.url))); - reg.add_field_method_get("link_to", |_, me| Ok(me.link_to().map(Url::from))); - reg.add_field_method_get("is_link", |_, me| Ok(me.is_link())); - reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden())); - - // Metadata - reg.add_field_method_get("is_dir", |_, me| Ok(me.is_dir())); - reg.add_field_method_get("is_symlink", |_, me| Ok(me.is_link())); - #[cfg(unix)] - { - reg.add_field_method_get("is_block_device", |_, me| Ok(me.is_block_device())); - reg.add_field_method_get("is_char_device", |_, me| Ok(me.is_char_device())); - reg.add_field_method_get("is_fifo", |_, me| Ok(me.is_fifo())); - reg.add_field_method_get("is_socket", |_, me| Ok(me.is_socket())); - } - reg.add_field_method_get("length", |_, me| Ok(me.len)); - reg.add_field_method_get("created", |_, me| { - Ok(me.created.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) - }); - reg.add_field_method_get("modified", |_, me| { - Ok(me.modified.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) - }); - reg.add_field_method_get("accessed", |_, me| { - Ok(me.accessed.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) - }); - reg.add_method("permissions", |_, me, ()| { - Ok( - #[cfg(unix)] - Some(yazi_shared::fs::permissions(me.permissions)), - #[cfg(windows)] - None::, - ) - }); - - // Extension - reg.add_field_method_get("name", |_, me| { - Ok(me.url.file_name().map(|n| n.to_string_lossy().to_string())) - }); - reg.add_function("size", |_, me: AnyUserData| { - let file = me.borrow::()?; - if !file.is_dir() { - return Ok(Some(file.len)); - } - - let folder = me.named_user_value::>("folder")?; - Ok(folder.files.sizes.get(&file.url).copied()) - }); - reg.add_function("mime", |_, me: AnyUserData| { - let manager = me.named_user_value::>("manager")?; - let file = me.borrow::()?; - Ok(manager.mimetype.get(&file.url).cloned()) - }); - reg.add_function("prefix", |_, me: AnyUserData| { - let folder = me.named_user_value::>("folder")?; - if !folder.cwd.is_search() { - return Ok(None); - } - - let file = me.borrow::()?; - let mut p = file.url.strip_prefix(&folder.cwd).unwrap_or(&file.url).components(); - p.next_back(); - Ok(Some(p.as_path().to_string_lossy().to_string())) - }); - reg.add_method("icon", |_, me, ()| { - Ok( - THEME - .icons - .iter() - .find(|&x| x.name.match_path(&me.url, Some(me.is_dir()))) - .map(|x| x.display.to_string()), - ) - }); - reg.add_function("style", |_, me: AnyUserData| { - let manager = me.named_user_value::>("manager")?; - let file = me.borrow::()?; - let mime = manager.mimetype.get(&file.url); - Ok( - THEME - .filetypes - .iter() - .find(|&x| x.matches(&file.url, mime, file.is_dir())) - .map(|x| Style::from(x.style)), - ) - }); - reg.add_function("is_hovered", |_, me: AnyUserData| { - let folder = me.named_user_value::>("folder")?; - let file = me.borrow::()?; - Ok(matches!(folder.hovered(), Some(f) if f.url == file.url)) - }); - reg.add_function("is_yanked", |_, me: AnyUserData| { - let manager = me.named_user_value::>("manager")?; - let file = me.borrow::()?; - Ok(if !manager.yanked.1.contains(&file.url) { - 0u8 - } else if manager.yanked.0 { - 2u8 - } else { - 1u8 - }) - }); - reg.add_function("is_selected", |_, me: AnyUserData| { - let manager = me.named_user_value::>("manager")?; - let folder = me.named_user_value::>("folder")?; - let file = me.borrow::()?; - - let selected = folder.files.is_selected(&file.url); - Ok(if !manager.active().mode.is_visual() { - selected - } else { - let idx: usize = me.named_user_value("idx")?; - manager.active().mode.pending(folder.offset + idx, selected) - }) - }); - reg.add_function("found", |lua, me: AnyUserData| { - let manager = me.named_user_value::>("manager")?; - let Some(finder) = &manager.active().finder else { - return Ok(None); - }; - - let file = me.borrow::()?; - if let Some(idx) = finder.matched_idx(&file.url) { - return Some( - lua.create_sequence_from([idx.into_lua(lua)?, finder.matched().len().into_lua(lua)?]), - ) - .transpose(); - } - Ok(None) - }); - reg.add_function("highlights", |_, me: AnyUserData| { - let manager = me.named_user_value::>("manager")?; - let Some(finder) = &manager.active().finder else { - return Ok(None); - }; - - let file = me.borrow::()?; - let Some(h) = file.name().and_then(|n| finder.highlighted(n)) else { - return Ok(None); - }; - - Ok(Some(h.into_iter().map(Range::from).collect::>())) - }); - })?; - - Ok(()) - } -} diff --git a/yazi-plugin/src/bindings/mod.rs b/yazi-plugin/src/bindings/mod.rs index a6ee7ff71..87b62e375 100644 --- a/yazi-plugin/src/bindings/mod.rs +++ b/yazi-plugin/src/bindings/mod.rs @@ -1,16 +1,19 @@ #![allow(clippy::module_inception)] -mod active; mod bindings; -mod files; -mod shared; -mod tabs; -mod tasks; +mod cha; +mod file; +mod range; +mod url; +mod window; -pub use active::*; pub use bindings::*; -#[allow(unused_imports)] -pub use files::*; -pub use shared::*; -pub use tabs::*; -pub use tasks::*; +pub use cha::*; +pub use file::*; +pub use range::*; +pub use url::*; +pub use window::*; + +pub trait Cast { + fn cast(lua: &mlua::Lua, data: T) -> mlua::Result; +} diff --git a/yazi-plugin/src/bindings/range.rs b/yazi-plugin/src/bindings/range.rs new file mode 100644 index 000000000..b1ee2a7ac --- /dev/null +++ b/yazi-plugin/src/bindings/range.rs @@ -0,0 +1,17 @@ +use mlua::IntoLua; + +pub struct Range(std::ops::Range); + +impl From> for Range { + fn from(value: std::ops::Range) -> Self { Self(value) } +} + +impl<'lua, T> IntoLua<'lua> for Range +where + T: IntoLua<'lua>, +{ + fn into_lua(self, lua: &'lua mlua::Lua) -> mlua::Result { + let tbl = lua.create_sequence_from([self.0.start, self.0.end])?; + tbl.into_lua(lua) + } +} diff --git a/yazi-plugin/src/bindings/shared.rs b/yazi-plugin/src/bindings/shared.rs deleted file mode 100644 index 348c96ef9..000000000 --- a/yazi-plugin/src/bindings/shared.rs +++ /dev/null @@ -1,43 +0,0 @@ -use mlua::{IntoLua, MetaMethod, UserData, UserDataRef}; - -// --- Range -pub struct Range(std::ops::Range); - -impl From> for Range { - fn from(value: std::ops::Range) -> Self { Self(value) } -} - -impl<'lua, T> IntoLua<'lua> for Range -where - T: IntoLua<'lua>, -{ - fn into_lua(self, lua: &'lua mlua::Lua) -> mlua::Result { - let tbl = lua.create_sequence_from([self.0.start, self.0.end])?; - tbl.into_lua(lua) - } -} - -// --- Url -pub struct Url(yazi_shared::fs::Url); - -impl From<&yazi_shared::fs::Url> for Url { - fn from(value: &yazi_shared::fs::Url) -> Self { Self(value.clone()) } -} - -impl UserData for Url { - fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("frag", |_, me| Ok(me.0.frag().map(ToOwned::to_owned))); - fields.add_field_method_get("is_regular", |_, me| Ok(me.0.is_regular())); - fields.add_field_method_get("is_search", |_, me| Ok(me.0.is_search())); - fields.add_field_method_get("is_archive", |_, me| Ok(me.0.is_archive())); - } - - fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_meta_function( - MetaMethod::Eq, - |_, (lhs, rhs): (UserDataRef, UserDataRef)| Ok(lhs.0 == rhs.0), - ); - - methods.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.0.display().to_string())); - } -} diff --git a/yazi-plugin/src/bindings/url.rs b/yazi-plugin/src/bindings/url.rs new file mode 100644 index 000000000..34ddd1e85 --- /dev/null +++ b/yazi-plugin/src/bindings/url.rs @@ -0,0 +1,38 @@ +use mlua::{AnyUserData, Lua, MetaMethod, UserDataFields, UserDataMethods, UserDataRef}; + +use super::Cast; + +pub type UrlRef<'lua> = UserDataRef<'lua, yazi_shared::fs::Url>; + +pub struct Url; + +impl Url { + pub fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("frag", |_, me| Ok(me.frag().map(ToOwned::to_owned))); + reg.add_field_method_get("is_regular", |_, me| Ok(me.is_regular())); + reg.add_field_method_get("is_search", |_, me| Ok(me.is_search())); + reg.add_field_method_get("is_archive", |_, me| Ok(me.is_archive())); + + reg.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me == &*other)); + + reg.add_meta_method(MetaMethod::ToString, |lua, me, ()| { + lua.create_string(me.as_os_str().as_encoded_bytes()) + }); + + reg.add_meta_method(MetaMethod::Concat, |lua, me, other: mlua::String| { + let me = me.as_os_str().as_encoded_bytes(); + let other = other.as_bytes(); + + let mut out = Vec::with_capacity(me.len() + other.len()); + out.extend_from_slice(me); + out.extend_from_slice(other); + lua.create_string(out) + }); + }) + } +} + +impl> Cast for Url { + fn cast(lua: &Lua, data: T) -> mlua::Result { lua.create_any_userdata(data.into()) } +} diff --git a/yazi-plugin/src/bindings/window.rs b/yazi-plugin/src/bindings/window.rs new file mode 100644 index 000000000..3fcb2cf58 --- /dev/null +++ b/yazi-plugin/src/bindings/window.rs @@ -0,0 +1,26 @@ +use mlua::{prelude::LuaUserDataFields, FromLua, UserData}; +use yazi_shared::term::Term; + +#[derive(Debug, Clone, Copy, FromLua)] +pub struct Window { + pub rows: u16, + pub cols: u16, + pub width: u16, + pub height: u16, +} + +impl Default for Window { + fn default() -> Self { + let ws = Term::size(); + Self { rows: ws.rows, cols: ws.columns, width: ws.width, height: ws.height } + } +} + +impl UserData for Window { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("rows", |_, me| Ok(me.rows)); + fields.add_field_method_get("cols", |_, me| Ok(me.cols)); + fields.add_field_method_get("width", |_, me| Ok(me.width)); + fields.add_field_method_get("height", |_, me| Ok(me.height)); + } +} diff --git a/yazi-plugin/src/cast.rs b/yazi-plugin/src/cast.rs new file mode 100644 index 000000000..f06e424f3 --- /dev/null +++ b/yazi-plugin/src/cast.rs @@ -0,0 +1,133 @@ +use std::collections::HashMap; + +use anyhow::bail; +use mlua::{prelude::{Lua, LuaResult}, AnyUserData, IntoLua, Value}; +use yazi_shared::OrderedFloat; + +use crate::elements::Renderable; + +pub fn cast_to_renderable(ud: AnyUserData) -> Option> { + if let Ok(c) = ud.take::() { + Some(Box::new(c)) + } else if let Ok(c) = ud.take::() { + Some(Box::new(c)) + } else if let Ok(c) = ud.take::() { + Some(Box::new(c)) + } else if let Ok(c) = ud.take::() { + Some(Box::new(c)) + } else if let Ok(c) = ud.take::() { + Some(Box::new(c)) + } else { + None + } +} + +pub enum ValueSendable { + Nil, + Boolean(bool), + Integer(i64), + Number(f64), + String(Vec), + Table(HashMap), +} + +impl<'a> TryFrom> for ValueSendable { + type Error = anyhow::Error; + + fn try_from(value: Value) -> Result { + Ok(match value { + Value::Nil => ValueSendable::Nil, + Value::Boolean(b) => ValueSendable::Boolean(b), + Value::LightUserData(_) => bail!("light userdata is not supported"), + Value::Integer(n) => ValueSendable::Integer(n), + Value::Number(n) => ValueSendable::Number(n), + Value::String(s) => ValueSendable::String(s.as_bytes().to_vec()), + Value::Table(t) => { + let mut map = HashMap::with_capacity(t.len().map(|l| l as usize)?); + for result in t.pairs::() { + let (k, v) = result?; + map.insert(ValueSendable::try_from(k)?.try_into()?, v.try_into()?); + } + ValueSendable::Table(map) + } + Value::Function(_) => bail!("function is not supported"), + Value::Thread(_) => bail!("thread is not supported"), + Value::UserData(_) => bail!("userdata is not supported"), + Value::Error(_) => bail!("error is not supported"), + }) + } +} + +impl<'lua> IntoLua<'lua> for ValueSendable { + fn into_lua(self, lua: &Lua) -> LuaResult { + match self { + ValueSendable::Nil => Ok(Value::Nil), + ValueSendable::Boolean(b) => Ok(Value::Boolean(b)), + ValueSendable::Integer(n) => Ok(Value::Integer(n)), + ValueSendable::Number(n) => Ok(Value::Number(n)), + ValueSendable::String(s) => Ok(Value::String(lua.create_string(s)?)), + ValueSendable::Table(t) => { + let table = lua.create_table()?; + for (k, v) in t { + table.set(k.into_lua(lua)?, v.into_lua(lua)?)?; + } + Ok(Value::Table(table)) + } + } + } +} + +impl ValueSendable { + pub fn into_table_string(self) -> HashMap { + let ValueSendable::Table(table) = self else { + return Default::default(); + }; + + let mut map = HashMap::with_capacity(table.len()); + for pair in table { + let (ValueSendableKey::String(k), ValueSendable::String(v)) = pair else { + continue; + }; + if let (Ok(k), Ok(v)) = (String::from_utf8(k), String::from_utf8(v)) { + map.insert(k, v); + } + } + map + } +} + +#[derive(Hash, PartialEq, Eq)] +pub enum ValueSendableKey { + Nil, + Boolean(bool), + Integer(i64), + Number(OrderedFloat), + String(Vec), +} + +impl TryInto for ValueSendable { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + Ok(match self { + ValueSendable::Nil => ValueSendableKey::Nil, + ValueSendable::Boolean(b) => ValueSendableKey::Boolean(b), + ValueSendable::Integer(n) => ValueSendableKey::Integer(n), + ValueSendable::Number(n) => ValueSendableKey::Number(OrderedFloat::new(n)), + ValueSendable::String(s) => ValueSendableKey::String(s), + ValueSendable::Table(_) => bail!("table is not supported"), + }) + } +} + +impl<'lua> IntoLua<'lua> for ValueSendableKey { + fn into_lua(self, lua: &Lua) -> LuaResult { + match self { + ValueSendableKey::Nil => Ok(Value::Nil), + ValueSendableKey::Boolean(b) => Ok(Value::Boolean(b)), + ValueSendableKey::Integer(n) => Ok(Value::Integer(n)), + ValueSendableKey::Number(n) => Ok(Value::Number(n.get())), + ValueSendableKey::String(s) => Ok(Value::String(lua.create_string(s)?)), + } + } +} diff --git a/yazi-plugin/src/components/base.rs b/yazi-plugin/src/components/base.rs deleted file mode 100644 index 789dfc39e..000000000 --- a/yazi-plugin/src/components/base.rs +++ /dev/null @@ -1,43 +0,0 @@ -use mlua::{FromLua, Lua, Table, UserData, Value}; -use ratatui::widgets::Widget; - -use crate::{layout::Rect, GLOBALS, LUA}; - -#[derive(Clone)] -pub struct Base { - area: ratatui::layout::Rect, - - kind: u8, -} - -impl Base { - pub(crate) fn install() -> mlua::Result<()> { - let ui: Table = GLOBALS.get("ui")?; - let base: Table = ui.get("Base")?; - base.set( - "new", - LUA.create_function(|_, (area, kind): (Rect, u8)| Ok(Self { area: area.0, kind }))?, - ) - } - - pub fn render(self, cx: &yazi_core::Ctx, buf: &mut ratatui::buffer::Buffer) { - if self.kind == 0 { - super::Preview::new(cx).render(self.area, buf) - } - } -} - -impl<'lua> FromLua<'lua> for Base { - fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result { - match value { - Value::UserData(ud) => Ok(ud.borrow::()?.clone()), - _ => Err(mlua::Error::FromLuaConversionError { - from: value.type_name(), - to: "Base", - message: Some("expected a Base".to_string()), - }), - } - } -} - -impl UserData for Base {} diff --git a/yazi-plugin/src/components/components.rs b/yazi-plugin/src/components/components.rs deleted file mode 100644 index 72d58ffd1..000000000 --- a/yazi-plugin/src/components/components.rs +++ /dev/null @@ -1,41 +0,0 @@ -use mlua::{AnyUserData, Table}; -use yazi_shared::RoCell; - -use super::Base; -use crate::{layout::{Bar, Border, Gauge, List, Paragraph}, GLOBALS}; - -pub(super) static COMP_FOLDER: RoCell
= RoCell::new(); -pub(super) static COMP_HEADER: RoCell
= RoCell::new(); -pub(super) static COMP_MANAGER: RoCell
= RoCell::new(); -pub(super) static COMP_STATUS: RoCell
= RoCell::new(); - -pub fn init() -> mlua::Result<()> { - COMP_FOLDER.init(GLOBALS.get("Folder")?); - COMP_HEADER.init(GLOBALS.get("Header")?); - COMP_MANAGER.init(GLOBALS.get("Manager")?); - COMP_STATUS.init(GLOBALS.get("Status")?); - Ok(()) -} - -pub(super) fn layout( - values: Vec, - cx: &yazi_core::Ctx, - buf: &mut ratatui::prelude::Buffer, -) -> mlua::Result<()> { - for value in values { - if let Ok(c) = value.take::() { - c.render(buf) - } else if let Ok(c) = value.take::() { - c.render(buf) - } else if let Ok(c) = value.take::() { - c.render(buf) - } else if let Ok(c) = value.take::() { - c.render(cx, buf) - } else if let Ok(c) = value.take::() { - c.render(buf) - } else if let Ok(c) = value.take::() { - c.render(buf) - } - } - Ok(()) -} diff --git a/yazi-plugin/src/components/folder.rs b/yazi-plugin/src/components/folder.rs deleted file mode 100644 index 9c48eb7a1..000000000 --- a/yazi-plugin/src/components/folder.rs +++ /dev/null @@ -1,35 +0,0 @@ -use mlua::TableExt; -use ratatui::widgets::Widget; -use tracing::error; - -use super::{layout, COMP_FOLDER}; -use crate::{layout::Rect, LUA}; - -pub struct Folder<'a> { - cx: &'a yazi_core::Ctx, - kind: u8, -} - -impl<'a> Folder<'a> { - #[inline] - pub fn parent(cx: &'a yazi_core::Ctx) -> Self { Self { cx, kind: 0 } } - - #[inline] - pub fn current(cx: &'a yazi_core::Ctx) -> Self { Self { cx, kind: 1 } } - - #[inline] - pub fn preview(cx: &'a yazi_core::Ctx) -> Self { Self { cx, kind: 2 } } -} - -impl<'a> Widget for Folder<'a> { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { - let mut f = || { - let args = LUA.create_table()?; - args.set("kind", self.kind)?; - layout(COMP_FOLDER.call_method::<_, _>("render", (Rect(area), args))?, self.cx, buf) - }; - if let Err(e) = f() { - error!("{:?}", e); - } - } -} diff --git a/yazi-plugin/src/components/header.rs b/yazi-plugin/src/components/header.rs deleted file mode 100644 index 4a232c939..000000000 --- a/yazi-plugin/src/components/header.rs +++ /dev/null @@ -1,24 +0,0 @@ -use mlua::TableExt; -use ratatui::widgets::Widget; -use tracing::error; - -use super::{layout, COMP_HEADER}; -use crate::layout::Rect; - -pub struct Header<'a> { - cx: &'a yazi_core::Ctx, -} - -impl<'a> Header<'a> { - #[inline] - pub fn new(cx: &'a yazi_core::Ctx) -> Self { Self { cx } } -} - -impl<'a> Widget for Header<'a> { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { - let mut f = || layout(COMP_HEADER.call_method::<_, _>("render", Rect(area))?, self.cx, buf); - if let Err(e) = f() { - error!("{:?}", e); - } - } -} diff --git a/yazi-plugin/src/components/manager.rs b/yazi-plugin/src/components/manager.rs deleted file mode 100644 index 2ff4c990c..000000000 --- a/yazi-plugin/src/components/manager.rs +++ /dev/null @@ -1,23 +0,0 @@ -use mlua::TableExt; -use ratatui::widgets::Widget; -use tracing::error; - -use super::{layout, COMP_MANAGER}; -use crate::layout::Rect; - -pub struct Manager<'a> { - cx: &'a yazi_core::Ctx, -} - -impl<'a> Manager<'a> { - pub fn new(cx: &'a yazi_core::Ctx) -> Self { Self { cx } } -} - -impl<'a> Widget for Manager<'a> { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { - let mut f = || layout(COMP_MANAGER.call_method::<_, _>("render", Rect(area))?, self.cx, buf); - if let Err(e) = f() { - error!("{:?}", e); - } - } -} diff --git a/yazi-plugin/src/components/mod.rs b/yazi-plugin/src/components/mod.rs deleted file mode 100644 index a247286aa..000000000 --- a/yazi-plugin/src/components/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -#![allow(clippy::module_inception)] - -mod base; -mod components; -mod folder; -mod header; -mod manager; -mod preview; -mod status; - -pub use base::*; -pub use components::*; -pub use folder::*; -pub use header::*; -pub use manager::*; -use preview::*; -pub use status::*; diff --git a/yazi-plugin/src/components/preview.rs b/yazi-plugin/src/components/preview.rs deleted file mode 100644 index 74840c0bd..000000000 --- a/yazi-plugin/src/components/preview.rs +++ /dev/null @@ -1,33 +0,0 @@ -use ansi_to_tui::IntoText; -use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}}; -use yazi_core::Ctx; -use yazi_shared::event::PreviewData; - -use super::Folder; - -pub(super) struct Preview<'a> { - cx: &'a Ctx, -} - -impl<'a> Preview<'a> { - pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } } -} - -impl<'a> Widget for Preview<'a> { - fn render(self, area: Rect, buf: &mut Buffer) { - let Some(ref lock) = self.cx.manager.active().preview.lock else { - return; - }; - - match &lock.data { - PreviewData::Folder => { - Folder::preview(self.cx).render(area, buf); - } - PreviewData::Text(s) => { - let p = Paragraph::new(s.as_bytes().into_text().unwrap()); - p.render(area, buf); - } - PreviewData::Image => {} - } - } -} diff --git a/yazi-plugin/src/components/status.rs b/yazi-plugin/src/components/status.rs deleted file mode 100644 index 6cc637e58..000000000 --- a/yazi-plugin/src/components/status.rs +++ /dev/null @@ -1,24 +0,0 @@ -use mlua::TableExt; -use ratatui::widgets::Widget; -use tracing::error; - -use super::{layout, COMP_STATUS}; -use crate::layout::Rect; - -pub struct Status<'a> { - cx: &'a yazi_core::Ctx, -} - -impl<'a> Status<'a> { - #[inline] - pub fn new(cx: &'a yazi_core::Ctx) -> Self { Self { cx } } -} - -impl<'a> Widget for Status<'a> { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { - let mut f = || layout(COMP_STATUS.call_method::<_, _>("render", Rect(area))?, self.cx, buf); - if let Err(e) = f() { - error!("{:?}", e); - } - } -} diff --git a/yazi-plugin/src/config.rs b/yazi-plugin/src/config.rs index 2ab522446..0571af5d4 100644 --- a/yazi-plugin/src/config.rs +++ b/yazi-plugin/src/config.rs @@ -1,42 +1,33 @@ -use mlua::{LuaSerdeExt, SerializeOptions, Table}; -use yazi_config::{MANAGER, THEME}; +use mlua::{Lua, LuaSerdeExt, SerializeOptions}; +use yazi_config::{BOOT, MANAGER, PREVIEW, THEME}; -use crate::{layout::Rect, GLOBALS, LUA}; +const OPTIONS: SerializeOptions = + SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false); -#[derive(Clone, Copy)] -pub(super) struct Config; +pub struct Config<'a> { + lua: &'a Lua, +} -impl Config { - pub(super) fn install(self) -> mlua::Result<()> { - let options = - SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false); +impl<'a> Config<'a> { + pub fn new(lua: &'a Lua) -> Self { Self { lua } } - self.theme(options)?; - self.manager(options)?; - Ok(()) + pub fn install_boot(self) -> mlua::Result { + self.lua.globals().set("BOOT", self.lua.to_value_with(&*BOOT, OPTIONS)?)?; + Ok(self) } - fn theme(self, options: SerializeOptions) -> mlua::Result<()> { - GLOBALS.set("THEME", LUA.to_value_with(&*THEME, options)?) + pub fn install_manager(self) -> mlua::Result { + self.lua.globals().set("MANAGER", self.lua.to_value_with(&*MANAGER, OPTIONS)?)?; + Ok(self) } - fn manager(self, options: SerializeOptions) -> mlua::Result<()> { - let manager = LUA.to_value_with(&*MANAGER, options)?; - { - let layout: Table = manager.as_table().unwrap().get("layout")?; - - layout.set( - "preview_rect", - LUA.create_function(|_, ()| Ok(Rect(MANAGER.layout.preview_rect())))?, - )?; - layout - .set("preview_height", LUA.create_function(|_, ()| Ok(MANAGER.layout.preview_height()))?)?; - layout - .set("folder_rect", LUA.create_function(|_, ()| Ok(Rect(MANAGER.layout.folder_rect())))?)?; - layout - .set("folder_height", LUA.create_function(|_, ()| Ok(MANAGER.layout.folder_height()))?)?; - } + pub fn install_theme(self) -> mlua::Result { + self.lua.globals().set("THEME", self.lua.to_value_with(&*THEME, OPTIONS)?)?; + Ok(self) + } - GLOBALS.set("MANAGER", manager) + pub fn install_preview(self) -> mlua::Result { + self.lua.globals().set("PREVIEW", self.lua.to_value_with(&*PREVIEW, OPTIONS)?)?; + Ok(self) } } diff --git a/yazi-plugin/src/layout/bar.rs b/yazi-plugin/src/elements/bar.rs similarity index 76% rename from yazi-plugin/src/layout/bar.rs rename to yazi-plugin/src/elements/bar.rs index 6f53dc97f..d30726d39 100644 --- a/yazi-plugin/src/layout/bar.rs +++ b/yazi-plugin/src/elements/bar.rs @@ -1,7 +1,6 @@ -use mlua::{AnyUserData, FromLua, Lua, Table, UserData, Value}; +use mlua::{AnyUserData, ExternalError, Lua, Table, UserData, Value}; -use super::{Rect, Style}; -use crate::{GLOBALS, LUA}; +use super::{RectRef, Renderable, Style}; #[derive(Clone)] pub struct Bar { @@ -13,13 +12,12 @@ pub struct Bar { } impl Bar { - pub(crate) fn install() -> mlua::Result<()> { - let ui: Table = GLOBALS.get("ui")?; + pub fn install(lua: &Lua, ui: &Table) -> mlua::Result<()> { ui.set( "Bar", - LUA.create_function(|_, (area, direction): (Rect, u8)| { + lua.create_function(|_, (area, direction): (RectRef, u8)| { Ok(Self { - area: area.0, + area: *area, position: match direction { 1 => ratatui::widgets::Borders::TOP, @@ -34,8 +32,31 @@ impl Bar { })?, ) } +} + +impl UserData for Bar { + fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_function("symbol", |_, (ud, symbol): (AnyUserData, String)| { + ud.borrow_mut::()?.symbol = symbol; + Ok(ud) + }); + methods.add_function("style", |_, (ud, value): (AnyUserData, Value)| { + { + let mut me = ud.borrow_mut::()?; + match value { + Value::Nil => me.style = None, + Value::Table(tbl) => me.style = Some(Style::from(tbl).0), + Value::UserData(ud) => me.style = Some(ud.borrow::