Repository: sxyazi/yazi Branch: main Commit: a246e9c7c06d Files: 1064 Total size: 1.8 MB Directory structure: gitextract_ehhv1z_b/ ├── .cargo/ │ └── config.toml ├── .envrc ├── .github/ │ ├── DISCUSSION_TEMPLATE/ │ │ └── 1-q-a.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ ├── config.yml │ │ └── feature.yml │ ├── dependabot.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── cachix.yml │ ├── check.yml │ ├── draft.yml │ ├── lock.yml │ ├── no-response.yml │ ├── publish.yml │ ├── test.yml │ └── validate-form.yml ├── .gitignore ├── .luarc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── LICENSE-ICONS ├── README.md ├── assets/ │ └── yazi.desktop ├── cspell.json ├── flake.nix ├── nix/ │ ├── shell.nix │ ├── yazi-unwrapped.nix │ └── yazi.nix ├── rustfmt.toml ├── scripts/ │ ├── build.sh │ ├── bump.sh │ ├── icons/ │ │ └── generate.lua │ └── validate-form/ │ ├── main.js │ └── package.json ├── snap/ │ └── snapcraft.yaml ├── stylua.toml ├── yazi-actor/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── actor.rs │ ├── app/ │ │ ├── accept_payload.rs │ │ ├── bootstrap.rs │ │ ├── deprecate.rs │ │ ├── focus.rs │ │ ├── mod.rs │ │ ├── mouse.rs │ │ ├── plugin.rs │ │ ├── plugin_do.rs │ │ ├── quit.rs │ │ ├── reflow.rs │ │ ├── resize.rs │ │ ├── resume.rs │ │ ├── stop.rs │ │ ├── title.rs │ │ └── update_progress.rs │ ├── cmp/ │ │ ├── arrow.rs │ │ ├── close.rs │ │ ├── mod.rs │ │ ├── show.rs │ │ └── trigger.rs │ ├── confirm/ │ │ ├── arrow.rs │ │ ├── close.rs │ │ ├── mod.rs │ │ └── show.rs │ ├── context.rs │ ├── core/ │ │ ├── mod.rs │ │ └── preflight.rs │ ├── help/ │ │ ├── arrow.rs │ │ ├── escape.rs │ │ ├── filter.rs │ │ ├── mod.rs │ │ └── toggle.rs │ ├── input/ │ │ ├── close.rs │ │ ├── complete.rs │ │ ├── escape.rs │ │ ├── mod.rs │ │ └── show.rs │ ├── lib.rs │ ├── lives/ │ │ ├── core.rs │ │ ├── file.rs │ │ ├── files.rs │ │ ├── filter.rs │ │ ├── finder.rs │ │ ├── folder.rs │ │ ├── lives.rs │ │ ├── mod.rs │ │ ├── mode.rs │ │ ├── preference.rs │ │ ├── preview.rs │ │ ├── ptr.rs │ │ ├── selected.rs │ │ ├── tab.rs │ │ ├── tabs.rs │ │ ├── task.rs │ │ ├── tasks.rs │ │ ├── which.rs │ │ └── yanked.rs │ ├── mgr/ │ │ ├── arrow.rs │ │ ├── back.rs │ │ ├── bulk_rename.rs │ │ ├── cd.rs │ │ ├── close.rs │ │ ├── copy.rs │ │ ├── create.rs │ │ ├── displace.rs │ │ ├── displace_do.rs │ │ ├── download.rs │ │ ├── enter.rs │ │ ├── escape.rs │ │ ├── filter.rs │ │ ├── filter_do.rs │ │ ├── find.rs │ │ ├── find_arrow.rs │ │ ├── find_do.rs │ │ ├── follow.rs │ │ ├── forward.rs │ │ ├── hardlink.rs │ │ ├── hidden.rs │ │ ├── hover.rs │ │ ├── leave.rs │ │ ├── linemode.rs │ │ ├── link.rs │ │ ├── mod.rs │ │ ├── open.rs │ │ ├── open_do.rs │ │ ├── paste.rs │ │ ├── peek.rs │ │ ├── quit.rs │ │ ├── refresh.rs │ │ ├── remove.rs │ │ ├── rename.rs │ │ ├── reveal.rs │ │ ├── search.rs │ │ ├── seek.rs │ │ ├── shell.rs │ │ ├── sort.rs │ │ ├── spot.rs │ │ ├── stash.rs │ │ ├── suspend.rs │ │ ├── tab_close.rs │ │ ├── tab_create.rs │ │ ├── tab_rename.rs │ │ ├── tab_swap.rs │ │ ├── tab_switch.rs │ │ ├── toggle.rs │ │ ├── toggle_all.rs │ │ ├── unyank.rs │ │ ├── update_files.rs │ │ ├── update_mimes.rs │ │ ├── update_paged.rs │ │ ├── update_peeked.rs │ │ ├── update_spotted.rs │ │ ├── update_yanked.rs │ │ ├── upload.rs │ │ ├── visual_mode.rs │ │ ├── watch.rs │ │ └── yank.rs │ ├── notify/ │ │ ├── mod.rs │ │ ├── push.rs │ │ └── tick.rs │ ├── pick/ │ │ ├── arrow.rs │ │ ├── close.rs │ │ ├── mod.rs │ │ └── show.rs │ ├── spot/ │ │ ├── arrow.rs │ │ ├── close.rs │ │ ├── copy.rs │ │ ├── mod.rs │ │ └── swipe.rs │ ├── tasks/ │ │ ├── arrow.rs │ │ ├── cancel.rs │ │ ├── close.rs │ │ ├── inspect.rs │ │ ├── mod.rs │ │ ├── open_shell_compat.rs │ │ ├── process_open.rs │ │ ├── show.rs │ │ └── update_succeed.rs │ └── which/ │ ├── activate.rs │ ├── dismiss.rs │ └── mod.rs ├── yazi-adapter/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── adapter.rs │ ├── adapters.rs │ ├── drivers/ │ │ ├── chafa.rs │ │ ├── iip.rs │ │ ├── kgp.rs │ │ ├── kgp_old.rs │ │ ├── mod.rs │ │ ├── sixel.rs │ │ └── ueberzug.rs │ ├── icc.rs │ ├── image.rs │ ├── info.rs │ └── lib.rs ├── yazi-binding/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── access.rs │ ├── calculator.rs │ ├── cha.rs │ ├── chan.rs │ ├── chord_cow.rs │ ├── color.rs │ ├── composer.rs │ ├── elements/ │ │ ├── align.rs │ │ ├── area.rs │ │ ├── bar.rs │ │ ├── border.rs │ │ ├── cell.rs │ │ ├── clear.rs │ │ ├── constraint.rs │ │ ├── edge.rs │ │ ├── elements.rs │ │ ├── gauge.rs │ │ ├── layout.rs │ │ ├── line.rs │ │ ├── list.rs │ │ ├── mod.rs │ │ ├── pad.rs │ │ ├── pos.rs │ │ ├── rect.rs │ │ ├── renderable.rs │ │ ├── row.rs │ │ ├── span.rs │ │ ├── table.rs │ │ ├── text.rs │ │ └── wrap.rs │ ├── error.rs │ ├── fd.rs │ ├── file.rs │ ├── handle.rs │ ├── icon.rs │ ├── id.rs │ ├── image.rs │ ├── input.rs │ ├── iter.rs │ ├── layer.rs │ ├── lib.rs │ ├── macros.rs │ ├── mouse.rs │ ├── path.rs │ ├── permit.rs │ ├── range.rs │ ├── runtime.rs │ ├── scheme.rs │ ├── stage.rs │ ├── style.rs │ ├── url.rs │ └── utils.rs ├── yazi-boot/ │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src/ │ ├── actions/ │ │ ├── actions.rs │ │ ├── clear_cache.rs │ │ ├── debug.rs │ │ ├── mod.rs │ │ ├── rustc.rs │ │ ├── triple.rs │ │ └── version.rs │ ├── args.rs │ ├── boot.rs │ └── lib.rs ├── yazi-build/ │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src/ │ └── main.rs ├── yazi-cli/ │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src/ │ ├── args.rs │ ├── dds/ │ │ ├── draw.rs │ │ ├── exec.rs │ │ ├── mod.rs │ │ └── shot.rs │ ├── main.rs │ ├── package/ │ │ ├── add.rs │ │ ├── delete.rs │ │ ├── dependency.rs │ │ ├── deploy.rs │ │ ├── git.rs │ │ ├── hash.rs │ │ ├── install.rs │ │ ├── mod.rs │ │ ├── package.rs │ │ └── upgrade.rs │ └── shared/ │ ├── mod.rs │ └── shared.rs ├── yazi-codegen/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ └── lib.rs ├── yazi-config/ │ ├── Cargo.toml │ ├── README.md │ ├── preset/ │ │ ├── README.md │ │ ├── keymap-default.toml │ │ ├── theme-dark.toml │ │ ├── theme-light.toml │ │ ├── vfs-default.toml │ │ └── yazi-default.toml │ └── src/ │ ├── icon.rs │ ├── keymap/ │ │ ├── chord.rs │ │ ├── cow.rs │ │ ├── deserializers.rs │ │ ├── key.rs │ │ ├── keymap.rs │ │ ├── mod.rs │ │ └── rules.rs │ ├── layout.rs │ ├── lib.rs │ ├── mgr/ │ │ ├── mgr.rs │ │ ├── mod.rs │ │ ├── mouse.rs │ │ └── ratio.rs │ ├── open/ │ │ ├── mod.rs │ │ ├── open.rs │ │ └── rule.rs │ ├── opener/ │ │ ├── mod.rs │ │ ├── opener.rs │ │ └── rule.rs │ ├── pattern.rs │ ├── platform.rs │ ├── plugin/ │ │ ├── fetcher.rs │ │ ├── mod.rs │ │ ├── plugin.rs │ │ ├── preloader.rs │ │ ├── previewer.rs │ │ └── spotter.rs │ ├── popup/ │ │ ├── confirm.rs │ │ ├── input.rs │ │ ├── mod.rs │ │ ├── offset.rs │ │ ├── options.rs │ │ ├── origin.rs │ │ ├── pick.rs │ │ └── position.rs │ ├── preset.rs │ ├── preview/ │ │ ├── mod.rs │ │ ├── preview.rs │ │ └── wrap.rs │ ├── priority.rs │ ├── style.rs │ ├── tasks/ │ │ ├── mod.rs │ │ └── tasks.rs │ ├── theme/ │ │ ├── filetype.rs │ │ ├── flavor.rs │ │ ├── icon.rs │ │ ├── is.rs │ │ ├── mod.rs │ │ └── theme.rs │ ├── utils.rs │ ├── vfs/ │ │ ├── mod.rs │ │ ├── service.rs │ │ ├── services.rs │ │ └── vfs.rs │ ├── which/ │ │ ├── mod.rs │ │ ├── sorting.rs │ │ └── which.rs │ └── yazi.rs ├── yazi-core/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── cmp/ │ │ ├── cmp.rs │ │ └── mod.rs │ ├── confirm/ │ │ ├── confirm.rs │ │ └── mod.rs │ ├── core.rs │ ├── help/ │ │ ├── help.rs │ │ └── mod.rs │ ├── input/ │ │ ├── input.rs │ │ └── mod.rs │ ├── lib.rs │ ├── mgr/ │ │ ├── mgr.rs │ │ ├── mimetype.rs │ │ ├── mod.rs │ │ ├── tabs.rs │ │ └── yanked.rs │ ├── notify/ │ │ ├── message.rs │ │ ├── mod.rs │ │ └── notify.rs │ ├── pick/ │ │ ├── mod.rs │ │ └── pick.rs │ ├── spot/ │ │ ├── mod.rs │ │ └── spot.rs │ ├── tab/ │ │ ├── backstack.rs │ │ ├── finder.rs │ │ ├── folder.rs │ │ ├── history.rs │ │ ├── mod.rs │ │ ├── mode.rs │ │ ├── preference.rs │ │ ├── preview.rs │ │ ├── selected.rs │ │ └── tab.rs │ ├── tasks/ │ │ ├── file.rs │ │ ├── mod.rs │ │ ├── prework.rs │ │ ├── process.rs │ │ └── tasks.rs │ └── which/ │ ├── mod.rs │ ├── sorter.rs │ └── which.rs ├── yazi-dds/ │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src/ │ ├── client.rs │ ├── ember/ │ │ ├── bulk.rs │ │ ├── bye.rs │ │ ├── cd.rs │ │ ├── custom.rs │ │ ├── delete.rs │ │ ├── download.rs │ │ ├── duplicate.rs │ │ ├── ember.rs │ │ ├── hey.rs │ │ ├── hi.rs │ │ ├── hover.rs │ │ ├── load.rs │ │ ├── mod.rs │ │ ├── mount.rs │ │ ├── move.rs │ │ ├── rename.rs │ │ ├── tab.rs │ │ ├── trash.rs │ │ └── yank.rs │ ├── lib.rs │ ├── macros.rs │ ├── payload.rs │ ├── pubsub.rs │ ├── pump.rs │ ├── sendable.rs │ ├── server.rs │ ├── spark/ │ │ ├── kind.rs │ │ ├── mod.rs │ │ └── spark.rs │ ├── state.rs │ └── stream.rs ├── yazi-emulator/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── brand.rs │ ├── dimension.rs │ ├── emulator.rs │ ├── lib.rs │ ├── mux.rs │ └── unknown.rs ├── yazi-ffi/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── cf_dict.rs │ ├── cf_string.rs │ ├── disk_arbitration.rs │ ├── io_kit.rs │ └── lib.rs ├── yazi-fm/ │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src/ │ ├── app/ │ │ ├── app.rs │ │ ├── mod.rs │ │ └── render.rs │ ├── cmp/ │ │ ├── cmp.rs │ │ └── mod.rs │ ├── confirm/ │ │ ├── body.rs │ │ ├── buttons.rs │ │ ├── confirm.rs │ │ ├── list.rs │ │ └── mod.rs │ ├── dispatcher.rs │ ├── executor.rs │ ├── help/ │ │ ├── bindings.rs │ │ ├── help.rs │ │ └── mod.rs │ ├── input/ │ │ ├── input.rs │ │ └── mod.rs │ ├── logs.rs │ ├── main.rs │ ├── mgr/ │ │ ├── mod.rs │ │ ├── modal.rs │ │ └── preview.rs │ ├── notify/ │ │ ├── mod.rs │ │ └── notify.rs │ ├── panic.rs │ ├── pick/ │ │ ├── list.rs │ │ ├── mod.rs │ │ └── pick.rs │ ├── root.rs │ ├── router.rs │ ├── signals.rs │ ├── spot/ │ │ ├── mod.rs │ │ └── spot.rs │ ├── tasks/ │ │ ├── list.rs │ │ ├── mod.rs │ │ ├── progress.rs │ │ └── tasks.rs │ └── which/ │ ├── cand.rs │ ├── mod.rs │ └── which.rs ├── yazi-fs/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── cha/ │ │ ├── cha.rs │ │ ├── kind.rs │ │ ├── mod.rs │ │ ├── mode.rs │ │ └── type.rs │ ├── cwd.rs │ ├── error/ │ │ ├── error.rs │ │ ├── mod.rs │ │ └── serde.rs │ ├── file.rs │ ├── files.rs │ ├── filter.rs │ ├── fns.rs │ ├── hash.rs │ ├── lib.rs │ ├── mounts/ │ │ ├── linux.rs │ │ ├── macos.rs │ │ ├── mod.rs │ │ ├── partition.rs │ │ └── partitions.rs │ ├── op.rs │ ├── path/ │ │ ├── clean.rs │ │ ├── expand.rs │ │ ├── mod.rs │ │ ├── path.rs │ │ ├── percent.rs │ │ └── relative.rs │ ├── provider/ │ │ ├── attrs.rs │ │ ├── capabilities.rs │ │ ├── local/ │ │ │ ├── absolute.rs │ │ │ ├── calculator.rs │ │ │ ├── casefold.rs │ │ │ ├── copier.rs │ │ │ ├── dir_entry.rs │ │ │ ├── gate.rs │ │ │ ├── identical.rs │ │ │ ├── local.rs │ │ │ ├── mod.rs │ │ │ └── read_dir.rs │ │ ├── mod.rs │ │ └── traits.rs │ ├── scheme.rs │ ├── sorter.rs │ ├── sorting.rs │ ├── splatter.rs │ ├── stage.rs │ ├── url.rs │ └── xdg.rs ├── yazi-macro/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── actor.rs │ ├── asset.rs │ ├── context.rs │ ├── event.rs │ ├── fmt.rs │ ├── fs.rs │ ├── lib.rs │ ├── log.rs │ ├── module.rs │ ├── platform.rs │ ├── render.rs │ └── stdio.rs ├── yazi-packing/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ └── lib.rs ├── yazi-parser/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── app/ │ │ ├── deprecate.rs │ │ ├── mod.rs │ │ ├── mouse.rs │ │ ├── plugin.rs │ │ ├── quit.rs │ │ ├── reflow.rs │ │ ├── resume.rs │ │ ├── stop.rs │ │ ├── title.rs │ │ └── update_progress.rs │ ├── arrow.rs │ ├── cmp/ │ │ ├── close.rs │ │ ├── mod.rs │ │ ├── show.rs │ │ └── trigger.rs │ ├── confirm/ │ │ ├── close.rs │ │ ├── mod.rs │ │ └── show.rs │ ├── help/ │ │ ├── mod.rs │ │ └── toggle.rs │ ├── input/ │ │ ├── close.rs │ │ └── mod.rs │ ├── lib.rs │ ├── mgr/ │ │ ├── cd.rs │ │ ├── close.rs │ │ ├── copy.rs │ │ ├── create.rs │ │ ├── displace_do.rs │ │ ├── download.rs │ │ ├── escape.rs │ │ ├── filter.rs │ │ ├── find.rs │ │ ├── find_arrow.rs │ │ ├── find_do.rs │ │ ├── hardlink.rs │ │ ├── hidden.rs │ │ ├── hover.rs │ │ ├── linemode.rs │ │ ├── link.rs │ │ ├── mod.rs │ │ ├── open.rs │ │ ├── open_do.rs │ │ ├── paste.rs │ │ ├── peek.rs │ │ ├── remove.rs │ │ ├── rename.rs │ │ ├── reveal.rs │ │ ├── search.rs │ │ ├── seek.rs │ │ ├── shell.rs │ │ ├── sort.rs │ │ ├── spot.rs │ │ ├── stash.rs │ │ ├── tab_close.rs │ │ ├── tab_create.rs │ │ ├── tab_rename.rs │ │ ├── tab_switch.rs │ │ ├── toggle.rs │ │ ├── toggle_all.rs │ │ ├── update_files.rs │ │ ├── update_mimes.rs │ │ ├── update_paged.rs │ │ ├── update_peeked.rs │ │ ├── update_spotted.rs │ │ ├── update_yanked.rs │ │ ├── upload.rs │ │ ├── visual_mode.rs │ │ └── yank.rs │ ├── notify/ │ │ ├── mod.rs │ │ ├── push.rs │ │ └── tick.rs │ ├── pick/ │ │ ├── close.rs │ │ ├── mod.rs │ │ └── show.rs │ ├── spot/ │ │ ├── copy.rs │ │ └── mod.rs │ ├── tasks/ │ │ ├── mod.rs │ │ ├── process_open.rs │ │ └── update_succeed.rs │ ├── void.rs │ └── which/ │ ├── activate.rs │ └── mod.rs ├── yazi-plugin/ │ ├── Cargo.toml │ ├── README.md │ ├── preset/ │ │ ├── compat.lua │ │ ├── components/ │ │ │ ├── current.lua │ │ │ ├── entity.lua │ │ │ ├── header.lua │ │ │ ├── linemode.lua │ │ │ ├── marker.lua │ │ │ ├── modal.lua │ │ │ ├── parent.lua │ │ │ ├── preview.lua │ │ │ ├── progress.lua │ │ │ ├── rail.lua │ │ │ ├── root.lua │ │ │ ├── status.lua │ │ │ ├── tab.lua │ │ │ ├── tabs.lua │ │ │ └── tasks.lua │ │ ├── plugins/ │ │ │ ├── archive.lua │ │ │ ├── code.lua │ │ │ ├── dds.lua │ │ │ ├── empty.lua │ │ │ ├── extract.lua │ │ │ ├── file.lua │ │ │ ├── folder.lua │ │ │ ├── font.lua │ │ │ ├── fzf.lua │ │ │ ├── image.lua │ │ │ ├── init.lua │ │ │ ├── json.lua │ │ │ ├── magick.lua │ │ │ ├── mime-dir.lua │ │ │ ├── mime-local.lua │ │ │ ├── mime-remote.lua │ │ │ ├── mime.lua │ │ │ ├── multi.lua │ │ │ ├── noop.lua │ │ │ ├── null.lua │ │ │ ├── pdf.lua │ │ │ ├── session.lua │ │ │ ├── svg.lua │ │ │ ├── vfs.lua │ │ │ ├── video.lua │ │ │ └── zoxide.lua │ │ ├── setup.lua │ │ └── ya.lua │ └── src/ │ ├── elements/ │ │ ├── elements.rs │ │ └── mod.rs │ ├── external/ │ │ ├── fd.rs │ │ ├── highlighter.rs │ │ ├── mod.rs │ │ ├── rg.rs │ │ └── rga.rs │ ├── fs/ │ │ ├── fs.rs │ │ ├── mod.rs │ │ └── op.rs │ ├── isolate/ │ │ ├── entry.rs │ │ ├── fetch.rs │ │ ├── isolate.rs │ │ ├── mod.rs │ │ ├── peek.rs │ │ ├── preload.rs │ │ ├── seek.rs │ │ └── spot.rs │ ├── lib.rs │ ├── loader/ │ │ ├── chunk.rs │ │ ├── loader.rs │ │ ├── mod.rs │ │ └── require.rs │ ├── lua.rs │ ├── process/ │ │ ├── child.rs │ │ ├── command.rs │ │ ├── mod.rs │ │ ├── output.rs │ │ ├── process.rs │ │ └── status.rs │ ├── pubsub/ │ │ ├── mod.rs │ │ └── pubsub.rs │ ├── runtime/ │ │ ├── mod.rs │ │ ├── plugin.rs │ │ ├── runtime.rs │ │ └── term.rs │ ├── theme/ │ │ ├── mod.rs │ │ └── theme.rs │ └── utils/ │ ├── app.rs │ ├── cache.rs │ ├── call.rs │ ├── image.rs │ ├── json.rs │ ├── layer.rs │ ├── log.rs │ ├── mod.rs │ ├── preview.rs │ ├── process.rs │ ├── spot.rs │ ├── sync.rs │ ├── target.rs │ ├── text.rs │ ├── time.rs │ ├── user.rs │ └── utils.rs ├── yazi-proxy/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── app.rs │ ├── cmp.rs │ ├── confirm.rs │ ├── input.rs │ ├── lib.rs │ ├── macros.rs │ ├── mgr.rs │ ├── notify.rs │ ├── pick.rs │ ├── tasks.rs │ └── which.rs ├── yazi-scheduler/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── fetch/ │ │ ├── fetch.rs │ │ ├── in.rs │ │ ├── mod.rs │ │ ├── out.rs │ │ └── progress.rs │ ├── file/ │ │ ├── file.rs │ │ ├── in.rs │ │ ├── macros.rs │ │ ├── mod.rs │ │ ├── out.rs │ │ ├── progress.rs │ │ ├── transaction.rs │ │ └── traverse.rs │ ├── hook/ │ │ ├── hook.rs │ │ ├── in.rs │ │ ├── macros.rs │ │ └── mod.rs │ ├── lib.rs │ ├── macros.rs │ ├── ongoing.rs │ ├── op.rs │ ├── out.rs │ ├── plugin/ │ │ ├── in.rs │ │ ├── macros.rs │ │ ├── mod.rs │ │ ├── out.rs │ │ ├── plugin.rs │ │ └── progress.rs │ ├── preload/ │ │ ├── in.rs │ │ ├── mod.rs │ │ ├── out.rs │ │ ├── preload.rs │ │ └── progress.rs │ ├── process/ │ │ ├── in.rs │ │ ├── macros.rs │ │ ├── mod.rs │ │ ├── out.rs │ │ ├── process.rs │ │ ├── progress.rs │ │ └── shell.rs │ ├── progress.rs │ ├── runner.rs │ ├── scheduler.rs │ ├── size/ │ │ ├── in.rs │ │ ├── mod.rs │ │ ├── out.rs │ │ ├── progress.rs │ │ └── size.rs │ ├── snap.rs │ └── task.rs ├── yazi-sftp/ │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src/ │ ├── de.rs │ ├── error.rs │ ├── fs/ │ │ ├── attrs.rs │ │ ├── dir_entry.rs │ │ ├── file.rs │ │ ├── flags.rs │ │ ├── mod.rs │ │ └── read_dir.rs │ ├── id.rs │ ├── lib.rs │ ├── macros.rs │ ├── operator.rs │ ├── packet.rs │ ├── path.rs │ ├── receiver.rs │ ├── requests/ │ │ ├── close.rs │ │ ├── extended.rs │ │ ├── fstat.rs │ │ ├── init.rs │ │ ├── lstat.rs │ │ ├── mkdir.rs │ │ ├── mod.rs │ │ ├── open.rs │ │ ├── open_dir.rs │ │ ├── read.rs │ │ ├── read_dir.rs │ │ ├── readlink.rs │ │ ├── realpath.rs │ │ ├── remove.rs │ │ ├── rename.rs │ │ ├── rmdir.rs │ │ ├── set_stat.rs │ │ ├── stat.rs │ │ ├── symlink.rs │ │ └── write.rs │ ├── responses/ │ │ ├── attrs.rs │ │ ├── data.rs │ │ ├── extended.rs │ │ ├── handle.rs │ │ ├── mod.rs │ │ ├── name.rs │ │ ├── status.rs │ │ └── version.rs │ ├── ser.rs │ └── session.rs ├── yazi-shared/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── alias.rs │ ├── bytes.rs │ ├── chars.rs │ ├── completion_token.rs │ ├── condition.rs │ ├── data/ │ │ ├── any.rs │ │ ├── data.rs │ │ ├── de.rs │ │ ├── key.rs │ │ └── mod.rs │ ├── debounce.rs │ ├── env.rs │ ├── errors/ │ │ ├── mod.rs │ │ └── peek.rs │ ├── event/ │ │ ├── action.rs │ │ ├── cow.rs │ │ ├── event.rs │ │ └── mod.rs │ ├── id.rs │ ├── last_value.rs │ ├── layer.rs │ ├── lib.rs │ ├── loc/ │ │ ├── able.rs │ │ ├── buf.rs │ │ ├── loc.rs │ │ └── mod.rs │ ├── localset.rs │ ├── natsort.rs │ ├── os.rs │ ├── path/ │ │ ├── buf.rs │ │ ├── component.rs │ │ ├── components.rs │ │ ├── conversion.rs │ │ ├── cow.rs │ │ ├── display.rs │ │ ├── error.rs │ │ ├── kind.rs │ │ ├── like.rs │ │ ├── mod.rs │ │ ├── path.rs │ │ └── view.rs │ ├── pool/ │ │ ├── cow.rs │ │ ├── mod.rs │ │ ├── pool.rs │ │ ├── ptr.rs │ │ ├── symbol.rs │ │ └── traits.rs │ ├── predictor.rs │ ├── ro_cell.rs │ ├── scheme/ │ │ ├── cow.rs │ │ ├── encode.rs │ │ ├── kind.rs │ │ ├── mod.rs │ │ ├── ref.rs │ │ ├── scheme.rs │ │ └── traits.rs │ ├── shell/ │ │ ├── error.rs │ │ ├── mod.rs │ │ ├── unix.rs │ │ └── windows.rs │ ├── source.rs │ ├── strand/ │ │ ├── buf.rs │ │ ├── conversion.rs │ │ ├── cow.rs │ │ ├── error.rs │ │ ├── extensions.rs │ │ ├── kind.rs │ │ ├── like.rs │ │ ├── mod.rs │ │ ├── strand.rs │ │ └── view.rs │ ├── sync_cell.rs │ ├── terminal.rs │ ├── tests.rs │ ├── throttle.rs │ ├── time.rs │ ├── translit/ │ │ ├── mod.rs │ │ ├── table.rs │ │ └── traits.rs │ ├── url/ │ │ ├── buf.rs │ │ ├── component.rs │ │ ├── components.rs │ │ ├── cov.rs │ │ ├── cow.rs │ │ ├── display.rs │ │ ├── encode.rs │ │ ├── like.rs │ │ ├── mod.rs │ │ ├── traits.rs │ │ └── url.rs │ ├── utf8.rs │ └── wtf8/ │ ├── mod.rs │ ├── validator.rs │ └── wtf8.rs ├── yazi-shim/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── crossterm/ │ │ ├── if.rs │ │ ├── mod.rs │ │ ├── restore_background.rs │ │ ├── restore_cursor.rs │ │ └── set_background.rs │ ├── lib.rs │ ├── ratatui/ │ │ ├── mod.rs │ │ └── paragraph.rs │ └── twox.rs ├── yazi-term/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── lib.rs │ ├── option.rs │ ├── semaphore.rs │ ├── state.rs │ └── term.rs ├── yazi-tty/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── handle.rs │ ├── lib.rs │ ├── tty.rs │ └── windows.rs ├── yazi-vfs/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── cha.rs │ ├── file.rs │ ├── files.rs │ ├── fns.rs │ ├── lib.rs │ ├── op.rs │ └── provider/ │ ├── calculator.rs │ ├── copier.rs │ ├── dir_entry.rs │ ├── gate.rs │ ├── mod.rs │ ├── provider.rs │ ├── providers.rs │ ├── read_dir.rs │ ├── rw_file.rs │ └── sftp/ │ ├── absolute.rs │ ├── conn.rs │ ├── gate.rs │ ├── metadata.rs │ ├── mod.rs │ ├── read_dir.rs │ └── sftp.rs ├── yazi-watcher/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── backend.rs │ ├── lib.rs │ ├── local/ │ │ ├── linked.rs │ │ ├── local.rs │ │ └── mod.rs │ ├── remote/ │ │ ├── mod.rs │ │ └── remote.rs │ ├── reporter.rs │ ├── watched.rs │ ├── watchee.rs │ └── watcher.rs └── yazi-widgets/ ├── Cargo.toml ├── README.md └── src/ ├── clear.rs ├── clipboard.rs ├── input/ │ ├── actor/ │ │ ├── actor.rs │ │ ├── backspace.rs │ │ ├── backward.rs │ │ ├── casefy.rs │ │ ├── complete.rs │ │ ├── delete.rs │ │ ├── escape.rs │ │ ├── forward.rs │ │ ├── insert.rs │ │ ├── kill.rs │ │ ├── mod.rs │ │ ├── move.rs │ │ ├── paste.rs │ │ ├── redo.rs │ │ ├── replace.rs │ │ ├── type.rs │ │ ├── undo.rs │ │ ├── visual.rs │ │ └── yank.rs │ ├── event.rs │ ├── input.rs │ ├── mod.rs │ ├── mode.rs │ ├── op.rs │ ├── opt.rs │ ├── parser/ │ │ ├── backspace.rs │ │ ├── backward.rs │ │ ├── casefy.rs │ │ ├── complete.rs │ │ ├── delete.rs │ │ ├── forward.rs │ │ ├── insert.rs │ │ ├── kill.rs │ │ ├── mod.rs │ │ ├── move.rs │ │ └── paste.rs │ ├── separator.rs │ ├── snap.rs │ ├── snaps.rs │ └── widget.rs ├── lib.rs ├── scrollable.rs └── step.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [env] MACOSX_DEPLOYMENT_TARGET = "10.12" JEMALLOC_SYS_WITH_LG_PAGE = "16" # environment variable for tikv-jemalloc-sys # # https://jemalloc.net/jemalloc.3.html#opt.narenas # narenas is the maximum number of arenas to use for automatic multiplexing # of threads and arenas. The default is four times the number of CPUs, # or one if there is a single CPU. # # Improve memory efficiency by reducing fragmentation and ensuring all threads allocate from the same pool JEMALLOC_SYS_WITH_MALLOC_CONF = "narenas:1" [target.aarch64-apple-darwin] rustflags = [ "-Ctarget-cpu=apple-m1" ] ================================================ FILE: .envrc ================================================ use flake ================================================ FILE: .github/DISCUSSION_TEMPLATE/1-q-a.yml ================================================ body: - type: dropdown id: os attributes: label: What system are you running Yazi on? options: - Linux X11 - Linux Wayland - macOS - Windows - Windows WSL - FreeBSD X11 - FreeBSD Wayland - Android validations: required: true - type: input id: terminal attributes: label: What terminal are you running Yazi in? placeholder: "ex: kitty v0.32.2" validations: required: true - type: textarea id: debug attributes: label: "`yazi --debug` output" description: Please run `yazi --debug` and paste the debug information here. render: Shell validations: required: true - type: textarea id: description attributes: label: Describe the question description: A clear and concise description of what the question is placeholder: Tell us what you want to know validations: required: true - type: textarea id: other attributes: label: Anything else? description: | Add any other context about the problem here. You can attach screenshots by clicking this area to highlight it and then drag the files in. - type: checkboxes id: checklist attributes: label: Checklist description: Before submitting the post, please make sure you have completed the following options: - label: I have read all the documentation required: true - label: I have searched the existing discussions/issues required: true ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ name: 🐞 Bug Report description: Create a report to help us improve labels: [bug] assignees: [] body: - type: dropdown id: os attributes: label: What system are you running Yazi on? options: - Linux X11 - Linux Wayland - macOS - Windows - Windows WSL - FreeBSD X11 - FreeBSD Wayland - Android validations: required: true - type: input id: terminal attributes: label: What terminal are you running Yazi in? placeholder: "ex: kitty v0.32.2" validations: required: true - type: textarea id: debug attributes: label: "`yazi --debug` output" description: Please run `yazi --debug` and paste the debug information here. render: Shell validations: required: true - type: textarea id: description attributes: label: Describe the bug description: A clear and concise description of what the bug is placeholder: Tell us what happened validations: required: true - type: textarea id: reproducer attributes: label: Minimal reproducer description: A [minimal reproducer](https://stackoverflow.com/help/minimal-reproducible-example) is required, otherwise the issue might be closed without further notice. placeholder: | Please include as much information as possible that can help to reproduce and understand the issue. validations: required: true - type: textarea id: other attributes: label: Anything else? description: | Add any other context about the problem here. You can attach screenshots by clicking this area to highlight it and then drag the files in. - type: checkboxes id: checklist attributes: label: Checklist description: Before submitting the issue, please make sure you have completed the following options: - label: I tried the [latest nightly build](https://yazi-rs.github.io/docs/installation#binaries), and the issue is still reproducible required: true - label: I updated the debug information (`yazi --debug`) input box to the nightly that I tried required: true - label: I can reproduce it after disabling all custom configs/plugins (`mv ~/.config/yazi ~/.config/yazi-backup`) ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: 🚧 Build Issues url: https://github.com/sxyazi/yazi/discussions/new?category=3-build-issues about: If you have issues building Yazi from source code - name: 📝 Documentation Improvement url: https://github.com/yazi-rs/yazi-rs.github.io about: If you'd like to help improve the documentation - name: 💬 GitHub Discussions url: https://github.com/sxyazi/yazi/discussions/new?category=1-q-a about: When you have questions that are not bug reports or feature requests - name: 🌐 Discord Server / Telegram Group url: https://github.com/sxyazi/yazi#discussion about: If you'd prefer more realtime conversation with the community ================================================ FILE: .github/ISSUE_TEMPLATE/feature.yml ================================================ name: 💡 Feature Request description: Suggest an idea for this project labels: [feature] assignees: [] body: - type: textarea id: debug attributes: label: "`yazi --debug` output" description: Please run `yazi --debug` and paste the debug information here. render: Shell validations: required: true - type: textarea id: problem attributes: label: Please describe the problem you're trying to solve description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] validations: required: true - type: checkboxes id: contribute attributes: label: Would you be willing to contribute this feature? description: The feature has a much higher chance of completion if you are willing to get involved! options: - label: Yes, I'll give it a shot - type: textarea id: solution attributes: label: Describe the solution you'd like description: A clear and concise description of what you want to happen. validations: required: true - type: textarea id: context attributes: label: Additional context description: Add any other context or screenshots about the feature request here. - type: checkboxes id: checklist attributes: label: Checklist description: Before submitting the issue, please make sure you have completed the following options: - label: I have searched the existing issues/discussions required: true - label: The [latest nightly build](https://yazi-rs.github.io/docs/installation/#binaries) doesn't already have this feature required: true ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" commit-message: prefix: "ci" - package-ecosystem: "npm" directory: "/scripts/validate-form" schedule: interval: "weekly" commit-message: prefix: "ci" ================================================ FILE: .github/pull_request_template.md ================================================ ## Which issue does this PR resolve? Resolves # ## Rationale of this PR ================================================ FILE: .github/workflows/cachix.yml ================================================ name: Cachix on: push: branches: [main] permissions: contents: read jobs: publish: name: Publish Flake strategy: matrix: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - name: Install Nix uses: cachix/install-nix-action@v31 - name: Authenticate with Cachix uses: cachix/cachix-action@v16 with: name: yazi authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Build Flake run: nix build -L ================================================ FILE: .github/workflows/check.yml ================================================ name: Check on: push: branches: [main] pull_request: branches: [main] env: SCCACHE_GHA_ENABLED: true RUSTC_WRAPPER: sccache permissions: contents: read jobs: clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup Rust toolchain run: | rustup toolchain install stable --profile minimal rustup component add clippy - name: Setup sccache uses: mozilla-actions/sccache-action@v0.0.9 - name: Clippy run: cargo clippy --all rustfmt: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Rust toolchain run: | rustup toolchain install nightly --profile minimal rustup component add rustfmt --toolchain nightly - name: Setup sccache uses: mozilla-actions/sccache-action@v0.0.9 - name: Rustfmt run: rustfmt +nightly --check **/*.rs stylua: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v6 - uses: JohnnyMorganz/stylua-action@v4 with: token: ${{ secrets.GITHUB_TOKEN }} version: latest args: --color always --check . ================================================ FILE: .github/workflows/draft.yml ================================================ name: Draft on: push: branches: [main] tags: ["v[0-9]+.[0-9]+.[0-9]+"] workflow_dispatch: env: SCCACHE_GHA_ENABLED: true permissions: contents: read jobs: build-unix: strategy: matrix: include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu - os: ubuntu-latest target: aarch64-unknown-linux-gnu gcc: gcc-aarch64-linux-gnu - os: ubuntu-latest target: i686-unknown-linux-gnu gcc: gcc-i686-linux-gnu - os: ubuntu-latest target: riscv64gc-unknown-linux-gnu gcc: gcc-riscv64-linux-gnu - os: ubuntu-latest target: sparc64-unknown-linux-gnu gcc: gcc-sparc64-linux-gnu - os: macos-latest target: x86_64-apple-darwin - os: macos-latest target: aarch64-apple-darwin runs-on: ${{ matrix.os }} env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc CARGO_TARGET_I686_UNKNOWN_LINUX_GNU_LINKER: i686-linux-gnu-gcc CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc CARGO_TARGET_SPARC64_UNKNOWN_LINUX_GNU_LINKER: sparc64-linux-gnu-gcc steps: - uses: actions/checkout@v6 - name: Install gcc if: matrix.gcc != '' run: sudo apt update && sudo apt install -yq ${{ matrix.gcc }} - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Setup sccache uses: mozilla-actions/sccache-action@v0.0.9 - name: Build run: ./scripts/build.sh ${{ matrix.target }} - name: Upload artifact uses: actions/upload-artifact@v7 with: name: ${{ matrix.target }} path: | yazi-${{ matrix.target }}.zip yazi-${{ matrix.target }}.deb build-windows: strategy: matrix: include: - os: windows-latest target: x86_64-pc-windows-msvc - os: windows-latest target: aarch64-pc-windows-msvc runs-on: ${{ matrix.os }} env: RUSTC_WRAPPER: sccache YAZI_GEN_COMPLETIONS: true CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: lld-link.exe CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER: lld-link.exe steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Setup sccache uses: mozilla-actions/sccache-action@v0.0.9 - name: Build run: cargo build --profile release-windows --locked --target ${{ matrix.target }} - name: Pack artifact env: TARGET_NAME: yazi-${{ matrix.target }} run: | New-Item -ItemType Directory -Path ${env:TARGET_NAME} Copy-Item -Path "target\${{ matrix.target }}\release-windows\ya.exe" -Destination ${env:TARGET_NAME} Copy-Item -Path "target\${{ matrix.target }}\release-windows\yazi.exe" -Destination ${env:TARGET_NAME} Copy-Item -Path "yazi-boot\completions" -Destination ${env:TARGET_NAME} -Recurse Copy-Item -Path "README.md", "LICENSE" -Destination ${env:TARGET_NAME} Compress-Archive -Path ${env:TARGET_NAME} -DestinationPath "${env:TARGET_NAME}.zip" - name: Upload artifact uses: actions/upload-artifact@v7 with: name: ${{ matrix.target }} path: yazi-${{ matrix.target }}.zip build-musl: runs-on: ubuntu-latest strategy: matrix: include: - target: x86_64-unknown-linux-musl - target: aarch64-unknown-linux-musl container: image: docker://ghcr.io/cross-rs/${{ matrix.target }}:edge steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Setup sccache uses: mozilla-actions/sccache-action@v0.0.9 - name: Build run: ./scripts/build.sh ${{ matrix.target }} - name: Upload artifact uses: actions/upload-artifact@v7 with: name: ${{ matrix.target }} path: | yazi-${{ matrix.target }}.zip yazi-${{ matrix.target }}.deb build-snap: strategy: matrix: include: - os: ubuntu-latest arch: amd64 - os: ubuntu-24.04-arm arch: arm64 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup LXD uses: canonical/setup-lxd@v1 - name: Setup snapcraft run: sudo snap install --classic snapcraft - name: Build snap run: snapcraft --verbose - name: Rename snap run: mv yazi_*.snap yazi-${{ matrix.arch }}.snap - name: Upload artifact uses: actions/upload-artifact@v7 with: name: snap-${{ matrix.arch }} path: yazi-${{ matrix.arch }}.snap snap: runs-on: ubuntu-latest needs: [build-snap] steps: - uses: actions/download-artifact@v8 with: pattern: snap-* merge-multiple: true - name: Setup snapcraft run: sudo snap install --classic snapcraft - name: Push snap to candidate channel if: startsWith(github.ref, 'refs/tags/') env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} run: | parallel 'snapcraft upload -v --release latest/candidate {}' ::: yazi-*.snap || true - name: Push snap to edge channel if: ${{ !startsWith(github.ref, 'refs/tags/') }} env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} run: | parallel 'snapcraft upload -v --release latest/edge {}' ::: yazi-*.snap || true draft: if: startsWith(github.ref, 'refs/tags/') permissions: contents: write runs-on: ubuntu-latest needs: [build-unix, build-windows, build-musl, build-snap] steps: - uses: actions/download-artifact@v8 with: merge-multiple: true - name: Draft uses: softprops/action-gh-release@v2 with: draft: true files: | yazi-*.zip yazi-*.deb yazi-*.snap generate_release_notes: true nightly: if: ${{ !startsWith(github.ref, 'refs/tags/') }} permissions: contents: write runs-on: ubuntu-latest needs: [build-unix, build-windows, build-musl, build-snap] steps: - run: | echo 'NIGHTLY_BODY<> $GITHUB_ENV echo "From commit: ${GITHUB_SHA:0:8}" >> $GITHUB_ENV echo "Generated on: $(date -u +"%Y-%m-%d %H:%M") UTC" >> $GITHUB_ENV echo "Nightly changelog: https://github.com/sxyazi/yazi/blob/main/CHANGELOG.md#unreleased" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV - uses: actions/checkout@v6 - uses: actions/download-artifact@v8 with: merge-multiple: true - name: Update the tag run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git tag --force nightly && git push --force origin tag nightly - name: Nightly uses: softprops/action-gh-release@v2 with: tag_name: nightly prerelease: true files: | yazi-*.zip yazi-*.deb yazi-*.snap name: Nightly Build body: ${{ env.NIGHTLY_BODY }} target_commitish: ${{ github.sha }} ================================================ FILE: .github/workflows/lock.yml ================================================ name: Lock Threads on: schedule: - cron: "5 3 * * *" workflow_dispatch: concurrency: group: lock jobs: lock: runs-on: ubuntu-latest permissions: issues: write pull-requests: write discussions: write steps: - uses: dessant/lock-threads@v6 with: issue-inactive-days: "30" issue-comment: > I'm going to lock this issue because it has been closed for _30 days_. ⏳ This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please file a new issue and complete the issue template so we can capture all the details necessary to investigate further. pr-inactive-days: "30" discussion-inactive-days: "30" process-only: "issues,prs,discussions" ================================================ FILE: .github/workflows/no-response.yml ================================================ name: No Response on: issue_comment: types: [created] schedule: - cron: "10 * * * *" jobs: no-response: runs-on: ubuntu-latest permissions: issues: write steps: - uses: lee-dohm/no-response@v0.5.0 with: token: ${{ github.token }} daysUntilClose: 7 responseRequiredLabel: waiting on op ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish on: release: types: [published] permissions: contents: read jobs: winget: runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v8 with: merge-multiple: true - name: Publish to Winget uses: vedantmgoyal9/winget-releaser@main with: identifier: sxyazi.yazi installers-regex: 'yazi-(x86_64|aarch64)-pc-windows-msvc\.zip$' token: ${{ secrets.WINGET_TOKEN }} snapcraft: runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v8 with: merge-multiple: true - name: Promote snap to stable env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} run: | sudo snap install --classic snapcraft snapcraft promote yazi --from-channel latest/candidate --to-channel latest/stable --yes ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: branches: [main] pull_request: branches: [main] env: SCCACHE_GHA_ENABLED: true RUSTC_WRAPPER: sccache CARGO_TERM_COLOR: always permissions: contents: read jobs: test: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - name: Setup Rust toolchain run: rustup toolchain install stable --profile minimal - name: Setup sccache uses: mozilla-actions/sccache-action@v0.0.9 - name: Build run: cargo build --verbose - name: Test run: cargo test --workspace --verbose ================================================ FILE: .github/workflows/validate-form.yml ================================================ name: Validate Form on: issues: types: [opened, edited] schedule: - cron: "20 * * * *" jobs: check-version: runs-on: ubuntu-latest permissions: issues: write steps: - uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 20 - name: Install Dependencies run: | cd scripts/validate-form npm ci - name: Validate Form uses: actions/github-script@v8 with: script: | const script = require('./scripts/validate-form/main.js') await script({github, context, core}) env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ target/ yazi-*/completions node_modules/ .DS_Store result result-* .direnv .idea/ .vscode/ *.snap ================================================ FILE: .luarc.json ================================================ { "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", "runtime.version": "Lua 5.5", "runtime.special": { "fail": "error" }, "workspace.library": ["~/.config/yazi/plugins/types.yazi/"], "diagnostics.disable": ["redefined-local"] } ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - `Added` for new features. - `Changed` for changes in existing functionality. - `Deprecated` for soon-to-be removed features. - `Fixed` for any bug fixes. - `Improved` for performance improvements. ## [Unreleased] ### Added - Custom tab name ([#3666]) - New `--in` for `search` action to set search directory ([#3696]) - Multi-file spotter ([#3733]) - Certificate authentication for SFTP VFS provider ([#3716]) - New `hovered` condition specifying different icons for hovered files ([#3728]) - Allow using `ps.sub()` in `init.lua` directly without a plugin ([#3638]) - New `ya.exec()` API and `ya exec` subcommand to execute an action and await its result ([#3780]) - New `sort_fallback` option to control fallback sorting behavior ([#3077]) - New `fs.access()` API to access the filesystem ([#3668]) - New `relay-notify-push` DDS event to customize the notification handler ([#3642]) - New `ind-app-title` DDS event to customize the app title ([#3684]) - New `ind-hidden` and `key-hidden` DDS events to change hidden status in Lua ([#3748]) - New `marker_symbol` option to specify the symbol used for marking files ([#3689]) - New `fs.unique()` creates a unique file or directory ([#3677]) - New `download` DDS event fires when remote files are downloaded ([#3687]) - New `ind-which-activate` DDS event to change the which component behavior ([#3608]) - New `hey` DDS event fires when static messages are restored from persistence ([#3725]) - New `cx.which` API to access the which component state ([#3617]) - New experimental `ya.co()` API that creates a coroutine ([#3757]) ### Changed - Upgrade Lua to 5.5 ([#3633]) - Change preset t for creating tabs to tt to avoid conflict with new tr for renaming tabs ([#3666]) - Remove `title_format` in favor of new `ind-app-title` DDS event for flexible title customization ([#3684]) - Remove `micro_workers` and `macro_workers` in favor of finer control over concurrent workers ([#3661]) ### Deprecated - Deprecate `fs.unique_name()` in favor of `fs.unique()` to fix a TOCTOU race condition ([#3677]) ### Fixed - Chafa v1.18.1 causes random ghost keypresses when previewing images ([#3678]) - Be a little defensive while parsing the output of `7zz -ba` ([#3744]) - Make `ya pkg` ignore default remote name in user Git config ([#3648]) - Archive extraction fails for target paths with non-ASCII characters on Windows ([#3607]) - Escape backslashes in ImageMagick font path parameter ([#3708]) ### Improved - Reduce memory allocations by using Lua 5.5 external strings ([#3634]) - Reuse previewed and spotted widgets when possible ([#3765]) ## [v26.1.22] ### Added - Tree view for the preset archive previewer ([#3525]) - Support compressed tarballs (`.tar.gz`, `.tar.bz2`, etc.) in the preset archive previewer ([#3518]) - Check and refresh the file list when the terminal gains focus ([#3561]) - Experimental module-level async support ([#3594]) - Disable ANSI escape sequences in `ya pkg` when stdout is not a TTY ([#3566]) - New `Path.os()` API creates an OS-native `Path` ([#3541]) ### Fixed - Smart-case in interactive `cd` broken due to a typo ([#3540]) - Fix shell formatting for non-spread opener rules ([#3532]) - `sort extension` excludes directories since only files have extensions ([#3582]) - Account for URL covariance in `Url:join()` ([#3514]) ## [v26.1.4] ### Added - Support VFS for preset previewers that rely on external commands ([#3477]) - Support 8-bit images in RGB, CIELAB, and GRAY color spaces ([#3358]) ### Fixed - `ya pkg` fails to write `package.toml` when the config directory does not exist ([#3482]) - A race condition generating unique filenames for concurrent file operations ([#3494]) ## [v25.12.29] ### Added - Remote file management ([#3396]) - Virtual file system ([#3034], [#3035], [#3094], [#3108], [#3187], [#3203]) - Shell formatting ([#3232]) - Multi-entry support for plugin system ([#3154]) - Zoom in or out of the preview image ([#2864]) - Improve the UX of the pick and input components ([#2906], [#2935]) - Show progress of each task in task manager ([#3121], [#3131], [#3134]) - New `fs.copy()` and `fs.rename()` APIs ([#3467]) - New experimental `ya.async()` API ([#3422]) - New `overall` option to set the overall background color ([#3317]) - Rounded corners for indicator bar ([#3419]) - New `bulk_rename` action always renames files with the editor ([#2984]) - `key-*` DDS events to allow changing or canceling user key events ([#3005], [#3037]) - New `--bg` specifying image background color in the preset SVG and ImageMagick previewers ([#3189]) - `filter` by full path (prefix + filename) in search view instead of just filename ([#2915]) - New `casefy` action for case conversion of the input content ([#3235]) - Allow dynamic adjustment of layout ratio via `rt.mgr.ratio` ([#2964]) - Support `.deb` packages ([#2807], [#3128], [#3209]) - Port several widespread GUI keys to the input component ([#2849]) - Support invalid UTF-8 paths throughout the codebase ([#2884], [#2889], [#2890], [#2895], [#3023], [#3290], [#3369]) - Allow upgrading only specific packages with `ya pkg` ([#2841]) - Respect the user's `image_filter` setting in the preset ImageMagick previewer ([#3286]) - New `duplicate` DDS event for copying files ([#3456]) - New `ind-sort` and `key-sort` DDS events to change sorting in Lua ([#3391]) - Allow custom mouse click behavior for individual files ([#2925]) - Display newlines in input as spaces to improve readability ([#2932]) - Fill in error messages if preview fails ([#2917], [#3383], [#3387]) - Search view shares file selection and yank state ([#2855]) - Offload mimetype fetching on opening files to the task scheduler ([#3141]) - Increase terminal response timeout to better tolerate slow SSH network environments ([#2843]) ### Changed - Rename `name` to `url` for open, fetchers, spotters, preloaders, previewers, filetype, and `globs` icon rules to support virtual file system ([#3034]) - Rename `mime` fetcher to `mime.local`, and introduce `mime.dir` fetcher to support folder MIME types ([#3222]) - Reclassify `hovered` and `preview_hovered` under `[mgr]` of `theme.toml` into `[indicator]` as `current` and `preview`, respectively ([#3419]) - Remove `$0` parameter in opener rules to make the `open` action work under empty directories ([#3226]) - Return `Path` instead of `Url` from `Url:strip_prefix()` and `File.link_to` to enforce type safety ([#3361], [#3385]) - Use `body` instead of the term `content` in confirmations ([#2921]) - Use `u16` instead of `u32` as the type of `max_width` and `max_height` options to avoid memory exhaustion ([#3313]) - Implement `__pairs` metamethod instead of `__index` for the callback argument of the `@yank` DDS event ([#2997]) ### Deprecated - Deprecate `$n`, `$@` (\*nix) and `%n`, `%*` (Windows) in `shell` action and opener rules in favor of new shell formatting ([#3232]) - Deprecate `ya.hide`, `ya.render`, and `ya.truncate` in favor of `ui.hide`, `ui.render`, and `ui.truncate` ([#2939]) - Deprecate `position` property of `ya.input()` in favor of `pos` to align with `ya.confirm()` and its type `ui.Pos` ([#2921]) - Deprecate `cx.tasks.progress` in favor of `cx.tasks.summary` ([#3131]) - Deprecate `frag` properly of `Url` in favor of `domain` ([#3034]) - Deprecate `ui.Rect.default` in favor of `ui.Rect {}` ([#2927]) ### Fixed - User-prepended open rules do not override presets ([#3360]) - Respect user's system media opener instead of hardcoding `mpv` ([#2959]) - Incorrect `$0` and `$@` parameters in `shell` action under empty directories ([#3225]) - Avoid appending a newline when reading clipboard contents ([#3059]) - Renew package `rev` only when it's empty ([#3200]) - Suspend only when there is a parent process ([#3008]) - Preserve open order for files with post-resolved MIME types ([#2931]) - A race condition in concurrent directory loading on a slow device ([#3271]) - Erase overlapping image portions when previewing errors ([#3067]) - Force Git checkout for plugin cache repositories ([#3169]) - Check compatibility when reusing previewer bytecode cache ([#3190]) - Disable kitty keyboard protocol on Windows due to `crossterm` inability to handle it ([#3250]) - Prevent quotes in file(1) arguments from being stripped under MSYS2 ([#3364]) - Expose `ya` CLI in the Snap build ([#2904]) - Fallback to `PollWatcher` for file changes watching on NetBSD ([#2941]) - Generate unique image IDs for Kgp to tolerate tmux ([#3038]) ### Improved - Make copy, cut, delete, link, hardlink, download, and upload tasks immediately cancellable ([#3429]) - Make preload tasks discardable ([#2875]) - Reduce file change event frequency ([#2820]) - Upload and download of a single file over SFTP in chunks concurrently ([#3393]) - Do not listen for file changes in inactive tabs ([#2958]) - Switch to a higher-performance hash algorithm ([#3083]) - Sequence-based rendering merge strategy ([#2861]) - Store only `Urn` instead of full `Url` in find results ([#2914]) - Zero-copy `UrlBuf` to `Url` conversion ([#3117]) - String interning to reduce memory usage of mimetype and URL domain ([#3084], [#3091]) - Do not pre-allocate memory for Lua tables ([#2879]) - Copy-on-write on command data, and avoid converting primitive types to strings thereby allocating memory ([#2862]) - Use `AnyUserData::type_id()` to reduce stack pushes ([#2834]) - App data instead of Lua registry to reduce stack pushes ([#2880]) ## [v25.5.31] ### Fixed - Expose `ui.Wrap` ([#2810]) - `forward --end-of-word` of the input should consider the mode's delta ([#2811]) - Make every effort to carry hidden states for dummy files ([#2814]) ## [v25.5.28] ### Added - Redesign tabs ([#2745]) - Support embedded cover for video preview ([#2640]) - Calculate real-time directory size in spotter ([#2695]) - Truncate long items in the file list ([#2754], [#2759], [#2778]) - Obscure input component for inputting passwords ([#2675]) - Improve path auto-completion results ([#2765]) - New `ya pkg` subcommand ([#2770]) - New `ya.emit()` API ([#2653]) - New `fs.calc_size()` API ([#2691]) - Allow custom exit code with `quit --code` ([#2609]) - New `--hovered` for the `copy` action ([#2709]) - `s` and `S` keybinds in the input component ([#2678]) - Limit memory usage for previewing large images ([#2602]) - Show error when image preview fails ([#2706]) - New `ui.Align`, `ui.Wrap`, and `ui.Edge` ([#2802]) - Make `ui.Line` renderable ([#2743]) - Checks in `ya pub` and `ya emit` subcommands to verify receiver exists and has necessary abilities ([#2696]) - Make the hover state for `reveal`, `sort`, and `hidden` actions stable ([#2657]) - New `--no-dummy` option for `reveal` action ([#2664]) - Fall back to `CSI 16 t` when PowerShell OpenSSH returns a fake terminal size ([#2636]) ### Changed - Deprecate `[manager]` in favor of `[mgr]` to make it consistent with other APIs ([#2803]) - Remove `tab_width` as it no longer needs to be set manually ([#2745]) - Move `tab_active` and `tab_inactive` to a dedicated `[tabs]` section ([#2745]) - Remove `sixel_fraction` as it's no longer needed ([#2707]) ### Deprecated - Deprecate `ya.mgr_emit()`, `ya.app_emit()` and `ya.input_emit()` ([#2653]) - Deprecate `ya.preview_widgets()` ([#2706]) - Deprecate the `Command:args()` method ([#2752]) - Deprecate the `ya pack` subcommand in favor of `ya pkg` ([#2770]) - Deprecate `LEFT`, `CENTER`, and `RIGHT` on `ui.Line` and `ui.Text` in favor of `ui.Align` ([#2802]) - Deprecate `NONE`, `TOP`, `RIGHT`, `BOTTOM`, `LEFT`, and `ALL` on `ui.Bar` and `ui.Border` in favor of `ui.Edge` ([#2802]) - Deprecate `WRAP_NO`, `WRAP` and `WRAP_TRIM` on `ui.Text` in favor of `ui.Wrap` ([#2802]) ### Fixed - Respect the user's `max_width` setting in the preset video previewer ([#2560]) - Reverse the mixing order of theme and flavor configuration ([#2594]) - No title is set when starts the first time ([#2700]) - `ya pub-to 0` checks if any peer is able to receive the message ([#2697]) - Detach background and orphan processes from the controlling terminal with `setsid()` ([#2723]) - Always try to create state directory before draining DDS data ([#2769]) - Avoid tmux interfering with kitty graphical sequences ([#2734]) ### Improved - Double directory size calculation speed ([#2683]) - 9x faster Sixel image preview ([#2707]) - Remove intermediate variables in natural sorting algorithm to avoid unnecessary allocation ([#2764]) - Avoid unnecessary memory allocation in `ya.truncate()` ([#2753]) ## [v25.4.8] ### Added - Enhance `fzf` integration ([#2553]) - Platform-specific key binding ([#2526]) - Custom search engine Lua API ([#2452]) - New `follow` action to follow files pointed to by symlinks ([#2543]) - Allow `tab_swap` to cycle tabs ([#2456]) - Show error message when directory fails to load ([#2527]) - New `symlink_target` to style the target of symbolic links ([#2522]) - Use Yazi in Helix directly without Zellij or tmux ([#2461]) - New `` and `` keybindings to select entire line for the input component ([#2439]) - New `fs.expand_url()` API ([#2476]) - New `ui.Text:scroll()` API for setting text to scroll horizontally or vertically ([#2589]) - Allow initializing input when opening it with actions like `rename`, `create`, `find`, `filter`, etc. ([#2578]) - New `@sync peek` annotation for sync previewers ([#2487]) - New `ya.id("app")` to get `YAZI_ID` in plugins ([#2503]) - New `base` field for the `Url` ([#2492]) - New `rt.term` exports terminal emulator information ([#2442]) - Allow bulk renaming to include trailing content in addition to the required new names ([#2494]) - Log `tmux` call execution time to logs ([#2444]) ### Changed - Navigation wraparound with new `arrow prev` and `arrow next` actions ([#2485], [#2540]) - Swap default key bindings for fzf and zoxide ([#2546]) - Switch to `resvg` as the SVG renderer ([#2581]) - Make `frag`, `name`, `stem`, `ext`, and `parent` on `Url`, `name` on `tab::Tab`, and `is_hovered` on `fs::File` properties ([#2572]) - Replace `tasks_show` and `close_input` with `tasks:show` and `input:close` ([#2530]) - Replace `sync = true` with the `@sync peek` annotation ([#2487]) ### Deprecated - Deprecate `ui.Padding` and `ui.Rect:padding()` ([#2574]) ### Fixed - Always show the size in the status bar even in empty directories ([#2449]) - Remove the temporary extraction directory forcefully ([#2458]) - Align the behavior of the end-of-options marker (`--`) with that of the shell ([#2431]) - Respect hidden status of directory junctions and symlinks themselves on Windows ([#2471]) ### Improved - Rewrite config parser to double the startup speed ([#2508]) - Lazy compile and cache lua plugins as binary bytecode ([#2490]) - Faster image preview with optimized `magick` arguments ([#2533]) - Cache UserData fields ([#2572]) ## [v25.3.2] ### Added - Expose all theme fields in Lua ([#2405]) - Expose almost the entirety of the user's configuration in Lua ([#2413]) ### Fixed - `STDIN_FILENO` poll always returns 0 under SSH ([#2427]) - Ignore stdin redirection to ensure always accessing the real tty ([#2425]) - Incorrect deprecation warning when the plugin doesn't exist ([#2418]) ## [v25.2.26] ### Added - Allow to specify layer for keymap actions ([#2399]) - New `rt` and `th` allow to access user configuration and theme scheme in sync/async plugins consistently ([#2389], [#2392], [#2393], [#2397]) - New `tbl_col` and `tbl_cell` in theme system for spotter table styling ([#2391]) - Allow different separators to be applied individually to the left and right sides of the status bar ([#2313]) - `ripgrep-all` support for the `search` action ([#2383]) - Respect the user's `max_width` setting in the preset PDF preloader ([#2331]) - Respect the user's `wrap` setting in the preset JSON previewer ([#2337]) - Respect the user's `image_alloc` setting in the preset ImageMagick previewer ([#2403]) - New `external` and `removable` fields in the `fs.partitions()` API ([#2343]) - CSI-based Vim and Neovim built-in terminal detection for better accuracy ([#2327]) ### Changed - Replace `separator_open` and `separator_close` with `sep_left` and `sep_right` ([#2313]) - Rename the `[completion]` component to `[cmp]` ([#2399]) ### Deprecated - Deprecate `MANAGER`, `PREVIEW`, `PLUGIN`, and `THEME` in favor of `rt` and `th` ([#2389]) - Deprecate `ya.manager_emit()` in favor of `ya.mgr_emit()` ([#2397]) ### Fixed - Didn't reset previous `Cha` when loading directories in chunks ([#2366]) - Load mount points with the best effort even if the `/dev/disk/by-label` directory does not exist ([#2326]) - Add maximum preview limit under `/proc` virtual file system ([#2355]) ## [v25.2.11] ### Added - New `overall` option under `[status]` to allow specifying the overall style of the status bar ([#2321]) - Reduce terminal response wait timeout ([#2314]) ### Fixed - Unable to delete sealed files on Windows due to platform differences ([#2319]) - Reverse the order of CSI-based and environment-based terminal detection ([#2310]) ## [v25.2.7] ### Added - Mount manager ([#2199]) - New `ya.confirm()` API ([#2095]) - New `arrow top` and `arrow bot` actions to jump to the top and bottom ([#2294]) - Support end of options (`--`) marker for all actions ([#2298]) - Replace mode and Vim motions (`W`, `E`, `B`, `^`, `_`) for inputs ([#2143]) - New `ya pack -d` subcommand to delete packages ([#2181]) - `ya pack` supports adding and deleting multiple packages at once ([#2257]) - Theme support for the spotter border and title ([#2002]) - Use positional argument instead of `--args` for the `plugin` action ([#2299]) - Support and hide Windows system files by default ([#2149]) - New `--no-cwd-file` option for the `close` action ([#2185]) - Prompt users missing fzf in the zoxide plugin ([#2122]) - More decent package locking mechanism ([#2168]) - Custom modal component API ([#2205]) - Support local `tmux` image preview over SSH - New `@since` plugin annotation to specify the minimum supported Yazi version ([#2290]) - Allow preloaders to return an optional `Error` to describe the failure ([#2253]) - ARM64 Snap package ([#2188]) - Support `package.toml` as a symlink ([#2245]) - New `cx.layer` API to determine the current UI layer ([#2247]) - Channel and multi-concurrent task join support for the plugin system ([#2210]) - Support `application/mbox` mimetype ([#2173]) - `cbr` and `cbz` as valid archive extensions ([#2077]) ### Deprecated - Deprecate `--args` in the `plugin` action in favor of a 2nd positional parameter ([#2299]) - Deprecate plugin entry file `init.lua` in favor of `main.lua` ([#2168]) - Deprecate `arrow -99999999` and `arrow 99999999` in favor of `arrow top` and `arrow bot` ([#2294]) - Deprecate the numeric return value of preloaders in favor of a boolean return value ([#2253]) - Deprecate `ya.md5()` in favor of `ya.hash()` ([#2168]) ### Fixed - `before_ext` excludes directories since only files have extensions ([#2132]) - Element style of `ui.Text` was not applied to the entire area ([#2093]) - Incorrect monorepo sub-plugin path resolution ([#2186]) - Use `u32` for parsing Linux partition blocks ([#2234]) - Unmangle the hexadecimal space strings (`"\x20"`) in Linux partition labels ([#2233]) - JSON value `null` should be deserialized as Lua `nil`, not lightweight userdata `null` ([#2242]) - Don't check if has a hovered file in advance, only do so when `--hovered` is explicitly specified ([#2105]) - Handle broken pipe errors gracefully ([#2110]) ### Improved - Detach the watch registration from the main thread ([#2224]) ## [v0.4.2] ### Added - More supported archive formats to the preset config ([#1926]) - New `fs.create()` Lua API ([#2068]) - New `--cwd` parameter for the `shell` action and `fs.cwd()` API ([#2060]) - Allow `noop` for single-key chords by removing the mixing length limit ([#2064]) - Support for Android platform in the `for` qualifier of opener ([#2041]) ### Fixed - Set the current working directory in a thread-safe way ([#2043]) - Interactive `cd` autocomplete doesn't follow the latest `CWD` changes ([#2025]) - Offset cursor shift when deleting multiple files in bulk ([#2030]) - Missing a render after resuming from an external blocking process ([#2071]) - Missing a hover after reordering from an external plugin ([#2072]) - Use a less intrusive `DSR` instead of `DA1` workaround to forward terminal responses twice in tmux ([#2058]) - `allow-passthrough` must be set to `on` to prevent `tmux` from forwarding the real terminal's response to the inactive pane ([#2052]) ## [v0.4.1] ### Fixed - Correctly handle CRLF on Windows in preset archive and JSON plugins ([#2017]) - Failed to parse certain image dimensions for Überzug++ backend ([#2020]) - Disable passthrough when the user launches Yazi in Neovim inside tmux ([#2014]) ## [v0.4.0] ### Added - Spotter ([#1802]) - Support transparent image preview ([#1556]) - Auto switch between dark and light icons/flavors based on terminal backgrounds ([#1946]) - Allow disabling certain preset keybinds with the new `noop` virtual action ([#1882]) - New `ya emit` and `ya emit-to` subcommands to emit actions to a specified instance for execution ([#1979]) - Custom styles for the `confirm` component ([#1789]) - Make the builtin `extract` plugin support compressed tarballs (`*.tar.gz`, `*.tar.bz2`, etc.) ([#1583]) - Launch from preset settings if the user's config cannot be parsed ([#1832]) - Prioritize paths that need to be processed first during bulk renaming ([#1801]) - New `copy --separator` option to allow specifying the path separator ([#1877]) - Set a different input title for `create --dir` ([#1650]) - Include package revision hash in `ya pack --list` ([#1884]) - New `load` DDS event ([#1980]) - New log system ([#1945]) - New `ui.Text` and `ui.Table` layout elements ([#1776]) - Support passing arguments to previewer/preloader/spotter/fetcher ([#1966]) - Move notification from top-right to bottom-right corner to avoid covering content as much as possible ([#1984]) - Append the suffix to the end instead of start when generating unique filenames for directories ([#1784]) - Allow overriding and rewriting the sync methods of built-in plugins ([#1695]) - Fallback to `CSI 16 t` for certain terminals that do not support `TIOCGWINSZ` ([#2004]) - Support calling methods in built-in plugins with arbitrary types of arguments (`self` can now be omitted) ([#1666]) - Support `assets` installation for the `ya pack` subcommand ([#1973]) - Complete and consistent support for the `ui.Style()` API - Image ICC profiles for better color accuracy ([#1808]) - Support reading non-UTF8 data with `Child:read_line()` API ([#1816]) - New `area()` method for renderable elements ([#1667]) - `yazi --debug` supports detecting whether `tmux` is built with `--enable-sixel` ([#1762]) ### Changed - Eliminate the `x-` prefix in MIME types ([#1927]) - Remove the `vnd.` prefix from mimetype to solve differences introduced in the newest `file(1)` v5.46 ([#1995]) - Rename the term `select` to `toggle` to reserve `select` for future use ([#1773]) - Correct the misuse of the term `ctime` and unify others ([#1761]) - Replace `ffmpegthumbnailer` with `ffmpeg` as the video preview backend to support spotter ([#1928]) - Use an `Error` userdata instead of a plain error code for I/O errors ([#1939]) - Remove `ui.ListItem` since it's no longer necessary ([#1772]) - Decouple coordinates from `ui.List`, `ui.Bar`, `ui.Border`, and `ui.Gauge` ([#1782]) - Make `backspace` action not close the input even when value is empty ([#1680]) - Remove the meaningless `--confirm` option to simplify the `shell` action ([#1982]) - Use `dark` and `light` instead of `use` under `[flavor]` to support auto-switching between light and dark modes ([#1946]) - Unify the `fg_dark` and `fg_light` into one `fg` since `fg_light` is redundant and never used ([#1946]) - Extend the available styles for `mode` by separating `mode` from the `separator` styles ([#1953]) ### Deprecated - Deprecate `--sync` option for the `plugin` action ([#1891]) - Deprecate `ui.Paragraph` in favor of `ui.Text` ([#1776]) - Deprecate the task info of `peek()`, `seek()`, and `preload()` from `self` in favor of a `job` parameter ([#1966]) - Deprecate parameter list of `entry()` from its first argument in favor of a `job` parameter ([#1966]) - Deprecate the number of units of `seek()` from its first argument in favor of a `job` parameter ([#1966]) ### Fixed - Introduce a new `btime` term to align `ctime` with Unix ([#1761]) - Match icon by extension case-insensitively ([#1614]) - Copy the CWD path with `c => d` regardless even if the directory is empty ([#1849]) - Respect the `image_quality` setting in preset PDF previewer ([#2006]) - Images were not cleared when closing a tab in front of the current tab ([#1792]) - Replace control characters to printable characters in plain text preview ([#1704]) - One file's MIME type changed multiple times without triggering a preview again ([#1682]) - Reset image rendering and skip peeking if the TUI in the background ([#1833]) - File upserting should handle deleting in edge cases where the source and target URNs are different ([#1737]) - Revise `revision` if the new file list is empty but the previous one was not ([#2003]) - Update `rustix` to fix the `enable_raw_mode()` error on WSL/Android ### Improved - Merge multiple file operations into one to greatly speed up updates in large directories ([#1745]) - Eliminate all memory reallocations during sorting ([#1846]) - Introduce URN to speed up large directory sorting, updating, locating ([#1622], [#1652], [#1648]) - Improve jemalloc memory allocator efficiency ([#1689]) - Lazy load `ui`, `ya`, `fs`, and `ps` ([#1903]) - Avoid unnecessary allocations in base64 encoding of inline image protocol ([#1639]) - Introduce copy-on-write for event system to eliminate all memory reallocations ([#1962]) - Apply rotate in place to images with orientation ([#1807]) - Introduce reflow for the rendering engine ([#1863]) ## [v0.3.3] ### Added - `size` linemode supports showing the file count for directories ([#1591]) - Support image preview in Windows Terminal ([#1588]) - Add `is_absolute`, `has_root`, `starts_with`, `ends_with`, `strip_prefix` to `Url` ([#1605]) ### Fixed - Keybindings disappear when mixing presets with a wrong filter condition ([#1568]) - Squeeze `offset` of the file list after resizing window ([#1500]) - Check compositor support status before using ueberzug wayland output ([#1566]) - Fallback to `PollWatcher` for file changes watching on WSL ([#1574]) ### Improved - Truncate long lists in confirm dialogs ([#1590]) ## [v0.3.2] ### Added - New confirm component ([#1167]) - Word wrapping in `code` previewer ([#1159]) - New `--dir` option for `create` action ([#1505]) - New `ext()` method for `Url` ([#1528]) - Make the builtin `code` previewer handle invalid carriage return chars and binary streams better ([#1550]) ### Fixed - Wait till mimetype is resolved to avoid preview flickering ([#1542]) - Use a different cache directory for each user to avoid permission issues ([#1541]) - Filter out candidates that overlap with longer key chords from the which component ([#1562]) - Overlong single-line text preview containing escape sequences was not being properly escaped ([#1497]) ### Improved - New `image_delay` option debounces image previews to avoid lag caused by terminal image decoding during fast scrolling ([#1512]) - Only scan the first 1024 bytes to detect if it's binary, apply `\r` fixes only to content within the visible range, avoid unnecessary allocations during natural sorting ([#1551]) ## [v0.3.1] ### Added - Start with multiple tabs with different paths ([#1443]) - Key notion shorthands such as `` as `` ([#1448]) - Support `F13` - `F19` keys ([#1446]) - New `--cursor` for the `shell` action ([#1422]) - New `search_do` action to make it easier to achieve a flat view ([#1431]) - Portrait orientation preview for EXIF image ([#1412]) - Keybinding for the `hardlink` action ([#1461]) - New `empty` previewer for empty and `/proc/*` files ([#1482]) - Note about filtering in the help menu ([#1361]) - New `tab` DDS event on tab switch ([#1474]) - New `status()` method for `Command` ([#1473]) ### Fixed - Directory loading status ([#1439]) - Resolve relative path when expanding path ([#1428]) - DDS static messages only work when at least two instances are running ([#1467]) - Escape files containing special `\x1b` characters and render it as plain text ([#1395]) - 7zip shows different error messages for wrong password ([#1451]) - 7zip shows different error messages for RAR and ZIP files ([#1468]) - Newly created directories with the same name causing a false positive in directory loading optimization due to having the same modification time ([#1434]) - Close stdin before waiting for child process ([#1464]) ## [v0.3.0] ### Added - Package manager ([#985], [#1110]) - Support mouse event ([#1038], [#1139], [#1232]) - New `extract` built-in plugin for archive extracting ([#1321]) - Redesign icons ([#1086]) - Font preview ([#1048]) - SVG, HEIC, AVIF, and JPEG XL preview support ([#1050], [#1249]) - Simplify keybindings ([#1241]) - New `hardlink` action to create hard links ([#1268]) - Keep file creation time on macOS and Windows ([#1169]) - Sort randomly ([#1291]) - New linemode to show file ownership ([#1238]) - New linemode to show file ctime ([#1295]) - New `--hovered` option for the `rename` and `remove` actions ([#1227]) - Support Super/Command/Windows key with `D-` notation ([#1069]) - Interactive `cd` path auto-completion supports `~` expansion ([#1081]) - Preview files containing non-UTF-8 characters ([#958]) - Expand Windows paths like "D:" that only have a drive letter but no root ([#948]) - Close confirmation dialogs and exit automatically when the ongoing task gone ([#997]) - Case-insensitive special keys in keymappings ([#1082]) - Transliteration for natural sorting ([#1053]) - New `ya.clipboard()` API ([#980]) - New `debounce` option for the `ya.input()` API ([#1025]) - Support `yazi-cli` for Nix flake ([#944]) - Support `stdin` and pipe for `Child` API ([#1033]) - New `ya sub` subcommand to subscribe to DDS events ([#1004]) - Allow specifying `$YAZI_ID` with a command-line argument ([#1305]) - DDS client-server version check ([#1111]) - New `bulk` DDS event ([#937]) - Support `cargo binstall yazi-fm` and `cargo binstall yazi-cli` ([#1003]) - Show `ya` CLI version in the `yazi --debug` output ([#1005]) - Detect terminal type in tmux with CSI sequence in passthrough mode ([#977]) ### Changed - Use Ctrl+c instead of Ctrl+q as the universal close key to follow the conventions - Replace Alt+k/Alt+j with K/J as the `seek` keybindings to avoid Alt key not working in certain terminals - Replace Ctrl+Enter with Shift+Enter as the alternative key for Shift+o so that it corresponds to Enter being `o` (without Shift) - keep original state of `sort` action in favor of specifying `yes` or `no` to explicitly apply a new state to its `--reverse`, `--dir-first`, and `--translit` - Move `mime` plugin from `[plugin.preloaders]` to `[plugin.fetchers]` of yazi.toml - Turn `success` and `code` into properties of `Status` instead of methods - Remove `fs.cha_follow(url)` in favor of `fs.cha(url, true)` - Rename `is_block_device`, `is_char_device`, and `is_socket` of `Cha` to `is_block`, `is_char`, and `is_sock` for simplicity ### Fixed - Different filenames should be treated as the same file on case-insensitive file systems ([#1151]) - Suppress warnings for different name representations of the same file in the case-insensitive file system when renaming ([#1185]) - Avoid duplicate candidates in the `which` component ([#975]) - Sixel support from certain `st` forks cannot be detected ([#1094]) - Move the DDS socket file out of the cache directory to avoid being affected by `yazi --clear-cache` - Build `jemalloc` with 64KB pagesize on linux/arm64 ([#1270]) - Cursor gets out of sync occasionally at image previewing through IIP under tmux ([#1070]) ### Improved - Reimplement and significantly speed up archive previewing ([#1220]) ## [v0.2.5] ### Added - Data distribution service ([#826], [#855], [#861], [#867], [#868], [#871], [#880], [#895], [#913], [#928], [#933], [#940]) - Re-implement fzf and zoxide as built-in plugins ([#884], [#881]) - Preserve files' modified at timestamp while copying ([#926]) - New `--orphan` option for `shell` action ([#887]) - Smart-case for completion of interactive `cd` paths ([#910]) - Allow creating a tab with the startup directory through `tab_create` without specifying a path ([#917]) - Bunch of new debugging information to `yazi --debug` ([#824]) - Time-based selection order preservation ([#843]) - Placeholder message when there are no files in the list ([#900]) - Enhance `ya.dbg()` and `ya.err()` by support arbitrary types ([#835]) - Trigger path completion with both `/` and `\` on Windows ([#909]) - Allow opening interactively with the `--chosen-file` flag ([#920]) - Support `YAZI_FILE_ONE` in the preset `file` previewer ([#846]) ### Deprecated - Deprecate the `jump` action in favor of `plugin fzf` and `plugin zoxide` ([#884], [#881]) ### Fixed - Kill all spawned processes on exit ([#812]) - Prevent pasting a directory into itself ([#925]) - Use `BTreeSet` for selected files to maintain order ([#799]) - CJK text rendering issue where the input popup component overlaps with images ([#879]) ### Improved - Accelerate kitty graphics protocol encoding by avoiding string reallocation ([#837]) - Wrap stderr with `BufWriter` to avoid frequent system calls thereby increase rendering frame rate ([#849]) - Switch to `globset` to reduce CPU time spent on matching icons ([#908]) - Re-implement file watcher in an async way ([#877]) - Cache each file's icon to avoid redundant calculations at rendering ([#931]) - Port `require()` and `ya.sync()` to Rust to avoid plugin information initialization ([#853]) ## [v0.2.4] ### Added - Vim-like notification with new `ya.notify()` API ([#659], [#749], [#780]) - New `ya.input()` API to request user input ([#762]) - Cross-directory selection ([#693]) - Colorize the icons ([#683]) - Flavors ([#753]) - New counter component shows the number of yanked/selected items ([#646]) - New `scrolloff` option to keep a margin when scrolling ([#679]) - New ``, ``, and `` keybindings for inputs ([#665]) - New `` and `` for the select component to move the cursor up/down ([#779]) - New `Ctrl+[` as an alternative to the escape key ([#763]) - New option `--hovered` for the `open` action allows only to open the currently hovered file ([#687]) - Support musl builds for Linux ([#759]) - New `--debug` flag to print debug information ([#794]) - Send a foreground notification to the user when the process fails to run ([#775]) - Nested conflict detection for cross-directory selections ([#689]) - New `prepend_rules` and `append_rules` for `[open]` and `[icon]` ([#754], [#670]) - Call sync functions within an async plugin ([#649]) - Allow access to complete app data from all tabs ([#644]) - Ability to sort candidates in the which component ([#662]) - Expose selected/yanked files as Lua API ([#674]) - New `cx.yanked` API to access yanked files ([#788]) - New `$0` (Unix) / `%0` (Windows) to access the hovered file in `shell` action ([#738]) - New `ya.hide()` API to hide the UI temporarily ([#792]) - Allow both `/` and `\` for folder creation on Windows ([#751]) - New `parse()` method for the line elements to parse ANSI sequences - New `ui.Clear` component to clear areas ([#786]) - Support `YAZI_FILE_ONE` environment variable for `file(1)` path ([#752]) - Merge wildcard preloader and previewer rules via `append_preloaders` and `append_previewers` ### Deprecated - Deprecate the `exec` property in yazi.toml, keymap.toml, and theme.toml in favor of `run` ### Fixed - Rendering fails when no file type style is matched ([#721]) ### Improved - Cache loaded plugins ([#710]) - Cheaper sync context initialization ([#643]) - Prefer `raw_get()` and `raw_set()` ## [v0.2.3] ### Added - Preview image over SSH ([#585]) - New `unyank` action ([#313]) - Customize number of columns of the which component ([#571]) - Support passing arguments to plugin ([#587]) - New `image_quality` and `sixel_fraction` options to configure the image preview quality ([#576]) - New `ya.which()` API for custom key events ([#617]) - New `ya.quote()` API to quote strings safely - `plugin` action for each layer - Plugin-specific state persistence ([#590]) - Allow to configure image filter ([#586]) - Shorten unit names and add more units to `ya.readable_size()` - Support char device in `[filetype]` ([#628]) - File hidden attributes on Windows ([#632]) - Make `trash` crate optional on Android ([#600]) ### Fixed - Parent folder not tracking CWD ([#581]) - Input offset is not reset when renaming with `--cursor=start` and the filename is too long ([#575]) ### Improved - Read directory in bulk in the background at startup ([#599]) - Lazy sorting when loading large directories to reduce CPU consumption ([#607]) ## [v0.2.2] ### Added - `prepend_keymap` and `append_keymap` for configuration mixing ([#546]) - `file(1)` as the file fallback previewer ([#543]) - Submit both completion and input with a single press of enter ([#565]) - Allow the spawned child processes to suspend ([#556]) - New `ya.host_name()` API ([#550]) - Desktop entry and logo ([#534]) - Snap package ([#531]) - Support Windows ARM64 ([#558]) - Image preview in Tabby terminal ([#569]) ### Fixed - Can't display file name with invalid UTF-8 ([#529]) ### Improved - New event system allows multiple actions to reuse a single render ([#561]) ## [v0.2.1] ### Fixed - Renaming may cause a crash when encountering Unicode characters ([#519]) ## [v0.2.0] ### Added - New `filter` action to filter files on the fly ([#454]) - Sort by file extension ([#405]) - Custom preloader and previewer ([#401]) - New `plugin` action to run Lua plugins - Auto-completion for input component ([#324], [#353], [#352]) - Start with the specified file hovers over ([#358]) - Emacs readline keybindings for inputs ([#345], [#382]) - New `--empty` and `--cursor` options for the `rename` action ([#513]) - New `--follow` option for `paste` action ([#436]) - Make `copy` action work over SSH with OSC 52 ([#447]) - New `reveal` action ([#341]) - Support colored icons ([#503]) - Support highlighting specific file types ([#510]) - Make the position of input and select components customizable ([#361]) - New `prepend_preloaders`, `append_preloaders`, `prepend_previewers`, `append_previewers` options for configuration mixing - Cursor and page key navigation parity with Vim bindings ([#386]) - Use terminal ANSI colors for code highlighting by default - New `image_alloc` and `image_bound` options to control image preview memory usage ([#376]) - New `suppress_preload` option to hide preload tasks ([#430]) - New kitty graphics protocol implementation for better compatibility with `tmux` through Unicode placeholders ([#365]) - New `ya.user_name()` and `ya.group_name()` API ([#469]) - New `ya.render()` to trigger a UI render - Image orientation support ([#488]) - Raise open file descriptors limit at startup ([#342]) - Support image preview on WSL ([#315]) - Fine-grained scheduling priority ([#462]) - New `YAZI_LEVEL` environment variable to indicate the nested level ([#514]) - New `QuadrantInside` and `QuadrantOutside` border type ### Changed - Rename the option `layout` to `ratio` to make it more self-explanatory - Rename the `peek` action to `seek` to better convey the action of "seeking for" content to preview - Rename the `--dir_first` option of `sort` action to `--dir-first` to make it consistent with the style of other actions - Replace `[plugins.preload]` with the `init.lua` entry file ### Fixed - `jq` previews empty when the user sets `tab_size=8` ([#320]) - Precache n-1 and n+1 pages ([#349]) - Popup components being covered by previewed images ([#360]) - Rust panics instead of returning an error when file times are invalid ([#357]) - Clear Sixel image with empty characters instead of `\x2B[K` to be compatible with GNOME VTE ([#309]) - Use `WAYLAND_DISPLAY` and `DISPLAY` to detect Wayland/X11 when `XDG_SESSION_TYPE` is not set ([#312]) ### Improved - Chunk loading for MIME types ([#467]) - Fallback to plain highlighter for long text ([#329]) - Reduce peak memory footprint during decoding large images ([#375]) - Clear only necessary cells when hiding images ([#369]) - New UI rendering architecture ([#468]) - Partial rendering progress and composite into a complete UI to reduce CPU consumption caused by frequent progress updates ([#509]) ## [v0.1.5] ### Added - New `find` action to find files ([#104]) - Linemode to show extra file info ([#291]) - New `sort_sensitive` option ([#155]) - Cross-platform opener rules ([#289]) - Multiple openers for a single open rule ([#154]) - Vim-like `gg`, `G` in the preset key mappings for boundary jumps - Theme system ([#161]) - New `--force` option for `remove`, `create`, `rename` actions ([#179], [#208]) - Image preview within tmux ([#147]) - New `link` action creates symlinks to the yanked files ([#167]) - New `orphan` option for opener rules to detach processes from the task scheduler ([#216]) - New `backward` and `forward` actions - New `--smart` option for the `find` action to support smart case ([#240]) - Sorting for each tab individually ([#131]) - Suspend process with `Ctrl+z` ([#120]) - Percentage values for the `arrow` action to scroll half/full page (with newly added Vi-like ``, ``, ``, and `` keybindings) ([#213]) - Show keywords when in search mode ([#152]) - Tab switch wraparound ([#160]) - Highlight matched keywords in find mode ([#211]) - Customizable main UI border styles ([#278]) - `` key notion ([#209]) - Use of environment variables in `cd` paths ([#241]) - Nix Flakes package ([#205]) - New `V`, `D`, `C` Vim-like keybindings for the input component - New `--no-cwd-file` option for the `quit` action to exit without writing the CWD file ([#245]) - Fallback to built-in code highlighting if `jq` is not installed ([#151]) - New `realtime` option for the input to support real-time input feedback ([#127]) - RGBA-16 image preview ([#250]) - FreeBSD and NetBSD support ([#169], [#178]) - Trash files on NetBSD ([#251]) - Image preview support on Mintty (Git Bash) terminal ### Changed - Make glob expressions case-insensitive by default (with new `\s` for sensitive) ([#156]) - Make help items filtering case-insensitive ### Fixed - `show_hidden` not properly applied to hovered folder ([#124]) - Notification of file changes in linked directories ([#121]) - Restore default cursor style when closing input from insert mode - Task manager cursor position not reset after task cancellation - Redirect clipboard process' stderr to /dev/null - Delegate the `SIGINT` signal of processes with `orphan=true` to their parent ([#290]) - Inconsistent `Shift` key behavior on Unix and Windows ([#174]) ### Improved - Load large folders in chunks ([#117]) - Reimplemented natural sorting algorithm for ~6x faster case-insensitive sorting - Kill process immediately after getting enough JSON or archive preview content to avoid wasting CPU resources ([#128]) ## [v0.1.4] ### Added - Help menu ([#93]) - Scrollable preview ([#86]) - Natural sorting ([#82]) - Windows support - New `copy` action to copy file paths to clipboard ([#72]) - File chooser mode ([#69]) - Show symlink path ([#67]) - Respect `$EDITOR` environment variable when opening text files ([#91]) - Customizable main UI layout ratio ([#76]) - Allow accessing selected files when running shell commands ([#73]) - Update MIME type when file changes are detected ([#78]) - More clipboard backend: `xclip` and `xsel`, and Windows ([#74], [#75]) - New `cache_dir` option ([#96]) - New `YAZI_CONFIG_HOME` to specify the configuration directory ([#97]) - Black Box terminal image preview support ([#99]) ### Deprecated - Deprecate `--cwd` in favor of the positional argument ([#100]) ### Fixed - Make file(1) follow symbolic links when fetching file MIME type ([#77]) - Wrong height of the select component ([#65]) - Regression causing UI tearing when previewing images - Specify `doNotMoveCursor` to make WezTerm render images sensibly ## [v0.1.3] ### Added - Bulk rename ([#50]) - PDF preview and precache ([#18]) - New `sort_dir_first` option ([#49]) - Code highlighting supports more languages ([#22]) - Change the shell CWD on exit with the shell wrapper ([#40]) - Allow customizing the display name of openers ([#31]) - New `shell` action ([#24]) - Command template support for the `shell` action ([#48]) - Interactive `cd` ([#43]) - Show the output of running tasks in real time ([#17]) - Allow using the current directory name as tab name ([#41]) - Custom status bar separator ([#30]) - Fallback for opening files when no openers are available - Preview files with `inode/empty` and `application/json` MIME types - Transparent image support for the Sixel backend ([#14]) - Refresh image preview after terminal restoration - New `micro_workers`, `macro_workers`, and `bizarre_retry` options to control task concurrency ([#53]) ### Fixed - PDF cache cannot be generated with a large `max_width` value ([#28]) - `show_hidden` option not working ([#47]) - Wrong task name when `shell` action has no arguments ### Improved - Make code highlighting discardable ([#20]) - Improved performance of highlighting large JSON files ([#23]) - Wrap `stdout` with `BufWriter` to improve image preview performance ([#55]) - Improved bulk rename performance ([#54]) ## [v0.1.2] ### Added - New `sort` action to change sorting method on the fly ([#7]) - Which-key component to support multi-key chords ([#4]) - Hover the cursor over newly created files automatically ([#10]) - Make folders openable ([#9]) - Several default goto key mappings - Support Überzug++ as the image preview backend for X11/Wayland ([#12]) - Cut input content to the system clipboard ([#6]) - Input component supports `undo` for cursor position - Support for bracketed paste ([#5]) ### Improved - Cache directory size to avoid redundant calculations ([#11]) ## [v0.1.1] ### Added - Arrow keys are now bound for navigation by default (along with existing Vim-style bindings) - Horizontal scrolling support for the `input` component - Visual mode for the input component - New `yank` and `paste` actions for the input component - New `undo` and `redo` actions for the `input` component ### Fixed - Cannot delete the last character of the input if at the end of the word ### Improved - Decode images in a dedicated blocking thread to avoid blocking the UI ## [v0.1.0] ### Added - Preset configurations - New `open` action - Select component for interactive `open` - Plain text and archive preview - Search files with `fd` and `rg` - Jump around with `fzf` and `zoxide` - Flat view for search results - Precache images and videos - Return to its parents if the CWD no longer exists - Confirm when deleting files or exiting - Custom status bar colors ### Fixed - Build errors on Linux - Number of remaining tasks cannot be updated [Unreleased]: https://github.com/sxyazi/yazi/compare/shipped...HEAD [v0.1.0]: https://github.com/sxyazi/yazi/releases/tag/v0.1.0 [v0.1.1]: https://github.com/sxyazi/yazi/compare/v0.1.0...v0.1.1 [v0.1.2]: https://github.com/sxyazi/yazi/compare/v0.1.1...v0.1.2 [v0.1.3]: https://github.com/sxyazi/yazi/compare/v0.1.2...v0.1.3 [v0.1.4]: https://github.com/sxyazi/yazi/compare/v0.1.3...v0.1.4 [v0.1.5]: https://github.com/sxyazi/yazi/compare/v0.1.4...v0.1.5 [v0.2.0]: https://github.com/sxyazi/yazi/compare/v0.1.5...v0.2.0 [v0.2.1]: https://github.com/sxyazi/yazi/compare/v0.2.0...v0.2.1 [v0.2.2]: https://github.com/sxyazi/yazi/compare/v0.2.1...v0.2.2 [v0.2.3]: https://github.com/sxyazi/yazi/compare/v0.2.2...v0.2.3 [v0.2.4]: https://github.com/sxyazi/yazi/compare/v0.2.3...v0.2.4 [v0.2.5]: https://github.com/sxyazi/yazi/compare/v0.2.4...v0.2.5 [v0.3.0]: https://github.com/sxyazi/yazi/compare/v0.2.5...v0.3.0 [v0.3.1]: https://github.com/sxyazi/yazi/compare/v0.3.0...v0.3.1 [v0.3.2]: https://github.com/sxyazi/yazi/compare/v0.3.1...v0.3.2 [v0.3.3]: https://github.com/sxyazi/yazi/compare/v0.3.2...v0.3.3 [v0.4.0]: https://github.com/sxyazi/yazi/compare/v0.3.3...v0.4.0 [v0.4.1]: https://github.com/sxyazi/yazi/compare/v0.4.0...v0.4.1 [v0.4.2]: https://github.com/sxyazi/yazi/compare/v0.4.1...v0.4.2 [v25.2.7]: https://github.com/sxyazi/yazi/compare/v0.4.2...v25.2.7 [v25.2.11]: https://github.com/sxyazi/yazi/compare/v25.2.7...v25.2.11 [v25.2.26]: https://github.com/sxyazi/yazi/compare/v25.2.11...v25.2.26 [v25.3.2]: https://github.com/sxyazi/yazi/compare/v25.2.26...v25.3.2 [v25.4.8]: https://github.com/sxyazi/yazi/compare/v25.3.2...v25.4.8 [v25.5.28]: https://github.com/sxyazi/yazi/compare/v25.4.8...v25.5.28 [v25.5.31]: https://github.com/sxyazi/yazi/compare/v25.5.28...v25.5.31 [v25.12.29]: https://github.com/sxyazi/yazi/compare/v25.5.31...v25.12.29 [v26.1.4]: https://github.com/sxyazi/yazi/compare/v25.12.29...v26.1.4 [v26.1.22]: https://github.com/sxyazi/yazi/compare/v26.1.4...v26.1.22 [#4]: https://github.com/sxyazi/yazi/pull/4 [#5]: https://github.com/sxyazi/yazi/pull/5 [#6]: https://github.com/sxyazi/yazi/pull/6 [#7]: https://github.com/sxyazi/yazi/pull/7 [#9]: https://github.com/sxyazi/yazi/pull/9 [#10]: https://github.com/sxyazi/yazi/pull/10 [#11]: https://github.com/sxyazi/yazi/pull/11 [#12]: https://github.com/sxyazi/yazi/pull/12 [#14]: https://github.com/sxyazi/yazi/pull/14 [#17]: https://github.com/sxyazi/yazi/pull/17 [#18]: https://github.com/sxyazi/yazi/pull/18 [#20]: https://github.com/sxyazi/yazi/pull/20 [#22]: https://github.com/sxyazi/yazi/pull/22 [#23]: https://github.com/sxyazi/yazi/pull/23 [#24]: https://github.com/sxyazi/yazi/pull/24 [#28]: https://github.com/sxyazi/yazi/pull/28 [#30]: https://github.com/sxyazi/yazi/pull/30 [#31]: https://github.com/sxyazi/yazi/pull/31 [#40]: https://github.com/sxyazi/yazi/pull/40 [#41]: https://github.com/sxyazi/yazi/pull/41 [#43]: https://github.com/sxyazi/yazi/pull/43 [#47]: https://github.com/sxyazi/yazi/pull/47 [#48]: https://github.com/sxyazi/yazi/pull/48 [#49]: https://github.com/sxyazi/yazi/pull/49 [#50]: https://github.com/sxyazi/yazi/pull/50 [#53]: https://github.com/sxyazi/yazi/pull/53 [#54]: https://github.com/sxyazi/yazi/pull/54 [#55]: https://github.com/sxyazi/yazi/pull/55 [#65]: https://github.com/sxyazi/yazi/pull/65 [#67]: https://github.com/sxyazi/yazi/pull/67 [#69]: https://github.com/sxyazi/yazi/pull/69 [#72]: https://github.com/sxyazi/yazi/pull/72 [#73]: https://github.com/sxyazi/yazi/pull/73 [#74]: https://github.com/sxyazi/yazi/pull/74 [#75]: https://github.com/sxyazi/yazi/pull/75 [#76]: https://github.com/sxyazi/yazi/pull/76 [#77]: https://github.com/sxyazi/yazi/pull/77 [#78]: https://github.com/sxyazi/yazi/pull/78 [#82]: https://github.com/sxyazi/yazi/pull/82 [#86]: https://github.com/sxyazi/yazi/pull/86 [#91]: https://github.com/sxyazi/yazi/pull/91 [#93]: https://github.com/sxyazi/yazi/pull/93 [#96]: https://github.com/sxyazi/yazi/pull/96 [#97]: https://github.com/sxyazi/yazi/pull/97 [#99]: https://github.com/sxyazi/yazi/pull/99 [#100]: https://github.com/sxyazi/yazi/pull/100 [#104]: https://github.com/sxyazi/yazi/pull/104 [#117]: https://github.com/sxyazi/yazi/pull/117 [#120]: https://github.com/sxyazi/yazi/pull/120 [#121]: https://github.com/sxyazi/yazi/pull/121 [#124]: https://github.com/sxyazi/yazi/pull/124 [#127]: https://github.com/sxyazi/yazi/pull/127 [#128]: https://github.com/sxyazi/yazi/pull/128 [#131]: https://github.com/sxyazi/yazi/pull/131 [#147]: https://github.com/sxyazi/yazi/pull/147 [#151]: https://github.com/sxyazi/yazi/pull/151 [#152]: https://github.com/sxyazi/yazi/pull/152 [#154]: https://github.com/sxyazi/yazi/pull/154 [#155]: https://github.com/sxyazi/yazi/pull/155 [#156]: https://github.com/sxyazi/yazi/pull/156 [#160]: https://github.com/sxyazi/yazi/pull/160 [#161]: https://github.com/sxyazi/yazi/pull/161 [#167]: https://github.com/sxyazi/yazi/pull/167 [#169]: https://github.com/sxyazi/yazi/pull/169 [#174]: https://github.com/sxyazi/yazi/pull/174 [#178]: https://github.com/sxyazi/yazi/pull/178 [#179]: https://github.com/sxyazi/yazi/pull/179 [#205]: https://github.com/sxyazi/yazi/pull/205 [#208]: https://github.com/sxyazi/yazi/pull/208 [#209]: https://github.com/sxyazi/yazi/pull/209 [#211]: https://github.com/sxyazi/yazi/pull/211 [#213]: https://github.com/sxyazi/yazi/pull/213 [#216]: https://github.com/sxyazi/yazi/pull/216 [#240]: https://github.com/sxyazi/yazi/pull/240 [#241]: https://github.com/sxyazi/yazi/pull/241 [#245]: https://github.com/sxyazi/yazi/pull/245 [#250]: https://github.com/sxyazi/yazi/pull/250 [#251]: https://github.com/sxyazi/yazi/pull/251 [#278]: https://github.com/sxyazi/yazi/pull/278 [#289]: https://github.com/sxyazi/yazi/pull/289 [#290]: https://github.com/sxyazi/yazi/pull/290 [#291]: https://github.com/sxyazi/yazi/pull/291 [#309]: https://github.com/sxyazi/yazi/pull/309 [#312]: https://github.com/sxyazi/yazi/pull/312 [#313]: https://github.com/sxyazi/yazi/pull/313 [#315]: https://github.com/sxyazi/yazi/pull/315 [#320]: https://github.com/sxyazi/yazi/pull/320 [#324]: https://github.com/sxyazi/yazi/pull/324 [#329]: https://github.com/sxyazi/yazi/pull/329 [#341]: https://github.com/sxyazi/yazi/pull/341 [#342]: https://github.com/sxyazi/yazi/pull/342 [#345]: https://github.com/sxyazi/yazi/pull/345 [#349]: https://github.com/sxyazi/yazi/pull/349 [#352]: https://github.com/sxyazi/yazi/pull/352 [#353]: https://github.com/sxyazi/yazi/pull/353 [#357]: https://github.com/sxyazi/yazi/pull/357 [#358]: https://github.com/sxyazi/yazi/pull/358 [#360]: https://github.com/sxyazi/yazi/pull/360 [#361]: https://github.com/sxyazi/yazi/pull/361 [#365]: https://github.com/sxyazi/yazi/pull/365 [#369]: https://github.com/sxyazi/yazi/pull/369 [#375]: https://github.com/sxyazi/yazi/pull/375 [#376]: https://github.com/sxyazi/yazi/pull/376 [#382]: https://github.com/sxyazi/yazi/pull/382 [#386]: https://github.com/sxyazi/yazi/pull/386 [#401]: https://github.com/sxyazi/yazi/pull/401 [#405]: https://github.com/sxyazi/yazi/pull/405 [#430]: https://github.com/sxyazi/yazi/pull/430 [#436]: https://github.com/sxyazi/yazi/pull/436 [#447]: https://github.com/sxyazi/yazi/pull/447 [#454]: https://github.com/sxyazi/yazi/pull/454 [#462]: https://github.com/sxyazi/yazi/pull/462 [#467]: https://github.com/sxyazi/yazi/pull/467 [#468]: https://github.com/sxyazi/yazi/pull/468 [#469]: https://github.com/sxyazi/yazi/pull/469 [#488]: https://github.com/sxyazi/yazi/pull/488 [#503]: https://github.com/sxyazi/yazi/pull/503 [#509]: https://github.com/sxyazi/yazi/pull/509 [#510]: https://github.com/sxyazi/yazi/pull/510 [#513]: https://github.com/sxyazi/yazi/pull/513 [#514]: https://github.com/sxyazi/yazi/pull/514 [#519]: https://github.com/sxyazi/yazi/pull/519 [#529]: https://github.com/sxyazi/yazi/pull/529 [#531]: https://github.com/sxyazi/yazi/pull/531 [#534]: https://github.com/sxyazi/yazi/pull/534 [#543]: https://github.com/sxyazi/yazi/pull/543 [#546]: https://github.com/sxyazi/yazi/pull/546 [#550]: https://github.com/sxyazi/yazi/pull/550 [#556]: https://github.com/sxyazi/yazi/pull/556 [#558]: https://github.com/sxyazi/yazi/pull/558 [#561]: https://github.com/sxyazi/yazi/pull/561 [#565]: https://github.com/sxyazi/yazi/pull/565 [#569]: https://github.com/sxyazi/yazi/pull/569 [#571]: https://github.com/sxyazi/yazi/pull/571 [#575]: https://github.com/sxyazi/yazi/pull/575 [#576]: https://github.com/sxyazi/yazi/pull/576 [#581]: https://github.com/sxyazi/yazi/pull/581 [#585]: https://github.com/sxyazi/yazi/pull/585 [#586]: https://github.com/sxyazi/yazi/pull/586 [#587]: https://github.com/sxyazi/yazi/pull/587 [#590]: https://github.com/sxyazi/yazi/pull/590 [#599]: https://github.com/sxyazi/yazi/pull/599 [#600]: https://github.com/sxyazi/yazi/pull/600 [#607]: https://github.com/sxyazi/yazi/pull/607 [#617]: https://github.com/sxyazi/yazi/pull/617 [#628]: https://github.com/sxyazi/yazi/pull/628 [#632]: https://github.com/sxyazi/yazi/pull/632 [#643]: https://github.com/sxyazi/yazi/pull/643 [#644]: https://github.com/sxyazi/yazi/pull/644 [#646]: https://github.com/sxyazi/yazi/pull/646 [#649]: https://github.com/sxyazi/yazi/pull/649 [#659]: https://github.com/sxyazi/yazi/pull/659 [#662]: https://github.com/sxyazi/yazi/pull/662 [#665]: https://github.com/sxyazi/yazi/pull/665 [#670]: https://github.com/sxyazi/yazi/pull/670 [#674]: https://github.com/sxyazi/yazi/pull/674 [#679]: https://github.com/sxyazi/yazi/pull/679 [#683]: https://github.com/sxyazi/yazi/pull/683 [#687]: https://github.com/sxyazi/yazi/pull/687 [#689]: https://github.com/sxyazi/yazi/pull/689 [#693]: https://github.com/sxyazi/yazi/pull/693 [#710]: https://github.com/sxyazi/yazi/pull/710 [#721]: https://github.com/sxyazi/yazi/pull/721 [#738]: https://github.com/sxyazi/yazi/pull/738 [#749]: https://github.com/sxyazi/yazi/pull/749 [#751]: https://github.com/sxyazi/yazi/pull/751 [#752]: https://github.com/sxyazi/yazi/pull/752 [#753]: https://github.com/sxyazi/yazi/pull/753 [#754]: https://github.com/sxyazi/yazi/pull/754 [#759]: https://github.com/sxyazi/yazi/pull/759 [#762]: https://github.com/sxyazi/yazi/pull/762 [#763]: https://github.com/sxyazi/yazi/pull/763 [#775]: https://github.com/sxyazi/yazi/pull/775 [#779]: https://github.com/sxyazi/yazi/pull/779 [#780]: https://github.com/sxyazi/yazi/pull/780 [#786]: https://github.com/sxyazi/yazi/pull/786 [#788]: https://github.com/sxyazi/yazi/pull/788 [#792]: https://github.com/sxyazi/yazi/pull/792 [#794]: https://github.com/sxyazi/yazi/pull/794 [#799]: https://github.com/sxyazi/yazi/pull/799 [#812]: https://github.com/sxyazi/yazi/pull/812 [#824]: https://github.com/sxyazi/yazi/pull/824 [#826]: https://github.com/sxyazi/yazi/pull/826 [#835]: https://github.com/sxyazi/yazi/pull/835 [#837]: https://github.com/sxyazi/yazi/pull/837 [#843]: https://github.com/sxyazi/yazi/pull/843 [#846]: https://github.com/sxyazi/yazi/pull/846 [#849]: https://github.com/sxyazi/yazi/pull/849 [#853]: https://github.com/sxyazi/yazi/pull/853 [#855]: https://github.com/sxyazi/yazi/pull/855 [#861]: https://github.com/sxyazi/yazi/pull/861 [#867]: https://github.com/sxyazi/yazi/pull/867 [#868]: https://github.com/sxyazi/yazi/pull/868 [#871]: https://github.com/sxyazi/yazi/pull/871 [#877]: https://github.com/sxyazi/yazi/pull/877 [#879]: https://github.com/sxyazi/yazi/pull/879 [#880]: https://github.com/sxyazi/yazi/pull/880 [#881]: https://github.com/sxyazi/yazi/pull/881 [#884]: https://github.com/sxyazi/yazi/pull/884 [#887]: https://github.com/sxyazi/yazi/pull/887 [#895]: https://github.com/sxyazi/yazi/pull/895 [#900]: https://github.com/sxyazi/yazi/pull/900 [#908]: https://github.com/sxyazi/yazi/pull/908 [#909]: https://github.com/sxyazi/yazi/pull/909 [#910]: https://github.com/sxyazi/yazi/pull/910 [#913]: https://github.com/sxyazi/yazi/pull/913 [#917]: https://github.com/sxyazi/yazi/pull/917 [#920]: https://github.com/sxyazi/yazi/pull/920 [#925]: https://github.com/sxyazi/yazi/pull/925 [#926]: https://github.com/sxyazi/yazi/pull/926 [#928]: https://github.com/sxyazi/yazi/pull/928 [#931]: https://github.com/sxyazi/yazi/pull/931 [#933]: https://github.com/sxyazi/yazi/pull/933 [#937]: https://github.com/sxyazi/yazi/pull/937 [#940]: https://github.com/sxyazi/yazi/pull/940 [#944]: https://github.com/sxyazi/yazi/pull/944 [#948]: https://github.com/sxyazi/yazi/pull/948 [#958]: https://github.com/sxyazi/yazi/pull/958 [#975]: https://github.com/sxyazi/yazi/pull/975 [#977]: https://github.com/sxyazi/yazi/pull/977 [#980]: https://github.com/sxyazi/yazi/pull/980 [#985]: https://github.com/sxyazi/yazi/pull/985 [#997]: https://github.com/sxyazi/yazi/pull/997 [#1003]: https://github.com/sxyazi/yazi/pull/1003 [#1004]: https://github.com/sxyazi/yazi/pull/1004 [#1005]: https://github.com/sxyazi/yazi/pull/1005 [#1025]: https://github.com/sxyazi/yazi/pull/1025 [#1033]: https://github.com/sxyazi/yazi/pull/1033 [#1038]: https://github.com/sxyazi/yazi/pull/1038 [#1048]: https://github.com/sxyazi/yazi/pull/1048 [#1050]: https://github.com/sxyazi/yazi/pull/1050 [#1053]: https://github.com/sxyazi/yazi/pull/1053 [#1069]: https://github.com/sxyazi/yazi/pull/1069 [#1070]: https://github.com/sxyazi/yazi/pull/1070 [#1081]: https://github.com/sxyazi/yazi/pull/1081 [#1082]: https://github.com/sxyazi/yazi/pull/1082 [#1086]: https://github.com/sxyazi/yazi/pull/1086 [#1094]: https://github.com/sxyazi/yazi/pull/1094 [#1110]: https://github.com/sxyazi/yazi/pull/1110 [#1111]: https://github.com/sxyazi/yazi/pull/1111 [#1139]: https://github.com/sxyazi/yazi/pull/1139 [#1151]: https://github.com/sxyazi/yazi/pull/1151 [#1159]: https://github.com/sxyazi/yazi/pull/1159 [#1167]: https://github.com/sxyazi/yazi/pull/1167 [#1169]: https://github.com/sxyazi/yazi/pull/1169 [#1185]: https://github.com/sxyazi/yazi/pull/1185 [#1220]: https://github.com/sxyazi/yazi/pull/1220 [#1227]: https://github.com/sxyazi/yazi/pull/1227 [#1232]: https://github.com/sxyazi/yazi/pull/1232 [#1238]: https://github.com/sxyazi/yazi/pull/1238 [#1241]: https://github.com/sxyazi/yazi/pull/1241 [#1249]: https://github.com/sxyazi/yazi/pull/1249 [#1268]: https://github.com/sxyazi/yazi/pull/1268 [#1270]: https://github.com/sxyazi/yazi/pull/1270 [#1291]: https://github.com/sxyazi/yazi/pull/1291 [#1295]: https://github.com/sxyazi/yazi/pull/1295 [#1305]: https://github.com/sxyazi/yazi/pull/1305 [#1321]: https://github.com/sxyazi/yazi/pull/1321 [#1361]: https://github.com/sxyazi/yazi/pull/1361 [#1395]: https://github.com/sxyazi/yazi/pull/1395 [#1412]: https://github.com/sxyazi/yazi/pull/1412 [#1422]: https://github.com/sxyazi/yazi/pull/1422 [#1428]: https://github.com/sxyazi/yazi/pull/1428 [#1431]: https://github.com/sxyazi/yazi/pull/1431 [#1434]: https://github.com/sxyazi/yazi/pull/1434 [#1439]: https://github.com/sxyazi/yazi/pull/1439 [#1443]: https://github.com/sxyazi/yazi/pull/1443 [#1446]: https://github.com/sxyazi/yazi/pull/1446 [#1448]: https://github.com/sxyazi/yazi/pull/1448 [#1451]: https://github.com/sxyazi/yazi/pull/1451 [#1461]: https://github.com/sxyazi/yazi/pull/1461 [#1464]: https://github.com/sxyazi/yazi/pull/1464 [#1467]: https://github.com/sxyazi/yazi/pull/1467 [#1468]: https://github.com/sxyazi/yazi/pull/1468 [#1473]: https://github.com/sxyazi/yazi/pull/1473 [#1474]: https://github.com/sxyazi/yazi/pull/1474 [#1482]: https://github.com/sxyazi/yazi/pull/1482 [#1497]: https://github.com/sxyazi/yazi/pull/1497 [#1500]: https://github.com/sxyazi/yazi/pull/1500 [#1505]: https://github.com/sxyazi/yazi/pull/1505 [#1512]: https://github.com/sxyazi/yazi/pull/1512 [#1528]: https://github.com/sxyazi/yazi/pull/1528 [#1541]: https://github.com/sxyazi/yazi/pull/1541 [#1542]: https://github.com/sxyazi/yazi/pull/1542 [#1550]: https://github.com/sxyazi/yazi/pull/1550 [#1551]: https://github.com/sxyazi/yazi/pull/1551 [#1556]: https://github.com/sxyazi/yazi/pull/1556 [#1562]: https://github.com/sxyazi/yazi/pull/1562 [#1566]: https://github.com/sxyazi/yazi/pull/1566 [#1568]: https://github.com/sxyazi/yazi/pull/1568 [#1574]: https://github.com/sxyazi/yazi/pull/1574 [#1583]: https://github.com/sxyazi/yazi/pull/1583 [#1588]: https://github.com/sxyazi/yazi/pull/1588 [#1590]: https://github.com/sxyazi/yazi/pull/1590 [#1591]: https://github.com/sxyazi/yazi/pull/1591 [#1605]: https://github.com/sxyazi/yazi/pull/1605 [#1614]: https://github.com/sxyazi/yazi/pull/1614 [#1622]: https://github.com/sxyazi/yazi/pull/1622 [#1639]: https://github.com/sxyazi/yazi/pull/1639 [#1648]: https://github.com/sxyazi/yazi/pull/1648 [#1650]: https://github.com/sxyazi/yazi/pull/1650 [#1652]: https://github.com/sxyazi/yazi/pull/1652 [#1666]: https://github.com/sxyazi/yazi/pull/1666 [#1667]: https://github.com/sxyazi/yazi/pull/1667 [#1680]: https://github.com/sxyazi/yazi/pull/1680 [#1682]: https://github.com/sxyazi/yazi/pull/1682 [#1689]: https://github.com/sxyazi/yazi/pull/1689 [#1695]: https://github.com/sxyazi/yazi/pull/1695 [#1704]: https://github.com/sxyazi/yazi/pull/1704 [#1737]: https://github.com/sxyazi/yazi/pull/1737 [#1745]: https://github.com/sxyazi/yazi/pull/1745 [#1761]: https://github.com/sxyazi/yazi/pull/1761 [#1762]: https://github.com/sxyazi/yazi/pull/1762 [#1772]: https://github.com/sxyazi/yazi/pull/1772 [#1773]: https://github.com/sxyazi/yazi/pull/1773 [#1776]: https://github.com/sxyazi/yazi/pull/1776 [#1782]: https://github.com/sxyazi/yazi/pull/1782 [#1784]: https://github.com/sxyazi/yazi/pull/1784 [#1789]: https://github.com/sxyazi/yazi/pull/1789 [#1792]: https://github.com/sxyazi/yazi/pull/1792 [#1801]: https://github.com/sxyazi/yazi/pull/1801 [#1802]: https://github.com/sxyazi/yazi/pull/1802 [#1807]: https://github.com/sxyazi/yazi/pull/1807 [#1808]: https://github.com/sxyazi/yazi/pull/1808 [#1816]: https://github.com/sxyazi/yazi/pull/1816 [#1832]: https://github.com/sxyazi/yazi/pull/1832 [#1833]: https://github.com/sxyazi/yazi/pull/1833 [#1846]: https://github.com/sxyazi/yazi/pull/1846 [#1849]: https://github.com/sxyazi/yazi/pull/1849 [#1863]: https://github.com/sxyazi/yazi/pull/1863 [#1877]: https://github.com/sxyazi/yazi/pull/1877 [#1882]: https://github.com/sxyazi/yazi/pull/1882 [#1884]: https://github.com/sxyazi/yazi/pull/1884 [#1891]: https://github.com/sxyazi/yazi/pull/1891 [#1903]: https://github.com/sxyazi/yazi/pull/1903 [#1926]: https://github.com/sxyazi/yazi/pull/1926 [#1927]: https://github.com/sxyazi/yazi/pull/1927 [#1928]: https://github.com/sxyazi/yazi/pull/1928 [#1939]: https://github.com/sxyazi/yazi/pull/1939 [#1945]: https://github.com/sxyazi/yazi/pull/1945 [#1946]: https://github.com/sxyazi/yazi/pull/1946 [#1953]: https://github.com/sxyazi/yazi/pull/1953 [#1962]: https://github.com/sxyazi/yazi/pull/1962 [#1966]: https://github.com/sxyazi/yazi/pull/1966 [#1973]: https://github.com/sxyazi/yazi/pull/1973 [#1979]: https://github.com/sxyazi/yazi/pull/1979 [#1980]: https://github.com/sxyazi/yazi/pull/1980 [#1982]: https://github.com/sxyazi/yazi/pull/1982 [#1984]: https://github.com/sxyazi/yazi/pull/1984 [#1995]: https://github.com/sxyazi/yazi/pull/1995 [#2002]: https://github.com/sxyazi/yazi/pull/2002 [#2003]: https://github.com/sxyazi/yazi/pull/2003 [#2004]: https://github.com/sxyazi/yazi/pull/2004 [#2006]: https://github.com/sxyazi/yazi/pull/2006 [#2014]: https://github.com/sxyazi/yazi/pull/2014 [#2017]: https://github.com/sxyazi/yazi/pull/2017 [#2020]: https://github.com/sxyazi/yazi/pull/2020 [#2025]: https://github.com/sxyazi/yazi/pull/2025 [#2030]: https://github.com/sxyazi/yazi/pull/2030 [#2041]: https://github.com/sxyazi/yazi/pull/2041 [#2043]: https://github.com/sxyazi/yazi/pull/2043 [#2052]: https://github.com/sxyazi/yazi/pull/2052 [#2058]: https://github.com/sxyazi/yazi/pull/2058 [#2060]: https://github.com/sxyazi/yazi/pull/2060 [#2064]: https://github.com/sxyazi/yazi/pull/2064 [#2068]: https://github.com/sxyazi/yazi/pull/2068 [#2071]: https://github.com/sxyazi/yazi/pull/2071 [#2072]: https://github.com/sxyazi/yazi/pull/2072 [#2077]: https://github.com/sxyazi/yazi/pull/2077 [#2093]: https://github.com/sxyazi/yazi/pull/2093 [#2095]: https://github.com/sxyazi/yazi/pull/2095 [#2105]: https://github.com/sxyazi/yazi/pull/2105 [#2110]: https://github.com/sxyazi/yazi/pull/2110 [#2122]: https://github.com/sxyazi/yazi/pull/2122 [#2132]: https://github.com/sxyazi/yazi/pull/2132 [#2143]: https://github.com/sxyazi/yazi/pull/2143 [#2149]: https://github.com/sxyazi/yazi/pull/2149 [#2168]: https://github.com/sxyazi/yazi/pull/2168 [#2173]: https://github.com/sxyazi/yazi/pull/2173 [#2181]: https://github.com/sxyazi/yazi/pull/2181 [#2185]: https://github.com/sxyazi/yazi/pull/2185 [#2186]: https://github.com/sxyazi/yazi/pull/2186 [#2188]: https://github.com/sxyazi/yazi/pull/2188 [#2199]: https://github.com/sxyazi/yazi/pull/2199 [#2205]: https://github.com/sxyazi/yazi/pull/2205 [#2210]: https://github.com/sxyazi/yazi/pull/2210 [#2224]: https://github.com/sxyazi/yazi/pull/2224 [#2233]: https://github.com/sxyazi/yazi/pull/2233 [#2234]: https://github.com/sxyazi/yazi/pull/2234 [#2242]: https://github.com/sxyazi/yazi/pull/2242 [#2245]: https://github.com/sxyazi/yazi/pull/2245 [#2247]: https://github.com/sxyazi/yazi/pull/2247 [#2253]: https://github.com/sxyazi/yazi/pull/2253 [#2257]: https://github.com/sxyazi/yazi/pull/2257 [#2290]: https://github.com/sxyazi/yazi/pull/2290 [#2294]: https://github.com/sxyazi/yazi/pull/2294 [#2298]: https://github.com/sxyazi/yazi/pull/2298 [#2299]: https://github.com/sxyazi/yazi/pull/2299 [#2310]: https://github.com/sxyazi/yazi/pull/2310 [#2313]: https://github.com/sxyazi/yazi/pull/2313 [#2314]: https://github.com/sxyazi/yazi/pull/2314 [#2319]: https://github.com/sxyazi/yazi/pull/2319 [#2321]: https://github.com/sxyazi/yazi/pull/2321 [#2326]: https://github.com/sxyazi/yazi/pull/2326 [#2327]: https://github.com/sxyazi/yazi/pull/2327 [#2331]: https://github.com/sxyazi/yazi/pull/2331 [#2337]: https://github.com/sxyazi/yazi/pull/2337 [#2343]: https://github.com/sxyazi/yazi/pull/2343 [#2355]: https://github.com/sxyazi/yazi/pull/2355 [#2366]: https://github.com/sxyazi/yazi/pull/2366 [#2383]: https://github.com/sxyazi/yazi/pull/2383 [#2389]: https://github.com/sxyazi/yazi/pull/2389 [#2391]: https://github.com/sxyazi/yazi/pull/2391 [#2392]: https://github.com/sxyazi/yazi/pull/2392 [#2393]: https://github.com/sxyazi/yazi/pull/2393 [#2397]: https://github.com/sxyazi/yazi/pull/2397 [#2399]: https://github.com/sxyazi/yazi/pull/2399 [#2403]: https://github.com/sxyazi/yazi/pull/2403 [#2405]: https://github.com/sxyazi/yazi/pull/2405 [#2413]: https://github.com/sxyazi/yazi/pull/2413 [#2418]: https://github.com/sxyazi/yazi/pull/2418 [#2425]: https://github.com/sxyazi/yazi/pull/2425 [#2427]: https://github.com/sxyazi/yazi/pull/2427 [#2431]: https://github.com/sxyazi/yazi/pull/2431 [#2439]: https://github.com/sxyazi/yazi/pull/2439 [#2442]: https://github.com/sxyazi/yazi/pull/2442 [#2444]: https://github.com/sxyazi/yazi/pull/2444 [#2449]: https://github.com/sxyazi/yazi/pull/2449 [#2452]: https://github.com/sxyazi/yazi/pull/2452 [#2456]: https://github.com/sxyazi/yazi/pull/2456 [#2458]: https://github.com/sxyazi/yazi/pull/2458 [#2461]: https://github.com/sxyazi/yazi/pull/2461 [#2471]: https://github.com/sxyazi/yazi/pull/2471 [#2476]: https://github.com/sxyazi/yazi/pull/2476 [#2485]: https://github.com/sxyazi/yazi/pull/2485 [#2487]: https://github.com/sxyazi/yazi/pull/2487 [#2490]: https://github.com/sxyazi/yazi/pull/2490 [#2492]: https://github.com/sxyazi/yazi/pull/2492 [#2494]: https://github.com/sxyazi/yazi/pull/2494 [#2503]: https://github.com/sxyazi/yazi/pull/2503 [#2508]: https://github.com/sxyazi/yazi/pull/2508 [#2522]: https://github.com/sxyazi/yazi/pull/2522 [#2526]: https://github.com/sxyazi/yazi/pull/2526 [#2527]: https://github.com/sxyazi/yazi/pull/2527 [#2530]: https://github.com/sxyazi/yazi/pull/2530 [#2533]: https://github.com/sxyazi/yazi/pull/2533 [#2540]: https://github.com/sxyazi/yazi/pull/2540 [#2543]: https://github.com/sxyazi/yazi/pull/2543 [#2546]: https://github.com/sxyazi/yazi/pull/2546 [#2553]: https://github.com/sxyazi/yazi/pull/2553 [#2560]: https://github.com/sxyazi/yazi/pull/2560 [#2572]: https://github.com/sxyazi/yazi/pull/2572 [#2574]: https://github.com/sxyazi/yazi/pull/2574 [#2578]: https://github.com/sxyazi/yazi/pull/2578 [#2581]: https://github.com/sxyazi/yazi/pull/2581 [#2589]: https://github.com/sxyazi/yazi/pull/2589 [#2594]: https://github.com/sxyazi/yazi/pull/2594 [#2602]: https://github.com/sxyazi/yazi/pull/2602 [#2609]: https://github.com/sxyazi/yazi/pull/2609 [#2636]: https://github.com/sxyazi/yazi/pull/2636 [#2640]: https://github.com/sxyazi/yazi/pull/2640 [#2653]: https://github.com/sxyazi/yazi/pull/2653 [#2657]: https://github.com/sxyazi/yazi/pull/2657 [#2664]: https://github.com/sxyazi/yazi/pull/2664 [#2675]: https://github.com/sxyazi/yazi/pull/2675 [#2678]: https://github.com/sxyazi/yazi/pull/2678 [#2683]: https://github.com/sxyazi/yazi/pull/2683 [#2691]: https://github.com/sxyazi/yazi/pull/2691 [#2695]: https://github.com/sxyazi/yazi/pull/2695 [#2696]: https://github.com/sxyazi/yazi/pull/2696 [#2697]: https://github.com/sxyazi/yazi/pull/2697 [#2700]: https://github.com/sxyazi/yazi/pull/2700 [#2706]: https://github.com/sxyazi/yazi/pull/2706 [#2707]: https://github.com/sxyazi/yazi/pull/2707 [#2709]: https://github.com/sxyazi/yazi/pull/2709 [#2723]: https://github.com/sxyazi/yazi/pull/2723 [#2734]: https://github.com/sxyazi/yazi/pull/2734 [#2743]: https://github.com/sxyazi/yazi/pull/2743 [#2745]: https://github.com/sxyazi/yazi/pull/2745 [#2752]: https://github.com/sxyazi/yazi/pull/2752 [#2753]: https://github.com/sxyazi/yazi/pull/2753 [#2754]: https://github.com/sxyazi/yazi/pull/2754 [#2759]: https://github.com/sxyazi/yazi/pull/2759 [#2764]: https://github.com/sxyazi/yazi/pull/2764 [#2765]: https://github.com/sxyazi/yazi/pull/2765 [#2769]: https://github.com/sxyazi/yazi/pull/2769 [#2770]: https://github.com/sxyazi/yazi/pull/2770 [#2778]: https://github.com/sxyazi/yazi/pull/2778 [#2802]: https://github.com/sxyazi/yazi/pull/2802 [#2803]: https://github.com/sxyazi/yazi/pull/2803 [#2807]: https://github.com/sxyazi/yazi/pull/2807 [#2810]: https://github.com/sxyazi/yazi/pull/2810 [#2811]: https://github.com/sxyazi/yazi/pull/2811 [#2814]: https://github.com/sxyazi/yazi/pull/2814 [#2820]: https://github.com/sxyazi/yazi/pull/2820 [#2834]: https://github.com/sxyazi/yazi/pull/2834 [#2841]: https://github.com/sxyazi/yazi/pull/2841 [#2843]: https://github.com/sxyazi/yazi/pull/2843 [#2849]: https://github.com/sxyazi/yazi/pull/2849 [#2855]: https://github.com/sxyazi/yazi/pull/2855 [#2861]: https://github.com/sxyazi/yazi/pull/2861 [#2862]: https://github.com/sxyazi/yazi/pull/2862 [#2864]: https://github.com/sxyazi/yazi/pull/2864 [#2875]: https://github.com/sxyazi/yazi/pull/2875 [#2879]: https://github.com/sxyazi/yazi/pull/2879 [#2880]: https://github.com/sxyazi/yazi/pull/2880 [#2884]: https://github.com/sxyazi/yazi/pull/2884 [#2889]: https://github.com/sxyazi/yazi/pull/2889 [#2890]: https://github.com/sxyazi/yazi/pull/2890 [#2895]: https://github.com/sxyazi/yazi/pull/2895 [#2904]: https://github.com/sxyazi/yazi/pull/2904 [#2906]: https://github.com/sxyazi/yazi/pull/2906 [#2914]: https://github.com/sxyazi/yazi/pull/2914 [#2915]: https://github.com/sxyazi/yazi/pull/2915 [#2917]: https://github.com/sxyazi/yazi/pull/2917 [#2921]: https://github.com/sxyazi/yazi/pull/2921 [#2925]: https://github.com/sxyazi/yazi/pull/2925 [#2927]: https://github.com/sxyazi/yazi/pull/2927 [#2931]: https://github.com/sxyazi/yazi/pull/2931 [#2932]: https://github.com/sxyazi/yazi/pull/2932 [#2935]: https://github.com/sxyazi/yazi/pull/2935 [#2939]: https://github.com/sxyazi/yazi/pull/2939 [#2941]: https://github.com/sxyazi/yazi/pull/2941 [#2958]: https://github.com/sxyazi/yazi/pull/2958 [#2959]: https://github.com/sxyazi/yazi/pull/2959 [#2964]: https://github.com/sxyazi/yazi/pull/2964 [#2984]: https://github.com/sxyazi/yazi/pull/2984 [#2997]: https://github.com/sxyazi/yazi/pull/2997 [#3005]: https://github.com/sxyazi/yazi/pull/3005 [#3008]: https://github.com/sxyazi/yazi/pull/3008 [#3023]: https://github.com/sxyazi/yazi/pull/3023 [#3034]: https://github.com/sxyazi/yazi/pull/3034 [#3035]: https://github.com/sxyazi/yazi/pull/3035 [#3037]: https://github.com/sxyazi/yazi/pull/3037 [#3038]: https://github.com/sxyazi/yazi/pull/3038 [#3059]: https://github.com/sxyazi/yazi/pull/3059 [#3067]: https://github.com/sxyazi/yazi/pull/3067 [#3077]: https://github.com/sxyazi/yazi/pull/3077 [#3083]: https://github.com/sxyazi/yazi/pull/3083 [#3084]: https://github.com/sxyazi/yazi/pull/3084 [#3091]: https://github.com/sxyazi/yazi/pull/3091 [#3094]: https://github.com/sxyazi/yazi/pull/3094 [#3108]: https://github.com/sxyazi/yazi/pull/3108 [#3117]: https://github.com/sxyazi/yazi/pull/3117 [#3121]: https://github.com/sxyazi/yazi/pull/3121 [#3128]: https://github.com/sxyazi/yazi/pull/3128 [#3131]: https://github.com/sxyazi/yazi/pull/3131 [#3134]: https://github.com/sxyazi/yazi/pull/3134 [#3141]: https://github.com/sxyazi/yazi/pull/3141 [#3154]: https://github.com/sxyazi/yazi/pull/3154 [#3166]: https://github.com/sxyazi/yazi/pull/3166 [#3169]: https://github.com/sxyazi/yazi/pull/3169 [#3170]: https://github.com/sxyazi/yazi/pull/3170 [#3172]: https://github.com/sxyazi/yazi/pull/3172 [#3187]: https://github.com/sxyazi/yazi/pull/3187 [#3189]: https://github.com/sxyazi/yazi/pull/3189 [#3190]: https://github.com/sxyazi/yazi/pull/3190 [#3198]: https://github.com/sxyazi/yazi/pull/3198 [#3200]: https://github.com/sxyazi/yazi/pull/3200 [#3201]: https://github.com/sxyazi/yazi/pull/3201 [#3203]: https://github.com/sxyazi/yazi/pull/3203 [#3209]: https://github.com/sxyazi/yazi/pull/3209 [#3222]: https://github.com/sxyazi/yazi/pull/3222 [#3225]: https://github.com/sxyazi/yazi/pull/3225 [#3226]: https://github.com/sxyazi/yazi/pull/3226 [#3232]: https://github.com/sxyazi/yazi/pull/3232 [#3235]: https://github.com/sxyazi/yazi/pull/3235 [#3243]: https://github.com/sxyazi/yazi/pull/3243 [#3250]: https://github.com/sxyazi/yazi/pull/3250 [#3264]: https://github.com/sxyazi/yazi/pull/3264 [#3268]: https://github.com/sxyazi/yazi/pull/3268 [#3271]: https://github.com/sxyazi/yazi/pull/3271 [#3286]: https://github.com/sxyazi/yazi/pull/3286 [#3290]: https://github.com/sxyazi/yazi/pull/3290 [#3313]: https://github.com/sxyazi/yazi/pull/3313 [#3317]: https://github.com/sxyazi/yazi/pull/3317 [#3358]: https://github.com/sxyazi/yazi/pull/3358 [#3360]: https://github.com/sxyazi/yazi/pull/3360 [#3361]: https://github.com/sxyazi/yazi/pull/3361 [#3364]: https://github.com/sxyazi/yazi/pull/3364 [#3369]: https://github.com/sxyazi/yazi/pull/3369 [#3383]: https://github.com/sxyazi/yazi/pull/3383 [#3385]: https://github.com/sxyazi/yazi/pull/3385 [#3387]: https://github.com/sxyazi/yazi/pull/3387 [#3391]: https://github.com/sxyazi/yazi/pull/3391 [#3393]: https://github.com/sxyazi/yazi/pull/3393 [#3396]: https://github.com/sxyazi/yazi/pull/3396 [#3419]: https://github.com/sxyazi/yazi/pull/3419 [#3422]: https://github.com/sxyazi/yazi/pull/3422 [#3429]: https://github.com/sxyazi/yazi/pull/3429 [#3456]: https://github.com/sxyazi/yazi/pull/3456 [#3467]: https://github.com/sxyazi/yazi/pull/3467 [#3477]: https://github.com/sxyazi/yazi/pull/3477 [#3482]: https://github.com/sxyazi/yazi/pull/3482 [#3494]: https://github.com/sxyazi/yazi/pull/3494 [#3514]: https://github.com/sxyazi/yazi/pull/3514 [#3518]: https://github.com/sxyazi/yazi/pull/3518 [#3525]: https://github.com/sxyazi/yazi/pull/3525 [#3532]: https://github.com/sxyazi/yazi/pull/3532 [#3540]: https://github.com/sxyazi/yazi/pull/3540 [#3541]: https://github.com/sxyazi/yazi/pull/3541 [#3561]: https://github.com/sxyazi/yazi/pull/3561 [#3566]: https://github.com/sxyazi/yazi/pull/3566 [#3582]: https://github.com/sxyazi/yazi/pull/3582 [#3594]: https://github.com/sxyazi/yazi/pull/3594 [#3607]: https://github.com/sxyazi/yazi/pull/3607 [#3608]: https://github.com/sxyazi/yazi/pull/3608 [#3617]: https://github.com/sxyazi/yazi/pull/3617 [#3633]: https://github.com/sxyazi/yazi/pull/3633 [#3634]: https://github.com/sxyazi/yazi/pull/3634 [#3638]: https://github.com/sxyazi/yazi/pull/3638 [#3642]: https://github.com/sxyazi/yazi/pull/3642 [#3648]: https://github.com/sxyazi/yazi/pull/3648 [#3661]: https://github.com/sxyazi/yazi/pull/3661 [#3666]: https://github.com/sxyazi/yazi/pull/3666 [#3668]: https://github.com/sxyazi/yazi/pull/3668 [#3677]: https://github.com/sxyazi/yazi/pull/3677 [#3678]: https://github.com/sxyazi/yazi/pull/3678 [#3684]: https://github.com/sxyazi/yazi/pull/3684 [#3687]: https://github.com/sxyazi/yazi/pull/3687 [#3689]: https://github.com/sxyazi/yazi/pull/3689 [#3696]: https://github.com/sxyazi/yazi/pull/3696 [#3708]: https://github.com/sxyazi/yazi/pull/3708 [#3716]: https://github.com/sxyazi/yazi/pull/3716 [#3725]: https://github.com/sxyazi/yazi/pull/3725 [#3728]: https://github.com/sxyazi/yazi/pull/3728 [#3733]: https://github.com/sxyazi/yazi/pull/3733 [#3744]: https://github.com/sxyazi/yazi/pull/3744 [#3748]: https://github.com/sxyazi/yazi/pull/3748 [#3757]: https://github.com/sxyazi/yazi/pull/3757 [#3765]: https://github.com/sxyazi/yazi/pull/3765 [#3780]: https://github.com/sxyazi/yazi/pull/3780 ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## Goal Our goal is to create a welcoming and safe space where anyone can contribute and seek help for this project in a respectful, collaborative and harassment-free way. All contributions are welcome and encourage everyone to participate regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Standards Our standards are as follows: - Respect different viewpoints and opinions - Do not harass, attack, or discriminate against others for any reason. We have zero tolerance for harassment - Communicate professionally and constructively. Do not post spam or go off-topic - Assume goodwill in conversations. Misunderstandings happen, so give others the benefit of the doubt before jumping to conclusions Examples of unacceptable behavior include: - Trolling, insulting or derogatory comments, and personal attacks - Harassing or bullying another person - Publishing others' private information without their explicit permission - Posting things unrelated to the topic being discussed - Other conduct that could reasonably be considered inappropriate in a professional setting ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported here: https://discord.com/users/638378964804698112 All reporters are guaranteed privacy, and your reports will always be kept private and secure. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate. The following is a list of consequences that you may face if you are found to be breaking the terms of the CoC. ### 1. Correction A private warning with an explanation as to why the behavior was not appropriate. ### 2. Warning A private or public warning with consequences for continued behavior. ### 3. Temporary Ban A temporary ban from any sort of interaction or public communication with the community for a specified period of time. ### 4. Permanent Ban A permanent ban from any sort of public interaction within the community. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Yazi Thank you for your interest in contributing to Yazi! We welcome contributions in the form of bug reports, feature requests, documentation improvements, and code changes. This guide will help you understand how to contribute to the project. ## Table of Contents 1. [Getting Started](#getting-started) 2. [Project Structure](#project-structure) 3. [Development Setup](#development-setup) 4. [How to Contribute](#how-to-contribute) 5. [Pull Requests](#pull-requests) ## Getting Started ### Prerequisites Before you begin, ensure you have met the following requirements: - Rust installed on your machine. You can download it from [rustup.rs](https://rustup.rs). - Familiarity with Git and GitHub. ### Fork the Repository 1. Fork the [Yazi repository](https://github.com/sxyazi/yazi) to your GitHub account. 2. Clone your fork to your local machine: ```sh git clone https://github.com//yazi.git ``` 3. Set up the upstream remote: ```sh git remote add upstream https://github.com/sxyazi/yazi.git ``` ## Project Structure A brief overview of the project's structure: ```sh . ├── assets/ # Assets like images and fonts ├── nix/ # Nix-related configurations ├── scripts/ # Helper scripts used by CI/CD ├── snap/ # Snapcraft configuration ├── yazi-adapter/ # Yazi image adapter ├── yazi-binding/ # Yazi Lua bindings ├── yazi-boot/ # Yazi bootstrapper ├── yazi-cli/ # Yazi command-line interface ├── yazi-codegen/ # Yazi code generator ├── yazi-config/ # Yazi configuration file parser ├── yazi-core/ # Yazi core logic ├── yazi-dds/ # Yazi data distribution service ├── yazi-ffi/ # Yazi foreign function interface ├── yazi-fm/ # Yazi file manager ├── yazi-fs/ # Yazi file system ├── yazi-macro/ # Yazi macros ├── yazi-plugin/ # Yazi plugin system ├── yazi-proxy/ # Yazi event proxy ├── yazi-scheduler/ # Yazi task scheduler ├── yazi-shared/ # Yazi shared library ├── yazi-term/ # Yazi terminal extensions ├── yazi-widgets/ # Yazi user interface widgets ├── .github/ # GitHub-specific files and workflows ├── Cargo.toml # Rust workflow configuration └── README.md # Project overview ``` ## Development Setup 1. Ensure the latest stable Rust is installed: ```sh rustc --version cargo --version ``` 2. Build the project: ```sh cargo build ``` 3. Run the tests: ```sh cargo test --workspace --verbose ``` 4. Format the code (requires `rustfmt` nightly): ```sh rustup component add rustfmt --toolchain nightly rustfmt +nightly **/*.rs ``` ## How to Contribute ### Reporting Bugs If you encounter a bug and have found a way to reliably reproduce it on the latest `main` branch, please file a [bug report](https://github.com/sxyazi/yazi/issues/new?template=bug.yml) with a [minimal reproducer](https://stackoverflow.com/help/minimal-reproducible-example). ### Suggesting Features If you want to request a feature, please file a [feature request](https://github.com/sxyazi/yazi/issues/new?template=feature.yml). Please make sure to search for existing issues and discussions before submitting. ### Improving Documentation Yazi's documentation placed at [yazi-rs/yazi-rs.github.io](https://github.com/yazi-rs/yazi-rs.github.io), contributions related to documentation need to be made there. ### Improving Icons Yazi's icon originates from [`nvim-web-devicons`](https://github.com/nvim-tree/nvim-web-devicons), and it is periodically grabbed and updated with the latest changes from upstream via [`generate.lua`](https://github.com/sxyazi/yazi/blob/main/scripts/icons/generate.lua). Contributions related to the icon should be made upstream to facilitate easier automation of this process. ### Submitting Code Changes 1. Create a new branch for your changes: ```sh git checkout -b your-branch-name ``` 2. Make your changes. Ensure that your code follows the project's [coding style](https://github.com/sxyazi/yazi/blob/main/rustfmt.toml) and passes all tests. 3. Commit your changes with a descriptive commit message: ```sh git commit -m "feat: an awesome feature" ``` 4. Push your changes to your fork: ```sh git push origin your-branch-name ``` ## Pull Requests If you have an idea, before raising a pull request, we encourage you to file an issue to propose it, ensuring that we are aligned and reducing the risk of re-work. We want you to succeed, and it can be discouraging to find that a lot of re-work is needed. ### Process 1. Ensure your fork is up-to-date with the upstream repository: ```sh git fetch upstream git checkout main git merge upstream/main ``` 2. Rebase your feature branch onto the `main` branch: ```sh git checkout your-branch-name git rebase main ``` 3. Create a pull request to the `main` branch of the upstream repository. Follow the pull request template and ensure that: - Your code passes all tests and lints. - Your pull request description clearly explains the changes and why they are needed. 4. Address any review comments. Make sure to push updates to the same branch on your fork. ================================================ FILE: Cargo.toml ================================================ [workspace] resolver = "3" members = [ "yazi-*" ] default-members = [ "yazi-fm", "yazi-cli" ] [workspace.package] edition = "2024" version = "26.2.2" license = "MIT" authors = [ "sxyazi " ] homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" rust-version = "1.92.0" [profile.dev] debug = "line-tables-only" [profile.release] codegen-units = 1 lto = true panic = "abort" strip = true [profile.release-windows] inherits = "release" panic = "unwind" [profile.dev-opt] inherits = "release" codegen-units = 256 incremental = true lto = false [profile.dev.package."*"] debug = false [workspace.dependencies] ansi-to-tui = "8.0.1" anyhow = "1.0.102" base64 = "0.22.1" bitflags = { version = "2.11.0", features = [ "serde" ] } chrono = "0.4.44" clap = { version = "4.6.0", features = [ "derive" ] } core-foundation-sys = "0.8.7" crossterm = { version = "0.29.0", features = [ "event-stream" ] } dirs = "6.0.0" dyn-clone = "1.0.20" either = { version = "1.15.0" } foldhash = "0.2.0" futures = "0.3.32" globset = "0.4.18" hashbrown = { version = "0.16.1", features = [ "serde" ] } indexmap = { version = "2.13.0", features = [ "serde" ] } libc = "0.2.183" lru = "0.16.3" mlua = { version = "0.11.6", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] } objc2 = "0.6.4" ordered-float = { version = "5.1.0", features = [ "serde" ] } parking_lot = "0.12.5" paste = "1.0.15" percent-encoding = "2.3.2" rand = { version = "0.9.2", default-features = false, features = [ "os_rng", "small_rng", "std" ] } ratatui = { version = "0.30.0", features = [ "serde", "unstable-rendered-line-info", "unstable-widget-ref" ] } regex = "1.12.3" russh = { version = "0.57.1", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" serde = { version = "1.0.228", features = [ "derive" ] } serde_json = "1.0.149" serde_with = "3.18.0" syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } thiserror = "2.0.18" tokio = { version = "1.50.0", features = [ "full" ] } tokio-stream = "0.1.18" tokio-util = "0.7.18" toml = { version = "1.0.7" } tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] } twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] } typed-path = "0.12.3" unicode-width = { version = "0.2.2", default-features = false } uzers = "0.12.2" [workspace.lints.clippy] format_push_string = "warn" if_same_then_else = "allow" implicit_clone = "warn" len_without_is_empty = "allow" missing_safety_doc = "allow" module_inception = "allow" option_map_unit_fn = "allow" unit_arg = "allow" use_self = "warn" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 - sxyazi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: LICENSE-ICONS ================================================ MIT License Copyright (c) 2023 nvim-tree Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ## Yazi - ⚡️ Blazing Fast Terminal File Manager 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++ and Chafa, covering almost all terminals. - 🌟 **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, custom previewer/preloader/spotter/fetcher; Just some pieces of Lua. - ☁️ **Virtual Filesystem**: Remote file management, custom search engines. - 📡 **Data Distribution Service**: Built on a client-server architecture (no additional server process required), integrated with a Lua-based publish-subscribe model, achieving cross-instance communication and state persistence. - 📦 **Package Manager**: Install plugins and themes with one command, keeping them up-to-date, or pin them to a specific version. - 🧰 Integration with ripgrep, fd, fzf, zoxide - 💫 Vim-like input/pick/confirm/which/notify component, auto-completion for cd paths - 🏷️ Multi-Tab Support, Cross-directory selection, Scrollable Preview (for videos, PDFs, archives, code, directories, etc.) - 🔄 Bulk Renaming, Archive Extraction, Visual Mode, File Chooser, [Git Integration](https://github.com/yazi-rs/plugins/tree/main/git.yazi), [Mount Manager](https://github.com/yazi-rs/plugins/tree/main/mount.yazi) - 🎨 Theme System, Mouse Support, Trash Bin, Custom Layouts, CSI u, OSC 52 - ... and more! https://github.com/sxyazi/yazi/assets/17523360/92ff23fa-0cd5-4f04-b387-894c12265cc7 ## Project status Public beta, can be used as a daily driver. Yazi is currently in heavy development, expect breaking changes. ## Documentation - Usage: https://yazi-rs.github.io/docs/installation - Features: https://yazi-rs.github.io/features ## Discussion - Discord Server (English mainly): https://discord.gg/qfADduSdJu - Telegram Group (Chinese mainly): https://t.me/yazi_rs ## Image Preview | Platform | Protocol | Support | | ---------------------------------------------------------------------------- | -------------------------------------- | ---------------------------------------- | | [kitty](https://github.com/kovidgoyal/kitty) (>= 0.28.0) | [Kitty unicode placeholders][kgp] | ✅ Built-in | | [iTerm2](https://iterm2.com) | [Inline images protocol][iip] | ✅ Built-in | | [WezTerm](https://github.com/wez/wezterm) | [Inline images protocol][iip] | ✅ Built-in | | [Konsole](https://invent.kde.org/utilities/konsole) | [Kitty old protocol][kgp-old] | ✅ Built-in | | [foot](https://codeberg.org/dnkl/foot) | [Sixel graphics format][sixel] | ✅ Built-in | | [Ghostty](https://github.com/ghostty-org/ghostty) | [Kitty unicode placeholders][kgp] | ✅ Built-in | | [Windows Terminal](https://github.com/microsoft/terminal) (>= v1.22.10352.0) | [Sixel graphics format][sixel] | ✅ Built-in | | [st with Sixel patch](https://github.com/bakkeby/st-flexipatch) | [Sixel graphics format][sixel] | ✅ Built-in | | [Warp](https://www.warp.dev) (macOS/Linux only) | [Inline images protocol][iip] | ✅ Built-in | | [Tabby](https://github.com/Eugeny/tabby) | [Inline images protocol][iip] | ✅ Built-in | | [VSCode](https://github.com/microsoft/vscode) | [Inline images protocol][iip] | ✅ Built-in | | [Rio](https://github.com/raphamorim/rio) | [Inline images protocol][iip] | ❌ Rio renders images at incorrect sizes | | [Black Box](https://gitlab.gnome.org/raggesilver/blackbox) | [Sixel graphics format][sixel] | ✅ Built-in | | [Bobcat](https://github.com/ismail-yilmaz/Bobcat) | [Inline images protocol][iip] | ✅ Built-in | | X11 / Wayland | Window system protocol | ☑️ [Überzug++][ueberzug] required | | Fallback | [ASCII art (Unicode block)][ascii-art] | ☑️ [Chafa][chafa] required (>= 1.16.0) | See https://yazi-rs.github.io/docs/image-preview for details. [kgp]: https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders [kgp-old]: https://github.com/sxyazi/yazi/blob/main/yazi-adapter/src/drivers/kgp_old.rs [iip]: https://iterm2.com/documentation-images.html [sixel]: https://www.vt100.net/docs/vt3xx-gp/chapter14.html [ascii-art]: https://en.wikipedia.org/wiki/ASCII_art [ueberzug]: https://github.com/jstkdng/ueberzugpp [chafa]: https://hpjansson.org/chafa/ ## Special Thanks RustRover logo Thanks to RustRover team for providing open-source licenses to support the maintenance of Yazi. Active code contributors can contact @sxyazi to get a license (if any are still available). ## License Yazi is MIT-licensed. For more information check the [LICENSE](LICENSE) file. ================================================ FILE: assets/yazi.desktop ================================================ [Desktop Entry] Name=Yazi File Manager Icon=yazi Comment=Blazing fast terminal file manager written in Rust, based on async I/O Terminal=true TryExec=yazi Exec=yazi %u Type=Application MimeType=inode/directory Categories=System;FileManager;FileTools;ConsoleOnly Keywords=File;Manager;Explorer;Browser;Launcher ================================================ FILE: cspell.json ================================================ {"version":"0.2","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee"],"language":"en"} ================================================ FILE: flake.nix ================================================ { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-utils.url = "github:numtide/flake-utils"; rust-overlay = { url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }: flake-utils.lib.eachDefaultSystem ( system: let pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlays.default ]; }; toolchain = pkgs.rust-bin.stable.latest.default; rustPlatform = pkgs.makeRustPlatform { cargo = toolchain; rustc = toolchain; }; rev = self.shortRev or self.dirtyShortRev or "dirty"; date = self.lastModifiedDate or self.lastModified or "19700101"; version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).workspace.package.version + "pre${builtins.substring 0 8 date}_${rev}"; in { packages = { yazi-unwrapped = pkgs.callPackage ./nix/yazi-unwrapped.nix { inherit version rev date rustPlatform ; }; yazi = pkgs.callPackage ./nix/yazi.nix { inherit (self.packages.${system}) yazi-unwrapped; }; default = self.packages.${system}.yazi; }; devShells = { default = pkgs.callPackage ./nix/shell.nix { inherit toolchain; inherit (self.packages.${system}) yazi yazi-unwrapped; }; }; formatter = pkgs.nixfmt-tree; } ) // { overlays = { default = self.overlays.yazi; yazi = _: prev: { inherit (self.packages.${prev.stdenv.system}) yazi yazi-unwrapped; }; }; }; } ================================================ FILE: nix/shell.nix ================================================ { mkShell, yazi, toolchain, nodePackages, yazi-unwrapped, }: mkShell { packages = yazi.passthru.runtimePaths ++ [ (toolchain.override { extensions = [ "rust-src" "rustfmt" "rust-analyzer" "clippy" ]; }) nodePackages.cspell ]; inputsFrom = [ yazi-unwrapped ]; env.RUST_BACKTRACE = "1"; } ================================================ FILE: nix/yazi-unwrapped.nix ================================================ { rustPlatform, version ? "git", rev ? "unknown", date ? "19700101", lib, installShellFiles, fetchFromGitHub, rust-jemalloc-sys, imagemagick, }: let src = lib.fileset.toSource { root = ../.; fileset = lib.fileset.unions [ ../assets ../Cargo.toml ../Cargo.lock (lib.fileset.fromSource (lib.sources.sourceByRegex ../. [ "^yazi-.*" ])) ]; }; in rustPlatform.buildRustPackage (finalAttrs: { pname = "yazi"; inherit version src; cargoLock = { lockFile = "${src}/Cargo.lock"; #outputHashes = { # "mlua-0.10.0" = "sha256-Xg6/jc+UP8tbJJ6x1sbAgt8ZHt051xEBBcjmikQqYlw="; #}; }; env = { YAZI_GEN_COMPLETIONS = true; VERGEN_GIT_SHA = rev; VERGEN_BUILD_DATE = builtins.concatStringsSep "-" (builtins.match "(.{4})(.{2})(.{2}).*" date); }; nativeBuildInputs = [ installShellFiles imagemagick ]; buildInputs = [ rust-jemalloc-sys ]; postInstall = '' installShellCompletion --cmd yazi \ --bash ./yazi-boot/completions/yazi.bash \ --fish ./yazi-boot/completions/yazi.fish \ --zsh ./yazi-boot/completions/_yazi # Resize logo for RES in 16 24 32 48 64 128 256; do mkdir -p $out/share/icons/hicolor/"$RES"x"$RES"/apps magick assets/logo.png -resize "$RES"x"$RES" $out/share/icons/hicolor/"$RES"x"$RES"/apps/yazi.png done installManPage ${finalAttrs.passthru.srcs.man_src}/yazi{.1,-config.5} mkdir -p $out/share/applications install -m644 assets/yazi.desktop $out/share/applications/ ''; passthru.srcs = { man_src = fetchFromGitHub { name = "manpages"; # needed to ensure name is unique owner = "yazi-rs"; repo = "manpages"; rev = "8950e968f4a1ad0b83d5836ec54a070855068dbf"; hash = "sha256-kEVXejDg4ChFoMNBvKlwdFEyUuTcY2VuK9j0PdafKus="; }; }; meta = { description = "Blazing fast terminal file manager written in Rust, based on async I/O"; homepage = "https://github.com/sxyazi/yazi"; license = lib.licenses.mit; mainProgram = "yazi"; }; }) ================================================ FILE: nix/yazi.nix ================================================ { lib, formats, runCommand, makeWrapper, runtimeDeps ? (ps: ps), # deps file, yazi-unwrapped, # default optional deps jq, poppler-utils, _7zz, ffmpeg, fd, ripgrep, resvg, fzf, zoxide, imagemagick, chafa, settings ? { }, plugins ? { }, flavors ? { }, initLua ? null, }: let inherit (lib) concatStringsSep concatMapStringsSep optionalString makeBinPath mapAttrsToList ; defaultDeps = [ jq poppler-utils _7zz ffmpeg fd ripgrep resvg fzf zoxide imagemagick chafa ]; runtimePaths = [ file ] ++ (runtimeDeps defaultDeps); settingsFormat = formats.toml { }; files = [ "yazi" "theme" "keymap" ]; configHome = if (settings == { } && initLua == null && plugins == { } && flavors == { }) then null else runCommand "YAZI_CONFIG_HOME" { } '' mkdir -p $out ${concatMapStringsSep "\n" ( name: optionalString (settings ? ${name} && settings.${name} != { }) '' ln -s ${settingsFormat.generate "${name}.toml" settings.${name}} $out/${name}.toml '' ) files} mkdir $out/plugins ${optionalString (plugins != { }) '' ${concatStringsSep "\n" ( mapAttrsToList (name: value: "ln -s ${value} $out/plugins/${name}") plugins )} ''} mkdir $out/flavors ${optionalString (flavors != { }) '' ${concatStringsSep "\n" ( mapAttrsToList (name: value: "ln -s ${value} $out/flavors/${name}") flavors )} ''} ${optionalString (initLua != null) "ln -s ${initLua} $out/init.lua"} ''; in runCommand yazi-unwrapped.name { inherit (yazi-unwrapped) pname version meta; nativeBuildInputs = [ makeWrapper ]; passthru.runtimePaths = runtimePaths; } '' mkdir -p $out/bin ln -s ${yazi-unwrapped}/share $out/share ln -s ${yazi-unwrapped}/bin/ya $out/bin/ya makeWrapper ${yazi-unwrapped}/bin/yazi $out/bin/yazi \ --prefix PATH : "${makeBinPath runtimePaths}" \ ${optionalString (configHome != null) "--set YAZI_CONFIG_HOME ${configHome}"} '' ================================================ FILE: rustfmt.toml ================================================ color = "Never" condense_wildcard_suffixes = true edition = "2024" enum_discrim_align_threshold = 99 fn_single_line = true format_code_in_doc_comments = false format_generated_files = false format_macro_matchers = true format_macro_bodies = true format_strings = false hard_tabs = true hex_literal_case = "Lower" show_parse_errors = false imports_indent = "Visual" imports_layout = "Horizontal" imports_granularity = "Crate" newline_style = "Unix" normalize_comments = true normalize_doc_attributes = false overflow_delimited_expr = true reorder_impl_items = true group_imports = "StdExternalCrate" reorder_modules = true struct_field_align_threshold = 99 tab_spaces = 2 unstable_features = true use_field_init_shorthand = true use_small_heuristics = "Max" use_try_shorthand = true style_edition = "2024" wrap_comments = true ================================================ FILE: scripts/build.sh ================================================ #!/usr/bin/env bash set -euo pipefail export ARTIFACT_NAME="yazi-$1" export YAZI_GEN_COMPLETIONS=1 # Build the target git config --global --add safe.directory "*" cargo build --release --locked --target "$1" # Copy the binaries to a known location mkdir -p "target/release" cp "target/$1/release/ya" "target/release/ya" cp "target/$1/release/yazi" "target/release/yazi" # Package deb if [[ "$ARTIFACT_NAME" == *-linux-* ]] && { [[ "$ARTIFACT_NAME" == *-aarch64-* ]] || [[ "$ARTIFACT_NAME" == *-x86_64-* ]]; }; then cargo install cargo-deb cargo deb -p yazi-packing --no-build --target "$1" -o "$ARTIFACT_NAME.deb" fi # Create the artifact mkdir -p "$ARTIFACT_NAME/completions" cp "target/release/ya" "$ARTIFACT_NAME" cp "target/release/yazi" "$ARTIFACT_NAME" cp yazi-cli/completions/* "$ARTIFACT_NAME/completions" cp yazi-boot/completions/* "$ARTIFACT_NAME/completions" cp README.md LICENSE "$ARTIFACT_NAME" # Zip the artifact if ! command -v zip &> /dev/null; then apt-get update && apt-get install -yq zip fi zip -r "$ARTIFACT_NAME.zip" "$ARTIFACT_NAME" ================================================ FILE: scripts/bump.sh ================================================ #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR/.." echo "Bumping version: $1" TOML_FILES="$(git ls-files '*Cargo.toml')" perl -pi -e 's/^version .*= .*$/version = "'"$1"'"/' -- $TOML_FILES perl -pi -e 's/^(yazi-[a-z]+)\s*=\s*{.*$/\1 = { path = "..\/\1", version = "'"$1"'" }/' -- $TOML_FILES # Insert "## [v$1]" after "## [Unreleased]" perl -0777 -pe "s/^(## \[Unreleased\]\s*)/\\1## [v$1]\n\n/m" -i CHANGELOG.md # Determine previous version and append compare link prev_ver=$(grep -oE '^\[v[0-9][^]]+\]' CHANGELOG.md | tail -n1 | tr -d '[]') link="\[v$1\]: https://github.com/sxyazi/yazi/compare/$prev_ver...v$1" perl -pi -e 's{(\['"$prev_ver"'\]:[^\n]+\n)}{$1'"$link"'\n}s' CHANGELOG.md ESLINT_USE_FLAT_CONFIG=true eslint -c ~/.config/rules/eslint/eslint.config.cjs --fix -- $TOML_FILES ================================================ FILE: scripts/icons/generate.lua ================================================ local dark = { icons_by_filename = require("default.icons_by_filename"), icons_by_file_extension = require("default.icons_by_file_extension"), } local light = { icons_by_filename = require("light.icons_by_filename"), icons_by_file_extension = require("light.icons_by_file_extension"), } function rearrange(by) local map = {} local source = by == "exts" and "icons_by_file_extension" or "icons_by_filename" for k, v in pairs(dark[source]) do map[k] = map[k] or {} map[k].icon = v.icon map[k].fg_dark = v.color:lower() end for k, v in pairs(light[source]) do map[k].fg_light = v.color:lower() end return map end function dump(map) local list = {} for k, v in pairs(map) do list[#list + 1] = { name = k, text = v.icon, fg_dark = v.fg_dark, fg_light = v.fg_light } end table.sort(list, function(a, b) return a.name:lower() < b.name:lower() end) local dark, light = "", "" for _, v in ipairs(list) do -- stylua: ignore dark = dark .. string.format('\t{ name = "%s", text = "%s", fg = "%s" },\n', v.name, v.text, v.fg_dark) light = light .. string.format('\t{ name = "%s", text = "%s", fg = "%s" },\n', v.name, v.text, v.fg_light) end return dark, light end function save(typ, files, exts) local p = string.format("../../yazi-config/preset/theme-%s.toml", typ) local s = io.open(p, "r"):read("*a") s = s:gsub("files = %[\n(.-)\n%]", string.format("files = [\n%s]", files)) s = s:gsub("exts = %[\n(.-)\n%]", string.format("exts = [\n%s]", exts)) io.open(p, "w"):write(s) end local dark_files, light_files = dump(rearrange("files")) local dark_exts, light_exts = dump(rearrange("exts")) save("dark", dark_files, dark_exts) save("light", light_files, light_exts) ================================================ FILE: scripts/validate-form/main.js ================================================ const LABEL_NAME = "needs info" const RE_VERSION = /Yazi\s+Version\s*:\s\d+\.\d+\.\d+\s\(/gm const RE_DEPENDENCIES = /Dependencies\s+[/a-z]+\s*:\s/gm const RE_CHECKLIST = /#{3}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm function bugReportBody(creator, content, hash) { if (RE_DEPENDENCIES.test(content) && RE_CHECKLIST.test(content) && new RegExp(` \\(${hash}[a-f0-9]? `).test(content)) { return null } return `Hey @${creator}, thank you for opening the issue to help improve Yazi, appreciate it! I noticed that you did not correctly follow the issue template. Please ensure that: - The bug can still be reproduced on the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries). - The debug information (\`yazi --debug\`) is updated for the newest nightly. - The non-optional items in the checklist are checked. Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity. Our maintainers work on Yazi in their free time, this helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thanks for your understanding! 🙏 ` } function featureRequestBody(creator, content) { if (RE_VERSION.test(content) && RE_DEPENDENCIES.test(content) && RE_CHECKLIST.test(content)) { return null } return `Hey @${creator}, thank you for opening the issue to help improve Yazi, appreciate it! I noticed that you did not correctly follow the issue template. Please ensure that: - The requested feature does not exist in the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries). - The debug information (\`yazi --debug\`) is updated for the newest nightly. - The non-optional items in the checklist are checked. Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity. Our maintainers work on Yazi in their free time, this helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thanks for your understanding! 🙏 ` } module.exports = async ({ github, context, core }) => { async function nightlyHash() { try { const { data: tagRef } = await github.rest.git.getRef({ owner: "sxyazi", repo: "yazi", ref: "tags/nightly" }) return tagRef.object.sha.slice(0, 7) } catch (e) { if (e.status === 404) { core.error("Nightly tag not found") } else { core.error(`Error fetching nightly version: ${e.message}`) } return null } } async function hasLabel(id, label) { try { const { data: labels } = await github.rest.issues.listLabelsOnIssue({ ...context.repo, issue_number: id, }) return labels.some(l => l.name === label) } catch (e) { core.error(`Error checking labels: ${e.message}`) return false } } async function lastLabeledAt(id) { try { const { data: events } = await github.rest.issues.listEvents({ ...context.repo, issue_number: id, per_page: 100, }) const all = events.filter(v => v.event === "labeled" && v.label?.name === LABEL_NAME) return all.at(-1)?.created_at } catch (e) { core.error(`Error getting label timestamp: ${e.message}`) return null } } async function removedLabelManually(id) { try { const { data: events } = await github.rest.issues.listEvents({ ...context.repo, issue_number: id, per_page: 100, }) const all = events.filter(v => v.event === "unlabeled" && v.label?.name === LABEL_NAME) return all.length === 0 ? false : !all.at(-1).actor.login.endsWith("[bot]") } catch (e) { core.error(`Error checking label removal history: ${e.message}`) return false } } async function updateLabels(id, mark, body) { try { const marked = await hasLabel(id, LABEL_NAME) if (!mark && marked) { await github.rest.issues.removeLabel({ ...context.repo, issue_number: id, name: LABEL_NAME, }) await hideOldComments(id) } else if (mark && !marked && !await removedLabelManually(id)) { await github.rest.issues.addLabels({ ...context.repo, issue_number: id, labels: [LABEL_NAME], }) await hideOldComments(id) await github.rest.issues.createComment({ ...context.repo, issue_number: id, body, }) } } catch (e) { core.error(`Error updating labels: ${e.message}`) } } async function hideOldComments(id) { try { const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, issue_number: id, per_page: 100, }) for (const c of comments) { const byBot = c.user?.login?.endsWith("[bot]") || c.user?.type === "Bot" const contains = c?.body?.includes("or closed after 2 days of inactivity") if (!byBot || !contains || !c.node_id) continue try { await github.graphql( `mutation($subjectId: ID!, $classifier: ReportedContentClassifiers!) { minimizeComment(input: {subjectId: $subjectId, classifier: $classifier}) { minimizedComment { isMinimized } } }`, { subjectId: c.node_id, classifier: "OUTDATED" }, ) } catch (e) { core.error(`Error minimizing comment ${c.id}: ${e.message}`) } } } catch (e) { core.error(`Error listing comments: ${e.message}`) } } async function closeOldIssues() { try { const { data: issues } = await github.rest.issues.listForRepo({ ...context.repo, state: "open", labels: LABEL_NAME, }) const now = new Date() const twoDaysAgo = new Date(now - 2 * 24 * 60 * 60 * 1000) for (const issue of issues) { const markedAt = new Date(await lastLabeledAt(issue.number) || issue.created_at) if (markedAt < twoDaysAgo) { await github.rest.issues.update({ ...context.repo, issue_number: issue.number, state: "closed", state_reason: "not_planned", }) await github.rest.issues.createComment({ ...context.repo, issue_number: issue.number, body: `This issue has been automatically closed because it was marked as \`${LABEL_NAME}\` for more than 2 days without updates. If the problem persists, please file a new issue and complete the issue template so we can capture all the details necessary to investigate further.`, }) } } } catch (e) { core.error(`Error checking old issues: ${e.message}`) } } async function closeUnsupportedIssue(id) { try { await github.rest.issues.update({ ...context.repo, issue_number: id, state: "closed", state_reason: "not_planned", }) await github.rest.issues.createComment({ ...context.repo, issue_number: id, body: `Unsupported issue template. Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.yml) or [Feature Request](https://github.com/sxyazi/yazi/issues/new?template=feature.yml) template should be used.`, }) } catch (e) { core.error(`Error closing unsupported issue: ${e.message}`) } } async function main() { const hash = await nightlyHash() if (!hash) return if (context.eventName === "schedule") { await closeOldIssues() return } if (context.eventName === "issues") { const id = context.payload.issue.number const content = context.payload.issue.body || "" const creator = context.payload.issue.user.login if (await hasLabel(id, "bug")) { const body = bugReportBody(creator, content, hash) await updateLabels(id, !!body, body) } else if (await hasLabel(id, "feature")) { const body = featureRequestBody(creator, content) await updateLabels(id, !!body, body) } else if (context.payload.action === "opened") { await closeUnsupportedIssue(id) } } } await main() } ================================================ FILE: scripts/validate-form/package.json ================================================ { "name": "validate-form", "version": "1.0.0", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "private": true, "dependencies": { "@actions/core": "^3.0.0", "@actions/github": "^9.0.0" } } ================================================ FILE: snap/snapcraft.yaml ================================================ name: yazi base: core24 adopt-info: yazi summary: Blazing fast terminal file manager written in Rust, based on async I/O. description: | Yazi 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. license: MIT grade: stable confinement: classic platforms: amd64: arm64: apps: yazi: command: yazi environment: PATH: $SNAP/bin:$SNAP/usr/bin:$PATH ya: command: ya environment: PATH: $SNAP/bin:$SNAP/usr/bin:$PATH parts: yazi: plugin: rust source: https://github.com/sxyazi/yazi.git stage-snaps: - jq stage-packages: - 7zip-standalone - chafa - fd-find - ffmpeg # - fzf - libglu1-mesa - libglut3.12 - poppler-utils - ripgrep - wl-clipboard - xclip - zoxide override-build: | craftctl default craftctl set version=$(git describe --tags --abbrev=0) build-attributes: - enable-patchelf organize: # Ubuntu's `fd` package installs a binary named `fdfind`. Rename it in the snap. usr/bin/fdfind: usr/bin/fd prime: # Remove unused items bought in by dependency packages - -usr/bin/fc-* - -usr/bin/ffplay - -usr/bin/pdfattach - -usr/bin/pdfdetach - -usr/bin/pdffonts - -usr/bin/pdfimages - -usr/bin/pdfinfo - -usr/bin/pdfseparate - -usr/bin/pdfsig - -usr/bin/pdftocairo - -usr/bin/pdftohtml - -usr/bin/pdftops - -usr/bin/pdftotext - -usr/bin/pdfunite # Remove unused libraries identified by snapcraft's linter - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_bad_* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_civil_time.so.* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_cord* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_examine_stack.so.* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_exponential_biased.so.* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_failure_signal_handler.so.* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_flags* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_hash* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_log_severity.so.* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_periodic_sampler.so.* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_random* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_raw_hash_set.so.* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_scoped_set_env.so.* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_statusor.so.* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_status.so.* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_strerror.so.* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libabsl_str_format_internal.so.* - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libcaca++.so.0.99.20 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libcjson_utils.so.1.7.17 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libfftw3_omp.so.3.6.10 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libfftw3_threads.so.3.6.10 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libflite_cmu_grapheme_lang.so.2.2 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libflite_cmu_grapheme_lex.so.2.2 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libflite_cmu_indic_lang.so.2.2 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libflite_cmu_indic_lex.so.2.2 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libflite_cmu_time_awb.so.2.2 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libfreebl3.so - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libfreeblpriv3.so - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libGLX_mesa.so.0.0.0 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libhwy_contrib.so.1.0.7 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libhwy_test.so.1.0.7 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libicui18n.so.74.2 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libicuio.so.74.2 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libicutest.so.74.2 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libicutu.so.74.2 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libjacknet.so.0.1.0 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libjackserver.so.0.1.0 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libnssckbi.so - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libnssdbm3.so - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libpulse-simple.so.0.1.1 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libsoftokn3.so - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libsphinxad.so.3.0.0 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libssl3.so - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libtheora.so.0.3.10 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxcb-dri2.so.0.0.0 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxcb-glx.so.0.0.0 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxcb-present.so.0.0.0 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxcb-sync.so.1.0.0 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxshmfence.so.1.0.0 - -usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libzvbi-chains.so.0.0.0 # The Ubuntu `imagemagick` package does not ship a `magick` binary, # which `yazi` looks for. Building the package oursleves yields the # expected binary without much overhead, and works across platforms. magick: plugin: autotools source: https://github.com/ImageMagick/ImageMagick.git source-type: git source-tag: 7.1.2-0 source-depth: 1 stage-packages: - libgomp1 autotools-configure-parameters: - --prefix=/usr build-attributes: - enable-patchelf prime: - usr/bin/magick - usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libgomp.so.1 - usr/lib/libMagickCore-7.Q16HDRI.so* - usr/lib/libMagickWand-7.Q16HDRI.so* cleanup: after: [yazi, magick] plugin: nil build-packages: - patchelf build-snaps: - core24 override-prime: | # Ubuntu's /usr/bin/7zz is a simple bash wrapper that just exec's # /usr/lib/7zip/7zz - this just shortcuts that and places the actual # executable at $SNAP/usr/bin/7zz. mv $CRAFT_PRIME/usr/lib/7zip/7zz $CRAFT_PRIME/usr/bin/7zz # Ensure we don't ship duplicates of files that exist in the core24 # snap. cd /snap/core24/current find . -type f,l -exec rm -rf "${CRAFT_PRIME}/{}" \; ================================================ FILE: stylua.toml ================================================ syntax = "Lua54" indent_width = 2 call_parentheses = "NoSingleTable" collapse_simple_statement = "FunctionOnly" [sort_requires] enabled = true ================================================ FILE: yazi-actor/Cargo.toml ================================================ [package] name = "yazi-actor" description = "Yazi actor model" version.workspace = true edition.workspace = true license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true rust-version.workspace = true [lints] workspace = true [features] default = [ "vendored-lua" ] vendored-lua = [ "mlua/vendored" ] [dependencies] yazi-binding = { path = "../yazi-binding", version = "26.2.2" } yazi-boot = { path = "../yazi-boot", version = "26.2.2" } yazi-config = { path = "../yazi-config", version = "26.2.2" } yazi-core = { path = "../yazi-core", version = "26.2.2" } yazi-dds = { path = "../yazi-dds", version = "26.2.2" } yazi-emulator = { path = "../yazi-emulator", version = "26.2.2" } yazi-fs = { path = "../yazi-fs", version = "26.2.2" } yazi-macro = { path = "../yazi-macro", version = "26.2.2" } yazi-parser = { path = "../yazi-parser", version = "26.2.2" } yazi-plugin = { path = "../yazi-plugin", version = "26.2.2" } yazi-proxy = { path = "../yazi-proxy", version = "26.2.2" } yazi-scheduler = { path = "../yazi-scheduler", version = "26.2.2" } yazi-shared = { path = "../yazi-shared", version = "26.2.2" } yazi-term = { path = "../yazi-term", version = "26.2.2" } yazi-tty = { path = "../yazi-tty", version = "26.2.2" } yazi-vfs = { path = "../yazi-vfs", version = "26.2.2" } yazi-watcher = { path = "../yazi-watcher", version = "26.2.2" } yazi-widgets = { path = "../yazi-widgets", version = "26.2.2" } # External dependencies anyhow = { workspace = true } crossterm = { workspace = true } either = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } mlua = { workspace = true } paste = { workspace = true } ratatui = { workspace = true } scopeguard = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } tracing = { workspace = true } [target."cfg(unix)".dependencies] libc = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } ================================================ FILE: yazi-actor/README.md ================================================ # yazi-actor This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. [source]: https://github.com/sxyazi/yazi ================================================ FILE: yazi-actor/src/actor.rs ================================================ use anyhow::Result; use yazi_dds::spark::SparkKind; use yazi_shared::data::Data; use crate::Ctx; pub trait Actor { type Options; const NAME: &str; fn act(cx: &mut Ctx, opt: Self::Options) -> Result; fn hook(_cx: &Ctx, _opt: &Self::Options) -> Option { None } } ================================================ FILE: yazi-actor/src/app/accept_payload.rs ================================================ use anyhow::Result; use mlua::IntoLua; use tracing::error; use yazi_actor::lives::Lives; use yazi_binding::runtime_scope; use yazi_dds::{LOCAL, Payload, REMOTE}; use yazi_macro::succ; use yazi_plugin::LUA; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct AcceptPayload; impl Actor for AcceptPayload { type Options = Payload<'static>; const NAME: &str = "accept_payload"; fn act(cx: &mut Ctx, payload: Payload) -> Result { let kind = payload.body.kind(); let lock = if payload.receiver == 0 || payload.receiver != payload.sender { REMOTE.read() } else { LOCAL.read() }; let Some(handlers) = lock.get(kind).filter(|&m| !m.is_empty()).cloned() else { succ!() }; drop(lock); let kind = kind.to_owned(); succ!(Lives::scope(cx.core, || { let body = payload.body.into_lua(&LUA)?; for (id, cb) in handlers { if let Err(e) = runtime_scope!(LUA, &id, cb.call::<()>(body.clone())) { error!("Failed to run `{kind}` event handler in your `{id}` plugin: {e}"); } } Ok(()) })?); } } ================================================ FILE: yazi-actor/src/app/bootstrap.rs ================================================ use anyhow::Result; use yazi_actor::Ctx; use yazi_boot::BOOT; use yazi_macro::{act, succ}; use yazi_parser::{VoidOpt, mgr::CdSource}; use yazi_shared::{data::Data, strand::StrandLike, url::UrlLike}; use crate::Actor; pub struct Bootstrap; impl Actor for Bootstrap { type Options = VoidOpt; const NAME: &str = "bootstrap"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { cx.mgr.tabs.resize_with(BOOT.files.len(), Default::default); for (i, file) in BOOT.files.iter().enumerate().rev() { cx.tab = i; if file.is_empty() { act!(mgr:cd, cx, (BOOT.cwds[i].clone(), CdSource::Tab))?; } else if let Ok(u) = BOOT.cwds[i].try_join(file) { act!(mgr:reveal, cx, (u, CdSource::Tab))?; } } succ!(); } } ================================================ FILE: yazi-actor/src/app/deprecate.rs ================================================ use anyhow::Result; use yazi_macro::act; use yazi_parser::{app::DeprecateOpt, notify::{PushLevel, PushOpt}}; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Deprecate; impl Actor for Deprecate { type Options = DeprecateOpt; const NAME: &str = "deprecate"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(notify:push, cx, PushOpt { title: "Deprecated API".to_owned(), content: opt.content.into_owned(), level: PushLevel::Warn, timeout: std::time::Duration::from_secs(20), }) } } ================================================ FILE: yazi-actor/src/app/focus.rs ================================================ use anyhow::Result; use yazi_actor::Ctx; use yazi_macro::act; use yazi_parser::VoidOpt; use yazi_shared::data::Data; use crate::Actor; pub struct Focus; impl Actor for Focus { type Options = VoidOpt; const NAME: &str = "focus"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { act!(mgr:refresh, cx) } } ================================================ FILE: yazi-actor/src/app/mod.rs ================================================ yazi_macro::mod_flat!( accept_payload bootstrap deprecate focus mouse plugin plugin_do quit reflow resize resume stop title update_progress ); ================================================ FILE: yazi-actor/src/app/mouse.rs ================================================ use anyhow::Result; use crossterm::event::MouseEventKind; use mlua::{ObjectLike, Table}; use tracing::error; use yazi_actor::lives::Lives; use yazi_binding::runtime_scope; use yazi_config::YAZI; use yazi_macro::succ; use yazi_parser::app::MouseOpt; use yazi_plugin::LUA; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Mouse; impl Actor for Mouse { type Options = MouseOpt; const NAME: &str = "mouse"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let event = yazi_binding::MouseEvent::from(opt.event); let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() }; let area = yazi_binding::elements::Rect::from(size); let result = Lives::scope(cx.core, move || { let root = runtime_scope!(LUA, "root", { LUA.globals().raw_get::("Root")?.call_method::
("new", area) })?; if matches!(event.kind, MouseEventKind::Down(_) if YAZI.mgr.mouse_events.get().draggable()) { root.raw_set("_drag_start", event)?; } match event.kind { MouseEventKind::Down(_) => root.call_method("click", (event, false))?, MouseEventKind::Up(_) => root.call_method("click", (event, true))?, MouseEventKind::ScrollDown => root.call_method("scroll", (event, 1))?, MouseEventKind::ScrollUp => root.call_method("scroll", (event, -1))?, MouseEventKind::ScrollRight => root.call_method("touch", (event, 1))?, MouseEventKind::ScrollLeft => root.call_method("touch", (event, -1))?, MouseEventKind::Moved => root.call_method("move", event)?, MouseEventKind::Drag(_) => root.call_method("drag", event)?, } Ok(()) }); if let Err(ref e) = result { error!("{e}"); } succ!(result?); } } ================================================ FILE: yazi-actor/src/app/plugin.rs ================================================ use anyhow::Result; use yazi_macro::{act, succ}; use yazi_parser::app::{PluginMode, PluginOpt}; use yazi_plugin::loader::LOADER; use yazi_proxy::{AppProxy, NotifyProxy}; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Plugin; impl Actor for Plugin { type Options = PluginOpt; const NAME: &str = "plugin"; fn act(cx: &mut Ctx, mut opt: Self::Options) -> Result { let mut hits = false; if let Some(chunk) = LOADER.read().get(&*opt.id) { hits = true; opt.mode = opt.mode.auto_then(chunk.sync_entry); } if opt.mode == PluginMode::Async { succ!(cx.core.tasks.scheduler.plugin_entry(opt)); } else if opt.mode == PluginMode::Sync && hits { return act!(app:plugin_do, cx, opt); } tokio::spawn(async move { match LOADER.ensure(&opt.id, |_| ()).await { Ok(()) => AppProxy::plugin_do(opt), Err(e) => NotifyProxy::push_error("Plugin load failed", e), } }); succ!(); } } ================================================ FILE: yazi-actor/src/app/plugin_do.rs ================================================ use anyhow::Result; use mlua::ObjectLike; use scopeguard::defer; use tracing::{error, warn}; use yazi_binding::runtime_mut; use yazi_dds::Sendable; use yazi_macro::succ; use yazi_parser::app::{PluginMode, PluginOpt}; use yazi_plugin::{LUA, loader::{LOADER, Loader}}; use yazi_proxy::NotifyProxy; use yazi_shared::data::Data; use crate::{Actor, Ctx, lives::Lives}; pub struct PluginDo; impl Actor for PluginDo { type Options = PluginOpt; const NAME: &str = "plugin_do"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let loader = LOADER.read(); let Some(chunk) = loader.get(&*opt.id) else { succ!(warn!("plugin `{}` not found", opt.id)); }; if let Err(e) = Loader::compatible_or_error(&opt.id, chunk) { succ!(NotifyProxy::push_error("Incompatible plugin", e)); } if opt.mode.auto_then(chunk.sync_entry) != PluginMode::Sync { succ!(cx.core.tasks.scheduler.plugin_entry(opt)); } let blocking = runtime_mut!(LUA)?.critical_push(&opt.id, true); defer! { _ = runtime_mut!(LUA).map(|mut r| r.critical_pop(blocking)) } let plugin = match LOADER.load_chunk(&LUA, &opt.id, chunk) { Ok(t) => t, Err(e) => succ!(warn!("{e}")), }; drop(loader); let result = Lives::scope(cx.core, || { if let Some(cb) = opt.callback { cb(&LUA, plugin) } else { let job = LUA.create_table_from([("args", Sendable::args_to_table(&LUA, opt.args)?)])?; plugin.call_method("entry", job) } }); if let Err(ref e) = result { error!("Sync plugin `{}` failed: {e}", opt.id); } succ!(result?); } } ================================================ FILE: yazi-actor/src/app/quit.rs ================================================ use anyhow::Result; use yazi_boot::ARGS; use yazi_fs::provider::{Provider, local::Local}; use yazi_parser::app::QuitOpt; use yazi_shared::{data::Data, strand::{StrandBuf, StrandLike, ToStrand}}; use yazi_term::Term; use crate::{Actor, Ctx}; pub struct Quit; impl Actor for Quit { type Options = QuitOpt; const NAME: &str = "quit"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { cx.tasks.shutdown(); cx.mgr.shutdown(); futures::executor::block_on(async { _ = futures::join!( yazi_dds::shutdown(), yazi_dds::STATE.drain(), Self::cwd_to_file(cx, opt.no_cwd_file), Self::selected_to_file(opt.selected) ); }); Term::goodbye(|| opt.code); } } impl Quit { async fn cwd_to_file(cx: &Ctx<'_>, no: bool) { if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) { let cwd = cx.mgr.cwd().to_strand(); Local::regular(p).write(cwd.encoded_bytes()).await.ok(); } } async fn selected_to_file(selected: Option) { if let (Some(s), Some(p)) = (selected, &ARGS.chooser_file) { Local::regular(p).write(s.encoded_bytes()).await.ok(); } } } ================================================ FILE: yazi-actor/src/app/reflow.rs ================================================ use anyhow::Result; use mlua::Value; use ratatui::layout::Position; use tracing::error; use yazi_actor::lives::Lives; use yazi_config::LAYOUT; use yazi_macro::{render, succ}; use yazi_parser::app::ReflowOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Reflow; impl Actor for Reflow { type Options = ReflowOpt; const NAME: &str = "reflow"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() }; let mut layout = LAYOUT.get(); let result = Lives::scope(cx.core, || { let comps = (opt.reflow)((Position::ORIGIN, size).into())?; for v in comps.sequence_values::() { let Value::Table(t) = v? else { error!("`reflow()` must return a table of components"); continue; }; let id: mlua::String = t.get("_id")?; match &*id.as_bytes() { b"current" => layout.current = *t.raw_get::("_area")?, b"preview" => layout.preview = *t.raw_get::("_area")?, b"progress" => layout.progress = *t.raw_get::("_area")?, _ => {} } } Ok(()) }); if layout != LAYOUT.get() { LAYOUT.set(layout); render!(); } if let Err(ref e) = result { error!("Failed to `reflow()` the `Root` component:\n{e}"); } succ!(); } } ================================================ FILE: yazi-actor/src/app/resize.rs ================================================ use anyhow::Result; use yazi_actor::Ctx; use yazi_macro::act; use yazi_parser::app::ReflowOpt; use yazi_shared::data::Data; use crate::Actor; pub struct Resize; impl Actor for Resize { type Options = ReflowOpt; const NAME: &str = "resize"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(app:reflow, cx, opt)?; cx.current_mut().arrow(0); cx.parent_mut().map(|f| f.arrow(0)); cx.current_mut().sync_page(true); act!(mgr:peek, cx) } } ================================================ FILE: yazi-actor/src/app/resume.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::app::ResumeOpt; use yazi_shared::data::Data; use yazi_term::Term; use crate::{Actor, Ctx}; pub struct Resume; impl Actor for Resume { type Options = ResumeOpt; const NAME: &str = "resume"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { cx.active_mut().preview.reset(); *cx.term = Some(Term::start()?); // While the app resumes, it's possible that the terminal size has changed. // We need to trigger a resize, and render the UI based on the resized area. act!(app:resize, cx, opt.reflow)?; opt.tx.send((true, opt.token))?; act!(app:title, cx).ok(); succ!(render!()); } } ================================================ FILE: yazi-actor/src/app/stop.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::app::StopOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Stop; impl Actor for Stop { type Options = StopOpt; const NAME: &str = "stop"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { cx.active_mut().preview.reset_image(); // We need to destroy the `term` first before stopping the `signals` // to prevent any signal from triggering the term to render again // while the app is being suspended. *cx.term = None; opt.tx.send((false, opt.token))?; succ!(); } } ================================================ FILE: yazi-actor/src/app/title.rs ================================================ use anyhow::Result; use crossterm::{execute, terminal::SetTitle}; use yazi_actor::Ctx; use yazi_dds::spark::SparkKind; use yazi_macro::succ; use yazi_parser::app::TitleOpt; use yazi_shared::{Source, data::Data}; use yazi_term::TermState; use yazi_tty::TTY; use crate::Actor; pub struct Title; impl Actor for Title { type Options = TitleOpt; const NAME: &str = "title"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let s = opt.value.unwrap_or_else(|| format!("Yazi: {}", cx.tab().name()).into()); execute!(TTY.writer(), SetTitle(&s))?; yazi_term::STATE.set(TermState { title: !s.is_empty(), ..yazi_term::STATE.get() }); succ!() } fn hook(cx: &Ctx, _opt: &Self::Options) -> Option { match cx.source() { Source::Ind => Some(SparkKind::IndAppTitle), _ => None, } } } ================================================ FILE: yazi-actor/src/app/update_progress.rs ================================================ use anyhow::Result; use yazi_actor::Ctx; use yazi_macro::{act, render, render_partial, succ}; use yazi_parser::app::UpdateProgressOpt; use yazi_shared::data::Data; use crate::Actor; pub struct UpdateProgress; impl Actor for UpdateProgress { type Options = UpdateProgressOpt; const NAME: &str = "update_progress"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { // Update the progress of all tasks. let tasks = &mut cx.tasks; let progressed = tasks.summary != opt.summary; tasks.summary = opt.summary; // If the task manager is visible, update the snaps with a full render. if tasks.visible { let new = tasks.paginate(); if tasks.snaps != new { tasks.snaps = new; act!(tasks:arrow, cx)?; succ!(render!()); } } if !progressed { succ!() } else if tasks.summary.total == 0 { succ!(render!()) } else { succ!(render_partial!()) } } } ================================================ FILE: yazi-actor/src/cmp/arrow.rs ================================================ use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::ArrowOpt; use yazi_shared::data::Data; use yazi_widgets::Scrollable; use crate::{Actor, Ctx}; pub struct Arrow; impl Actor for Arrow { type Options = ArrowOpt; const NAME: &str = "arrow"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { succ!(render!(cx.cmp.scroll(opt.step))); } } ================================================ FILE: yazi-actor/src/cmp/close.rs ================================================ use std::mem; use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::CloseOpt; use yazi_shared::data::Data; use yazi_widgets::input::parser::CompleteOpt; use crate::{Actor, Ctx}; pub struct Close; impl Actor for Close { type Options = CloseOpt; const NAME: &str = "close"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let cmp = &mut cx.cmp; if let Some(item) = cmp.selected().filter(|_| opt.submit).cloned() { return act!(input:complete, cx, CompleteOpt { name: item.name, is_dir: item.is_dir, ticket: cmp.ticket }); } cmp.caches.clear(); cmp.ticket = Default::default(); cmp.handle.take().map(|h| h.abort()); succ!(render!(mem::replace(&mut cmp.visible, false))); } } ================================================ FILE: yazi-actor/src/cmp/mod.rs ================================================ yazi_macro::mod_flat!(arrow close show trigger); ================================================ FILE: yazi-actor/src/cmp/show.rs ================================================ use std::{mem, ops::ControlFlow}; use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt}; use yazi_shared::{data::Data, path::{AsPath, PathDyn}, strand::StrandLike}; use crate::{Actor, Ctx}; const LIMIT: usize = 30; pub struct Show; impl Actor for Show { type Options = ShowOpt; const NAME: &str = "show"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let cmp = &mut cx.cmp; if cmp.ticket != opt.ticket { succ!(); } if !opt.cache.is_empty() { cmp.caches.insert(opt.cache_name.clone(), opt.cache); } let Some(cache) = cmp.caches.get(&opt.cache_name) else { succ!(); }; cmp.matches = Self::match_candidates(opt.word.as_path(), cache); if cmp.matches.is_empty() { succ!(render!(mem::replace(&mut cmp.visible, false))); } cmp.offset = 0; cmp.cursor = 0; cmp.visible = true; succ!(render!()); } } impl Show { fn match_candidates(word: PathDyn, cache: &[CmpItem]) -> Vec { let smart = !word.encoded_bytes().iter().any(|&b| b.is_ascii_uppercase()); let flow = cache.iter().try_fold((Vec::new(), Vec::new()), |(mut exact, mut fuzzy), item| { let starts_with = if smart { item.name.starts_with_ignore_ascii_case(word) } else { item.name.starts_with(word) }; if starts_with { exact.push(item); if exact.len() >= LIMIT { return ControlFlow::Break((exact, fuzzy)); } } else if fuzzy.len() < LIMIT - exact.len() && item.name.contains(word) { // Here we don't break the control flow, since we want more exact matching. fuzzy.push(item) } ControlFlow::Continue((exact, fuzzy)) }); let (exact, fuzzy) = match flow { ControlFlow::Continue(v) => v, ControlFlow::Break(v) => v, }; let it = fuzzy.into_iter().take(LIMIT - exact.len()); exact.into_iter().chain(it).cloned().collect() } } ================================================ FILE: yazi-actor/src/cmp/trigger.rs ================================================ use std::{io, mem}; use anyhow::Result; use yazi_fs::{path::clean_url, provider::{DirReader, FileHolder}}; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_proxy::CmpProxy; use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::provider; use crate::{Actor, Ctx}; pub struct Trigger; impl Actor for Trigger { type Options = TriggerOpt; const NAME: &str = "trigger"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { if opt.ticket.is_some_and(|t| t != cx.cmp.ticket) { succ!(); } else if opt.ticket.is_none() { cx.cmp.ticket = cx.input.ticket.current(); } cx.cmp.handle.take().map(|h| h.abort()); let Some((parent, word)) = Self::split_url(&opt.word) else { return act!(cmp:close, cx, false); }; let ticket = cx.cmp.ticket; if cx.cmp.caches.contains_key(&parent) { return act!(cmp:show, cx, ShowOpt { cache: vec![], cache_name: parent, word, ticket }); } cx.cmp.handle = Some(tokio::spawn(async move { let mut dir = provider::read_dir(&parent).await?; let mut cache = vec![]; // "/" is both a directory separator and the root directory per se // As there's no parent directory for the FS root, it is a special case if parent.loc() == "/" { cache.push(CmpItem { name: Default::default(), is_dir: true }); } while let Ok(Some(ent)) = dir.next().await { if let Ok(ft) = ent.file_type().await { cache.push(CmpItem { name: ent.name().into_owned(), is_dir: ft.is_dir() }); } } if !cache.is_empty() { cache .sort_unstable_by(|a, b| natsort(a.name.encoded_bytes(), b.name.encoded_bytes(), false)); CmpProxy::show(ShowOpt { cache, cache_name: parent, word, ticket }); } Ok::<_, io::Error>(()) })); succ!(render!(mem::replace(&mut cx.cmp.visible, false))); } } impl Trigger { fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> { let sep = if cfg!(windows) { AnyAsciiChar::new(b"/\\").unwrap() } else { AnyAsciiChar::new(b"/").unwrap() }; let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?; if path.is_empty() && !sep.predicate(s.bytes().last()?) { return None; // We don't complete a `sftp://test`, but `sftp://test/` } // Scheme let scheme = scheme.zeroed(); if scheme.is_local() && path.as_strand() == "~" { return None; // We don't complete a `~`, but `~/` } // Child let child = path.rsplit_pred(sep).map_or(path.as_path(), |(_, c)| c).to_owned(); // Parent let url = UrlCow::try_from((scheme.clone().zeroed(), path)).ok()?; let abs = if let Some(u) = provider::try_absolute(&url) { u } else { url }; let parent = abs.loc().try_strip_suffix(&child).ok()?; Some((clean_url(UrlCow::try_from((scheme, parent)).ok()?), child)) } } #[cfg(test)] mod tests { use yazi_fs::CWD; use yazi_shared::url::UrlLike; use super::*; fn compare(s: &str, parent: &str, child: &str) { let (mut p, c) = Trigger::split_url(s).unwrap(); if let Ok(u) = p.try_strip_prefix(yazi_fs::CWD.load().as_ref()) { p = UrlBuf::Regular(u.as_os().unwrap().into()); } assert_eq!((p, c.to_str().unwrap()), (parent.parse().unwrap(), child)); } #[cfg(unix)] #[test] fn test_split() { yazi_shared::init_tests(); yazi_fs::init(); assert_eq!(Trigger::split_url(""), None); assert_eq!(Trigger::split_url("sftp://test"), None); compare(" ", "", " "); compare("/", "/", ""); compare("//", "/", ""); compare("///", "/", ""); compare("/foo", "/", "foo"); compare("//foo", "/", "foo"); compare("///foo", "/", "foo"); compare("/foo/", "/foo/", ""); compare("//foo/", "/foo/", ""); compare("/foo/bar", "/foo/", "bar"); compare("///foo/bar", "/foo/", "bar"); CWD.set(&"sftp://test".parse::().unwrap(), || {}); compare("sftp://test/a", "sftp://test/.", "a"); compare("sftp://test//a", "sftp://test//", "a"); compare("sftp://test2/a", "sftp://test2/.", "a"); compare("sftp://test2//a", "sftp://test2//", "a"); } #[cfg(windows)] #[test] fn test_split() { yazi_fs::init(); compare("foo", "", "foo"); compare(r"foo\", r"foo\", ""); compare(r"foo\bar", r"foo\", "bar"); compare(r"foo\bar\", r"foo\bar\", ""); compare(r"C:\", r"C:\", ""); compare(r"C:\foo", r"C:\", "foo"); compare(r"C:\foo\", r"C:\foo\", ""); compare(r"C:\foo\bar", r"C:\foo\", "bar"); } } ================================================ FILE: yazi-actor/src/confirm/arrow.rs ================================================ use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::ArrowOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Arrow; impl Actor for Arrow { type Options = ArrowOpt; const NAME: &str = "arrow"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let confirm = &mut cx.core.confirm; let area = cx.core.mgr.area(confirm.position); let len = confirm.list.line_count(area.width); let old = confirm.offset; confirm.offset = opt.step.add(confirm.offset, len, area.height as _); succ!(render!(old != confirm.offset)); } } ================================================ FILE: yazi-actor/src/confirm/close.rs ================================================ use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::confirm::CloseOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Close; impl Actor for Close { type Options = CloseOpt; const NAME: &str = "close"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { cx.confirm.token.complete(opt.submit); cx.confirm.visible = false; succ!(render!()); } } ================================================ FILE: yazi-actor/src/confirm/mod.rs ================================================ yazi_macro::mod_flat!(arrow close show); ================================================ FILE: yazi-actor/src/confirm/show.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::confirm::ShowOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Show; impl Actor for Show { type Options = ShowOpt; const NAME: &str = "show"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(confirm:close, cx)?; let confirm = &mut cx.confirm; confirm.title = opt.cfg.title; confirm.body = opt.cfg.body; confirm.list = opt.cfg.list; confirm.position = opt.cfg.position; confirm.offset = 0; confirm.token = opt.token; confirm.visible = true; succ!(render!()); } } ================================================ FILE: yazi-actor/src/context.rs ================================================ use std::ops::{Deref, DerefMut}; use anyhow::{Result, anyhow}; use yazi_core::{Core, mgr::Tabs, tab::{Folder, Tab}, tasks::Tasks}; use yazi_fs::File; use yazi_shared::{Id, Source, event::Action, url::UrlBuf}; use yazi_term::Term; pub struct Ctx<'a> { pub core: &'a mut Core, pub term: &'a mut Option, pub tab: usize, pub level: usize, pub source: Source, #[cfg(debug_assertions)] pub backtrace: Vec<&'static str>, } impl Deref for Ctx<'_> { type Target = Core; fn deref(&self) -> &Self::Target { self.core } } impl DerefMut for Ctx<'_> { fn deref_mut(&mut self) -> &mut Self::Target { self.core } } impl<'a> Ctx<'a> { #[inline] pub fn new(action: &Action, core: &'a mut Core, term: &'a mut Option) -> Result { let tab = if let Ok(id) = action.get::("tab") { core .mgr .tabs .iter() .position(|t| t.id == id) .ok_or_else(|| anyhow!("Tab with id {id} not found"))? } else { core.mgr.tabs.cursor }; Ok(Self { core, term, tab, level: 0, source: action.source, #[cfg(debug_assertions)] backtrace: vec![], }) } #[inline] pub fn renew<'b>(cx: &'a mut Ctx<'b>) -> Self { let tab = cx.core.mgr.tabs.cursor; Self { core: cx.core, term: cx.term, tab, level: cx.level, source: cx.source, #[cfg(debug_assertions)] backtrace: vec![], } } #[inline] pub fn active(core: &'a mut Core, term: &'a mut Option) -> Self { let tab = core.mgr.tabs.cursor; Self { core, term, tab, level: 0, source: Source::Unknown, #[cfg(debug_assertions)] backtrace: vec![], } } } impl<'a> Ctx<'a> { #[inline] pub fn tabs(&self) -> &Tabs { &self.mgr.tabs } #[inline] pub fn tabs_mut(&mut self) -> &mut Tabs { &mut self.mgr.tabs } #[inline] pub fn tab(&self) -> &Tab { &self.tabs()[self.tab] } #[inline] pub fn tab_mut(&mut self) -> &mut Tab { &mut self.core.mgr.tabs[self.tab] } #[inline] pub fn cwd(&self) -> &UrlBuf { self.tab().cwd() } #[inline] pub fn parent(&self) -> Option<&Folder> { self.tab().parent.as_ref() } #[inline] pub fn parent_mut(&mut self) -> Option<&mut Folder> { self.tab_mut().parent.as_mut() } #[inline] pub fn current(&self) -> &Folder { &self.tab().current } #[inline] pub fn current_mut(&mut self) -> &mut Folder { &mut self.tab_mut().current } #[inline] pub fn hovered(&self) -> Option<&File> { self.tab().hovered() } #[inline] pub fn hovered_folder(&self) -> Option<&Folder> { self.tab().hovered_folder() } #[inline] pub fn hovered_folder_mut(&mut self) -> Option<&mut Folder> { self.tab_mut().hovered_folder_mut() } #[inline] pub fn tasks(&self) -> &Tasks { &self.tasks } #[inline] pub fn source(&self) -> Source { if self.level != 1 { Source::Ind } else { self.source } } } ================================================ FILE: yazi-actor/src/core/mod.rs ================================================ yazi_macro::mod_flat!(preflight); ================================================ FILE: yazi-actor/src/core/preflight.rs ================================================ use anyhow::Result; use mlua::{ErrorContext, ExternalError, IntoLua, Value}; use yazi_binding::runtime_scope; use yazi_dds::{LOCAL, spark::{Spark, SparkKind}}; use yazi_plugin::LUA; use crate::{Ctx, lives::Lives}; pub struct Preflight; impl Preflight { pub fn act<'a>(cx: &mut Ctx, opt: (SparkKind, Spark<'a>)) -> Result> { let kind = opt.0; let Some(handlers) = LOCAL.read().get(kind.as_ref()).filter(|&m| !m.is_empty()).cloned() else { return Ok(opt.1); }; Ok(Lives::scope(cx.core, || { let mut body = opt.1.into_lua(&LUA)?; for (id, cb) in handlers { match runtime_scope!(LUA, &id, cb.call::(&body)) { Ok(Value::Nil) => { Err(format!("`{kind}` event cancelled by `{id}` plugin on preflight").into_lua_err())? } Ok(v) => body = v, Err(e) => Err( format!("Failed to run `{kind}` event handler in `{id}` plugin: {e}").into_lua_err(), )?, }; } Spark::from_lua(&LUA, kind, body) .with_context(|e| format!("Unexpected return type from `{kind}` event handlers: {e}")) })?) } } ================================================ FILE: yazi-actor/src/help/arrow.rs ================================================ use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::ArrowOpt; use yazi_shared::data::Data; use yazi_widgets::Scrollable; use crate::{Actor, Ctx}; pub struct Arrow; impl Actor for Arrow { type Options = ArrowOpt; const NAME: &str = "arrow"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { succ!(render!(cx.help.scroll(opt.step))); } } ================================================ FILE: yazi-actor/src/help/escape.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::VoidOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Escape; impl Actor for Escape { type Options = VoidOpt; const NAME: &str = "escape"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { if cx.help.keyword().is_none() { return act!(help:toggle, cx, cx.help.layer); } let help = &mut cx.help; help.keyword = String::new(); help.in_filter = None; help.filter_apply(); succ!(render!()); } } ================================================ FILE: yazi-actor/src/help/filter.rs ================================================ use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::VoidOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Filter; impl Actor for Filter { type Options = VoidOpt; const NAME: &str = "filter"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let help = &mut cx.help; help.in_filter = Some(Default::default()); help.filter_apply(); succ!(render!()); } } ================================================ FILE: yazi-actor/src/help/mod.rs ================================================ yazi_macro::mod_flat!(arrow escape filter toggle); ================================================ FILE: yazi-actor/src/help/toggle.rs ================================================ use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::help::ToggleOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Toggle; impl Actor for Toggle { type Options = ToggleOpt; const NAME: &str = "toggle"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let help = &mut cx.help; help.visible = !help.visible; help.layer = opt.layer; help.keyword = String::new(); help.in_filter = None; help.filter_apply(); help.offset = 0; help.cursor = 0; succ!(render!()); } } ================================================ FILE: yazi-actor/src/input/close.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::input::CloseOpt; use yazi_shared::data::Data; use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; pub struct Close; impl Actor for Close { type Options = CloseOpt; const NAME: &str = "close"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let input = &mut cx.input; input.visible = false; input.ticket.next(); if let Some(tx) = input.tx.take() { let value = input.snap().value.clone(); _ = tx.send(if opt.submit { InputEvent::Submit(value) } else { InputEvent::Cancel(value) }); } act!(cmp:close, cx)?; succ!(render!()); } } ================================================ FILE: yazi-actor/src/input/complete.rs ================================================ use anyhow::Result; use yazi_macro::{act, succ}; use yazi_shared::data::Data; use yazi_widgets::input::parser::CompleteOpt; use crate::{Actor, Ctx}; pub struct Complete; impl Actor for Complete { type Options = CompleteOpt; const NAME: &str = "complete"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let input = &mut cx.input; if !input.visible || input.ticket.current() != opt.ticket { succ!(); } act!(complete, input, opt) } } ================================================ FILE: yazi-actor/src/input/escape.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::VoidOpt; use yazi_shared::data::Data; use yazi_widgets::input::InputOp; use crate::{Actor, Ctx}; pub struct Escape; impl Actor for Escape { type Options = VoidOpt; const NAME: &str = "escape"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { use yazi_widgets::input::InputMode as M; let input = &mut cx.input; let mode = input.snap().mode; match mode { M::Normal if input.snap_mut().op == InputOp::None => act!(input:close, cx), M::Insert => act!(cmp:close, cx), M::Normal | M::Replace => Ok(().into()), }?; act!(escape, cx.input)?; succ!(render!()); } } ================================================ FILE: yazi-actor/src/input/mod.rs ================================================ yazi_macro::mod_flat!(close complete escape show); ================================================ FILE: yazi-actor/src/input/show.rs ================================================ use std::ops::DerefMut; use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_shared::data::Data; use yazi_widgets::input::InputOpt; use crate::{Actor, Ctx}; pub struct Show; impl Actor for Show { type Options = InputOpt; const NAME: &str = "show"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(input:close, cx)?; let input = &mut cx.input; input.visible = true; input.title = opt.cfg.title.clone(); input.position = opt.cfg.position; *input.deref_mut() = yazi_widgets::input::Input::new(opt)?; succ!(render!()); } } ================================================ FILE: yazi-actor/src/lib.rs ================================================ extern crate self as yazi_actor; yazi_macro::mod_pub!(app cmp confirm core help input lives mgr notify pick spot tasks which); yazi_macro::mod_flat!(actor context); ================================================ FILE: yazi-actor/src/lives/core.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, IntoLua, MetaMethod, UserData, Value}; use paste::paste; use super::{Lives, PtrCell}; pub(super) struct Core { inner: PtrCell, c_active: Option, c_tabs: Option, c_tasks: Option, c_yanked: Option, c_layer: Option, c_which: Option, } impl Deref for Core { type Target = yazi_core::Core; fn deref(&self) -> &Self::Target { &self.inner } } impl Core { pub(super) fn make(inner: &yazi_core::Core) -> mlua::Result { Lives::scoped_userdata(Self { inner: inner.into(), c_active: None, c_tabs: None, c_tasks: None, c_yanked: None, c_layer: None, c_which: None, }) } } impl UserData for Core { fn add_methods>(methods: &mut M) { methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: mlua::String| { macro_rules! reuse { ($key:ident, $value:expr) => { match paste! { &me.[] } { Some(v) => v.clone(), None => { let v = $value?.into_lua(lua)?; paste! { me.[] = Some(v.clone()); }; v } } }; } Ok(match &*key.as_bytes() { b"active" => reuse!(active, super::Tab::make(me.active())), b"tabs" => reuse!(tabs, super::Tabs::make(&me.mgr.tabs)), b"tasks" => reuse!(tasks, super::Tasks::make(&me.tasks)), b"yanked" => reuse!(yanked, super::Yanked::make(&me.mgr.yanked)), b"layer" => { reuse!(layer, Ok::<_, mlua::Error>(yazi_binding::Layer::from(me.layer()))) } b"which" => reuse!(which, super::Which::make(&me.which)), _ => Value::Nil, }) }); } } ================================================ FILE: yazi-actor/src/lives/file.rs ================================================ use std::{ops::Deref, ptr}; use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods, Value}; use yazi_binding::{Range, Style, cached_field}; use yazi_config::THEME; use yazi_shared::{path::AsPath, url::UrlLike}; use super::Lives; use crate::lives::PtrCell; pub(super) struct File { idx: usize, folder: PtrCell, tab: PtrCell, v_cha: Option, v_url: Option, v_link_to: Option, v_name: Option, v_path: Option, v_cache: Option, v_bare: Option, } impl Deref for File { type Target = yazi_fs::File; fn deref(&self) -> &Self::Target { &self.folder.files[self.idx] } } impl AsRef for File { fn as_ref(&self) -> &yazi_fs::File { self } } impl File { pub(super) fn make( idx: usize, folder: &yazi_core::tab::Folder, tab: &yazi_core::tab::Tab, ) -> mlua::Result { use hashbrown::hash_map::Entry; Ok(match super::FILE_CACHE.borrow_mut().entry(PtrCell(&folder.files[idx])) { Entry::Occupied(oe) => oe.into_mut().clone(), Entry::Vacant(ve) => { let ud = Lives::scoped_userdata(Self { idx, folder: folder.into(), tab: tab.into(), v_cha: None, v_url: None, v_link_to: None, v_name: None, v_path: None, v_cache: None, v_bare: None, })?; ve.insert(ud.clone()); ud } }) } #[inline] fn is_hovered(&self) -> bool { self.idx == self.folder.cursor } } impl UserData for File { fn add_fields>(fields: &mut F) { yazi_binding::impl_file_fields!(fields); cached_field!(fields, bare, |_, me| Ok(yazi_binding::File::new(&**me))); fields.add_field_method_get("idx", |_, me| Ok(me.idx + 1)); fields.add_field_method_get("is_hovered", |_, me| Ok(me.is_hovered())); fields.add_field_method_get("in_current", |_, me| Ok(ptr::eq(&*me.folder, &me.tab.current))); fields.add_field_method_get("in_preview", |_, me| { Ok(me.idx == me.folder.cursor && me.tab.hovered().is_some_and(|f| f.url == me.folder.url)) }); } fn add_methods>(methods: &mut M) { yazi_binding::impl_file_methods!(methods); methods.add_method("icon", |_, me, ()| { use yazi_binding::Icon; // TODO: use a cache Ok(yazi_config::THEME.icon.matches(me, me.is_hovered()).map(Icon::from)) }); methods.add_method("size", |_, me, ()| { Ok(if me.is_dir() { me.folder.files.sizes.get(&me.urn()).copied() } else { Some(me.len) }) }); methods.add_method("mime", |lua, me, ()| { lua.named_registry_value::("cx")?.borrow_scoped(|core: &yazi_core::Core| { core.mgr.mimetype.get(&me.url).map(|s| lua.create_string(s)).transpose() })? }); methods.add_method("prefix", |lua, me, ()| { if !me.url.has_trail() { return Ok(None); } let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components(); comp.next_back(); Some(lua.create_string(comp.as_path().encoded_bytes())).transpose() }); methods.add_method("style", |lua, me, ()| { lua.named_registry_value::("cx")?.borrow_scoped(|core: &yazi_core::Core| { let mime = core.mgr.mimetype.get(&me.url).unwrap_or_default(); THEME.filetype.iter().find(|&x| x.matches(me, mime)).map(|x| Style::from(x.style)) }) }); methods.add_method("is_yanked", |lua, me, ()| { lua.named_registry_value::("cx")?.borrow_scoped(|core: &yazi_core::Core| { if !core.mgr.yanked.contains(&me.url) { 0u8 } else if core.mgr.yanked.cut { 2u8 } else { 1u8 } }) }); methods.add_method("is_marked", |_, me, ()| { use yazi_core::tab::Mode::*; if !me.tab.mode.is_visual() || me.folder.url != me.tab.current.url { return Ok(0u8); } Ok(match &me.tab.mode { Select(_, indices) if indices.contains(&me.idx) => 1u8, Unset(_, indices) if indices.contains(&me.idx) => 2u8, _ => 0u8, }) }); methods.add_method("is_selected", |_, me, ()| Ok(me.tab.selected.contains(&me.url))); methods.add_method("found", |lua, me, ()| { lua.named_registry_value::("cx")?.borrow_scoped(|core: &yazi_core::Core| { let Some(finder) = &core.active().finder else { return Ok(None); }; let Some(idx) = finder.matched_idx(&me.folder, me.urn()) else { return Ok(None); }; Some(lua.create_sequence_from([idx.into_lua(lua)?, finder.matched.len().into_lua(lua)?])) .transpose() }) }); methods.add_method("highlights", |lua, me, ()| { lua.named_registry_value::("cx")?.borrow_scoped(|core: &yazi_core::Core| { let Some(finder) = &core.active().finder else { return None; }; if me.folder.url != me.tab.current.url { return None; } let h = finder.filter.highlighted(me.url.name()?)?; Some(h.into_iter().map(Range::from).collect::>()) }) }); } } ================================================ FILE: yazi-actor/src/lives/files.rs ================================================ use std::ops::{Deref, Range}; use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods, Value}; use yazi_binding::cached_field; use super::{File, Filter, Lives, PtrCell}; pub(super) struct Files { window: Range, folder: PtrCell, tab: PtrCell, v_filter: Option, } impl Deref for Files { type Target = yazi_fs::Files; fn deref(&self) -> &Self::Target { &self.folder.files } } impl Files { pub(super) fn make( window: Range, folder: &yazi_core::tab::Folder, tab: &yazi_core::tab::Tab, ) -> mlua::Result { Lives::scoped_userdata(Self { window, folder: folder.into(), tab: tab.into(), v_filter: None }) } } impl UserData for Files { fn add_fields>(fields: &mut F) { cached_field!(fields, filter, |_, me| me.filter().map(Filter::make).transpose()); } fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.window.end - me.window.start)); methods.add_meta_method(MetaMethod::Index, |_, me, mut idx: usize| { idx += me.window.start; if idx > me.window.end || idx == 0 { Ok(None) } else { Some(File::make(idx - 1, &me.folder, &me.tab)).transpose() } }); } } ================================================ FILE: yazi-actor/src/lives/filter.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, MetaMethod, UserData, UserDataMethods}; use super::{Lives, PtrCell}; pub(super) struct Filter { inner: PtrCell, } impl Deref for Filter { type Target = yazi_fs::Filter; fn deref(&self) -> &Self::Target { &self.inner } } impl Filter { pub(super) fn make(inner: &yazi_fs::Filter) -> mlua::Result { Lives::scoped_userdata(Self { inner: inner.into() }) } } impl UserData for Filter { fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.to_string())); } } ================================================ FILE: yazi-actor/src/lives/finder.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, MetaMethod, UserData, UserDataMethods}; use super::{Lives, PtrCell}; pub(super) struct Finder { inner: PtrCell, } impl Deref for Finder { type Target = yazi_core::tab::Finder; fn deref(&self) -> &Self::Target { &self.inner } } impl Finder { pub(super) fn make(inner: &yazi_core::tab::Finder) -> mlua::Result { Lives::scoped_userdata(Self { inner: inner.into() }) } } impl UserData for Finder { fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.filter.to_string())); } } ================================================ FILE: yazi-actor/src/lives/folder.rs ================================================ use std::ops::{Deref, Range}; use mlua::{AnyUserData, UserData, UserDataFields, Value}; use yazi_binding::{FolderStage, Url, cached_field}; use yazi_config::LAYOUT; use super::{File, Files, Lives, PtrCell}; pub(super) struct Folder { window: Range, inner: PtrCell, tab: PtrCell, v_cwd: Option, v_files: Option, v_stage: Option, v_window: Option, v_hovered: Option, } impl Deref for Folder { type Target = yazi_core::tab::Folder; fn deref(&self) -> &Self::Target { &self.inner } } impl Folder { pub(super) fn make( window: Option>, inner: &yazi_core::tab::Folder, tab: &yazi_core::tab::Tab, ) -> mlua::Result { let window = match window { Some(w) => w, None => { let limit = LAYOUT.get().preview.height as usize; inner.offset..inner.files.len().min(inner.offset + limit) } }; Lives::scoped_userdata(Self { window, inner: inner.into(), tab: tab.into(), v_cwd: None, v_files: None, v_stage: None, v_window: None, v_hovered: None, }) } } impl UserData for Folder { fn add_fields>(fields: &mut F) { cached_field!(fields, cwd, |_, me| Ok(Url::new(&me.url))); cached_field!(fields, files, |_, me| Files::make(0..me.files.len(), me, &me.tab)); cached_field!(fields, stage, |_, me| Ok(FolderStage::new(me.stage.clone()))); cached_field!(fields, window, |_, me| Files::make(me.window.clone(), me, &me.tab)); fields.add_field_method_get("offset", |_, me| Ok(me.offset)); fields.add_field_method_get("cursor", |_, me| Ok(me.cursor)); cached_field!(fields, hovered, |_, me| { me.hovered().map(|_| File::make(me.cursor, me, &me.tab)).transpose() }); } } ================================================ FILE: yazi-actor/src/lives/lives.rs ================================================ use std::cell::RefCell; use hashbrown::HashMap; use mlua::{AnyUserData, UserData}; use scopeguard::defer; use tracing::error; use yazi_plugin::LUA; use yazi_shared::RoCell; use super::{Core, PtrCell}; static TO_DESTROY: RoCell>> = RoCell::new_const(RefCell::new(Vec::new())); pub(super) static FILE_CACHE: RoCell, AnyUserData>>> = RoCell::new(); pub struct Lives; impl Lives { pub fn scope(core: &yazi_core::Core, f: F) -> mlua::Result where F: FnOnce() -> mlua::Result, { FILE_CACHE.init(Default::default()); defer! { FILE_CACHE.drop(); } let result = LUA.scope(|scope| { scope.add_destructor(|| { for ud in TO_DESTROY.borrow_mut().drain(..) { ud.destroy().expect("failed to destruct scoped userdata"); } }); LUA.set_named_registry_value("cx", scope.create_any_userdata_ref(core)?)?; LUA.globals().raw_set("cx", Core::make(core)?)?; f() }); if let Err(ref e) = result { error!("{e}"); } result } pub(crate) fn scoped_userdata(data: T) -> mlua::Result where T: UserData + 'static, { let ud = LUA.create_userdata(data)?; TO_DESTROY.borrow_mut().push(ud.clone()); Ok(ud) } } ================================================ FILE: yazi-actor/src/lives/mod.rs ================================================ yazi_macro::mod_flat!(core file files filter finder folder lives mode preference preview ptr selected tab tabs task tasks which yanked); ================================================ FILE: yazi-actor/src/lives/mode.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods}; use super::{Lives, PtrCell}; pub(super) struct Mode { inner: PtrCell, } impl Deref for Mode { type Target = yazi_core::tab::Mode; fn deref(&self) -> &Self::Target { &self.inner } } impl Mode { pub(super) fn make(inner: &yazi_core::tab::Mode) -> mlua::Result { Lives::scoped_userdata(Self { inner: inner.into() }) } } impl UserData for Mode { fn add_fields>(fields: &mut F) { fields.add_field_method_get("is_select", |_, me| Ok(me.is_select())); fields.add_field_method_get("is_unset", |_, me| Ok(me.is_unset())); fields.add_field_method_get("is_visual", |_, me| Ok(me.is_visual())); } fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.to_string())); } } ================================================ FILE: yazi-actor/src/lives/preference.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, UserData, UserDataFields, Value}; use yazi_binding::cached_field; use super::{Lives, PtrCell}; pub(super) struct Preference { inner: PtrCell, v_name: Option, v_linemode: Option, v_sort_by: Option, } impl Deref for Preference { type Target = yazi_core::tab::Preference; fn deref(&self) -> &Self::Target { &self.inner } } impl Preference { pub(super) fn make(inner: &yazi_core::tab::Preference) -> mlua::Result { Lives::scoped_userdata(Self { inner: inner.into(), v_name: None, v_linemode: None, v_sort_by: None, }) } } impl UserData for Preference { fn add_fields>(fields: &mut F) { // Display cached_field!(fields, name, |lua, me| lua.create_string(&me.name)); cached_field!(fields, linemode, |lua, me| lua.create_string(&me.linemode)); fields.add_field_method_get("show_hidden", |_, me| Ok(me.show_hidden)); // Sorting cached_field!(fields, sort_by, |_, me| Ok(me.sort_by.to_string())); fields.add_field_method_get("sort_sensitive", |_, me| Ok(me.sort_sensitive)); fields.add_field_method_get("sort_reverse", |_, me| Ok(me.sort_reverse)); fields.add_field_method_get("sort_dir_first", |_, me| Ok(me.sort_dir_first)); fields.add_field_method_get("sort_translit", |_, me| Ok(me.sort_translit)); fields.add_field_method_get("sort_fallback", |_, me| Ok(me.sort_fallback.to_string())); } } ================================================ FILE: yazi-actor/src/lives/preview.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, UserData, UserDataFields, Value}; use yazi_binding::cached_field; use yazi_config::LAYOUT; use super::{Folder, Lives, PtrCell}; pub(super) struct Preview { tab: PtrCell, v_folder: Option, } impl Deref for Preview { type Target = yazi_core::tab::Preview; fn deref(&self) -> &Self::Target { &self.tab.preview } } impl Preview { pub(super) fn make(tab: &yazi_core::tab::Tab) -> mlua::Result { Lives::scoped_userdata(Self { tab: tab.into(), v_folder: None }) } } impl UserData for Preview { fn add_fields>(fields: &mut F) { fields.add_field_method_get("skip", |_, me| Ok(me.skip)); cached_field!(fields, folder, |_, me| { me.tab .hovered_folder() .map(|f| { let limit = LAYOUT.get().preview.height as usize; Folder::make(Some(me.skip..f.files.len().min(me.skip + limit)), f, &me.tab) }) .transpose() }); } } ================================================ FILE: yazi-actor/src/lives/ptr.rs ================================================ use std::{hash::{Hash, Hasher}, ops::Deref}; pub(super) struct PtrCell(pub(super) *const T); impl Deref for PtrCell { type Target = T; fn deref(&self) -> &Self::Target { unsafe { &*self.0 } } } impl From<&T> for PtrCell { fn from(value: &T) -> Self { Self(value) } } impl Clone for PtrCell { fn clone(&self) -> Self { *self } } impl Copy for PtrCell {} impl PartialEq for PtrCell { fn eq(&self, other: &Self) -> bool { self.0.addr() == other.0.addr() } } impl Eq for PtrCell {} impl Hash for PtrCell { fn hash(&self, state: &mut H) { state.write_usize(self.0.addr()); } } impl PtrCell { #[inline] pub(super) fn as_static(&self) -> &'static T { unsafe { &*self.0 } } } ================================================ FILE: yazi-actor/src/lives/selected.rs ================================================ use mlua::AnyUserData; use super::Lives; use crate::lives::PtrCell; #[derive(Clone, Copy)] pub(super) struct Selected; impl Selected { pub(super) fn make(inner: &yazi_core::tab::Selected) -> mlua::Result { let inner = PtrCell::from(inner); Lives::scoped_userdata(yazi_binding::Iter::new( inner.as_static().values().map(yazi_binding::Url::new), Some(inner.len()), )) } } ================================================ FILE: yazi-actor/src/lives/tab.rs ================================================ use std::{borrow::Cow, ops::Deref}; use mlua::{AnyUserData, UserData, UserDataFields, UserDataMethods, Value}; use yazi_binding::{Id, UrlRef, cached_field}; use super::{Finder, Folder, Lives, Mode, Preference, Preview, PtrCell, Selected}; pub(super) struct Tab { inner: PtrCell, v_name: Option, v_mode: Option, v_pref: Option, v_current: Option, v_parent: Option, v_selected: Option, v_preview: Option, v_finder: Option, } impl Deref for Tab { type Target = yazi_core::tab::Tab; fn deref(&self) -> &Self::Target { &self.inner } } impl Tab { pub(super) fn make(inner: &yazi_core::tab::Tab) -> mlua::Result { Lives::scoped_userdata(Self { inner: inner.into(), v_name: None, v_mode: None, v_pref: None, v_current: None, v_parent: None, v_selected: None, v_preview: None, v_finder: None, }) } } impl UserData for Tab { fn add_fields>(fields: &mut F) { fields.add_field_method_get("id", |_, me| Ok(Id(me.id))); cached_field!(fields, name, |lua, me| { match me.name() { Cow::Borrowed(s) => lua.create_string(s), Cow::Owned(s) => lua.create_external_string(s), } }); cached_field!(fields, mode, |_, me| Mode::make(&me.mode)); cached_field!(fields, pref, |_, me| Preference::make(&me.pref)); cached_field!(fields, current, |_, me| Folder::make(None, &me.current, me)); cached_field!(fields, parent, |_, me| { me.parent.as_ref().map(|f| Folder::make(None, f, me)).transpose() }); cached_field!(fields, selected, |_, me| Selected::make(&me.selected)); cached_field!(fields, preview, |_, me| Preview::make(me)); cached_field!(fields, finder, |_, me| me.finder.as_ref().map(Finder::make).transpose()); } fn add_methods>(methods: &mut M) { methods.add_method("history", |_, me, url: UrlRef| { me.history.get(url.as_ref()).map(|f| Folder::make(None, f, me)).transpose() }); } } ================================================ FILE: yazi-actor/src/lives/tabs.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods}; use super::{Lives, PtrCell, Tab}; pub(super) struct Tabs { inner: PtrCell, } impl Deref for Tabs { type Target = yazi_core::mgr::Tabs; fn deref(&self) -> &Self::Target { &self.inner } } impl Tabs { pub(super) fn make(inner: &yazi_core::mgr::Tabs) -> mlua::Result { Lives::scoped_userdata(Self { inner: inner.into() }) } } impl UserData for Tabs { fn add_fields>(fields: &mut F) { fields.add_field_method_get("idx", |_, me| Ok(me.cursor + 1)); } fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); methods.add_meta_method(MetaMethod::Index, |_, me, idx: usize| { if idx > me.len() || idx == 0 { Ok(None) } else { Some(Tab::make(&me[idx - 1])).transpose() } }); } } ================================================ FILE: yazi-actor/src/lives/task.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields, Value}; use yazi_binding::{SER_OPT, cached_field}; use super::{Lives, PtrCell}; pub(super) struct TaskSnap { inner: PtrCell, v_name: Option, v_prog: Option, } impl Deref for TaskSnap { type Target = yazi_scheduler::TaskSnap; fn deref(&self) -> &Self::Target { &self.inner } } impl TaskSnap { pub(super) fn make(inner: &yazi_scheduler::TaskSnap) -> mlua::Result { Lives::scoped_userdata(Self { inner: inner.into(), v_name: None, v_prog: None }) } } impl UserData for TaskSnap { fn add_fields>(fields: &mut F) { cached_field!(fields, name, |lua, me| lua.create_string(&me.name)); cached_field!(fields, prog, |lua, me| lua.to_value_with(&me.prog, SER_OPT)); fields.add_field_method_get("cooked", |_, me| Ok(me.prog.cooked())); fields.add_field_method_get("running", |_, me| Ok(me.prog.running())); fields.add_field_method_get("success", |_, me| Ok(me.prog.success())); fields.add_field_method_get("failed", |_, me| Ok(me.prog.failed())); fields.add_field_method_get("percent", |_, me| Ok(me.prog.percent())); } } ================================================ FILE: yazi-actor/src/lives/tasks.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields, Value}; use yazi_binding::{SER_OPT, cached_field}; use super::{Lives, PtrCell}; use crate::lives::TaskSnap; pub(super) struct Tasks { inner: PtrCell, v_snaps: Option, v_summary: Option, } impl Deref for Tasks { type Target = yazi_core::tasks::Tasks; fn deref(&self) -> &Self::Target { &self.inner } } impl Tasks { pub(super) fn make(inner: &yazi_core::tasks::Tasks) -> mlua::Result { Lives::scoped_userdata(Self { inner: inner.into(), v_snaps: None, v_summary: None, }) } } impl UserData for Tasks { fn add_fields>(fields: &mut F) { fields.add_field_method_get("cursor", |_, me| Ok(me.cursor)); cached_field!(fields, snaps, |lua, me| { let tbl = lua.create_table_with_capacity(me.snaps.len(), 0)?; for snap in &me.snaps { tbl.raw_push(TaskSnap::make(snap)?)?; } Ok(tbl) }); cached_field!(fields, summary, |lua, me| lua.to_value_with(&me.summary, SER_OPT)); } } ================================================ FILE: yazi-actor/src/lives/which.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, UserData, UserDataFields, Value}; use yazi_binding::cached_field; use super::{Lives, PtrCell}; pub(super) struct Which { inner: PtrCell, v_tx: Option, v_cands: Option, } impl Deref for Which { type Target = yazi_core::which::Which; fn deref(&self) -> &Self::Target { &self.inner } } impl Which { pub(super) fn make(inner: &yazi_core::which::Which) -> mlua::Result { Lives::scoped_userdata(Self { inner: inner.into(), v_tx: None, v_cands: None }) } } impl UserData for Which { fn add_fields>(fields: &mut F) { cached_field!(fields, tx, |_, me| Ok(me.tx.clone().map(yazi_binding::MpscUnboundedTx))); fields.add_field_method_get("times", |_, me| Ok(me.inner.times)); cached_field!(fields, cands, |lua, me| { lua.create_sequence_from(me.inner.cands.iter().cloned().map(yazi_binding::ChordCow)) }); fields.add_field_method_get("active", |_, me| Ok(me.inner.active)); fields.add_field_method_get("silent", |_, me| Ok(me.inner.silent)); } } ================================================ FILE: yazi-actor/src/lives/yanked.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods}; use yazi_binding::{Iter, get_metatable}; use super::{Lives, PtrCell}; pub(super) struct Yanked { inner: PtrCell, iter: AnyUserData, } impl Deref for Yanked { type Target = yazi_core::mgr::Yanked; fn deref(&self) -> &Self::Target { &self.inner } } impl Yanked { pub(super) fn make(inner: &yazi_core::mgr::Yanked) -> mlua::Result { let inner = PtrCell::from(inner); Lives::scoped_userdata(Self { inner, iter: Lives::scoped_userdata(Iter::new( inner.as_static().iter().map(yazi_binding::Url::new), Some(inner.len()), ))?, }) } } impl UserData for Yanked { fn add_fields>(fields: &mut F) { fields.add_field_method_get("is_cut", |_, me| Ok(me.cut)); } fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); methods.add_meta_method(MetaMethod::Pairs, |lua, me, ()| { get_metatable(lua, &me.iter)? .call_function::(MetaMethod::Pairs.name(), me.iter.clone()) }); } } ================================================ FILE: yazi-actor/src/mgr/arrow.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::ArrowOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Arrow; impl Actor for Arrow { type Options = ArrowOpt; const NAME: &str = "arrow"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let tab = cx.tab_mut(); if !tab.current.arrow(opt.step) { succ!(); } // Visual selection if let Some((start, items)) = tab.mode.visual_mut() { let end = tab.current.cursor; *items = (start.min(end)..=end.max(start)).collect(); } act!(mgr:hover, cx)?; act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; succ!(render!()); } } ================================================ FILE: yazi-actor/src/mgr/back.rs ================================================ use anyhow::Result; use yazi_macro::{act, succ}; use yazi_parser::{VoidOpt, mgr::CdSource}; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Back; impl Actor for Back { type Options = VoidOpt; const NAME: &str = "back"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { if let Some(u) = cx.tab_mut().backstack.shift_backward().cloned() { act!(mgr:cd, cx, (u, CdSource::Back))?; } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/bulk_rename.rs ================================================ use std::{hash::Hash, io::{Read, Write}, ops::Deref, path::Path}; use anyhow::{Result, anyhow}; use crossterm::{execute, style::Print}; use hashbrown::HashMap; use scopeguard::defer; use tokio::io::AsyncWriteExt; use yazi_binding::Permit; use yazi_config::{YAZI, opener::OpenerRule}; use yazi_dds::Pubsub; use yazi_fs::{File, FilesOp, Splatter, max_common_root, path::skip_url, provider::{FileBuilder, Provider, local::{Gate, Local}}}; use yazi_macro::{err, succ}; use yazi_parser::VoidOpt; use yazi_proxy::{AppProxy, NotifyProxy, TasksProxy}; use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_term::YIELD_TO_SUBPROCESS; use yazi_tty::TTY; use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; pub struct BulkRename; impl Actor for BulkRename { type Options = VoidOpt; const NAME: &str = "bulk_rename"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let Some(opener) = Self::opener() else { succ!(NotifyProxy::push_warn("Bulk rename", "No text opener found")); }; let selected: Vec<_> = cx.tab().selected_or_hovered().cloned().collect(); if selected.is_empty() { succ!(NotifyProxy::push_warn("Bulk rename", "No files selected")); } let root = max_common_root(&selected); let old: Vec<_> = selected.iter().enumerate().map(|(i, u)| Tuple::new(i, skip_url(u, root))).collect(); let cwd = cx.cwd().clone(); tokio::spawn(async move { let tmp = YAZI.preview.tmpfile("bulk"); Gate::default() .write(true) .create_new(true) .open(&tmp) .await? .write_all(old.join(Strand::Utf8("\n")).encoded_bytes()) .await?; defer! { let tmp = tmp.clone(); tokio::spawn(async move { Local::regular(&tmp).remove_file().await }); } TasksProxy::process_exec( cwd.into(), Splatter::new(&[UrlCow::default(), tmp.as_url().into()]).splat(&opener.run), vec![UrlCow::default(), UrlBuf::from(&tmp).into()], opener.block, opener.orphan, ) .await; let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume()); AppProxy::stop().await; let new: Vec<_> = Local::regular(&tmp) .read_to_string() .await? .lines() .take(old.len()) .enumerate() .map(|(i, s)| Tuple::new(i, s)) .collect(); Self::r#do(root, old, new, selected).await }); succ!(); } } impl BulkRename { async fn r#do( root: usize, old: Vec, new: Vec, selected: Vec, ) -> Result<()> { terminal_clear(TTY.writer())?; if old.len() != new.len() { #[rustfmt::skip] let s = format!("Number of new and old file names mismatch (New: {}, Old: {}).\nPress to exit...", new.len(), old.len()); execute!(TTY.writer(), Print(s))?; TTY.reader().read_exact(&mut [0])?; return Ok(()); } let (old, new) = old.into_iter().zip(new).filter(|(o, n)| o != n).unzip(); let todo = Self::prioritized_paths(old, new); if todo.is_empty() { return Ok(()); } { let mut w = TTY.lockout(); for (old, new) in &todo { writeln!(w, "{} -> {}", old.display(), new.display())?; } write!(w, "Continue to rename? (y/N): ")?; w.flush()?; } let mut buf = [0; 10]; _ = TTY.reader().read(&mut buf)?; if buf[0] != b'y' && buf[0] != b'Y' { return Ok(()); } let permit = WATCHER.acquire().await.unwrap(); let (mut failed, mut succeeded) = (Vec::new(), HashMap::with_capacity(todo.len())); for (o, n) in todo { let (Ok(old), Ok(new)) = (Self::replace_url(&selected[o.0], root, &o), Self::replace_url(&selected[n.0], root, &n)) else { failed.push((o, n, anyhow!("Invalid new or old file name"))); continue; }; if maybe_exists(&new).await && !provider::must_identical(&old, &new).await { failed.push((o, n, anyhow!("Destination already exists"))); } else if let Err(e) = provider::rename(&old, &new).await { failed.push((o, n, e.into())); } else if let Ok(f) = File::new(new).await { succeeded.insert(old, f); } else { failed.push((o, n, anyhow!("Failed to retrieve file info"))); } } if !succeeded.is_empty() { let it = succeeded.iter().map(|(o, n)| (o.as_url(), n.url.as_url())); err!(Pubsub::pub_after_bulk(it)); FilesOp::rename(succeeded); } drop(permit); if !failed.is_empty() { Self::output_failed(failed).await?; } Ok(()) } fn opener() -> Option<&'static OpenerRule> { YAZI.opener.block(YAZI.open.all(Path::new("bulk-rename.txt"), "text/plain")) } fn replace_url(url: &UrlBuf, take: usize, rep: &StrandBuf) -> Result { Ok(url.try_replace(take, PathDyn::with(url.kind(), rep)?)?.into_owned()) } async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> { let mut stdout = TTY.lockout(); terminal_clear(&mut *stdout)?; writeln!(stdout, "Failed to rename:")?; for (old, new, err) in failed { writeln!(stdout, "{} -> {}: {err}", old.display(), new.display())?; } writeln!(stdout, "\nPress ENTER to exit")?; stdout.flush()?; TTY.reader().read_exact(&mut [0])?; Ok(()) } fn prioritized_paths(old: Vec, new: Vec) -> Vec<(Tuple, Tuple)> { let orders: HashMap<_, _> = old.iter().enumerate().map(|(i, t)| (t, i)).collect(); let mut incomes: HashMap<_, _> = old.iter().map(|t| (t, false)).collect(); let mut todos: HashMap<_, _> = old .iter() .zip(new) .map(|(o, n)| { incomes.get_mut(&n).map(|b| *b = true); (o, n) }) .collect(); let mut sorted = Vec::with_capacity(old.len()); while !todos.is_empty() { // Paths that are non-incomes and don't need to be prioritized in this round let mut outcomes: Vec<_> = incomes.iter().filter(|&(_, b)| !b).map(|(&t, _)| t).collect(); outcomes.sort_unstable_by(|a, b| orders[b].cmp(&orders[a])); // If there're no outcomes, it means there are cycles in the renaming if outcomes.is_empty() { let mut remain: Vec<_> = todos.into_iter().map(|(o, n)| (o.clone(), n)).collect(); remain.sort_unstable_by(|(a, _), (b, _)| orders[a].cmp(&orders[b])); sorted.reverse(); sorted.extend(remain); return sorted; } for old in outcomes { let Some(new) = todos.remove(old) else { unreachable!() }; incomes.remove(&old); incomes.get_mut(&new).map(|b| *b = false); sorted.push((old.clone(), new)); } } sorted.reverse(); sorted } } // --- Tuple #[derive(Clone, Debug)] struct Tuple(usize, StrandBuf); impl Deref for Tuple { type Target = StrandBuf; fn deref(&self) -> &Self::Target { &self.1 } } impl PartialEq for Tuple { fn eq(&self, other: &Self) -> bool { self.1 == other.1 } } impl Eq for Tuple {} impl Hash for Tuple { fn hash(&self, state: &mut H) { self.1.hash(state); } } impl AsStrand for &Tuple { fn as_strand(&self) -> Strand<'_> { self.1.as_strand() } } impl Tuple { fn new(index: usize, inner: impl Into) -> Self { Self(index, inner.into()) } } // --- Tests #[cfg(test)] mod tests { use super::*; #[test] fn test_sort() { fn cmp(input: &[(&str, &str)], expected: &[(&str, &str)]) { let sorted = BulkRename::prioritized_paths( input.iter().map(|&(o, _)| Tuple::new(0, o)).collect(), input.iter().map(|&(_, n)| Tuple::new(0, n)).collect(), ); let sorted: Vec<_> = sorted.iter().map(|(o, n)| (o.to_str().unwrap(), n.to_str().unwrap())).collect(); assert_eq!(sorted, expected); } #[rustfmt::skip] cmp( &[("2", "3"), ("1", "2"), ("3", "4")], &[("3", "4"), ("2", "3"), ("1", "2")] ); #[rustfmt::skip] cmp( &[("1", "3"), ("2", "3"), ("3", "4")], &[("3", "4"), ("1", "3"), ("2", "3")] ); #[rustfmt::skip] cmp( &[("2", "1"), ("1", "2")], &[("2", "1"), ("1", "2")] ); #[rustfmt::skip] cmp( &[("3", "2"), ("2", "1"), ("1", "3"), ("a", "b"), ("b", "c")], &[("b", "c"), ("a", "b"), ("3", "2"), ("2", "1"), ("1", "3")] ); #[rustfmt::skip] cmp( &[("b", "b_"), ("a", "a_"), ("c", "c_")], &[("b", "b_"), ("a", "a_"), ("c", "c_")], ); } } ================================================ FILE: yazi-actor/src/mgr/cd.rs ================================================ use std::{mem, time::Duration}; use anyhow::Result; use tokio::pin; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::popup::InputCfg; use yazi_dds::Pubsub; use yazi_fs::{File, FilesOp, path::{clean_url, expand_url}}; use yazi_macro::{act, err, render, succ}; use yazi_parser::mgr::CdOpt; use yazi_proxy::{CmpProxy, InputProxy, MgrProxy}; use yazi_shared::{Debounce, data::Data, url::{AsUrl, UrlBuf, UrlLike}}; use yazi_vfs::{VfsFile, provider}; use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; pub struct Cd; impl Actor for Cd { type Options = CdOpt; const NAME: &str = "cd"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(mgr:escape_visual, cx)?; if opt.interactive { return Self::cd_interactive(cx); } let tab = cx.tab_mut(); if opt.target == *tab.cwd() { succ!(); } // Take parent to history if let Some(t) = tab.parent.take() { tab.history.insert(t.url.clone(), t); } // Current let rep = tab.history.remove_or(&opt.target); let rep = mem::replace(&mut tab.current, rep); tab.history.insert(rep.url.clone(), rep); // Parent if let Some(parent) = opt.target.parent() { tab.parent = Some(tab.history.remove_or(parent)); } err!(Pubsub::pub_after_cd(tab.id, tab.cwd())); act!(mgr:displace, cx)?; act!(mgr:hidden, cx).ok(); act!(mgr:sort, cx).ok(); act!(mgr:hover, cx)?; act!(mgr:refresh, cx)?; act!(mgr:stash, cx, opt).ok(); act!(app:title, cx).ok(); succ!(render!()); } } impl Cd { fn cd_interactive(cx: &Ctx) -> Result { let input = InputProxy::show(InputCfg::cd(cx.cwd().as_url())); tokio::spawn(async move { let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50)); pin!(rx); while let Some(result) = rx.next().await { match result { InputEvent::Submit(s) => { let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return }; let Ok(url) = provider::absolute(&url).await else { return }; let url = clean_url(url); let Ok(file) = File::new(&url).await else { return }; if file.is_dir() { return MgrProxy::cd(&url); } if let Some(p) = url.parent() { FilesOp::Upserting(p.into(), [(url.urn().into(), file)].into()).emit(); } MgrProxy::reveal(url); } InputEvent::Trigger(before, ticket) => { CmpProxy::trigger(before, ticket); } _ => break, } } }); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/close.rs ================================================ use anyhow::Result; use yazi_macro::act; use yazi_parser::mgr::CloseOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Close; impl Actor for Close { type Options = CloseOpt; const NAME: &str = "close"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { if cx.tabs().len() > 1 { act!(mgr:tab_close, cx, cx.tabs().cursor) } else { act!(mgr:quit, cx, opt.0) } } } ================================================ FILE: yazi-actor/src/mgr/copy.rs ================================================ use anyhow::{Result, bail}; use yazi_macro::{act, succ}; use yazi_parser::mgr::CopyOpt; use yazi_shared::{data::Data, strand::ToStrand, url::UrlLike}; use yazi_widgets::CLIPBOARD; use crate::{Actor, Ctx}; pub struct Copy; impl Actor for Copy { type Options = CopyOpt; const NAME: &str = "copy"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(mgr:escape_visual, cx)?; let mut s = Vec::::new(); let mut it = if opt.hovered { Box::new(cx.hovered().map(|h| &h.url).into_iter()) } else { cx.tab().selected_or_hovered() } .peekable(); while let Some(u) = it.next() { match opt.r#type.as_ref() { // TODO: rename to "url" "path" => { s.extend_from_slice(&opt.separator.transform(&u.to_strand())); } "dirname" => { if let Some(p) = u.parent() { s.extend_from_slice(&opt.separator.transform(&p.to_strand())); } } "filename" => { s.extend_from_slice(&opt.separator.transform(&u.name().unwrap_or_default())); } "name_without_ext" => { s.extend_from_slice(&opt.separator.transform(&u.stem().unwrap_or_default())); } _ => bail!("Unknown copy type: {}", opt.r#type), }; if it.peek().is_some() { s.push(b'\n'); } } // Copy the CWD path regardless even if the directory is empty if s.is_empty() && opt.r#type == "dirname" { s.extend_from_slice(&opt.separator.transform(&cx.cwd().to_strand())); } futures::executor::block_on(CLIPBOARD.set(s)); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/create.rs ================================================ use anyhow::{Result, bail}; use yazi_config::popup::{ConfirmCfg, InputCfg}; use yazi_fs::{File, FilesOp}; use yazi_macro::{ok_or_not_found, succ}; use yazi_parser::mgr::CreateOpt; use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy}; use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_watcher::WATCHER; use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; pub struct Create; impl Actor for Create { type Options = CreateOpt; const NAME: &str = "create"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let cwd = cx.cwd().to_owned(); let mut input = InputProxy::show(InputCfg::create(opt.dir)); tokio::spawn(async move { let Some(InputEvent::Submit(name)) = input.recv().await else { return }; if name.is_empty() { return; } let Ok(new) = cwd.try_join(&name) else { return; }; if !opt.force && maybe_exists(&new).await && !ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await { return; } _ = Self::r#do(new, opt.dir || name.ends_with('/') || name.ends_with('\\')).await; }); succ!(); } } impl Create { async fn r#do(new: UrlBuf, dir: bool) -> Result<()> { let _permit = WATCHER.acquire().await.unwrap(); if dir { provider::create_dir_all(&new).await?; } else if let Ok(real) = provider::casefold(&new).await && let Some((parent, urn)) = real.pair() { ok_or_not_found!(provider::remove_file(&new).await); FilesOp::Deleting(parent.into(), [urn.into()].into()).emit(); provider::create(&new).await?; } else if let Some(parent) = new.parent() { provider::create_dir_all(parent).await.ok(); ok_or_not_found!(provider::remove_file(&new).await); provider::create(&new).await?; } else { bail!("Cannot create file at root"); } if let Ok(real) = provider::casefold(&new).await && let Some((parent, urn)) = real.pair() { let file = File::new(&real).await?; FilesOp::Upserting(parent.into(), [(urn.into(), file)].into()).emit(); MgrProxy::reveal(&real); } Ok(()) } } ================================================ FILE: yazi-actor/src/mgr/displace.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::{VoidOpt, mgr::DisplaceDoOpt}; use yazi_proxy::MgrProxy; use yazi_shared::{data::Data, url::UrlLike}; use yazi_vfs::provider; use crate::{Actor, Ctx}; pub struct Displace; impl Actor for Displace { type Options = VoidOpt; const NAME: &str = "displace"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { if cx.cwd().is_absolute() { succ!(); } let tab = cx.tab().id; let from = cx.cwd().to_owned(); tokio::spawn(async move { MgrProxy::displace_do(tab, DisplaceDoOpt { to: provider::canonicalize(&from).await.map_err(Into::into), from, }); }); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/displace_do.rs ================================================ use anyhow::{Result, bail}; use yazi_fs::FilesOp; use yazi_macro::{act, succ}; use yazi_parser::mgr::{CdSource, DisplaceDoOpt}; use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; pub struct DisplaceDo; impl Actor for DisplaceDo { type Options = DisplaceDoOpt; const NAME: &str = "displace_do"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { if cx.cwd() != opt.from { succ!() } let to = match opt.to { Ok(url) => url, Err(e) => return act!(mgr:update_files, cx, FilesOp::IOErr(opt.from, e)), }; if !to.is_absolute() { bail!("Target URL must be absolute"); } else if let Some(hovered) = cx.hovered() && let Ok(url) = to.try_join(hovered.urn()) { act!(mgr:reveal, cx, (url, CdSource::Displace)) } else { act!(mgr:cd, cx, (to, CdSource::Displace)) } } } ================================================ FILE: yazi-actor/src/mgr/download.rs ================================================ use std::{mem, time::{Duration, Instant}}; use anyhow::Result; use futures::{StreamExt, stream::FuturesUnordered}; use hashbrown::HashSet; use yazi_fs::{File, FsScheme, provider::{Provider, local::Local}}; use yazi_macro::succ; use yazi_parser::mgr::{DownloadOpt, OpenOpt}; use yazi_proxy::MgrProxy; use yazi_shared::{data::Data, url::{UrlCow, UrlLike}}; use yazi_vfs::VfsFile; use crate::{Actor, Ctx}; pub struct Download; impl Actor for Download { type Options = DownloadOpt; const NAME: &str = "download"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let cwd = cx.cwd().clone(); let scheduler = cx.tasks.scheduler.clone(); tokio::spawn(async move { Self::prepare(&opt.urls).await; let mut wg1 = FuturesUnordered::new(); for url in opt.urls { let done = scheduler.file_download(url.to_owned()); wg1.push(async move { (done.future().await, url) }); } let mut wg2 = vec![]; let mut urls = Vec::with_capacity(wg1.len()); let mut files = Vec::with_capacity(wg1.len()); let mut instant = Instant::now(); while let Some((success, url)) = wg1.next().await { if !success { continue; } let Ok(f) = File::new(&url).await else { continue }; urls.push(url); files.push(f); if instant.elapsed() >= Duration::from_secs(1) { wg2.push(scheduler.fetch_mimetype(mem::take(&mut files))); instant = Instant::now(); } } if !files.is_empty() { wg2.push(scheduler.fetch_mimetype(files)); } if futures::future::join_all(wg2).await.into_iter().any(|b| !b) { return; } if opt.open && !urls.is_empty() { MgrProxy::open(OpenOpt { cwd: Some(cwd.into()), targets: urls, interactive: false, hovered: false, }); } }); succ!(); } } impl Download { async fn prepare(urls: &[UrlCow<'_>]) { let roots: HashSet<_> = urls.iter().filter_map(|u| u.scheme().cache()).collect(); for mut root in roots { root.push("%lock"); Local::regular(&root).create_dir_all().await.ok(); } } } ================================================ FILE: yazi-actor/src/mgr/enter.rs ================================================ use anyhow::Result; use yazi_macro::{act, succ}; use yazi_parser::{VoidOpt, mgr::CdSource}; use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; pub struct Enter; impl Actor for Enter { type Options = VoidOpt; const NAME: &str = "enter"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let Some(h) = cx.hovered().filter(|h| h.is_dir()) else { succ!() }; let url = if h.url.is_search() { h.url.to_regular()? } else { h.url.clone() }; act!(mgr:cd, cx, (url, CdSource::Enter)) } } ================================================ FILE: yazi-actor/src/mgr/escape.rs ================================================ use anyhow::{Result, bail}; use yazi_macro::{act, render, render_and, succ}; use yazi_parser::{VoidOpt, mgr::EscapeOpt}; use yazi_proxy::NotifyProxy; use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; pub struct Escape; impl Actor for Escape { type Options = EscapeOpt; const NAME: &str = "escape"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { if opt.is_empty() { _ = act!(mgr:escape_find, cx)? != false || act!(mgr:escape_visual, cx)? != false || act!(mgr:escape_filter, cx)? != false || act!(mgr:escape_select, cx)? != false || act!(mgr:escape_search, cx)? != false; succ!(); } if opt.contains(EscapeOpt::FIND) { act!(mgr:escape_find, cx)?; } if opt.contains(EscapeOpt::VISUAL) { act!(mgr:escape_visual, cx)?; } if opt.contains(EscapeOpt::FILTER) { act!(mgr:escape_filter, cx)?; } if opt.contains(EscapeOpt::SELECT) { act!(mgr:escape_select, cx)?; } if opt.contains(EscapeOpt::SEARCH) { act!(mgr:escape_search, cx)?; } succ!(); } } // --- Find pub struct EscapeFind; impl Actor for EscapeFind { type Options = VoidOpt; const NAME: &str = "escape_find"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { succ!(render_and!(cx.tab_mut().finder.take().is_some())) } } // --- Visual pub struct EscapeVisual; impl Actor for EscapeVisual { type Options = VoidOpt; const NAME: &str = "escape_visual"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let tab = cx.tab_mut(); let select = tab.mode.is_select(); let Some((_, indices)) = tab.mode.take_visual() else { succ!(false) }; render!(); let urls: Vec<_> = indices.into_iter().filter_map(|i| tab.current.files.get(i)).map(|f| &f.url).collect(); if !select { tab.selected.remove_many(urls); } else if urls.len() != tab.selected.add_many(urls) { NotifyProxy::push_warn( "Escape visual mode", "Some files cannot be selected, due to path nesting conflict.", ); bail!("Some files cannot be selected, due to path nesting conflict."); } succ!(true) } } // --- Filter pub struct EscapeFilter; impl Actor for EscapeFilter { type Options = VoidOpt; const NAME: &str = "escape_filter"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { if cx.current_mut().files.filter().is_none() { succ!(false); } act!(mgr:filter_do, cx)?; render!(); succ!(true); } } // --- Select pub struct EscapeSelect; impl Actor for EscapeSelect { type Options = VoidOpt; const NAME: &str = "escape_select"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let tab = cx.tab_mut(); if tab.selected.is_empty() { succ!(false); } tab.selected.clear(); if tab.hovered().is_some_and(|h| h.is_dir()) { act!(mgr:peek, cx, true)?; } render!(); succ!(true); } } // --- Search pub struct EscapeSearch; impl Actor for EscapeSearch { type Options = VoidOpt; const NAME: &str = "escape_search"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let b = cx.cwd().is_search(); act!(mgr:search_stop, cx)?; succ!(render_and!(b)); } } ================================================ FILE: yazi-actor/src/mgr/filter.rs ================================================ use std::time::Duration; use anyhow::Result; use tokio::pin; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::popup::InputCfg; use yazi_macro::succ; use yazi_parser::mgr::FilterOpt; use yazi_proxy::{InputProxy, MgrProxy}; use yazi_shared::{Debounce, data::Data}; use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; pub struct Filter; impl Actor for Filter { type Options = FilterOpt; const NAME: &str = "filter"; fn act(_: &mut Ctx, opt: Self::Options) -> Result { let input = InputProxy::show(InputCfg::filter()); tokio::spawn(async move { let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50)); pin!(rx); while let Some(event) = rx.next().await { let done = event.is_submit(); let (InputEvent::Submit(s) | InputEvent::Type(s)) = event else { continue }; MgrProxy::filter_do(FilterOpt { query: s.into(), case: opt.case, done }); } }); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/filter_do.rs ================================================ use anyhow::Result; use yazi_fs::Filter; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::FilterOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct FilterDo; impl Actor for FilterDo { type Options = FilterOpt; const NAME: &str = "filter_do"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) }; let hovered = cx.hovered().map(|f| f.urn().into()); cx.current_mut().files.set_filter(filter); if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { act!(mgr:hover, cx, hovered)?; act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; } if opt.done { act!(mgr:update_paged, cx)?; } succ!(render!()); } } ================================================ FILE: yazi-actor/src/mgr/find.rs ================================================ use std::time::Duration; use anyhow::Result; use tokio::pin; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::popup::InputCfg; use yazi_macro::succ; use yazi_parser::mgr::{FindDoOpt, FindOpt}; use yazi_proxy::{InputProxy, MgrProxy}; use yazi_shared::{Debounce, data::Data}; use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; pub struct Find; impl Actor for Find { type Options = FindOpt; const NAME: &str = "find"; fn act(_: &mut Ctx, opt: Self::Options) -> Result { let input = InputProxy::show(InputCfg::find(opt.prev)); tokio::spawn(async move { let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50)); pin!(rx); while let Some(InputEvent::Submit(s) | InputEvent::Type(s)) = rx.next().await { MgrProxy::find_do(FindDoOpt { query: s.into(), prev: opt.prev, case: opt.case }); } }); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/find_arrow.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::FindArrowOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct FindArrow; impl Actor for FindArrow { type Options = FindArrowOpt; const NAME: &str = "find_arrow"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let tab = cx.tab_mut(); let Some(finder) = &mut tab.finder else { succ!() }; render!(finder.catchup(&tab.current)); let offset = if opt.prev { finder.prev(&tab.current.files, tab.current.cursor, false) } else { finder.next(&tab.current.files, tab.current.cursor, false) }; if let Some(offset) = offset { act!(mgr:arrow, cx, offset)?; } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/find_do.rs ================================================ use anyhow::Result; use yazi_core::tab::Finder; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::FindDoOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct FindDo; impl Actor for FindDo { type Options = FindDoOpt; const NAME: &str = "find_do"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { if opt.query.is_empty() { return act!(mgr:escape_find, cx); } let finder = Finder::new(&opt.query, opt.case)?; if matches!(&cx.tab().finder, Some(f) if f.filter == finder.filter) { succ!(); } let step = if opt.prev { finder.prev(&cx.current().files, cx.current().cursor, true) } else { finder.next(&cx.current().files, cx.current().cursor, true) }; if let Some(step) = step { act!(mgr:arrow, cx, step)?; } cx.tab_mut().finder = Some(finder); succ!(render!()); } } ================================================ FILE: yazi-actor/src/mgr/follow.rs ================================================ use anyhow::Result; use yazi_fs::path::clean_url; use yazi_macro::{act, succ}; use yazi_parser::{VoidOpt, mgr::CdSource}; use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; pub struct Follow; impl Actor for Follow { type Options = VoidOpt; const NAME: &str = "follow"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let Some(file) = cx.hovered() else { succ!() }; let Some(link_to) = &file.link_to else { succ!() }; let Some(parent) = file.url.parent() else { succ!() }; let Ok(joined) = parent.try_join(link_to) else { succ!() }; act!(mgr:reveal, cx, (clean_url(joined), CdSource::Follow)) } } ================================================ FILE: yazi-actor/src/mgr/forward.rs ================================================ use anyhow::Result; use yazi_macro::{act, succ}; use yazi_parser::{VoidOpt, mgr::CdSource}; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Forward; impl Actor for Forward { type Options = VoidOpt; const NAME: &str = "forward"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { if let Some(u) = cx.tab_mut().backstack.shift_forward().cloned() { act!(mgr:cd, cx, (u, CdSource::Forward))?; } succ!() } } ================================================ FILE: yazi-actor/src/mgr/hardlink.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::mgr::HardlinkOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Hardlink; impl Actor for Hardlink { type Options = HardlinkOpt; const NAME: &str = "hardlink"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let mgr = &mut cx.core.mgr; let tab = &mgr.tabs[cx.tab]; if !mgr.yanked.cut { cx.core.tasks.file_hardlink(&mgr.yanked, tab.cwd(), opt.force, opt.follow); } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/hidden.rs ================================================ use anyhow::Result; use yazi_core::tab::Folder; use yazi_dds::spark::SparkKind; use yazi_fs::FolderStage; use yazi_macro::{act, render, render_and, succ}; use yazi_parser::mgr::HiddenOpt; use yazi_shared::{Source, data::Data}; use crate::{Actor, Ctx}; pub struct Hidden; impl Actor for Hidden { type Options = HiddenOpt; const NAME: &str = "hidden"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let state = opt.state.bool(cx.tab().pref.show_hidden); cx.tab_mut().pref.show_hidden = state; let hovered = cx.hovered().map(|f| f.urn().to_owned()); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); false } else { f.files.set_show_hidden(state); render_and!(f.files.catchup_revision()) } }; // Apply to CWD and parent if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply)) && (a | b) { act!(mgr:hover, cx)?; act!(mgr:update_paged, cx)?; } // Apply to hovered if let Some(h) = cx.hovered_folder_mut() && apply(h) { render!(h.repos(None)); act!(mgr:peek, cx, true)?; } else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; } succ!() } fn hook(cx: &Ctx, _: &Self::Options) -> Option { match cx.source() { Source::Ind => Some(SparkKind::IndHidden), Source::Key => Some(SparkKind::KeyHidden), _ => None, } } } ================================================ FILE: yazi-actor/src/mgr/hover.rs ================================================ use anyhow::Result; use yazi_dds::Pubsub; use yazi_macro::{err, render, succ, tab}; use yazi_parser::mgr::HoverOpt; use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; pub struct Hover; impl Actor for Hover { type Options = HoverOpt; const NAME: &str = "hover"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let tab = tab!(cx); // Parent should always track CWD if let Some(p) = &mut tab.parent { render!(p.repos(tab.current.url.try_strip_prefix(&p.url).ok())); } // Repos CWD tab.current.repos(opt.urn.as_ref().map(Into::into)); // Turn on tracing if let (Some(h), Some(u)) = (tab.hovered(), opt.urn) && h.urn() == u { // `hover(Some)` occurs after user actions, such as create, rename, reveal, etc. // At this point, it's intuitive to track the location of the file regardless. tab.current.trace = Some(u.clone()); } // Publish through DDS err!(Pubsub::pub_after_hover(tab.id, tab.hovered().map(|h| &h.url))); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/leave.rs ================================================ use anyhow::Result; use yazi_macro::{act, succ}; use yazi_parser::{VoidOpt, mgr::CdSource}; use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; pub struct Leave; impl Actor for Leave { type Options = VoidOpt; const NAME: &str = "leave"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let url = cx .hovered() .and_then(|h| h.url.parent()) .filter(|u| u != cx.cwd()) .or_else(|| cx.cwd().parent()); let Some(mut url) = url else { succ!() }; if url.is_search() { url = url.as_regular()?; } act!(mgr:cd, cx, (url, CdSource::Leave)) } } ================================================ FILE: yazi-actor/src/mgr/linemode.rs ================================================ use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::mgr::LinemodeOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Linemode; impl Actor for Linemode { type Options = LinemodeOpt; const NAME: &str = "linemode"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let tab = cx.tab_mut(); if opt.new != tab.pref.linemode { tab.pref.linemode = opt.new.into_owned(); render!(); } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/link.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::mgr::LinkOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Link; impl Actor for Link { type Options = LinkOpt; const NAME: &str = "link"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let mgr = &mut cx.core.mgr; let tab = &mgr.tabs[cx.tab]; if !mgr.yanked.cut { cx.core.tasks.file_link(&mgr.yanked, tab.cwd(), opt.relative, opt.force); } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/mod.rs ================================================ yazi_macro::mod_flat!( arrow back bulk_rename cd close copy create displace displace_do download enter escape filter filter_do find find_arrow find_do follow forward hardlink hidden hover leave linemode link open open_do paste peek quit refresh remove rename reveal search seek shell sort spot stash suspend tab_close tab_create tab_rename tab_swap tab_switch toggle toggle_all unyank update_files update_mimes update_paged update_peeked update_spotted update_yanked upload visual_mode watch yank ); ================================================ FILE: yazi-actor/src/mgr/open.rs ================================================ use anyhow::Result; use yazi_boot::ARGS; use yazi_fs::File; use yazi_macro::{act, succ}; use yazi_parser::mgr::{OpenDoOpt, OpenOpt}; use yazi_proxy::MgrProxy; use yazi_shared::data::Data; use yazi_vfs::VfsFile; use crate::{Actor, Ctx, mgr::Quit}; pub struct Open; impl Actor for Open { type Options = OpenOpt; const NAME: &str = "open"; fn act(cx: &mut Ctx, mut opt: Self::Options) -> Result { if !opt.interactive && ARGS.chooser_file.is_some() { succ!(if !opt.targets.is_empty() { Quit::with_selected(opt.targets) } else if opt.hovered { Quit::with_selected(cx.hovered().map(|h| &h.url)) } else { act!(mgr:escape_visual, cx)?; Quit::with_selected(cx.tab().selected_or_hovered()) }); } if opt.targets.is_empty() { opt.targets = if opt.hovered { cx.hovered().map(|h| vec![h.url.clone().into()]).unwrap_or_default() } else { act!(mgr:escape_visual, cx)?; cx.tab().selected_or_hovered().cloned().map(Into::into).collect() }; } if opt.targets.is_empty() { succ!(); } let todo: Vec<_> = opt .targets .iter() .enumerate() .filter(|&(_, u)| !cx.mgr.mimetype.contains(u)) .map(|(i, _)| i) .collect(); let cwd = opt.cwd.unwrap_or_else(|| cx.cwd().clone().into()); if todo.is_empty() { return act!(mgr:open_do, cx, OpenDoOpt { cwd, targets: opt.targets, interactive: opt.interactive }); } let scheduler = cx.tasks.scheduler.clone(); tokio::spawn(async move { let mut files = Vec::with_capacity(todo.len()); for i in todo { if let Ok(f) = File::new(&opt.targets[i]).await { files.push(f); } } if scheduler.fetch_mimetype(files).await { MgrProxy::open_do(OpenDoOpt { cwd, targets: opt.targets, interactive: opt.interactive }); } }); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/open_do.rs ================================================ use anyhow::Result; use hashbrown::HashMap; use yazi_config::{YAZI, popup::PickCfg}; use yazi_macro::succ; use yazi_parser::{mgr::OpenDoOpt, tasks::ProcessOpenOpt}; use yazi_proxy::{PickProxy, TasksProxy}; use yazi_shared::{data::Data, url::UrlCow}; use crate::{Actor, Ctx}; pub struct OpenDo; impl Actor for OpenDo { type Options = OpenDoOpt; const NAME: &str = "open_do"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let targets: Vec<_> = opt .targets .into_iter() .map(|u| { let m = cx.mgr.mimetype.get(&u).unwrap_or_default(); (u, m) }) .filter(|(_, m)| !m.is_empty()) .collect(); if targets.is_empty() { succ!(); } else if !opt.interactive { succ!(Self::match_and_open(cx, opt.cwd, targets)); } let openers: Vec<_> = YAZI.opener.all(YAZI.open.common(&targets).into_iter()).collect(); if openers.is_empty() { succ!(); } let pick = PickProxy::show(PickCfg::open(openers.iter().map(|o| o.desc()).collect())); let urls: Vec<_> = [UrlCow::default()].into_iter().chain(targets.into_iter().map(|(u, _)| u)).collect(); tokio::spawn(async move { if let Some(choice) = pick.await { TasksProxy::open_shell_compat(ProcessOpenOpt { cwd: opt.cwd, cmd: openers[choice].run.clone().into(), args: urls, block: openers[choice].block, orphan: openers[choice].orphan, done: None, spread: openers[choice].spread, }); } }); succ!(); } } impl OpenDo { // TODO: remove fn match_and_open(cx: &Ctx, cwd: UrlCow<'static>, targets: Vec<(UrlCow<'static>, &str)>) { let mut openers = HashMap::new(); for (url, mime) in targets { if let Some(opener) = YAZI.opener.first(YAZI.open.all(&url, mime)) { openers.entry(opener).or_insert_with(|| vec![UrlCow::default()]).push(url); } } for (opener, args) in openers { cx.tasks.open_shell_compat(ProcessOpenOpt { cwd: cwd.clone(), cmd: opener.run.clone().into(), args, block: opener.block, orphan: opener.orphan, done: None, spread: opener.spread, }); } } } ================================================ FILE: yazi-actor/src/mgr/paste.rs ================================================ use anyhow::Result; use yazi_macro::{act, succ}; use yazi_parser::mgr::PasteOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Paste; impl Actor for Paste { type Options = PasteOpt; const NAME: &str = "paste"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let mgr = &mut cx.core.mgr; let tab = &mgr.tabs[cx.tab]; let dest = tab.cwd(); if mgr.yanked.cut { cx.core.tasks.file_cut(&mgr.yanked, dest, opt.force); mgr.tabs.iter_mut().for_each(|t| _ = t.selected.remove_many(mgr.yanked.iter())); act!(mgr:unyank, cx) } else { succ!(cx.core.tasks.file_copy(&mgr.yanked, dest, opt.force, opt.follow)); } } } ================================================ FILE: yazi-actor/src/mgr/peek.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::mgr::PeekOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Peek; impl Actor for Peek { type Options = PeekOpt; const NAME: &str = "peek"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let Some(hovered) = cx.hovered().cloned() else { succ!(cx.tab_mut().preview.reset()); }; if cx.term.is_none() { succ!(cx.tab_mut().preview.reset_image()); } let mime = cx.mgr.mimetype.owned(&hovered.url).unwrap_or_default(); let folder = cx.tab().hovered_folder().map(|f| (f.offset, f.cha)); if !cx.tab().preview.same_url(&hovered.url) { cx.tab_mut().preview.skip = folder.map(|f| f.0).unwrap_or_default(); } if !cx.tab().preview.same_file(&hovered, &mime) { cx.tab_mut().preview.reset(); } if !cx.tab().preview.same_folder(&hovered.url) { cx.tab_mut().preview.folder_lock = None; } if matches!(opt.only_if, Some(u) if u != hovered.url) { succ!(); } if let Some(skip) = opt.skip { let preview = &mut cx.tab_mut().preview; if opt.upper_bound { preview.skip = preview.skip.min(skip); } else { preview.skip = skip; } } if hovered.is_dir() { cx.tab_mut().preview.go_folder(hovered, folder.map(|(_, cha)| cha), mime, opt.force); } else { cx.tab_mut().preview.go(hovered, mime, opt.force); } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/quit.rs ================================================ use std::time::Duration; use anyhow::Result; use tokio::{select, time}; use yazi_config::popup::ConfirmCfg; use yazi_dds::spark::SparkKind; use yazi_macro::{act, succ}; use yazi_parser::app::QuitOpt; use yazi_proxy::{AppProxy, ConfirmProxy}; use yazi_shared::{data::Data, strand::{Strand, StrandLike, ToStrandJoin}, url::AsUrl}; use crate::{Actor, Ctx}; pub struct Quit; impl Actor for Quit { type Options = QuitOpt; const NAME: &str = "quit"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let ongoing = cx.tasks().ongoing().clone(); let (left, left_names) = { let ongoing = ongoing.lock(); (ongoing.len(), ongoing.values().take(11).map(|t| t.name.clone()).collect()) }; if left == 0 { return act!(app:quit, cx, opt); } tokio::spawn(async move { let mut i = 0; let token = ConfirmProxy::show_sync(ConfirmCfg::quit(left, left_names)); loop { select! { _ = time::sleep(Duration::from_millis(50)) => { i += 1; if i > 40 { break } else if ongoing.lock().is_empty() { AppProxy::quit(opt); return; } } b = token.future() => { if b { AppProxy::quit(opt); } return; } } } if token.future().await { AppProxy::quit(opt); } }); succ!(); } fn hook(cx: &Ctx, _opt: &Self::Options) -> Option { Some(SparkKind::KeyQuit).filter(|_| cx.source().is_key()) } } impl Quit { pub(super) fn with_selected(selected: I) where I: IntoIterator, I::Item: AsUrl, { let paths = selected.into_iter().join(Strand::Utf8("\n")); if !paths.is_empty() { AppProxy::quit(QuitOpt { selected: Some(paths), ..Default::default() }); } } } ================================================ FILE: yazi-actor/src/mgr/refresh.rs ================================================ use anyhow::Result; use yazi_core::tab::Folder; use yazi_fs::{CWD, Files, FilesOp, cha::Cha}; use yazi_macro::{act, succ}; use yazi_parser::VoidOpt; use yazi_proxy::MgrProxy; use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; use yazi_vfs::{VfsFiles, VfsFilesOp}; use crate::{Actor, Ctx}; pub struct Refresh; impl Actor for Refresh { type Options = VoidOpt; const NAME: &str = "refresh"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { CWD.set(cx.cwd(), Self::cwd_changed); if let Some(p) = cx.parent() { Self::trigger_dirs(&[cx.current(), p]); } else { Self::trigger_dirs(&[cx.current()]); } act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; act!(mgr:update_paged, cx)?; cx.tasks().prework_sorted(&cx.current().files); succ!(); } } impl Refresh { fn cwd_changed() { if CWD.load().kind().is_virtual() { MgrProxy::watch(); } } // TODO: performance improvement fn trigger_dirs(folders: &[&Folder]) { async fn go(dir: UrlBuf, cha: Cha) { let Some(cha) = Files::assert_stale(&dir, cha).await else { return }; match Files::from_dir_bulk(&dir).await { Ok(files) => FilesOp::Full(dir, files, cha).emit(), Err(e) => FilesOp::issue_error(&dir, e).await, } } let futs: Vec<_> = folders .iter() .filter(|&f| f.url.is_absolute() && f.url.is_internal()) .map(|&f| go(f.url.clone(), f.cha)) .collect(); if !futs.is_empty() { tokio::spawn(futures::future::join_all(futs)); } } } ================================================ FILE: yazi-actor/src/mgr/remove.rs ================================================ use anyhow::Result; use yazi_config::popup::ConfirmCfg; use yazi_macro::{act, succ}; use yazi_parser::mgr::RemoveOpt; use yazi_proxy::{ConfirmProxy, MgrProxy}; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Remove; impl Actor for Remove { type Options = RemoveOpt; const NAME: &str = "remove"; fn act(cx: &mut Ctx, mut opt: Self::Options) -> Result { act!(mgr:escape_visual, cx)?; opt.targets = if opt.hovered { cx.hovered().map_or(vec![], |h| vec![h.url.clone()]) } else { cx.tab().selected_or_hovered().cloned().collect() }; if opt.targets.is_empty() { succ!(); } else if opt.force { return act!(mgr:remove_do, cx, opt); } let confirm = ConfirmProxy::show(if opt.permanently { ConfirmCfg::delete(&opt.targets) } else { ConfirmCfg::trash(&opt.targets) }); tokio::spawn(async move { if confirm.await { MgrProxy::remove_do(opt.targets, opt.permanently); } }); succ!(); } } // --- Do pub struct RemoveDo; impl Actor for RemoveDo { type Options = RemoveOpt; const NAME: &str = "remove_do"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let mgr = &mut cx.mgr; mgr.tabs.iter_mut().for_each(|t| { t.selected.remove_many(&opt.targets); }); for u in &opt.targets { mgr.yanked.remove(u); } mgr.yanked.catchup_revision(false); cx.tasks.file_remove(opt.targets, opt.permanently); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/rename.rs ================================================ use anyhow::Result; use yazi_config::popup::{ConfirmCfg, InputCfg}; use yazi_dds::Pubsub; use yazi_fs::{File, FilesOp}; use yazi_macro::{act, err, ok_or_not_found, succ}; use yazi_parser::mgr::RenameOpt; use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy}; use yazi_shared::{Id, data::Data, url::{UrlBuf, UrlLike}}; use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_watcher::WATCHER; use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; pub struct Rename; impl Actor for Rename { type Options = RenameOpt; const NAME: &str = "rename"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(mgr:escape_visual, cx)?; if !opt.hovered && !cx.tab().selected.is_empty() { return act!(mgr:bulk_rename, cx); } let Some(hovered) = cx.hovered() else { succ!() }; let name = Self::empty_url_part(&hovered.url, &opt.empty); let cursor = match opt.cursor.as_ref() { "start" => Some(0), "before_ext" => name .chars() .rev() .position(|c| c == '.') .filter(|_| hovered.is_file()) .map(|i| name.chars().count() - i - 1) .filter(|&i| i != 0), _ => None, }; let (tab, old) = (cx.tab().id, hovered.url_owned()); let mut input = InputProxy::show(InputCfg::rename().with_value(name).with_cursor(cursor)); tokio::spawn(async move { let Some(InputEvent::Submit(name)) = input.recv().await else { return }; if name.is_empty() { return; } let Some(Ok(new)) = old.parent().map(|u| u.try_join(name)) else { return; }; if opt.force || !maybe_exists(&new).await || provider::must_identical(&old, &new).await { Self::r#do(tab, old, new).await.ok(); } else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await { Self::r#do(tab, old, new).await.ok(); } }); succ!(); } } impl Rename { async fn r#do(tab: Id, old: UrlBuf, new: UrlBuf) -> Result<()> { let Some((old_p, old_n)) = old.pair() else { return Ok(()) }; let Some(_) = new.pair() else { return Ok(()) }; let _permit = WATCHER.acquire().await.unwrap(); let overwritten = provider::casefold(&new).await; provider::rename(&old, &new).await?; if let Ok(u) = overwritten && u != new && let Some((parent, urn)) = u.pair() { ok_or_not_found!(provider::rename(&u, &new).await); FilesOp::Deleting(parent.to_owned(), [urn.into()].into()).emit(); } let new = provider::casefold(&new).await?; let Some((new_p, new_n)) = new.pair() else { return Ok(()) }; let file = File::new(&new).await?; if new_p == old_p { FilesOp::Upserting(old_p.into(), [(old_n.into(), file)].into()).emit(); } else { FilesOp::Deleting(old_p.into(), [old_n.into()].into()).emit(); FilesOp::Upserting(new_p.into(), [(new_n.into(), file)].into()).emit(); } MgrProxy::reveal(&new); err!(Pubsub::pub_after_rename(tab, &old, &new)); Ok(()) } fn empty_url_part(url: &UrlBuf, by: &str) -> String { if by == "all" { return String::new(); } let ext = url.ext(); match by { "stem" => ext.map_or_else(String::new, |s| format!(".{}", s.to_string_lossy())), "ext" if ext.is_some() => format!("{}.", url.stem().unwrap().to_string_lossy()), "dot_ext" if ext.is_some() => url.stem().unwrap().to_string_lossy().into_owned(), _ => url.name().unwrap_or_default().to_string_lossy().into_owned(), } } } ================================================ FILE: yazi-actor/src/mgr/reveal.rs ================================================ use anyhow::Result; use yazi_fs::{File, FilesOp}; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::RevealOpt; use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; pub struct Reveal; impl Actor for Reveal { type Options = RevealOpt; const NAME: &str = "reveal"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let Some((parent, child)) = opt.target.pair() else { succ!() }; // Cd to the parent directory act!(mgr:cd, cx, (parent, opt.source))?; // Try to hover over the child file let tab = cx.tab_mut(); render!(tab.current.hover(child)); // If the child is not hovered, which means it doesn't exist, // create a dummy file if !opt.no_dummy && tab.hovered().is_none_or(|f| child != f.urn()) { let op = FilesOp::Creating(parent.into(), vec![File::from_dummy(&opt.target, None)]); tab.current.update_pub(tab.id, op); } // Now, we can safely hover over the target act!(mgr:hover, cx, Some(child.into()))?; act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; succ!(); } } ================================================ FILE: yazi-actor/src/mgr/search.rs ================================================ use std::{borrow::Cow, time::Duration}; use anyhow::Result; use tokio::pin; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::popup::InputCfg; use yazi_fs::{FilesOp, cha::Cha}; use yazi_macro::{act, succ}; use yazi_parser::{VoidOpt, mgr::{CdSource, SearchOpt, SearchOptVia}}; use yazi_plugin::external; use yazi_proxy::{InputProxy, MgrProxy, NotifyProxy}; use yazi_shared::{data::Data, url::{AsUrl, UrlLike}}; use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; pub struct Search; impl Actor for Search { type Options = SearchOpt; const NAME: &str = "search"; fn act(cx: &mut Ctx, mut opt: Self::Options) -> Result { if let Some(handle) = cx.tab_mut().search.take() { handle.abort(); } let mut input = InputProxy::show(InputCfg::search(opt.via.into_str()).with_value(&*opt.subject)); tokio::spawn(async move { if let Some(InputEvent::Submit(subject)) = input.recv().await { opt.subject = Cow::Owned(subject); MgrProxy::search_do(opt); } }); succ!(); } } // --- Do pub struct SearchDo; impl Actor for SearchDo { type Options = SearchOpt; const NAME: &str = "search_do"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let tab = cx.tab_mut(); if let Some(handle) = tab.search.take() { handle.abort(); } let hidden = tab.pref.show_hidden; let r#in = opt.r#in.as_ref().map_or_else(|| tab.cwd().as_url(), |u| u.as_url()); let Ok(cwd) = r#in.to_search(&opt.subject) else { succ!(NotifyProxy::push_warn("Search", "Only local filesystem searches are supported")); }; tab.search = Some(tokio::spawn(async move { let rx = match opt.via { SearchOptVia::Rg => external::rg(external::RgOpt { cwd: cwd.clone(), hidden, subject: opt.subject.into_owned(), args: opt.args, }), SearchOptVia::Rga => external::rga(external::RgaOpt { cwd: cwd.clone(), hidden, subject: opt.subject.into_owned(), args: opt.args, }), SearchOptVia::Fd => external::fd(external::FdOpt { cwd: cwd.clone(), hidden, subject: opt.subject.into_owned(), args: opt.args, }), }?; let rx = UnboundedReceiverStream::new(rx).chunks_timeout(5000, Duration::from_millis(500)); pin!(rx); let ((), ticket) = (MgrProxy::cd(&cwd), FilesOp::prepare(&cwd)); while let Some(chunk) = rx.next().await { FilesOp::Part(cwd.clone(), chunk, ticket).emit(); } FilesOp::Done(cwd, Cha::default(), ticket).emit(); Ok(()) })); succ!(); } } // --- Stop pub struct SearchStop; impl Actor for SearchStop { type Options = VoidOpt; const NAME: &str = "search_stop"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let tab = cx.tab_mut(); if let Some(handle) = tab.search.take() { handle.abort(); } if !tab.cwd().is_search() { succ!(); } if let Some(u) = tab.backstack.current().cloned() { act!(mgr:cd, cx, (u, CdSource::Escape)) } else { act!(mgr:cd, cx, (tab.cwd().to_regular()?, CdSource::Escape)) } } } ================================================ FILE: yazi-actor/src/mgr/seek.rs ================================================ use anyhow::Result; use yazi_config::YAZI; use yazi_macro::succ; use yazi_parser::mgr::SeekOpt; use yazi_plugin::isolate; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Seek; impl Actor for Seek { type Options = SeekOpt; const NAME: &str = "seek"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let Some(hovered) = cx.hovered() else { succ!(cx.tab_mut().preview.reset()); }; let Some(mime) = cx.mgr.mimetype.get(&hovered.url) else { succ!(cx.tab_mut().preview.reset()); }; let Some(previewer) = YAZI.plugin.previewer(hovered, mime) else { succ!(cx.tab_mut().preview.reset()); }; isolate::seek_sync(&previewer.run, hovered.clone(), opt.units); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/shell.rs ================================================ use std::borrow::Cow; use anyhow::Result; use yazi_config::popup::InputCfg; use yazi_macro::{act, succ}; use yazi_parser::{mgr::ShellOpt, tasks::ProcessOpenOpt}; use yazi_proxy::{InputProxy, TasksProxy}; use yazi_shared::data::Data; use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; pub struct Shell; impl Actor for Shell { type Options = ShellOpt; const NAME: &str = "shell"; fn act(cx: &mut Ctx, mut opt: Self::Options) -> Result { act!(mgr:escape_visual, cx)?; let cwd = opt.cwd.take().unwrap_or(cx.cwd().into()).into_owned(); let selected: Vec<_> = cx.tab().hovered_and_selected().cloned().map(Into::into).collect(); let input = opt.interactive.then(|| { InputProxy::show(InputCfg::shell(opt.block).with_value(&*opt.run).with_cursor(opt.cursor)) }); tokio::spawn(async move { if let Some(mut rx) = input { match rx.recv().await { Some(InputEvent::Submit(e)) => opt.run = Cow::Owned(e), _ => return, } } if opt.run.is_empty() { return; } TasksProxy::open_shell_compat(ProcessOpenOpt { cwd: cwd.into(), cmd: opt.run.to_string().into(), args: selected, block: opt.block, orphan: opt.orphan, done: None, spread: true, }); }); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/sort.rs ================================================ use anyhow::Result; use yazi_core::tab::Folder; use yazi_dds::spark::SparkKind; use yazi_fs::{FilesSorter, FolderStage}; use yazi_macro::{act, render, render_and, succ}; use yazi_parser::mgr::SortOpt; use yazi_shared::{Source, data::Data}; use crate::{Actor, Ctx}; pub struct Sort; impl Actor for Sort { type Options = SortOpt; const NAME: &str = "sort"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let pref = &mut cx.tab_mut().pref; pref.sort_by = opt.by.unwrap_or(pref.sort_by); pref.sort_reverse = opt.reverse.unwrap_or(pref.sort_reverse); pref.sort_dir_first = opt.dir_first.unwrap_or(pref.sort_dir_first); pref.sort_sensitive = opt.sensitive.unwrap_or(pref.sort_sensitive); pref.sort_translit = opt.translit.unwrap_or(pref.sort_translit); pref.sort_fallback = opt.fallback.unwrap_or(pref.sort_fallback); let sorter = FilesSorter::from(&*pref); let hovered = cx.hovered().map(|f| f.urn().to_owned()); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); false } else { f.files.set_sorter(sorter); render_and!(f.files.catchup_revision()) } }; // Apply to CWD and parent if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply)) && (a | b) { act!(mgr:hover, cx)?; act!(mgr:update_paged, cx)?; cx.tasks.prework_sorted(&cx.mgr.tabs[cx.tab].current.files); } // Apply to hovered if let Some(h) = cx.hovered_folder_mut() && apply(h) { render!(h.repos(None)); act!(mgr:peek, cx, true)?; } else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; } succ!(); } fn hook(cx: &Ctx, _: &Self::Options) -> Option { match cx.source() { Source::Ind => Some(SparkKind::IndSort), Source::Key => Some(SparkKind::KeySort), _ => None, } } } ================================================ FILE: yazi-actor/src/mgr/spot.rs ================================================ use anyhow::Result; use yazi_macro::{act, succ}; use yazi_parser::mgr::SpotOpt; use yazi_shared::{data::Data, pool::InternStr}; use crate::{Actor, Ctx}; pub struct Spot; impl Actor for Spot { type Options = SpotOpt; const NAME: &str = "spot"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(mgr:escape_visual, cx)?; let Some(hovered) = cx.hovered().cloned() else { succ!() }; let mime = if cx.tab().selected.is_empty() { cx.mgr.mimetype.owned(&hovered.url).unwrap_or_default() } else { "multi/unknown".intern() }; // if !self.active().spot.same_file(&hovered, &mime) { // self.active_mut().spot.reset(); // } if let Some(skip) = opt.skip { cx.tab_mut().spot.skip = skip; } else if !cx.tab().spot.same_url(&hovered.url) { cx.tab_mut().spot.skip = 0; } cx.tab_mut().spot.go(hovered, mime); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/stash.rs ================================================ use anyhow::Result; use yazi_dds::spark::SparkKind; use yazi_macro::succ; use yazi_parser::mgr::StashOpt; use yazi_shared::{Source, data::Data, url::{AsUrl, UrlLike}}; use crate::{Actor, Ctx}; pub struct Stash; impl Actor for Stash { type Options = StashOpt; const NAME: &str = "stash"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { if opt.target.is_absolute() && opt.target.is_internal() { cx.tab_mut().backstack.push(opt.target.as_url()); } succ!() } fn hook(cx: &Ctx, _opt: &Self::Options) -> Option { match cx.source() { Source::Ind => Some(SparkKind::IndStash), Source::Relay => Some(SparkKind::RelayStash), _ => None, } } } ================================================ FILE: yazi-actor/src/mgr/suspend.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::VoidOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Suspend; impl Actor for Suspend { type Options = VoidOpt; const NAME: &str = "suspend"; fn act(_: &mut Ctx, _: Self::Options) -> Result { #[cfg(unix)] if !yazi_shared::session_leader() { unsafe { libc::raise(libc::SIGTSTP); } } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/tab_close.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::TabCloseOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct TabClose; impl Actor for TabClose { type Options = TabCloseOpt; const NAME: &str = "tab_close"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let len = cx.tabs().len(); if len < 2 || opt.idx >= len { succ!(); } let tabs = cx.tabs_mut(); tabs.remove(opt.idx).shutdown(); if opt.idx > tabs.cursor { tabs.set_idx(tabs.cursor); } else { tabs.set_idx(usize::min(tabs.cursor + 1, tabs.len() - 1)); } let cx = &mut Ctx::renew(cx); act!(mgr:refresh, cx)?; act!(mgr:peek, cx, true)?; act!(app:title, cx).ok(); succ!(render!()); } } ================================================ FILE: yazi-actor/src/mgr/tab_create.rs ================================================ use anyhow::Result; use yazi_core::tab::Tab; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::{CdSource, TabCreateOpt}; use yazi_proxy::NotifyProxy; use yazi_shared::{data::Data, url::UrlLike}; use crate::{Actor, Ctx}; const MAX_TABS: usize = 9; pub struct TabCreate; impl Actor for TabCreate { type Options = TabCreateOpt; const NAME: &str = "tab_create"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { if cx.tabs().len() >= MAX_TABS { succ!(NotifyProxy::push_warn( "Too many tabs", "You can only open up to 9 tabs at the same time." )); } let mut tab = Tab::default(); let (cd, url) = if let Some(wd) = opt.url { (true, wd.into_owned()) } else if let Some(h) = cx.hovered() { tab.pref = cx.tab().pref.clone(); (false, h.url.clone()) } else if !cx.cwd().is_search() { tab.pref = cx.tab().pref.clone(); (true, cx.cwd().clone()) } else if let Some(u) = tab.backstack.current().cloned() { tab.pref = cx.tab().pref.clone(); (true, u) } else { tab.pref = cx.tab().pref.clone(); (true, tab.cwd().to_regular()?) }; let tabs = &mut cx.mgr.tabs; tabs.items.insert(tabs.cursor + 1, tab); tabs.set_idx(tabs.cursor + 1); let cx = &mut Ctx::renew(cx); if cd { act!(mgr:cd, cx, (url, CdSource::Tab))?; } else { act!(mgr:reveal, cx, (url, CdSource::Tab))?; } act!(mgr:refresh, cx)?; act!(mgr:peek, cx, true)?; act!(app:title, cx).ok(); succ!(render!()); } } ================================================ FILE: yazi-actor/src/mgr/tab_rename.rs ================================================ use std::borrow::Cow; use anyhow::Result; use yazi_config::popup::InputCfg; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::TabRenameOpt; use yazi_proxy::{InputProxy, MgrProxy}; use yazi_shared::data::Data; use yazi_widgets::input::InputEvent; use crate::{Actor, Ctx}; pub struct TabRename; impl Actor for TabRename { type Options = TabRenameOpt; const NAME: &str = "tab_rename"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let tab = cx.tab().id; let pref = &mut cx.tab_mut().pref; if !opt.interactive { pref.name = opt.name.unwrap_or_default().into_owned(); act!(app:title, cx).ok(); succ!(render!()); } let mut input = InputProxy::show( InputCfg::tab_rename().with_value(opt.name.unwrap_or(Cow::Borrowed(&pref.name))), ); tokio::spawn(async move { if let Some(InputEvent::Submit(name)) = input.recv().await { MgrProxy::tab_rename(tab, name); } }); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/tab_swap.rs ================================================ use anyhow::Result; use yazi_dds::Pubsub; use yazi_macro::{err, render, succ}; use yazi_parser::ArrowOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct TabSwap; impl Actor for TabSwap { type Options = ArrowOpt; const NAME: &str = "tab_swap"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let tabs = cx.tabs_mut(); let new = opt.step.add(tabs.cursor, tabs.len(), 0); if new == tabs.cursor { succ!(); } tabs.items.swap(tabs.cursor, new); tabs.cursor = new; err!(Pubsub::pub_after_tab(cx.active().id)); succ!(render!()); } } ================================================ FILE: yazi-actor/src/mgr/tab_switch.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::TabSwitchOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct TabSwitch; impl Actor for TabSwitch { type Options = TabSwitchOpt; const NAME: &str = "tab_switch"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let tabs = cx.tabs_mut(); let idx = if opt.relative { opt.step.saturating_add_unsigned(tabs.cursor).rem_euclid(tabs.len() as _) as _ } else { opt.step as usize }; if idx == tabs.cursor || idx >= tabs.len() { succ!(); } tabs.set_idx(idx); let cx = &mut Ctx::renew(cx); act!(mgr:refresh, cx)?; act!(mgr:peek, cx, true)?; act!(app:title, cx).ok(); succ!(render!()); } } ================================================ FILE: yazi-actor/src/mgr/toggle.rs ================================================ use anyhow::Result; use yazi_macro::{render_and, succ}; use yazi_parser::mgr::ToggleOpt; use yazi_proxy::NotifyProxy; use yazi_shared::{data::Data, url::UrlCow}; use crate::{Actor, Ctx}; pub struct Toggle; impl Actor for Toggle { type Options = ToggleOpt; const NAME: &str = "toggle"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let tab = cx.tab_mut(); let Some(url) = opt.url.or(tab.current.hovered().map(|h| UrlCow::from(&h.url))) else { succ!(); }; let b = match opt.state { Some(true) => render_and!(tab.selected.add(&url)), Some(false) => render_and!(tab.selected.remove(&url)) | true, None => render_and!(tab.selected.remove(&url) || tab.selected.add(&url)), }; if !b { NotifyProxy::push_warn( "Toggle", "This file cannot be selected, due to path nesting conflict.", ); } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/toggle_all.rs ================================================ use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::mgr::ToggleAllOpt; use yazi_proxy::NotifyProxy; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct ToggleAll; impl Actor for ToggleAll { type Options = ToggleAllOpt; const NAME: &str = "toggle_all"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { use either::Either::*; let tab = cx.tab_mut(); let it = tab.current.files.iter().map(|f| &f.url); let either = match opt.state { Some(true) if opt.urls.is_empty() => Left((vec![], it.collect())), Some(true) => Right((vec![], opt.urls)), Some(false) if opt.urls.is_empty() => Left((it.collect(), vec![])), Some(false) => Right((opt.urls, vec![])), None if opt.urls.is_empty() => Left(it.partition(|&u| tab.selected.contains(u))), None => Right(opt.urls.into_iter().partition(|u| tab.selected.contains(u))), }; let warn = match either { Left((removal, addition)) => { render!(tab.selected.remove_many(removal) > 0); addition.len() != render!(tab.selected.add_many(addition), > 0) } Right((removal, addition)) => { render!(tab.selected.remove_many(&removal) > 0); render!(tab.selected.add_many(&addition), > 0) != addition.len() } }; if warn { NotifyProxy::push_warn( "Toggle all", "Some files cannot be selected, due to path nesting conflict.", ); } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/unyank.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::VoidOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Unyank; impl Actor for Unyank { type Options = VoidOpt; const NAME: &str = "unyank"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let repeek = cx.hovered().is_some_and(|f| f.is_dir() && cx.mgr.yanked.contains_in(&f.url)); cx.mgr.yanked.clear(); render!(cx.mgr.yanked.catchup_revision(false)); if repeek { act!(mgr:peek, cx, true)?; } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/update_files.rs ================================================ use anyhow::Result; use yazi_core::tab::Folder; use yazi_fs::FilesOp; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::UpdateFilesOpt; use yazi_shared::{data::Data, url::UrlLike}; use yazi_watcher::local::LINKED; use crate::{Actor, Ctx}; pub struct UpdateFiles; impl Actor for UpdateFiles { type Options = UpdateFilesOpt; const NAME: &str = "update_files"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let revision = cx.current().files.revision; let linked: Vec<_> = LINKED.read().from_dir(opt.op.cwd()).map(|u| opt.op.chdir(u)).collect(); for op in [opt.op].into_iter().chain(linked) { cx.mgr.yanked.apply_op(&op); Self::update_tab(cx, op).ok(); } render!(cx.mgr.yanked.catchup_revision(false)); act!(mgr:hidden, cx).ok(); act!(mgr:sort, cx).ok(); if revision != cx.current().files.revision { act!(mgr:hover, cx)?; act!(mgr:peek, cx)?; act!(mgr:watch, cx)?; act!(mgr:update_paged, cx)?; } succ!(); } } impl UpdateFiles { fn update_tab(cx: &mut Ctx, op: FilesOp) -> Result { let url = op.cwd(); cx.tab_mut().selected.apply_op(&op); if url == cx.cwd() { Self::update_current(cx, op) } else if matches!(cx.parent(), Some(p) if *url == p.url) { Self::update_parent(cx, op) } else if matches!(cx.hovered(), Some(h) if *url == h.url) { Self::update_hovered(cx, op) } else { Self::update_history(cx, op) } } fn update_parent(cx: &mut Ctx, op: FilesOp) -> Result { let tab = cx.tab_mut(); let urn = tab.current.url.urn(); let leave = matches!(op, FilesOp::Deleting(_, ref urns) if urns.contains(&urn)); if let Some(f) = tab.parent.as_mut() { render!(f.update_pub(tab.id, op)); render!(f.hover(urn)); } if leave { act!(mgr:leave, cx)?; } succ!(); } fn update_current(cx: &mut Ctx, op: FilesOp) -> Result { let calc = !matches!(op, FilesOp::Size(..) | FilesOp::Deleting(..)); let id = cx.tab().id; if !cx.current_mut().update_pub(id, op) { succ!(); } if calc { cx.tasks.prework_sorted(&cx.current().files); } succ!(); } fn update_hovered(cx: &mut Ctx, op: FilesOp) -> Result { let (id, url) = (cx.tab().id, op.cwd()); let folder = cx.tab_mut().history.entry_ref(url).or_insert_with(|| Folder::from(url)); if folder.update_pub(id, op) { act!(mgr:peek, cx, true)?; } succ!(); } fn update_history(cx: &mut Ctx, op: FilesOp) -> Result { let tab = &mut cx.tab_mut(); let leave = tab.parent.as_ref().and_then(|f| f.url.parent().map(|p| (p, f.url.urn()))).is_some_and( |(p, n)| matches!(op, FilesOp::Deleting(ref parent, ref urns) if *parent == p && urns.contains(&n)), ); tab .history .entry_ref(op.cwd()) .or_insert_with(|| Folder::from(op.cwd())) .update_pub(tab.id, op); if leave { act!(mgr:leave, cx)?; } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/update_mimes.rs ================================================ use anyhow::Result; use hashbrown::HashMap; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::UpdateMimesOpt; use yazi_shared::{data::Data, pool::InternStr, url::{AsUrl, UrlCov}}; use yazi_watcher::local::LINKED; use crate::{Actor, Ctx}; pub struct UpdateMimes; impl Actor for UpdateMimes { type Options = UpdateMimesOpt; const NAME: &str = "update_mimes"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let linked = LINKED.read(); let updates = opt .updates .into_iter() .flat_map(|(key, value)| key.into_url().zip(value.into_string())) .filter(|(url, mime)| cx.mgr.mimetype.get(url) != Some(mime)) .fold(HashMap::new(), |mut map, (u, m)| { for u in linked.from_file(u.as_url()) { map.insert(u.into(), m.intern()); } map.insert(u.into(), m.intern()); map }); drop(linked); if updates.is_empty() { succ!(); } let affected: Vec<_> = cx .current() .paginate(cx.current().page) .iter() .filter(|&f| updates.contains_key(&UrlCov::new(&f.url))) .cloned() .collect(); let repeek = cx.hovered().is_some_and(|f| updates.contains_key(&UrlCov::new(&f.url))); cx.mgr.mimetype.extend(updates); if repeek { act!(mgr:peek, cx)?; } cx.tasks.fetch_paged(&affected, &cx.mgr.mimetype); cx.tasks.preload_paged(&affected, &cx.mgr.mimetype); succ!(render!()); } } ================================================ FILE: yazi-actor/src/mgr/update_paged.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::mgr::UpdatePagedOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct UpdatePaged; impl Actor for UpdatePaged { type Options = UpdatePagedOpt; const NAME: &str = "update_paged"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { if opt.only_if.is_some_and(|u| u != *cx.cwd()) { succ!(); } let targets = cx.current().paginate(opt.page.unwrap_or(cx.current().page)); if !targets.is_empty() { cx.tasks().fetch_paged(targets, &cx.mgr.mimetype); cx.tasks().preload_paged(targets, &cx.mgr.mimetype); } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/update_peeked.rs ================================================ use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::mgr::UpdatePeekedOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct UpdatePeeked; impl Actor for UpdatePeeked { type Options = UpdatePeekedOpt; const NAME: &str = "update_peeked"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let Some(hovered) = cx.hovered().map(|h| &h.url) else { succ!(cx.tab_mut().preview.reset()); }; if opt.lock.url == *hovered { cx.tab_mut().preview.lock = Some(opt.lock); render!(); } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/update_spotted.rs ================================================ use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::mgr::UpdateSpottedOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct UpdateSpotted; impl Actor for UpdateSpotted { type Options = UpdateSpottedOpt; const NAME: &str = "update_spotted"; fn act(cx: &mut Ctx, mut opt: Self::Options) -> Result { let tab = cx.tab_mut(); let Some(hovered) = tab.hovered().map(|h| &h.url) else { succ!(tab.spot.reset()); }; if opt.lock.url != *hovered { succ!(); } if tab.spot.lock.as_ref().is_none_or(|l| l.id != opt.lock.id) { tab.spot.skip = opt.lock.selected().unwrap_or_default(); } else if let Some(s) = opt.lock.selected() { tab.spot.skip = s; } else { opt.lock.select(Some(tab.spot.skip)); } tab.spot.lock = Some(opt.lock); succ!(render!()); } } ================================================ FILE: yazi-actor/src/mgr/update_yanked.rs ================================================ use anyhow::Result; use yazi_core::mgr::Yanked; use yazi_macro::{render, succ}; use yazi_parser::mgr::UpdateYankedOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct UpdateYanked; impl Actor for UpdateYanked { type Options = UpdateYankedOpt<'static>; const NAME: &str = "update_yanked"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { if opt.urls.is_empty() && cx.mgr.yanked.is_empty() { succ!(); } cx.mgr.yanked = Yanked::new(opt.cut, opt.urls.into_owned()); succ!(render!()); } } ================================================ FILE: yazi-actor/src/mgr/upload.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::mgr::UploadOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Upload; impl Actor for Upload { type Options = UploadOpt; const NAME: &str = "upload"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { for url in opt.urls { cx.tasks.scheduler.file_upload(url.into_owned()); } succ!(); } } ================================================ FILE: yazi-actor/src/mgr/visual_mode.rs ================================================ use std::collections::BTreeSet; use anyhow::Result; use yazi_core::tab::Mode; use yazi_macro::{render, succ}; use yazi_parser::mgr::VisualModeOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct VisualMode; impl Actor for VisualMode { type Options = VisualModeOpt; const NAME: &str = "visual_mode"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let tab = cx.tab_mut(); let idx = tab.current.cursor; if opt.unset { tab.mode = Mode::Unset(idx, BTreeSet::from([idx])); } else { tab.mode = Mode::Select(idx, BTreeSet::from([idx])); }; succ!(render!()); } } ================================================ FILE: yazi-actor/src/mgr/watch.rs ================================================ use std::iter; use anyhow::Result; use yazi_macro::succ; use yazi_parser::VoidOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Watch; impl Actor for Watch { type Options = VoidOpt; const NAME: &str = "watch"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let it = iter::once(cx.core.mgr.tabs.active().cwd()) .chain(cx.core.mgr.tabs.parent().map(|p| &p.url)) .chain(cx.core.mgr.tabs.hovered().filter(|h| h.is_dir()).map(|h| &h.url)); cx.core.mgr.watcher.watch(it); succ!(); } } ================================================ FILE: yazi-actor/src/mgr/yank.rs ================================================ use anyhow::Result; use yazi_core::mgr::Yanked; use yazi_macro::{act, render}; use yazi_parser::mgr::YankOpt; use yazi_shared::{data::Data, url::UrlBufCov}; use crate::{Actor, Ctx}; pub struct Yank; impl Actor for Yank { type Options = YankOpt; const NAME: &str = "yank"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(mgr:escape_visual, cx)?; cx.mgr.yanked = Yanked::new(opt.cut, cx.tab().selected_or_hovered().cloned().map(UrlBufCov).collect()); render!(cx.mgr.yanked.catchup_revision(true)); act!(mgr:escape_select, cx) } } ================================================ FILE: yazi-actor/src/notify/mod.rs ================================================ yazi_macro::mod_flat!(push tick); ================================================ FILE: yazi-actor/src/notify/push.rs ================================================ use std::time::Instant; use anyhow::Result; use yazi_core::notify::Message; use yazi_dds::spark::SparkKind; use yazi_macro::{act, succ}; use yazi_parser::notify::PushOpt; use yazi_shared::{Source, data::Data}; use crate::{Actor, Ctx}; pub struct Push; impl Actor for Push { type Options = PushOpt; const NAME: &str = "push"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let instant = Instant::now(); let mut msg = Message::from(opt); msg.timeout += instant - cx.notify.messages.first().map_or(instant, |m| m.instant); if cx.notify.messages.iter().all(|m| m != &msg) { cx.notify.messages.push(msg); act!(notify:tick, cx)?; } succ!(); } fn hook(cx: &Ctx, _: &Self::Options) -> Option { match cx.source() { Source::Relay => Some(SparkKind::RelayNotifyPush), _ => None, } } } ================================================ FILE: yazi-actor/src/notify/tick.rs ================================================ use std::time::Duration; use anyhow::Result; use ratatui::layout::Rect; use yazi_core::notify::Notify; use yazi_emulator::Dimension; use yazi_macro::{render, render_partial, succ}; use yazi_parser::notify::TickOpt; use yazi_proxy::NotifyProxy; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Tick; impl Actor for Tick { type Options = TickOpt; const NAME: &str = "tick"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { cx.notify.tick_handle.take().map(|h| h.abort()); let Dimension { rows, columns, .. } = Dimension::available(); let area = Notify::available(Rect { x: 0, y: 0, width: columns, height: rows }); let limit = cx.notify.limit(area); if limit == 0 { succ!(); } for m in &mut cx.notify.messages[..limit] { if m.timeout.is_zero() { m.percent = m.percent.saturating_sub(20); } else if m.percent < 100 { m.percent += 20; } else { m.timeout = m.timeout.saturating_sub(opt.interval); } } cx.notify.messages.retain(|m| m.percent > 0 || !m.timeout.is_zero()); let limit = cx.notify.limit(area); let timeouts: Vec<_> = cx.notify.messages[..limit] .iter() .filter(|&m| m.percent == 100 && !m.timeout.is_zero()) .map(|m| m.timeout) .collect(); let interval = if timeouts.len() != limit { Duration::from_millis(50) } else if let Some(min) = timeouts.iter().min() { *min } else { succ!(render!()); }; cx.notify.tick_handle = Some(tokio::spawn(async move { tokio::time::sleep(interval).await; NotifyProxy::tick(interval); })); succ!(render_partial!()); } } ================================================ FILE: yazi-actor/src/pick/arrow.rs ================================================ use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::ArrowOpt; use yazi_shared::data::Data; use yazi_widgets::Scrollable; use crate::{Actor, Ctx}; pub struct Arrow; impl Actor for Arrow { type Options = ArrowOpt; const NAME: &str = "arrow"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { succ!(render!(cx.pick.scroll(opt.step))); } } ================================================ FILE: yazi-actor/src/pick/close.rs ================================================ use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::pick::CloseOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Close; impl Actor for Close { type Options = CloseOpt; const NAME: &str = "close"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let pick = &mut cx.pick; if let Some(cb) = pick.callback.take() { _ = cb.send(if opt.submit { Some(pick.cursor) } else { None }); } pick.cursor = 0; pick.offset = 0; pick.visible = false; succ!(render!()); } } ================================================ FILE: yazi-actor/src/pick/mod.rs ================================================ yazi_macro::mod_flat!(arrow close show); ================================================ FILE: yazi-actor/src/pick/show.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::pick::ShowOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Show; impl Actor for Show { type Options = ShowOpt; const NAME: &str = "show"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(pick:close, cx)?; let pick = &mut cx.pick; pick.title = opt.cfg.title; pick.items = opt.cfg.items; pick.position = opt.cfg.position; pick.callback = Some(opt.tx); pick.visible = true; succ!(render!()); } } ================================================ FILE: yazi-actor/src/spot/arrow.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::ArrowOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Arrow; impl Actor for Arrow { type Options = ArrowOpt; const NAME: &str = "arrow"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let spot = &mut cx.tab_mut().spot; let Some(lock) = &mut spot.lock else { succ!() }; let new = opt.step.add(spot.skip, lock.len().unwrap_or(u16::MAX as _), 0); let Some(old) = lock.selected() else { return act!(mgr:spot, cx, new); }; lock.select(Some(new)); let new = lock.selected().unwrap(); spot.skip = new; succ!(render!(new != old)); } } ================================================ FILE: yazi-actor/src/spot/close.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::VoidOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Close; impl Actor for Close { type Options = VoidOpt; const NAME: &str = "close"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { succ!(cx.tab_mut().spot.reset()); } } ================================================ FILE: yazi-actor/src/spot/copy.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::spot::CopyOpt; use yazi_shared::data::Data; use yazi_widgets::CLIPBOARD; use crate::{Actor, Ctx}; pub struct Copy; impl Actor for Copy { type Options = CopyOpt; const NAME: &str = "copy"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let spot = &cx.tab().spot; let Some(lock) = &spot.lock else { succ!() }; let Some(table) = lock.table() else { succ!() }; let mut s = String::new(); match opt.r#type.as_ref() { "cell" => { let Some(cell) = table.selected_cell() else { succ!() }; s = cell.to_string(); } "line" => { // TODO } _ => {} } futures::executor::block_on(CLIPBOARD.set(s)); succ!(); } } ================================================ FILE: yazi-actor/src/spot/mod.rs ================================================ yazi_macro::mod_flat!(arrow close copy swipe); ================================================ FILE: yazi-actor/src/spot/swipe.rs ================================================ use anyhow::Result; use yazi_macro::act; use yazi_parser::ArrowOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Swipe; impl Actor for Swipe { type Options = ArrowOpt; const NAME: &str = "swipe"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(mgr:arrow, cx, opt)?; act!(mgr:spot, cx) } } ================================================ FILE: yazi-actor/src/tasks/arrow.rs ================================================ use anyhow::Result; use yazi_core::tasks::Tasks; use yazi_macro::{render, succ}; use yazi_parser::ArrowOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Arrow; impl Actor for Arrow { type Options = ArrowOpt; const NAME: &str = "arrow"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let tasks = &mut cx.tasks; let old = tasks.cursor; tasks.cursor = opt.step.add(tasks.cursor, tasks.snaps.len(), Tasks::limit()); succ!(render!(tasks.cursor != old)); } } ================================================ FILE: yazi-actor/src/tasks/cancel.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::VoidOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Cancel; impl Actor for Cancel { type Options = VoidOpt; const NAME: &str = "cancel"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let tasks = &mut cx.tasks; let id = tasks.ongoing().lock().get_id(tasks.cursor); if id.map(|id| tasks.scheduler.cancel(id)) != Some(true) { succ!(); } tasks.snaps = tasks.paginate(); act!(tasks:arrow, cx)?; succ!(render!()); } } ================================================ FILE: yazi-actor/src/tasks/close.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::VoidOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Close; impl Actor for Close { type Options = VoidOpt; const NAME: &str = "close"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let tasks = &mut cx.tasks; if !tasks.visible { succ!(); } tasks.visible = false; tasks.snaps = Vec::new(); act!(tasks:arrow, cx)?; succ!(render!()); } } ================================================ FILE: yazi-actor/src/tasks/inspect.rs ================================================ use std::io::Write; use anyhow::Result; use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode}}; use scopeguard::defer; use tokio::{io::{AsyncReadExt, stdin}, select, sync::mpsc, time}; use yazi_binding::Permit; use yazi_macro::succ; use yazi_parser::VoidOpt; use yazi_proxy::AppProxy; use yazi_shared::{data::Data, terminal_clear}; use yazi_term::YIELD_TO_SUBPROCESS; use yazi_tty::TTY; use crate::{Actor, Ctx}; pub struct Inspect; impl Actor for Inspect { type Options = VoidOpt; const NAME: &str = "inspect"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let ongoing = cx.tasks.ongoing().clone(); let Some(id) = ongoing.lock().get_id(cx.tasks.cursor) else { succ!(); }; tokio::spawn(async move { let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume()); let (tx, mut rx) = mpsc::unbounded_channel(); let buffered = { let mut ongoing = ongoing.lock(); let Some(task) = ongoing.get_mut(id) else { return }; task.logger = Some(tx); task.logs.clone() }; AppProxy::stop().await; terminal_clear(TTY.writer()).ok(); TTY.writer().write_all(buffered.as_bytes()).ok(); TTY.writer().flush().ok(); defer! { disable_raw_mode().ok(); } enable_raw_mode().ok(); let mut stdin = stdin(); // TODO: stdin let mut answer = 0; loop { select! { Some(line) = rx.recv() => { execute!(TTY.writer(), crossterm::style::Print(line), crossterm::style::Print("\r\n")).ok(); } _ = time::sleep(time::Duration::from_millis(500)) => { if !ongoing.lock().exists(id) { execute!(TTY.writer(), crossterm::style::Print("Task finished, press `q` to quit\r\n")).ok(); break; } }, result = stdin.read_u8() => { answer = result.unwrap_or(b'q'); if answer == b'q' { break; } } } } if let Some(task) = ongoing.lock().get_mut(id) { task.logger = None; } while answer != b'q' { answer = stdin.read_u8().await.unwrap_or(b'q'); } }); succ!(); } } ================================================ FILE: yazi-actor/src/tasks/mod.rs ================================================ yazi_macro::mod_flat!(arrow cancel close open_shell_compat inspect process_open show update_succeed); ================================================ FILE: yazi-actor/src/tasks/open_shell_compat.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::tasks::ProcessOpenOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct OpenShellCompat; // TODO: remove impl Actor for OpenShellCompat { type Options = ProcessOpenOpt; const NAME: &str = "open_shell_compat"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { succ!(cx.tasks.open_shell_compat(opt)); } } ================================================ FILE: yazi-actor/src/tasks/process_open.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::tasks::ProcessOpenOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct ProcessOpen; impl Actor for ProcessOpen { type Options = ProcessOpenOpt; const NAME: &str = "process_open"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { succ!(cx.tasks.scheduler.process_open(opt)); } } ================================================ FILE: yazi-actor/src/tasks/show.rs ================================================ use anyhow::Result; use yazi_macro::{act, render, succ}; use yazi_parser::VoidOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Show; impl Actor for Show { type Options = VoidOpt; const NAME: &str = "show"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { let tasks = &mut cx.tasks; if tasks.visible { succ!(); } tasks.visible = true; tasks.snaps = tasks.paginate(); act!(tasks:arrow, cx)?; succ!(render!()); } } ================================================ FILE: yazi-actor/src/tasks/update_succeed.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::tasks::UpdateSucceedOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct UpdateSucceed; impl Actor for UpdateSucceed { type Options = UpdateSucceedOpt; const NAME: &str = "update_succeed"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { cx.mgr.watcher.report(opt.urls); succ!(); } } ================================================ FILE: yazi-actor/src/which/activate.rs ================================================ use anyhow::Result; use yazi_core::which::WhichSorter; use yazi_dds::spark::SparkKind; use yazi_macro::{render, succ}; use yazi_parser::which::ActivateOpt; use yazi_shared::{Source, data::Data}; use crate::{Actor, Ctx}; pub struct Activate; impl Actor for Activate { type Options = ActivateOpt; const NAME: &str = "activate"; fn act(cx: &mut Ctx, mut opt: Self::Options) -> Result { opt.cands.retain(|c| c.on.len() > opt.times); WhichSorter::default().sort(&mut opt.cands); if opt.cands.is_empty() { succ!(); } let which = &mut cx.which; which.tx = opt.tx; which.times = opt.times; which.cands = opt.cands; which.active = true; which.silent = opt.silent; succ!(render!()); } fn hook(cx: &Ctx, _opt: &Self::Options) -> Option { match cx.source() { Source::Unknown => Some(SparkKind::IndWhichActivate), _ => None, } } } ================================================ FILE: yazi-actor/src/which/dismiss.rs ================================================ use anyhow::Result; use yazi_macro::succ; use yazi_parser::VoidOpt; use yazi_shared::data::Data; use crate::{Actor, Ctx}; pub struct Dismiss; impl Actor for Dismiss { type Options = VoidOpt; const NAME: &str = "dismiss"; fn act(cx: &mut Ctx, _: Self::Options) -> Result { succ!(cx.which.dismiss(None)); } } ================================================ FILE: yazi-actor/src/which/mod.rs ================================================ yazi_macro::mod_flat!(activate dismiss); ================================================ FILE: yazi-adapter/Cargo.toml ================================================ [package] name = "yazi-adapter" description = "Yazi image adapter" version.workspace = true edition.workspace = true license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true rust-version.workspace = true [lints] workspace = true [dependencies] yazi-config = { path = "../yazi-config", version = "26.2.2" } yazi-emulator = { path = "../yazi-emulator", version = "26.2.2" } yazi-fs = { path = "../yazi-fs", version = "26.2.2" } yazi-macro = { path = "../yazi-macro", version = "26.2.2" } yazi-shared = { path = "../yazi-shared", version = "26.2.2" } yazi-tty = { path = "../yazi-tty", version = "26.2.2" } # External dependencies ansi-to-tui = { workspace = true } anyhow = { workspace = true } base64 = { workspace = true } crossterm = { workspace = true } image = { version = "0.25.10", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp" ] } moxcms = "0.8.1" palette = { version = "0.7.6", default-features = false } quantette = { version = "0.5.1", default-features = false } ratatui = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } ================================================ FILE: yazi-adapter/README.md ================================================ # yazi-adapter This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. [source]: https://github.com/sxyazi/yazi ================================================ FILE: yazi-adapter/src/adapter.rs ================================================ use std::{env, fmt::Display, path::PathBuf}; use anyhow::Result; use ratatui::layout::Rect; use tracing::warn; use yazi_emulator::{Emulator, TMUX}; use yazi_shared::env_exists; use crate::{Adapters, SHOWN, drivers}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Adapter { Kgp, KgpOld, Iip, Sixel, // Supported by Überzug++ X11, Wayland, Chafa, } impl Display for Adapter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Kgp => write!(f, "kgp"), Self::KgpOld => write!(f, "kgp-old"), Self::Iip => write!(f, "iip"), Self::Sixel => write!(f, "sixel"), Self::X11 => write!(f, "x11"), Self::Wayland => write!(f, "wayland"), Self::Chafa => write!(f, "chafa"), } } } impl Adapter { pub async fn image_show

(self, path: P, max: Rect) -> Result where P: Into, { if max.is_empty() { return Ok(Rect::default()); } let path = path.into(); match self { Self::Kgp => drivers::Kgp::image_show(path, max).await, Self::KgpOld => drivers::KgpOld::image_show(path, max).await, Self::Iip => drivers::Iip::image_show(path, max).await, Self::Sixel => drivers::Sixel::image_show(path, max).await, Self::X11 | Self::Wayland => drivers::Ueberzug::image_show(path, max).await, Self::Chafa => drivers::Chafa::image_show(path, max).await, } } pub fn image_hide(self) -> Result<()> { if let Some(area) = SHOWN.replace(None) { self.image_erase(area) } else { Ok(()) } } pub fn image_erase(self, area: Rect) -> Result<()> { match self { Self::Kgp => drivers::Kgp::image_erase(area), Self::KgpOld => drivers::KgpOld::image_erase(area), Self::Iip => drivers::Iip::image_erase(area), Self::Sixel => drivers::Sixel::image_erase(area), Self::X11 | Self::Wayland => drivers::Ueberzug::image_erase(area), Self::Chafa => drivers::Chafa::image_erase(area), } } #[inline] pub fn shown_load(self) -> Option { SHOWN.get() } #[inline] pub(super) fn shown_store(area: Rect) { SHOWN.set(Some(area)); } pub(super) fn start(self) { drivers::Ueberzug::start(self); } #[inline] pub(super) fn needs_ueberzug(self) -> bool { !matches!(self, Self::Kgp | Self::KgpOld | Self::Iip | Self::Sixel) } } impl Adapter { pub fn matches(emulator: &Emulator) -> Self { let mut adapters: Adapters = emulator.into(); if env_exists("ZELLIJ_SESSION_NAME") { adapters.retain(|p| *p == Self::Sixel); } else if TMUX.get() { adapters.retain(|p| *p != Self::KgpOld); } if let Some(p) = adapters.first() { return *p; } let supported_compositor = drivers::Ueberzug::supported_compositor(); match env::var("XDG_SESSION_TYPE").unwrap_or_default().as_str() { "x11" => return Self::X11, "wayland" if supported_compositor => return Self::Wayland, "wayland" if !supported_compositor => return Self::Chafa, _ => warn!("[Adapter] Could not identify XDG_SESSION_TYPE"), } if env_exists("WAYLAND_DISPLAY") { return if supported_compositor { Self::Wayland } else { Self::Chafa }; } match env::var("DISPLAY").unwrap_or_default().as_str() { s if !s.is_empty() && !s.contains("/org.xquartz") => return Self::X11, _ => {} } warn!("[Adapter] Falling back to chafa"); Self::Chafa } } ================================================ FILE: yazi-adapter/src/adapters.rs ================================================ use std::ops::{Deref, DerefMut}; use crate::Adapter; pub(super) struct Adapters(Vec); impl Deref for Adapters { type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for Adapters { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl From<&yazi_emulator::Emulator> for Adapters { fn from(value: &yazi_emulator::Emulator) -> Self { value.kind.either_into() } } impl From for Adapters { fn from(value: yazi_emulator::Brand) -> Self { use yazi_emulator::Brand as B; use crate::Adapter as A; Self(match value { B::Kitty => vec![A::Kgp], B::Konsole => vec![A::KgpOld], B::Iterm2 => vec![A::Iip, A::Sixel], B::WezTerm => vec![A::Iip, A::Sixel], B::Foot => vec![A::Sixel], B::Ghostty => vec![A::Kgp], B::Microsoft => vec![A::Sixel], B::Warp => vec![A::Iip, A::KgpOld], B::Rio => vec![A::Iip, A::Sixel], B::BlackBox => vec![A::Sixel], B::VSCode => vec![A::Iip, A::Sixel], B::Tabby => vec![A::Iip, A::Sixel], B::Hyper => vec![A::Iip, A::Sixel], B::Mintty => vec![A::Iip], B::Tmux => vec![], B::VTerm => vec![], B::Apple => vec![], B::Urxvt => vec![], B::Bobcat => vec![A::Iip, A::Sixel], }) } } impl From for Adapters { fn from(value: yazi_emulator::Unknown) -> Self { use Adapter as A; Self(match (value.kgp, value.sixel) { (true, true) => vec![A::Sixel, A::KgpOld], (true, false) => vec![A::KgpOld], (false, true) => vec![A::Sixel], (false, false) => vec![], }) } } ================================================ FILE: yazi-adapter/src/drivers/chafa.rs ================================================ use std::{io::Write, path::PathBuf, process::Stdio}; use ansi_to_tui::IntoText; use anyhow::{Result, anyhow, bail}; use crossterm::{cursor::MoveTo, queue}; use ratatui::layout::Rect; use tokio::process::Command; use yazi_emulator::Emulator; use crate::Adapter; pub(crate) struct Chafa; impl Chafa { pub(crate) async fn image_show(path: PathBuf, max: Rect) -> Result { let child = Command::new("chafa") .args([ "-f", "symbols", "--relative", "off", "--probe", "off", "--polite", "on", "--passthrough", "none", "--animate", "off", "--view-size", ]) .arg(format!("{}x{}", max.width, max.height)) .arg(path) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()) .kill_on_drop(true) .spawn() .map_err(|e| anyhow!("failed to spawn chafa: {e}"))?; let output = child.wait_with_output().await?; if !output.status.success() { bail!("chafa failed with status: {}", output.status); } else if output.stdout.is_empty() { bail!("chafa returned no output"); } let lines: Vec<_> = output.stdout.split(|&b| b == b'\n').collect(); let Ok(Some(first)) = lines[0].to_text().map(|mut t| t.lines.pop()) else { bail!("failed to parse chafa output"); }; let area = Rect { x: max.x, y: max.y, width: first.width() as u16, height: lines.len() as u16, }; Adapter::Chafa.image_hide()?; Adapter::shown_store(area); Emulator::move_lock((max.x, max.y), |w| { for (i, line) in lines.into_iter().enumerate() { w.write_all(line)?; queue!(w, MoveTo(max.x, max.y + i as u16 + 1))?; } Ok(area) }) } pub(crate) fn image_erase(area: Rect) -> Result<()> { let s = " ".repeat(area.width as usize); Emulator::move_lock((0, 0), |w| { for y in area.top()..area.bottom() { queue!(w, MoveTo(area.x, y))?; write!(w, "{s}")?; } Ok(()) }) } } ================================================ FILE: yazi-adapter/src/drivers/iip.rs ================================================ use std::{fmt::Write, io::Write as ioWrite, path::PathBuf}; use anyhow::Result; use base64::{Engine, engine::{Config, general_purpose::STANDARD}}; use crossterm::{cursor::MoveTo, queue}; use image::{DynamicImage, ExtendedColorType, ImageEncoder, codecs::{jpeg::JpegEncoder, png::PngEncoder}}; use ratatui::layout::Rect; use yazi_config::YAZI; use yazi_emulator::{CLOSE, Emulator, START}; use crate::{Image, adapter::Adapter}; pub(crate) struct Iip; impl Iip { pub(crate) async fn image_show(path: PathBuf, max: Rect) -> Result { let img = Image::downscale(path, max).await?; let area = Image::pixel_area((img.width(), img.height()), max); let b = Self::encode(img).await?; Adapter::Iip.image_hide()?; Adapter::shown_store(area); Emulator::move_lock((max.x, max.y), |w| { w.write_all(&b)?; Ok(area) }) } pub(crate) fn image_erase(area: Rect) -> Result<()> { let s = " ".repeat(area.width as usize); Emulator::move_lock((0, 0), |w| { for y in area.top()..area.bottom() { queue!(w, MoveTo(area.x, y))?; write!(w, "{s}")?; } Ok(()) }) } async fn encode(img: DynamicImage) -> Result> { tokio::task::spawn_blocking(move || { let (w, h) = (img.width(), img.height()); let mut b = vec![]; if img.color().has_alpha() { PngEncoder::new(&mut b).write_image(&img.into_rgba8(), w, h, ExtendedColorType::Rgba8)?; } else { JpegEncoder::new_with_quality(&mut b, YAZI.preview.image_quality).encode_image(&img)?; }; let mut buf = String::with_capacity( 200 + base64::encoded_len(b.len(), STANDARD.config().encode_padding()).unwrap_or(0), ); write!( buf, "{START}]1337;File=inline=1;size={};width={w}px;height={h}px;doNotMoveCursor=1:", b.len(), )?; STANDARD.encode_string(b, &mut buf); write!(buf, "\x07{CLOSE}")?; Ok(buf.into_bytes()) }) .await? } } ================================================ FILE: yazi-adapter/src/drivers/kgp.rs ================================================ use core::str; use std::{io::Write, path::PathBuf}; use anyhow::Result; use base64::{Engine, engine::general_purpose}; use crossterm::{cursor::MoveTo, queue}; use image::DynamicImage; use ratatui::layout::Rect; use yazi_emulator::{CLOSE, ESCAPE, Emulator, START}; use yazi_shared::SyncCell; use crate::{adapter::Adapter, image::Image}; static DIACRITICS: [char; 297] = [ '\u{0305}', '\u{030D}', '\u{030E}', '\u{0310}', '\u{0312}', '\u{033D}', '\u{033E}', '\u{033F}', '\u{0346}', '\u{034A}', '\u{034B}', '\u{034C}', '\u{0350}', '\u{0351}', '\u{0352}', '\u{0357}', '\u{035B}', '\u{0363}', '\u{0364}', '\u{0365}', '\u{0366}', '\u{0367}', '\u{0368}', '\u{0369}', '\u{036A}', '\u{036B}', '\u{036C}', '\u{036D}', '\u{036E}', '\u{036F}', '\u{0483}', '\u{0484}', '\u{0485}', '\u{0486}', '\u{0487}', '\u{0592}', '\u{0593}', '\u{0594}', '\u{0595}', '\u{0597}', '\u{0598}', '\u{0599}', '\u{059C}', '\u{059D}', '\u{059E}', '\u{059F}', '\u{05A0}', '\u{05A1}', '\u{05A8}', '\u{05A9}', '\u{05AB}', '\u{05AC}', '\u{05AF}', '\u{05C4}', '\u{0610}', '\u{0611}', '\u{0612}', '\u{0613}', '\u{0614}', '\u{0615}', '\u{0616}', '\u{0617}', '\u{0657}', '\u{0658}', '\u{0659}', '\u{065A}', '\u{065B}', '\u{065D}', '\u{065E}', '\u{06D6}', '\u{06D7}', '\u{06D8}', '\u{06D9}', '\u{06DA}', '\u{06DB}', '\u{06DC}', '\u{06DF}', '\u{06E0}', '\u{06E1}', '\u{06E2}', '\u{06E4}', '\u{06E7}', '\u{06E8}', '\u{06EB}', '\u{06EC}', '\u{0730}', '\u{0732}', '\u{0733}', '\u{0735}', '\u{0736}', '\u{073A}', '\u{073D}', '\u{073F}', '\u{0740}', '\u{0741}', '\u{0743}', '\u{0745}', '\u{0747}', '\u{0749}', '\u{074A}', '\u{07EB}', '\u{07EC}', '\u{07ED}', '\u{07EE}', '\u{07EF}', '\u{07F0}', '\u{07F1}', '\u{07F3}', '\u{0816}', '\u{0817}', '\u{0818}', '\u{0819}', '\u{081B}', '\u{081C}', '\u{081D}', '\u{081E}', '\u{081F}', '\u{0820}', '\u{0821}', '\u{0822}', '\u{0823}', '\u{0825}', '\u{0826}', '\u{0827}', '\u{0829}', '\u{082A}', '\u{082B}', '\u{082C}', '\u{082D}', '\u{0951}', '\u{0953}', '\u{0954}', '\u{0F82}', '\u{0F83}', '\u{0F86}', '\u{0F87}', '\u{135D}', '\u{135E}', '\u{135F}', '\u{17DD}', '\u{193A}', '\u{1A17}', '\u{1A75}', '\u{1A76}', '\u{1A77}', '\u{1A78}', '\u{1A79}', '\u{1A7A}', '\u{1A7B}', '\u{1A7C}', '\u{1B6B}', '\u{1B6D}', '\u{1B6E}', '\u{1B6F}', '\u{1B70}', '\u{1B71}', '\u{1B72}', '\u{1B73}', '\u{1CD0}', '\u{1CD1}', '\u{1CD2}', '\u{1CDA}', '\u{1CDB}', '\u{1CE0}', '\u{1DC0}', '\u{1DC1}', '\u{1DC3}', '\u{1DC4}', '\u{1DC5}', '\u{1DC6}', '\u{1DC7}', '\u{1DC8}', '\u{1DC9}', '\u{1DCB}', '\u{1DCC}', '\u{1DD1}', '\u{1DD2}', '\u{1DD3}', '\u{1DD4}', '\u{1DD5}', '\u{1DD6}', '\u{1DD7}', '\u{1DD8}', '\u{1DD9}', '\u{1DDA}', '\u{1DDB}', '\u{1DDC}', '\u{1DDD}', '\u{1DDE}', '\u{1DDF}', '\u{1DE0}', '\u{1DE1}', '\u{1DE2}', '\u{1DE3}', '\u{1DE4}', '\u{1DE5}', '\u{1DE6}', '\u{1DFE}', '\u{20D0}', '\u{20D1}', '\u{20D4}', '\u{20D5}', '\u{20D6}', '\u{20D7}', '\u{20DB}', '\u{20DC}', '\u{20E1}', '\u{20E7}', '\u{20E9}', '\u{20F0}', '\u{2CEF}', '\u{2CF0}', '\u{2CF1}', '\u{2DE0}', '\u{2DE1}', '\u{2DE2}', '\u{2DE3}', '\u{2DE4}', '\u{2DE5}', '\u{2DE6}', '\u{2DE7}', '\u{2DE8}', '\u{2DE9}', '\u{2DEA}', '\u{2DEB}', '\u{2DEC}', '\u{2DED}', '\u{2DEE}', '\u{2DEF}', '\u{2DF0}', '\u{2DF1}', '\u{2DF2}', '\u{2DF3}', '\u{2DF4}', '\u{2DF5}', '\u{2DF6}', '\u{2DF7}', '\u{2DF8}', '\u{2DF9}', '\u{2DFA}', '\u{2DFB}', '\u{2DFC}', '\u{2DFD}', '\u{2DFE}', '\u{2DFF}', '\u{A66F}', '\u{A67C}', '\u{A67D}', '\u{A6F0}', '\u{A6F1}', '\u{A8E0}', '\u{A8E1}', '\u{A8E2}', '\u{A8E3}', '\u{A8E4}', '\u{A8E5}', '\u{A8E6}', '\u{A8E7}', '\u{A8E8}', '\u{A8E9}', '\u{A8EA}', '\u{A8EB}', '\u{A8EC}', '\u{A8ED}', '\u{A8EE}', '\u{A8EF}', '\u{A8F0}', '\u{A8F1}', '\u{AAB0}', '\u{AAB2}', '\u{AAB3}', '\u{AAB7}', '\u{AAB8}', '\u{AABE}', '\u{AABF}', '\u{AAC1}', '\u{FE20}', '\u{FE21}', '\u{FE22}', '\u{FE23}', '\u{FE24}', '\u{FE25}', '\u{FE26}', '\u{10A0F}', '\u{10A38}', '\u{1D185}', '\u{1D186}', '\u{1D187}', '\u{1D188}', '\u{1D189}', '\u{1D1AA}', '\u{1D1AB}', '\u{1D1AC}', '\u{1D1AD}', '\u{1D242}', '\u{1D243}', '\u{1D244}', ]; pub(crate) struct Kgp; impl Kgp { pub(crate) async fn image_show(path: PathBuf, max: Rect) -> Result { let img = Image::downscale(path, max).await?; let area = Image::pixel_area((img.width(), img.height()), max); let b1 = Self::encode(img).await?; let b2 = Self::place(&area)?; Adapter::Kgp.image_hide()?; Adapter::shown_store(area); Emulator::move_lock((area.x, area.y), |w| { w.write_all(&b1)?; w.write_all(&b2)?; Ok(area) }) } pub(crate) fn image_erase(area: Rect) -> Result<()> { let s = " ".repeat(area.width as usize); Emulator::move_lock((0, 0), |w| { for y in area.top()..area.bottom() { queue!(w, MoveTo(area.x, y))?; write!(w, "{s}")?; } write!(w, "{START}_Gq=2,a=d,d=A{ESCAPE}\\{CLOSE}")?; Ok(()) }) } async fn encode(img: DynamicImage) -> Result> { fn output(raw: &[u8], format: u8, size: (u32, u32)) -> Result> { let b64 = general_purpose::STANDARD.encode(raw).into_bytes(); let mut it = b64.chunks(4096).peekable(); let mut buf = Vec::with_capacity(b64.len() + it.len() * 50); if let Some(first) = it.next() { write!( buf, "{START}_Gq=2,a=T,C=1,U=1,f={format},s={},v={},i={},m={};{}{ESCAPE}\\{CLOSE}", size.0, size.1, Kgp::image_id(), it.peek().is_some() as u8, unsafe { str::from_utf8_unchecked(first) }, )?; } while let Some(chunk) = it.next() { write!(buf, "{START}_Gm={};{}{ESCAPE}\\{CLOSE}", it.peek().is_some() as u8, unsafe { str::from_utf8_unchecked(chunk) })?; } write!(buf, "{CLOSE}")?; Ok(buf) } let size = (img.width(), img.height()); tokio::task::spawn_blocking(move || match img { DynamicImage::ImageRgb8(v) => output(v.as_raw(), 24, size), DynamicImage::ImageRgba8(v) => output(v.as_raw(), 32, size), v => output(v.into_rgb8().as_raw(), 24, size), }) .await? } fn place(area: &Rect) -> Result> { let mut buf = Vec::with_capacity(area.width as usize * area.height as usize * 3 + 50); let id = Self::image_id(); let (r, g, b) = ((id >> 16) & 0xff, (id >> 8) & 0xff, id & 0xff); write!(buf, "\x1b[38;2;{r};{g};{b}m")?; for y in 0..area.height { write!(buf, "\x1b[{};{}H", area.y + y + 1, area.x + 1)?; for x in 0..area.width { write!(buf, "\u{10EEEE}")?; write!(buf, "{}", *DIACRITICS.get(y as usize).unwrap_or(&DIACRITICS[0]))?; write!(buf, "{}", *DIACRITICS.get(x as usize).unwrap_or(&DIACRITICS[0]))?; } } Ok(buf) } fn image_id() -> u32 { static CACHE: SyncCell> = SyncCell::new(None); match CACHE.get() { Some(n) => n, None => { let n = std::process::id() % (0xffffff + 1); CACHE.set(Some(n)); n } } } } ================================================ FILE: yazi-adapter/src/drivers/kgp_old.rs ================================================ use core::str; use std::{io::Write, path::PathBuf}; use anyhow::Result; use base64::{Engine, engine::general_purpose}; use image::DynamicImage; use ratatui::layout::Rect; use yazi_emulator::{CLOSE, ESCAPE, Emulator, START}; use yazi_tty::TTY; use crate::{Image, adapter::Adapter}; pub(crate) struct KgpOld; impl KgpOld { pub(crate) async fn image_show(path: PathBuf, max: Rect) -> Result { let img = Image::downscale(path, max).await?; let area = Image::pixel_area((img.width(), img.height()), max); let b = Self::encode(img).await?; Adapter::KgpOld.image_hide()?; Adapter::shown_store(area); Emulator::move_lock((area.x, area.y), |w| { w.write_all(&b)?; Ok(area) }) } pub(crate) fn image_erase(_: Rect) -> Result<()> { let mut w = TTY.lockout(); write!(w, "{START}_Gq=2,a=d,d=A{ESCAPE}\\{CLOSE}")?; w.flush()?; Ok(()) } async fn encode(img: DynamicImage) -> Result> { fn output(raw: &[u8], format: u8, size: (u32, u32)) -> Result> { let b64 = general_purpose::STANDARD.encode(raw).into_bytes(); let mut it = b64.chunks(4096).peekable(); let mut buf = Vec::with_capacity(b64.len() + it.len() * 50); if let Some(first) = it.next() { write!( buf, "{START}_Gq=2,a=T,z=-1,C=1,f={format},s={},v={},m={};{}{ESCAPE}\\{CLOSE}", size.0, size.1, it.peek().is_some() as u8, unsafe { str::from_utf8_unchecked(first) }, )?; } while let Some(chunk) = it.next() { write!(buf, "{START}_Gm={};{}{ESCAPE}\\{CLOSE}", it.peek().is_some() as u8, unsafe { str::from_utf8_unchecked(chunk) })?; } write!(buf, "{CLOSE}")?; Ok(buf) } let size = (img.width(), img.height()); tokio::task::spawn_blocking(move || match img { DynamicImage::ImageRgb8(v) => output(v.as_raw(), 24, size), DynamicImage::ImageRgba8(v) => output(v.as_raw(), 32, size), v => output(v.into_rgb8().as_raw(), 24, size), }) .await? } } ================================================ FILE: yazi-adapter/src/drivers/mod.rs ================================================ yazi_macro::mod_flat!(chafa iip kgp kgp_old sixel ueberzug); ================================================ FILE: yazi-adapter/src/drivers/sixel.rs ================================================ use std::{io::Write, path::PathBuf}; use anyhow::{Result, bail}; use crossterm::{cursor::MoveTo, queue}; use image::{DynamicImage, GenericImageView, RgbImage}; use palette::{Srgb, cast::ComponentsAs}; use quantette::{PaletteSize, color_map::IndexedColorMap, wu::{BinnerU8x3, WuU8x3}}; use ratatui::layout::Rect; use yazi_emulator::{CLOSE, ESCAPE, Emulator, START}; use crate::{Image, adapter::Adapter}; pub(crate) struct Sixel; struct QuantizeOutput { indices: Vec, palette: Vec, } impl Sixel { pub(crate) async fn image_show(path: PathBuf, max: Rect) -> Result { let img = Image::downscale(path, max).await?; let area = Image::pixel_area((img.width(), img.height()), max); let b = Self::encode(img).await?; Adapter::Sixel.image_hide()?; Adapter::shown_store(area); Emulator::move_lock((area.x, area.y), |w| { w.write_all(&b)?; Ok(area) }) } pub(crate) fn image_erase(area: Rect) -> Result<()> { let s = " ".repeat(area.width as usize); Emulator::move_lock((0, 0), |w| { for y in area.top()..area.bottom() { queue!(w, MoveTo(area.x, y))?; write!(w, "{s}")?; } Ok(()) }) } async fn encode(img: DynamicImage) -> Result> { let alpha = img.color().has_alpha(); if img.width() == 0 || img.height() == 0 { bail!("image is empty"); } let (qo, img) = tokio::task::spawn_blocking(move || match &img { DynamicImage::ImageRgb8(rgb) => Self::quantify(rgb, false).map(|q| (q, img)), _ => Self::quantify(&img.to_rgb8(), alpha).map(|q| (q, img)), }) .await??; tokio::task::spawn_blocking(move || { let mut buf = vec![]; write!(buf, "{START}P9;1q\"1;1;{};{}", img.width(), img.height())?; // Palette for (i, c) in qo.palette.iter().enumerate() { write!( buf, "#{};2;{};{};{}", i + alpha as usize, c.red as u16 * 100 / 255, c.green as u16 * 100 / 255, c.blue as u16 * 100 / 255 )?; } for y in 0..img.height() { let c = (b'?' + (1 << (y % 6))) as char; let mut last = 0; let mut repeat = 0usize; for x in 0..img.width() { let idx = if img.get_pixel(x, y)[3] == 0 { 0 } else { qo.indices[y as usize * img.width() as usize + x as usize] + alpha as u8 }; if idx == last || repeat == 0 { (last, repeat) = (idx, repeat + 1); continue; } if repeat > 1 { write!(buf, "#{last}!{repeat}{c}")?; } else { write!(buf, "#{last}{c}")?; } (last, repeat) = (idx, 1); } if repeat > 1 { write!(buf, "#{last}!{repeat}{c}")?; } else { write!(buf, "#{last}{c}")?; } write!(buf, "$")?; if y % 6 == 5 { write!(buf, "-")?; } } write!(buf, "{ESCAPE}\\{CLOSE}")?; Ok(buf) }) .await? } fn quantify(rgb: &RgbImage, alpha: bool) -> Result>> { let buf = &rgb.as_raw()[..(rgb.pixels().len() * 3)]; let colors: &[Srgb] = buf.components_as(); let wu = WuU8x3::run_slice(colors, BinnerU8x3::rgb())?; let color_map = wu.color_map(PaletteSize::try_from(256u16 - alpha as u16)?); Ok(QuantizeOutput { indices: color_map.map_to_indices(colors), palette: color_map.into_palette().into_vec(), }) } } ================================================ FILE: yazi-adapter/src/drivers/ueberzug.rs ================================================ use std::{path::PathBuf, process::Stdio}; use anyhow::{Result, bail}; use image::ImageReader; use ratatui::layout::Rect; use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}}; use tracing::{debug, warn}; use yazi_config::YAZI; use yazi_emulator::Dimension; use yazi_shared::{LOG_LEVEL, RoCell, env_exists}; use crate::Adapter; type Cmd = Option<(PathBuf, Rect)>; static DEMON: RoCell>> = RoCell::new(); pub(crate) struct Ueberzug; impl Ueberzug { pub(crate) fn start(adapter: Adapter) { if !adapter.needs_ueberzug() { return DEMON.init(None); } let mut child = Self::create_demon(adapter).ok(); let (tx, mut rx) = mpsc::unbounded_channel(); tokio::spawn(async move { while let Some(cmd) = rx.recv().await { let exit = child.as_mut().and_then(|c| c.try_wait().ok()); if exit != Some(None) { child = None; } if child.is_none() { child = Self::create_demon(adapter).ok(); } if let Some(c) = &mut child { Self::send_command(adapter, c, cmd).await.ok(); } } }); DEMON.init(Some(tx)) } pub(crate) async fn image_show(path: PathBuf, max: Rect) -> Result { let Some(tx) = &*DEMON else { bail!("uninitialized ueberzugpp"); }; let (w, h, path) = tokio::task::spawn_blocking(move || { ImageReader::open(&path)?.with_guessed_format()?.into_dimensions().map(|(w, h)| (w, h, path)) }) .await??; let area = Dimension::cell_size() .map(|(cw, ch)| Rect { x: max.x, y: max.y, width: max.width.min((w.min(YAZI.preview.max_width as _) as f64 / cw).ceil() as _), height: max.height.min((h.min(YAZI.preview.max_height as _) as f64 / ch).ceil() as _), }) .unwrap_or(max); tx.send(Some((path, area)))?; Adapter::shown_store(area); Ok(area) } pub(crate) fn image_erase(_: Rect) -> Result<()> { if let Some(tx) = &*DEMON { Ok(tx.send(None)?) } else { bail!("uninitialized ueberzugpp"); } } // Currently Überzug++'s Wayland output only supports Sway, Hyprland and Wayfire // as it requires information from specific compositor socket directly. // These environment variables are from ueberzugpp src/canvas/wayland/config.cpp pub(crate) fn supported_compositor() -> bool { env_exists("SWAYSOCK") || env_exists("HYPRLAND_INSTANCE_SIGNATURE") || env_exists("WAYFIRE_SOCKET") } fn create_demon(adapter: Adapter) -> Result { let result = Command::new("ueberzugpp") .args(["layer", "-so", &adapter.to_string()]) .env("SPDLOG_LEVEL", if LOG_LEVEL.get().is_none() { "" } else { "debug" }) .kill_on_drop(true) .stdin(Stdio::piped()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn(); if let Err(ref e) = result { warn!("Failed to start ueberzugpp: {e}"); } Ok(result?) } fn adjust_rect(mut rect: Rect) -> Rect { let scale = YAZI.preview.ueberzug_scale; let (x, y, w, h) = YAZI.preview.ueberzug_offset; rect.x = 0f32.max(rect.x as f32 * scale + x) as u16; rect.y = 0f32.max(rect.y as f32 * scale + y) as u16; rect.width = 0f32.max(rect.width as f32 * scale + w) as u16; rect.height = 0f32.max(rect.height as f32 * scale + h) as u16; rect } async fn send_command(adapter: Adapter, child: &mut Child, cmd: Cmd) -> Result<()> { let s = if let Some((path, rect)) = cmd { debug!("ueberzugpp rect before adjustment: {:?}", rect); let rect = Self::adjust_rect(rect); debug!("ueberzugpp rect after adjustment: {:?}", rect); format!( r#"{{"action":"add","identifier":"yazi","x":{},"y":{},"max_width":{},"max_height":{},"path":"{}"}}{}"#, rect.x, rect.y, rect.width, rect.height, path.to_string_lossy(), '\n' ) } else { format!(r#"{{"action":"remove","identifier":"yazi"}}{}"#, '\n') }; debug!("`ueberzugpp layer -so {adapter}` command: {s}"); child.stdin.as_mut().unwrap().write_all(s.as_bytes()).await?; Ok(()) } } ================================================ FILE: yazi-adapter/src/icc.rs ================================================ use anyhow::Context; use image::{ColorType, DynamicImage, GrayAlphaImage, GrayImage, ImageDecoder, RgbImage, RgbaImage, metadata::Cicp}; use moxcms::{CicpColorPrimaries, ColorProfile, DataColorSpace, Layout, TransferCharacteristics, TransformOptions}; pub(super) struct Icc; impl Icc { pub(super) fn transform(mut decoder: impl ImageDecoder) -> anyhow::Result { if let Some(layout) = Self::color_type_to_layout(decoder.color_type()) && let Some(icc) = decoder.icc_profile().unwrap_or_default() && let Ok(profile) = ColorProfile::new_from_slice(&icc) && Self::requires_transform(&profile) { let mut buf = vec![0u8; decoder.total_bytes() as usize]; let (w, h) = decoder.dimensions(); decoder.read_image(&mut buf)?; let transformer = profile // TODO: Use `create_transform_in_place_nbit` in the next minor version of moxcms. .create_transform_8bit(layout, &ColorProfile::new_srgb(), layout, TransformOptions::default()) .context("cannot make a profile transformer")?; let mut converted = vec![0u8; buf.len()]; transformer.transform(&buf, &mut converted).context("cannot transform image")?; let mut image: DynamicImage = match layout { Layout::Gray => { GrayImage::from_raw(w, h, converted).context("cannot load transformed image")?.into() } Layout::GrayAlpha => { GrayAlphaImage::from_raw(w, h, converted).context("cannot load transformed image")?.into() } Layout::Rgb => { RgbImage::from_raw(w, h, converted).context("cannot load transformed image")?.into() } Layout::Rgba => { RgbaImage::from_raw(w, h, converted).context("cannot load transformed image")?.into() } _ => unreachable!(), }; image.set_rgb_primaries(Cicp::SRGB.primaries); image.set_transfer_function(Cicp::SRGB.transfer); Ok(image) } else { Ok(DynamicImage::from_decoder(decoder)?) } } fn color_type_to_layout(color_type: ColorType) -> Option { match color_type { ColorType::L8 => Some(Layout::Gray), ColorType::La8 => Some(Layout::GrayAlpha), ColorType::Rgb8 => Some(Layout::Rgb), ColorType::Rgba8 => Some(Layout::Rgba), _ => None, } } fn requires_transform(profile: &ColorProfile) -> bool { if profile.color_space == DataColorSpace::Cmyk { return false; } profile.cicp.is_none_or(|c| { c.color_primaries != CicpColorPrimaries::Bt709 || c.transfer_characteristics != TransferCharacteristics::Srgb }) } } ================================================ FILE: yazi-adapter/src/image.rs ================================================ use std::path::{Path, PathBuf}; use anyhow::Result; use image::{DynamicImage, ImageDecoder, ImageError, ImageReader, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation}; use ratatui::layout::Rect; use yazi_config::YAZI; use yazi_emulator::Dimension; use yazi_fs::provider::{Provider, local::Local}; use crate::Icc; pub struct Image; impl Image { pub async fn precache(src: PathBuf, cache: &Path) -> Result<()> { let (mut img, orientation) = Self::decode_from(src).await?; let (w, h) = Self::flip_size(orientation, (YAZI.preview.max_width, YAZI.preview.max_height)); let buf = tokio::task::spawn_blocking(move || { if img.width() > w || img.height() > h { img = img.resize(w, h, Self::filter()); } if orientation != Orientation::NoTransforms { img.apply_orientation(orientation); } let mut buf = Vec::new(); if img.color().has_alpha() { let encoder = PngEncoder::new(&mut buf); img.write_with_encoder(encoder)?; } else { let encoder = JpegEncoder::new_with_quality(&mut buf, YAZI.preview.image_quality); img.write_with_encoder(encoder)?; } Ok::<_, ImageError>(buf) }) .await??; Ok(Local::regular(&cache).write(buf).await?) } pub(super) async fn downscale(path: PathBuf, rect: Rect) -> Result { let (mut img, orientation) = Self::decode_from(path).await?; let (w, h) = Self::flip_size(orientation, Self::max_pixel(rect)); // Fast path. if img.width() <= w && img.height() <= h && orientation == Orientation::NoTransforms { return Ok(img); } let img = tokio::task::spawn_blocking(move || { if img.width() > w || img.height() > h { img = img.resize(w, h, Self::filter()) } if orientation != Orientation::NoTransforms { img.apply_orientation(orientation); } img }) .await?; Ok(img) } pub(super) fn max_pixel(rect: Rect) -> (u16, u16) { Dimension::cell_size() .map(|(cw, ch)| { let (w, h) = ((rect.width as f64 * cw) as u16, (rect.height as f64 * ch) as u16); (w.min(YAZI.preview.max_width), h.min(YAZI.preview.max_height)) }) .unwrap_or((YAZI.preview.max_width, YAZI.preview.max_height)) } pub(super) fn pixel_area(size: (u32, u32), rect: Rect) -> Rect { Dimension::cell_size() .map(|(cw, ch)| Rect { x: rect.x, y: rect.y, width: (size.0 as f64 / cw).ceil() as u16, height: (size.1 as f64 / ch).ceil() as u16, }) .unwrap_or(rect) } fn filter() -> FilterType { match YAZI.preview.image_filter.as_str() { "nearest" => FilterType::Nearest, "triangle" => FilterType::Triangle, "catmull-rom" => FilterType::CatmullRom, "gaussian" => FilterType::Gaussian, "lanczos3" => FilterType::Lanczos3, _ => FilterType::Triangle, } } async fn decode_from(path: PathBuf) -> Result<(DynamicImage, Orientation)> { let mut limits = Limits::no_limits(); if YAZI.tasks.image_alloc > 0 { limits.max_alloc = Some(YAZI.tasks.image_alloc as u64); } if YAZI.tasks.image_bound[0] > 0 { limits.max_image_width = Some(YAZI.tasks.image_bound[0] as u32); } if YAZI.tasks.image_bound[1] > 0 { limits.max_image_height = Some(YAZI.tasks.image_bound[1] as u32); } tokio::task::spawn_blocking(move || { let mut reader = ImageReader::open(path)?; reader.limits(limits); let mut decoder = reader.with_guessed_format()?.into_decoder()?; let orientation = decoder.orientation().unwrap_or(Orientation::NoTransforms); Ok((Icc::transform(decoder)?, orientation)) }) .await .map_err(|e| ImageError::IoError(e.into()))? } fn flip_size(orientation: Orientation, (w, h): (u16, u16)) -> (u32, u32) { use image::metadata::Orientation::{Rotate90, Rotate90FlipH, Rotate270, Rotate270FlipH}; match orientation { Rotate90 | Rotate270 | Rotate90FlipH | Rotate270FlipH => (h as u32, w as u32), _ => (w as u32, h as u32), } } } ================================================ FILE: yazi-adapter/src/info.rs ================================================ use std::path::PathBuf; use image::{ImageDecoder, ImageError}; pub type ImageFormat = image::ImageFormat; pub type ImageColor = image::ColorType; pub type ImageOrientation = image::metadata::Orientation; #[derive(Clone, Copy)] pub struct ImageInfo { pub format: ImageFormat, pub width: u32, pub height: u32, pub color: ImageColor, pub orientation: Option, } impl ImageInfo { pub async fn new(path: PathBuf) -> image::ImageResult { tokio::task::spawn_blocking(move || { let reader = image::ImageReader::open(path)?.with_guessed_format()?; let Some(format) = reader.format() else { return Err(ImageError::IoError(std::io::Error::new( std::io::ErrorKind::InvalidData, "unknown image format", ))); }; let mut decoder = reader.into_decoder()?; let (width, height) = decoder.dimensions(); Ok(Self { format, width, height, color: decoder.color_type(), orientation: decoder.orientation().ok(), }) }) .await .map_err(|e| ImageError::IoError(e.into()))? } } ================================================ FILE: yazi-adapter/src/lib.rs ================================================ yazi_macro::mod_pub!(drivers); yazi_macro::mod_flat!(adapter adapters icc image info); use yazi_emulator::{Brand, CLOSE, EMULATOR, ESCAPE, Emulator, Mux, START, TMUX}; use yazi_shared::{SyncCell, in_wsl}; pub static ADAPTOR: SyncCell = SyncCell::new(Adapter::Chafa); // Image state static SHOWN: SyncCell> = SyncCell::new(None); // WSL support pub static WSL: SyncCell = SyncCell::new(false); pub fn init() -> anyhow::Result<()> { // WSL support WSL.set(in_wsl()); // Emulator detection let mut emulator = Emulator::detect().unwrap_or_default(); TMUX.set(emulator.kind.left() == Some(Brand::Tmux)); // Tmux support if TMUX.get() { ESCAPE.set("\x1b\x1b"); START.set("\x1bPtmux;\x1b\x1b"); CLOSE.set("\x1b\\"); Mux::tmux_passthrough(); emulator = Emulator::detect().unwrap_or_default(); } EMULATOR.init(emulator); yazi_config::init_flavor(EMULATOR.light)?; ADAPTOR.set(Adapter::matches(&EMULATOR)); ADAPTOR.get().start(); Ok(()) } ================================================ FILE: yazi-binding/Cargo.toml ================================================ [package] name = "yazi-binding" description = "Yazi Lua bindings" version.workspace = true edition.workspace = true license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true rust-version.workspace = true [lints] workspace = true [features] default = [ "vendored-lua" ] vendored-lua = [ "mlua/vendored" ] [dependencies] yazi-adapter = { path = "../yazi-adapter", version = "26.2.2" } yazi-codegen = { path = "../yazi-codegen", version = "26.2.2" } yazi-config = { path = "../yazi-config", version = "26.2.2" } yazi-fs = { path = "../yazi-fs", version = "26.2.2" } yazi-macro = { path = "../yazi-macro", version = "26.2.2" } yazi-shared = { path = "../yazi-shared", version = "26.2.2" } yazi-vfs = { path = "../yazi-vfs", version = "26.2.2" } yazi-widgets = { path = "../yazi-widgets", version = "26.2.2" } # External dependencies ansi-to-tui = { workspace = true } anyhow = { workspace = true } crossterm = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } mlua = { workspace = true } paste = { workspace = true } ratatui = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } tracing = { workspace = true } unicode-width = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } ================================================ FILE: yazi-binding/README.md ================================================ # yazi-binding This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. [source]: https://github.com/sxyazi/yazi ================================================ FILE: yazi-binding/src/access.rs ================================================ use mlua::{AnyUserData, IntoLuaMulti, UserData, UserDataMethods, Value}; use yazi_fs::provider::FileBuilder; use crate::{Error, Fd, UrlRef}; #[derive(Default)] pub struct Access(yazi_vfs::provider::Gate); impl UserData for Access { fn add_methods>(methods: &mut M) { methods.add_function_mut("append", |_, (ud, append): (AnyUserData, bool)| { ud.borrow_mut::()?.0.append(append); Ok(ud) }); methods.add_function_mut("create", |_, (ud, create): (AnyUserData, bool)| { ud.borrow_mut::()?.0.create(create); Ok(ud) }); methods.add_function_mut("create_new", |_, (ud, create_new): (AnyUserData, bool)| { ud.borrow_mut::()?.0.create_new(create_new); Ok(ud) }); methods.add_async_method("open", |lua, me, url: UrlRef| async move { match me.0.open(&*url).await { Ok(fd) => Fd(fd).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); methods.add_function_mut("read", |_, (ud, read): (AnyUserData, bool)| { ud.borrow_mut::()?.0.read(read); Ok(ud) }); methods.add_function_mut("truncate", |_, (ud, truncate): (AnyUserData, bool)| { ud.borrow_mut::()?.0.truncate(truncate); Ok(ud) }); methods.add_function_mut("write", |_, (ud, write): (AnyUserData, bool)| { ud.borrow_mut::()?.0.write(write); Ok(ud) }); } } ================================================ FILE: yazi-binding/src/calculator.rs ================================================ use mlua::{IntoLuaMulti, UserData, UserDataFields, UserDataMethods, Value}; use crate::{Cha, Error}; pub enum SizeCalculator { Local(yazi_fs::provider::local::SizeCalculator), Remote(yazi_vfs::provider::SizeCalculator), } impl UserData for SizeCalculator { fn add_fields>(fields: &mut F) { fields.add_field_method_get("cha", |_, me| { Ok(Cha(match me { Self::Local(c) => c.cha(), Self::Remote(c) => c.cha(), })) }); } fn add_methods>(methods: &mut M) { methods.add_async_method_mut("recv", |lua, mut me, ()| async move { let next = match &mut *me { Self::Local(c) => c.next().await, Self::Remote(c) => c.next().await, }; match next { Ok(value) => value.into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); } } ================================================ FILE: yazi-binding/src/cha.rs ================================================ use std::{ops::Deref, time::{Duration, SystemTime}}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, UserData, UserDataFields, UserDataMethods}; use yazi_fs::{FsHash128, cha::{ChaKind, ChaMode}}; #[derive(Clone, Copy, FromLua)] pub struct Cha(pub yazi_fs::cha::Cha); impl Deref for Cha { type Target = yazi_fs::cha::Cha; fn deref(&self) -> &Self::Target { &self.0 } } impl Cha { pub fn install(lua: &Lua) -> mlua::Result<()> { fn parse_time(f: Option) -> mlua::Result> { Ok(match f { Some(n) if n >= 0.0 => Some(SystemTime::UNIX_EPOCH + Duration::from_secs_f64(n)), Some(n) => Err(format!("Invalid timestamp: {n}").into_lua_err())?, None => None, }) } lua.globals().raw_set( "Cha", lua.create_function(|lua, t: Table| { let kind = ChaKind::from_bits(t.raw_get("kind").unwrap_or_default()) .ok_or_else(|| "Invalid kind".into_lua_err())?; let mode = ChaMode::try_from(t.raw_get::("mode")?)?; Self(yazi_fs::cha::Cha { kind, mode, len: t.raw_get("len").unwrap_or_default(), atime: parse_time(t.raw_get("atime").ok())?, btime: parse_time(t.raw_get("btime").ok())?, ctime: parse_time(t.raw_get("ctime").ok())?, mtime: parse_time(t.raw_get("mtime").ok())?, dev: t.raw_get("dev").unwrap_or_default(), uid: t.raw_get("uid").unwrap_or_default(), gid: t.raw_get("gid").unwrap_or_default(), nlink: t.raw_get("nlink").unwrap_or_default(), }) .into_lua(lua) })?, ) } } impl UserData for Cha { fn add_fields>(fields: &mut F) { fields.add_field_method_get("mode", |_, me| Ok(me.mode.bits())); fields.add_field_method_get("is_dir", |_, me| Ok(me.is_dir())); fields.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden())); fields.add_field_method_get("is_link", |_, me| Ok(me.is_link())); fields.add_field_method_get("is_orphan", |_, me| Ok(me.is_orphan())); fields.add_field_method_get("is_dummy", |_, me| Ok(me.is_dummy())); fields.add_field_method_get("is_block", |_, me| Ok(me.is_block())); fields.add_field_method_get("is_char", |_, me| Ok(me.is_char())); fields.add_field_method_get("is_fifo", |_, me| Ok(me.is_fifo())); fields.add_field_method_get("is_sock", |_, me| Ok(me.is_sock())); fields.add_field_method_get("is_exec", |_, me| Ok(me.is_exec())); fields.add_field_method_get("is_sticky", |_, me| Ok(me.is_sticky())); fields.add_field_method_get("len", |_, me| Ok(me.len)); fields.add_field_method_get("atime", |_, me| Ok(me.atime_dur().ok().map(|d| d.as_secs_f64()))); fields.add_field_method_get("btime", |_, me| Ok(me.btime_dur().ok().map(|d| d.as_secs_f64()))); fields.add_field_method_get("ctime", |_, me| Ok(me.ctime_dur().ok().map(|d| d.as_secs_f64()))); fields.add_field_method_get("mtime", |_, me| Ok(me.mtime_dur().ok().map(|d| d.as_secs_f64()))); fields.add_field_method_get("dev", |_, me| Ok(me.dev)); fields.add_field_method_get("uid", |_, me| Ok(me.uid)); fields.add_field_method_get("gid", |_, me| Ok(me.gid)); fields.add_field_method_get("nlink", |_, me| Ok(me.nlink)); } fn add_methods>(methods: &mut M) { methods.add_method("hash", |_, me, long: Option| { Ok(if long.unwrap_or(false) { format!("{:x}", me.hash_u128()) } else { Err("Short hash not supported".into_lua_err())? }) }); methods.add_method("perm", |lua, _me, ()| { Ok( #[cfg(unix)] lua.create_string(_me.mode.permissions(_me.is_dummy())), #[cfg(windows)] Ok::<_, mlua::Error>(mlua::Value::Nil), ) }); } } ================================================ FILE: yazi-binding/src/chan.rs ================================================ use mlua::{ExternalError, FromLua, IntoLua, IntoLuaMulti, UserData, Value}; use yazi_codegen::FromLuaOwned; use crate::Error; #[derive(FromLuaOwned)] pub struct MpscTx(pub tokio::sync::mpsc::Sender); pub struct MpscRx(pub tokio::sync::mpsc::Receiver); impl UserData for MpscTx { fn add_methods>(methods: &mut M) { methods.add_async_method("send", |lua, me, value: Value| async move { match me.0.send(T::from_lua(value, &lua)?).await { Ok(()) => true.into_lua_multi(&lua), Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(&lua), } }); } } impl UserData for MpscRx { fn add_methods>(methods: &mut M) { methods.add_async_method_mut("recv", |lua, mut me, ()| async move { match me.0.recv().await { Some(value) => (value, true).into_lua_multi(&lua), None => (Value::Nil, false).into_lua_multi(&lua), } }); } } #[derive(FromLuaOwned)] pub struct MpscUnboundedTx(pub tokio::sync::mpsc::UnboundedSender); pub struct MpscUnboundedRx(pub tokio::sync::mpsc::UnboundedReceiver); impl UserData for MpscUnboundedTx { fn add_methods>(methods: &mut M) { methods.add_method("send", |lua, me, value: Value| match me.0.send(T::from_lua(value, lua)?) { Ok(()) => true.into_lua_multi(lua), Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(lua), }); } } impl UserData for MpscUnboundedRx { fn add_methods>(methods: &mut M) { methods.add_async_method_mut("recv", |lua, mut me, ()| async move { match me.0.recv().await { Some(value) => (value, true).into_lua_multi(&lua), None => (Value::Nil, false).into_lua_multi(&lua), } }); } } #[derive(FromLuaOwned)] pub struct OneshotTx(pub Option>); pub struct OneshotRx(pub Option>); impl UserData for OneshotTx { fn add_methods>(methods: &mut M) { methods.add_method_mut("send", |lua, me, value: Value| { let Some(tx) = me.0.take() else { return Err("Oneshot sender already used".into_lua_err()); }; match tx.send(T::from_lua(value, lua)?) { Ok(()) => true.into_lua_multi(lua), Err(_) => (false, Error::custom("Oneshot receiver closed")).into_lua_multi(lua), } }); } } impl UserData for OneshotRx { fn add_methods>(methods: &mut M) { methods.add_async_method_mut("recv", |lua, mut me, ()| async move { let Some(rx) = me.0.take() else { return Err("Oneshot receiver already used".into_lua_err()); }; match rx.await { Ok(value) => value.into_lua_multi(&lua), Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua), } }); } } ================================================ FILE: yazi-binding/src/chord_cow.rs ================================================ use mlua::UserData; use yazi_codegen::FromLuaOwned; #[derive(Clone, FromLuaOwned)] pub struct ChordCow(pub yazi_config::keymap::ChordCow); impl From for ChordCow { fn from(value: yazi_config::keymap::ChordCow) -> Self { Self(value) } } impl From for yazi_config::keymap::ChordCow { fn from(value: ChordCow) -> Self { value.0 } } impl UserData for ChordCow {} ================================================ FILE: yazi-binding/src/color.rs ================================================ use std::str::FromStr; use mlua::{ExternalError, ExternalResult, FromLua, Lua, UserData, Value}; #[derive(Clone, Copy, Default)] pub struct Color(pub ratatui::style::Color); impl FromLua for Color { fn from_lua(value: Value, _: &Lua) -> mlua::Result { Ok(Self(match value { Value::String(s) => ratatui::style::Color::from_str(&s.to_str()?).into_lua_err()?, Value::UserData(ud) => ud.borrow::()?.0, _ => Err("expected a Color".into_lua_err())?, })) } } impl UserData for Color {} ================================================ FILE: yazi-binding/src/composer.rs ================================================ use hashbrown::HashMap; use mlua::{Lua, MetaMethod, UserData, UserDataMethods, Value}; pub type ComposerGet = fn(&Lua, &[u8]) -> mlua::Result; pub type ComposerSet = fn(&Lua, &[u8], Value) -> mlua::Result; pub struct Composer { get: G, set: S, parent: Option<(G, S)>, cache: HashMap, Value>, } impl Composer where G: Fn(&Lua, &[u8]) -> mlua::Result + 'static, S: Fn(&Lua, &[u8], Value) -> mlua::Result + 'static, { #[inline] pub fn new(get: G, set: S) -> Self { Self { get, set, parent: None, cache: Default::default() } } #[inline] pub fn with_parent(get: G, set: S, p_get: G, p_set: S) -> Self { Self { get, set, parent: Some((p_get, p_set)), cache: Default::default() } } } impl UserData for Composer where G: Fn(&Lua, &[u8]) -> mlua::Result + 'static, S: Fn(&Lua, &[u8], Value) -> mlua::Result + 'static, { fn add_methods>(methods: &mut M) { methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: mlua::String| { let key = key.as_bytes(); if let Some(v) = me.cache.get(key.as_ref()) { return Ok(v.clone()); } let mut value = (me.get)(lua, &key)?; if value.is_nil() && let Some((p_get, _)) = &me.parent { value = p_get(lua, &key)?; } me.cache.insert(key.to_owned(), value.clone()); Ok(value) }); methods.add_meta_method_mut( MetaMethod::NewIndex, |lua, me, (key, value): (mlua::String, Value)| { let key = key.as_bytes(); let value = (me.set)(lua, key.as_ref(), value)?; if value.is_nil() { me.cache.remove(key.as_ref()); } else if let Some((_, p_set)) = &me.parent { match p_set(lua, key.as_ref(), value)? { Value::Nil => me.cache.remove(key.as_ref()), v => me.cache.insert(key.to_owned(), v), }; } else { me.cache.insert(key.to_owned(), value); } Ok(()) }, ); } } ================================================ FILE: yazi-binding/src/elements/align.rs ================================================ use std::ops::Deref; use mlua::{FromLua, IntoLua, Lua, Value}; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct Align(pub(super) ratatui::layout::Alignment); impl Deref for Align { type Target = ratatui::layout::Alignment; fn deref(&self) -> &Self::Target { &self.0 } } impl Align { pub fn compose(lua: &Lua) -> mlua::Result { lua.create_table_from([("LEFT", 0), ("CENTER", 1), ("RIGHT", 2)])?.into_lua(lua) } } impl FromLua for Align { fn from_lua(value: Value, _: &Lua) -> mlua::Result { let Value::Integer(n) = value else { return Err(mlua::Error::FromLuaConversionError { from: value.type_name(), to: "Align".to_string(), message: Some("expected an integer representation of Align".to_string()), }); }; Ok(Self(match n { 0 => ratatui::layout::Alignment::Left, 1 => ratatui::layout::Alignment::Center, 2 => ratatui::layout::Alignment::Right, _ => Err(mlua::Error::FromLuaConversionError { from: value.type_name(), to: "Align".to_string(), message: Some("invalid value for Align".to_string()), })?, })) } } ================================================ FILE: yazi-binding/src/elements/area.rs ================================================ use std::fmt::Debug; use mlua::{AnyUserData, ExternalError, FromLua, IntoLua, Lua, Value}; use super::{Pos, Rect}; const EXPECTED: &str = "expected a Pos or Rect"; #[derive(Clone, Copy)] pub enum Area { Pos(Pos), Rect(Rect), } impl Default for Area { fn default() -> Self { Self::Rect(Default::default()) } } impl Area { pub fn size(self) -> ratatui::layout::Size { match self { Self::Pos(pos) => { ratatui::layout::Size { width: pos.offset.width, height: pos.offset.height } } Self::Rect(rect) => (*rect).into(), } } pub fn inner(self, padding: ratatui::widgets::Padding) -> Self { match self { Self::Pos(mut pos) => { pos.pad += padding; Self::Pos(pos) } Self::Rect(rect) => Self::Rect(rect.pad(padding.into())), } } pub fn transform( self, f: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect, ) -> ratatui::layout::Rect { match self { Self::Pos(pos) => *Rect(f(*pos)).pad(pos.pad), Self::Rect(rect) => *rect, } } } impl From for Area { fn from(rect: Rect) -> Self { Self::Rect(rect) } } impl From for Area { fn from(rect: ratatui::layout::Rect) -> Self { Self::Rect(rect.into()) } } impl TryFrom for Area { type Error = mlua::Error; fn try_from(value: AnyUserData) -> Result { Ok(if let Ok(rect) = value.borrow::() { Self::Rect(*rect) } else if let Ok(pos) = value.borrow::() { Self::Pos(*pos) } else { return Err(EXPECTED.into_lua_err()); }) } } impl FromLua for Area { fn from_lua(value: Value, _: &Lua) -> mlua::Result { match value { Value::UserData(ud) => Self::try_from(ud), _ => Err(EXPECTED.into_lua_err()), } } } impl IntoLua for Area { fn into_lua(self, lua: &Lua) -> mlua::Result { match self { Self::Pos(pos) => pos.into_lua(lua), Self::Rect(rect) => rect.into_lua(lua), } } } impl Debug for Area { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Pos(pos) => write!(f, "{:?}", **pos), Self::Rect(rect) => write!(f, "{:?}", **rect), } } } ================================================ FILE: yazi-binding/src/elements/bar.rs ================================================ use mlua::{AnyUserData, IntoLua, Lua, MetaMethod, Table, UserData, Value}; use ratatui::widgets::{Borders, Widget}; use super::{Area, Edge}; #[derive(Clone, Debug, Default)] pub struct Bar { pub(super) area: Area, edge: Edge, symbol: String, style: ratatui::style::Style, } impl Bar { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, edge): (Table, Edge)| Ok(Self { edge, ..Default::default() }))?; let bar = lua.create_table()?; bar.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; bar.into_lua(lua) } } impl Widget for Bar { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { (&self).render(rect, buf); } } impl Widget for &Bar { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { if rect.area() == 0 { return; } let symbol = if !self.symbol.is_empty() { &self.symbol } else if self.edge.intersects(Borders::TOP | Borders::BOTTOM) { "─" } else if self.edge.intersects(Borders::LEFT | Borders::RIGHT) { "│" } else { " " }; if self.edge.contains(Borders::LEFT) { for y in rect.top()..rect.bottom() { buf[(rect.left(), y)].set_style(self.style).set_symbol(symbol); } } if self.edge.contains(Borders::TOP) { for x in rect.left()..rect.right() { buf[(x, rect.top())].set_style(self.style).set_symbol(symbol); } } if self.edge.contains(Borders::RIGHT) { let x = rect.right() - 1; for y in rect.top()..rect.bottom() { buf[(x, y)].set_style(self.style).set_symbol(symbol); } } if self.edge.contains(Borders::BOTTOM) { let y = rect.bottom() - 1; for x in rect.left()..rect.right() { buf[(x, y)].set_style(self.style).set_symbol(symbol); } } } } impl UserData for Bar { fn add_methods>(methods: &mut M) { crate::impl_area_method!(methods); crate::impl_style_method!(methods, style); methods.add_function_mut("edge", |_, (ud, edge): (AnyUserData, Edge)| { ud.borrow_mut::()?.edge = edge; Ok(ud) }); methods.add_function_mut("symbol", |_, (ud, symbol): (AnyUserData, String)| { ud.borrow_mut::()?.symbol = symbol; Ok(ud) }); } } ================================================ FILE: yazi-binding/src/elements/border.rs ================================================ use mlua::{AnyUserData, IntoLua, Lua, MetaMethod, Table, UserData, Value}; use ratatui::widgets::{Borders, Widget}; use super::{Area, Edge}; use crate::elements::Line; // Type const PLAIN: u8 = 0; const ROUNDED: u8 = 1; const DOUBLE: u8 = 2; const THICK: u8 = 3; const QUADRANT_INSIDE: u8 = 4; const QUADRANT_OUTSIDE: u8 = 5; #[derive(Clone, Debug, Default)] pub struct Border { pub area: Area, pub edge: Edge, pub r#type: ratatui::widgets::BorderType, pub style: ratatui::style::Style, pub titles: Vec<(ratatui::widgets::TitlePosition, ratatui::text::Line<'static>)>, } impl Border { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, edge): (Table, Edge)| { Ok(Self { edge, r#type: ratatui::widgets::BorderType::Rounded, ..Default::default() }) })?; let border = lua.create_table_from([ // Type ("PLAIN", PLAIN), ("ROUNDED", ROUNDED), ("DOUBLE", DOUBLE), ("THICK", THICK), ("QUADRANT_INSIDE", QUADRANT_INSIDE), ("QUADRANT_OUTSIDE", QUADRANT_OUTSIDE), ])?; border.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; border.into_lua(lua) } } impl Widget for Border { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { let mut block = ratatui::widgets::Block::default() .borders(self.edge.0) .border_type(self.r#type) .border_style(self.style); for title in self.titles { block = match title { (ratatui::widgets::TitlePosition::Top, line) => block.title_top(line), (ratatui::widgets::TitlePosition::Bottom, line) => block.title_bottom(line), }; } block.render(rect, buf); } } impl Widget for &Border { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { self.clone().render(rect, buf); } } impl UserData for Border { fn add_methods>(methods: &mut M) { crate::impl_area_method!(methods); crate::impl_style_method!(methods, style); methods.add_function_mut("type", |_, (ud, value): (AnyUserData, u8)| { ud.borrow_mut::()?.r#type = match value { ROUNDED => ratatui::widgets::BorderType::Rounded, DOUBLE => ratatui::widgets::BorderType::Double, THICK => ratatui::widgets::BorderType::Thick, QUADRANT_INSIDE => ratatui::widgets::BorderType::QuadrantInside, QUADRANT_OUTSIDE => ratatui::widgets::BorderType::QuadrantOutside, _ => ratatui::widgets::BorderType::Plain, }; Ok(ud) }); methods.add_function_mut( "title", |_, (ud, line, position): (AnyUserData, Line, Option)| { let position = if position == Some(Borders::BOTTOM.bits()) { ratatui::widgets::TitlePosition::Bottom } else { ratatui::widgets::TitlePosition::Top }; ud.borrow_mut::()?.titles.push((position, line.inner)); Ok(ud) }, ); methods.add_function_mut("edge", |_, (ud, edge): (AnyUserData, Edge)| { ud.borrow_mut::()?.edge = edge; Ok(ud) }); } } ================================================ FILE: yazi-binding/src/elements/cell.rs ================================================ use mlua::{FromLua, Lua, Value}; use super::Text; #[derive(Clone, Debug)] pub struct Cell { pub(super) text: ratatui::text::Text<'static>, } impl From for ratatui::widgets::Cell<'static> { fn from(value: Cell) -> Self { Self::new(value.text) } } impl FromLua for Cell { fn from_lua(value: Value, lua: &Lua) -> mlua::Result { Ok(Self { text: Text::from_lua(value, lua)?.inner }) } } ================================================ FILE: yazi-binding/src/elements/clear.rs ================================================ use mlua::{IntoLua, Lua, MetaMethod, Table, UserData, Value}; use ratatui::widgets::Widget; use super::Area; #[derive(Clone, Copy, Debug, Default)] pub struct Clear { pub area: Area, } impl Clear { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, area): (Table, Area)| Ok(Self { area }))?; let clear = lua.create_table()?; clear.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; clear.into_lua(lua) } } impl Widget for Clear { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { (&self).render(rect, buf); } } impl Widget for &Clear { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { yazi_widgets::Clear.render(rect, buf); } } impl UserData for Clear { fn add_methods>(methods: &mut M) { crate::impl_area_method!(methods); } } ================================================ FILE: yazi-binding/src/elements/constraint.rs ================================================ use mlua::{FromLua, IntoLua, Lua, UserData, Value}; #[derive(Clone, Copy, Default, FromLua)] pub struct Constraint(pub(super) ratatui::layout::Constraint); impl Constraint { pub fn compose(lua: &Lua) -> mlua::Result { use ratatui::layout::Constraint as C; lua .create_table_from([ ("Min", lua.create_function(|_, n: u16| Ok(Self(C::Min(n))))?), ("Max", lua.create_function(|_, n: u16| Ok(Self(C::Max(n))))?), ("Length", lua.create_function(|_, n: u16| Ok(Self(C::Length(n))))?), ("Percentage", lua.create_function(|_, n: u16| Ok(Self(C::Percentage(n))))?), ("Ratio", lua.create_function(|_, (a, b): (u32, u32)| Ok(Self(C::Ratio(a, b))))?), ("Fill", lua.create_function(|_, n: u16| Ok(Self(C::Fill(n))))?), ])? .into_lua(lua) } } impl From for ratatui::layout::Constraint { fn from(value: Constraint) -> Self { value.0 } } impl UserData for Constraint {} ================================================ FILE: yazi-binding/src/elements/edge.rs ================================================ use std::ops::Deref; use mlua::{FromLua, IntoLua, Lua, Value}; use ratatui::widgets::Borders; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct Edge(pub Borders); impl Deref for Edge { type Target = Borders; fn deref(&self) -> &Self::Target { &self.0 } } impl Edge { pub fn compose(lua: &Lua) -> mlua::Result { lua .create_table_from([ ("NONE", Borders::NONE.bits()), ("TOP", Borders::TOP.bits()), ("RIGHT", Borders::RIGHT.bits()), ("BOTTOM", Borders::BOTTOM.bits()), ("LEFT", Borders::LEFT.bits()), ("ALL", Borders::ALL.bits()), ])? .into_lua(lua) } } impl FromLua for Edge { fn from_lua(value: Value, _: &Lua) -> mlua::Result { let Value::Integer(n) = value else { return Err(mlua::Error::FromLuaConversionError { from: value.type_name(), to: "Edge".to_string(), message: Some("expected an integer representation of an Edge".to_string()), }); }; let Ok(Some(b)) = u8::try_from(n).map(Borders::from_bits) else { return Err(mlua::Error::FromLuaConversionError { from: value.type_name(), to: "Edge".to_string(), message: Some("invalid bits for Edge".to_string()), }); }; Ok(Self(b)) } } ================================================ FILE: yazi-binding/src/elements/elements.rs ================================================ use mlua::{IntoLua, Lua, Value}; use tracing::error; use super::Renderable; use crate::{Composer, ComposerGet, ComposerSet}; pub fn compose(p_get: ComposerGet, p_set: ComposerSet) -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { match key { b"Align" => super::Align::compose(lua)?, b"Bar" => super::Bar::compose(lua)?, b"Border" => super::Border::compose(lua)?, b"Clear" => super::Clear::compose(lua)?, b"Constraint" => super::Constraint::compose(lua)?, b"Edge" => super::Edge::compose(lua)?, b"Gauge" => super::Gauge::compose(lua)?, b"Layout" => super::Layout::compose(lua)?, b"Line" => super::Line::compose(lua)?, b"List" => super::List::compose(lua)?, b"Pad" => super::Pad::compose(lua)?, b"Pos" => super::Pos::compose(lua)?, b"Rect" => super::Rect::compose(lua)?, b"Row" => super::Row::compose(lua)?, b"Span" => super::Span::compose(lua)?, b"Style" => crate::Style::compose(lua)?, b"Table" => super::Table::compose(lua)?, b"Text" => super::Text::compose(lua)?, b"Wrap" => super::Wrap::compose(lua)?, _ => return Ok(Value::Nil), } .into_lua(lua) } fn set(_: &Lua, _: &[u8], value: Value) -> mlua::Result { Ok(value) } Composer::with_parent(get, set, p_get, p_set) } pub fn render_once(value: Value, buf: &mut ratatui::buffer::Buffer, trans: F) where F: FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect + Copy, { match value { Value::Table(tbl) => { for widget in tbl.sequence_values::() { match widget { Ok(w) => w.render_with(buf, trans), Err(e) => error!("Failed to convert to renderable elements: {e}"), } } } Value::UserData(ud) => match Renderable::try_from(&ud) { Ok(w) => w.render_with(buf, trans), Err(e) => error!("Failed to convert to renderable element: {e}"), }, _ => error!("Expected a renderable element, or a table of them, got: {value:?}"), } } ================================================ FILE: yazi-binding/src/elements/gauge.rs ================================================ use mlua::{AnyUserData, ExternalError, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value}; use ratatui::widgets::Widget; use super::{Area, Span}; use crate::Style; #[derive(Clone, Debug, Default)] pub struct Gauge { pub(super) area: Area, ratio: f64, label: Option>, style: ratatui::style::Style, gauge_style: ratatui::style::Style, } impl Gauge { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, _: Table| Ok(Self::default()))?; let gauge = lua.create_table()?; gauge.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; gauge.into_lua(lua) } } impl Widget for Gauge { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { let mut gauge = ratatui::widgets::Gauge::default() .ratio(self.ratio) .style(self.style) .gauge_style(self.gauge_style); if let Some(s) = self.label { gauge = gauge.label(s) } gauge.render(rect, buf); } } impl Widget for &Gauge { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { self.clone().render(rect, buf); } } impl UserData for Gauge { fn add_methods>(methods: &mut M) { crate::impl_area_method!(methods); crate::impl_style_method!(methods, style); methods.add_function_mut("percent", |_, (ud, percent): (AnyUserData, u8)| { if percent > 100 { return Err("percent must be between 0 and 100".into_lua_err()); } ud.borrow_mut::()?.ratio = percent as f64 / 100.0; Ok(ud) }); methods.add_function_mut("ratio", |_, (ud, ratio): (AnyUserData, f64)| { if !(0.0..1.0).contains(&ratio) { return Err("ratio must be between 0 and 1".into_lua_err()); } ud.borrow_mut::()?.ratio = ratio; Ok(ud) }); methods.add_function_mut("label", |_, (ud, label): (AnyUserData, Span)| { ud.borrow_mut::()?.label = Some(label.0); Ok(ud) }); methods.add_function_mut("gauge_style", |_, (ud, style): (AnyUserData, Style)| { ud.borrow_mut::()?.gauge_style = style.0; Ok(ud) }); } } ================================================ FILE: yazi-binding/src/elements/layout.rs ================================================ use mlua::{AnyUserData, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value}; use super::{Constraint, Rect}; const HORIZONTAL: bool = true; const VERTICAL: bool = false; #[derive(Clone, Default)] pub struct Layout { direction: bool, margin: Option, constraints: Vec, } impl Layout { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, _: Table| Ok(Self::default()))?; let layout = lua.create_table_from([("HORIZONTAL", HORIZONTAL), ("VERTICAL", VERTICAL)])?; layout.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; layout.into_lua(lua) } } impl UserData for Layout { fn add_methods>(methods: &mut M) { methods.add_function_mut("direction", |_, (ud, value): (AnyUserData, bool)| { ud.borrow_mut::()?.direction = value; Ok(ud) }); methods.add_function_mut("margin", |_, (ud, value): (AnyUserData, u16)| { ud.borrow_mut::()?.margin = Some(ratatui::layout::Margin::new(value, value)); Ok(ud) }); methods.add_function_mut("margin_h", |_, (ud, value): (AnyUserData, u16)| { { let mut me = ud.borrow_mut::()?; if let Some(margin) = &mut me.margin { margin.horizontal = value; } else { me.margin = Some(ratatui::layout::Margin::new(value, 0)); } } Ok(ud) }); methods.add_function_mut("margin_v", |_, (ud, value): (AnyUserData, u16)| { { let mut me = ud.borrow_mut::()?; if let Some(margin) = &mut me.margin { margin.vertical = value; } else { me.margin = Some(ratatui::layout::Margin::new(0, value)); } } Ok(ud) }); methods.add_function_mut("constraints", |_, (ud, value): (AnyUserData, Vec)| { ud.borrow_mut::()?.constraints = value.into_iter().map(Into::into).collect(); Ok(ud) }); methods.add_method("split", |lua, me, value: Rect| { let mut layout = ratatui::layout::Layout::new( if me.direction == VERTICAL { ratatui::layout::Direction::Vertical } else { ratatui::layout::Direction::Horizontal }, &me.constraints, ); if let Some(margin) = me.margin { layout = layout.horizontal_margin(margin.horizontal); layout = layout.vertical_margin(margin.vertical); } lua.create_sequence_from(layout.split(*value).iter().map(|chunk| Rect::from(*chunk))) }); } } ================================================ FILE: yazi-binding/src/elements/line.rs ================================================ use std::{borrow::Cow, mem, ops::{Deref, DerefMut}}; use ansi_to_tui::IntoText; use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value}; use ratatui::widgets::Widget; use unicode_width::UnicodeWidthChar; use super::{Area, Span}; use crate::elements::Align; const EXPECTED: &str = "expected a string, Span, Line, or a table of them"; #[derive(Clone, Debug, Default)] pub struct Line { pub(super) area: Area, pub(super) inner: ratatui::text::Line<'static>, } impl Deref for Line { type Target = ratatui::text::Line<'static>; fn deref(&self) -> &Self::Target { &self.inner } } impl DerefMut for Line { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } } impl Line { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, line): (Table, Self)| Ok(line))?; let parse = lua.create_function(|_, code: mlua::String| { let code = code.as_bytes(); let Some(line) = code.split_inclusive(|&b| b == b'\n').next() else { return Ok(Self::default()); }; let mut lines = line.into_text().into_lua_err()?.lines; if lines.is_empty() { return Ok(Self::default()); } Ok(Self { inner: mem::take(&mut lines[0]), ..Default::default() }) })?; let line = lua.create_table_from([("parse", parse)])?; line.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; line.into_lua(lua) } } impl TryFrom

for Line { type Error = mlua::Error; fn try_from(tb: Table) -> Result { let mut spans = Vec::with_capacity(tb.raw_len()); for v in tb.sequence_values() { match v? { Value::String(s) => spans.push(s.to_string_lossy().into()), Value::UserData(ud) => { if let Ok(Span(span)) = ud.take() { spans.push(span); } else if let Ok(Self { inner: mut line, .. }) = ud.take() { line.spans.iter_mut().for_each(|s| s.style = line.style.patch(s.style)); spans.extend(line.spans); } else { return Err(EXPECTED.into_lua_err()); } } _ => Err(EXPECTED.into_lua_err())?, } } Ok(Self { inner: spans.into(), ..Default::default() }) } } impl From for ratatui::text::Line<'static> { fn from(value: Line) -> Self { value.inner } } impl Widget for Line { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { (&self).render(rect, buf); } } impl Widget for &Line { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { (&self.inner).render(rect, buf); } } impl FromLua for Line { fn from_lua(value: Value, _: &Lua) -> mlua::Result { Ok(Self { inner: match value { Value::Table(tb) => return Self::try_from(tb), Value::String(s) => s.to_string_lossy().into(), Value::UserData(ud) => { if let Ok(Span(span)) = ud.take() { span.into() } else if let Ok(line) = ud.take() { return Ok(line); } else { Err(EXPECTED.into_lua_err())? } } _ => Err(EXPECTED.into_lua_err())?, }, ..Default::default() }) } } impl UserData for Line { fn add_methods>(methods: &mut M) { crate::impl_area_method!(methods); crate::impl_style_method!(methods, style); crate::impl_style_shorthands!(methods, style); methods.add_method("width", |_, me, ()| Ok(me.width())); methods.add_function_mut("align", |_, (ud, align): (AnyUserData, Align)| { ud.borrow_mut::()?.alignment = Some(align.0); Ok(ud) }); methods.add_method("visible", |_, me, ()| { Ok(me.iter().flat_map(|s| s.content.chars()).any(|c| c.width().unwrap_or(0) > 0)) }); methods.add_function_mut("truncate", |lua, (ud, t): (AnyUserData, Table)| { let mut me = ud.borrow_mut::()?; let max = t.raw_get("max")?; if max < 1 { me.spans.clear(); return Ok(ud); } let ellipsis = match t.raw_get::("ellipsis")? { Value::Nil => (1, ratatui::text::Span::raw("…")), v => { let mut span = Span::from_lua(v, lua)?; (span.truncate(max), span.0) } }; fn traverse( max: usize, threshold: usize, it: impl Iterator, ) -> (Option<(usize, usize, usize)>, bool) { let (mut adv, mut cut) = (0, None); for (x, y, c) in it { adv += c.width().unwrap_or(0); if adv <= threshold { cut = Some((x, y, adv)); } else if adv > max { break; } } (cut, adv > max) } let rtl = t.raw_get("rtl")?; let (cut, remain) = if rtl { traverse( max, max - ellipsis.0, me.iter() .enumerate() .rev() .flat_map(|(x, s)| s.content.char_indices().rev().map(move |(y, c)| (x, y, c))), ) } else { traverse( max, max - ellipsis.0, me.iter() .enumerate() .flat_map(|(x, s)| s.content.char_indices().map(move |(y, c)| (x, y, c))), ) }; let Some((x, y, width)) = cut else { me.spans.clear(); me.spans.push(ellipsis.1); return Ok(ud); }; if !remain { return Ok(ud); } let spans = &mut me.spans; let len = match (rtl, width == max) { (a, b) if a == b => spans[x].content[y..].chars().next().map_or(0, |c| c.len_utf8()), _ => 0, }; if rtl { match &mut spans[x].content { Cow::Borrowed(s) => spans[x].content = Cow::Borrowed(&s[y + len..]), Cow::Owned(s) => _ = s.drain(..y + len), } spans.splice(..x, [ellipsis.1]); } else { match &mut spans[x].content { Cow::Borrowed(s) => spans[x].content = Cow::Borrowed(&s[..y + len]), Cow::Owned(s) => s.truncate(y + len), } spans.truncate(x + 1); spans.push(ellipsis.1); } Ok(ud) }); } } #[cfg(test)] mod tests { use mlua::{UserDataRef, chunk}; use super::*; fn truncate(s: &str, max: usize, rtl: bool) -> String { let lua = Lua::new(); let comp = Line::compose(&lua).unwrap(); let line: UserDataRef = lua .load(chunk! { return $comp($s):truncate { max = $max, rtl = $rtl } }) .call(()) .unwrap(); line.spans.iter().map(|s| &*s.content).collect() } #[test] fn test_truncate() { assert_eq!(truncate("你好,world", 0, false), ""); assert_eq!(truncate("你好,world", 1, false), "…"); assert_eq!(truncate("你好,world", 2, false), "…"); assert_eq!(truncate("你好,世界", 3, false), "你…"); assert_eq!(truncate("你好,世界", 4, false), "你…"); assert_eq!(truncate("你好,世界", 5, false), "你好…"); assert_eq!(truncate("Hello, world", 5, false), "Hell…"); assert_eq!(truncate("Ni好,世界", 3, false), "Ni…"); } #[test] fn test_truncate_rtl() { assert_eq!(truncate("world,你好", 0, true), ""); assert_eq!(truncate("world,你好", 1, true), "…"); assert_eq!(truncate("world,你好", 2, true), "…"); assert_eq!(truncate("你好,世界", 3, true), "…界"); assert_eq!(truncate("你好,世界", 4, true), "…界"); assert_eq!(truncate("你好,世界", 5, true), "…世界"); assert_eq!(truncate("Hello, world", 5, true), "…orld"); assert_eq!(truncate("你好,Shi界", 3, true), "…界"); } #[test] fn test_truncate_oboe() { assert_eq!(truncate("Hello, world", 11, false), "Hello, wor…"); assert_eq!(truncate("你好,世界", 9, false), "你好,世…"); assert_eq!(truncate("你好,世Jie", 9, false), "你好,世…"); assert_eq!(truncate("Hello, world", 11, true), "…llo, world"); assert_eq!(truncate("你好,世界", 9, true), "…好,世界"); assert_eq!(truncate("Ni好,世界", 9, true), "…好,世界"); } #[test] fn test_truncate_exact() { assert_eq!(truncate("Hello, world", 12, false), "Hello, world"); assert_eq!(truncate("你好,世界", 10, false), "你好,世界"); assert_eq!(truncate("Hello, world", 12, true), "Hello, world"); assert_eq!(truncate("你好,世界", 10, true), "你好,世界"); } #[test] fn test_truncate_overflow() { assert_eq!(truncate("Hello, world", 13, false), "Hello, world"); assert_eq!(truncate("你好,世界", 11, false), "你好,世界"); assert_eq!(truncate("Hello, world", 13, true), "Hello, world"); assert_eq!(truncate("你好,世界", 11, true), "你好,世界"); } } ================================================ FILE: yazi-binding/src/elements/list.rs ================================================ use mlua::{IntoLua, Lua, MetaMethod, Table, UserData, Value}; use ratatui::widgets::Widget; use super::{Area, Text}; // --- List #[derive(Clone, Debug, Default)] pub struct List { pub(super) area: Area, inner: ratatui::widgets::List<'static>, } impl List { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, items): (Table, Vec)| { Ok(Self { inner: ratatui::widgets::List::new(items), ..Default::default() }) })?; let list = lua.create_table()?; list.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; list.into_lua(lua) } } impl Widget for List { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { (&self).render(rect, buf); } } impl Widget for &List { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { (&self.inner).render(rect, buf); } } impl UserData for List { fn add_methods>(methods: &mut M) { crate::impl_area_method!(methods); } } ================================================ FILE: yazi-binding/src/elements/mod.rs ================================================ yazi_macro::mod_flat!(align area bar border cell clear constraint edge elements gauge layout line list pad pos rect renderable row span table text wrap); ================================================ FILE: yazi-binding/src/elements/pad.rs ================================================ use std::ops::{Add, AddAssign, Deref}; use mlua::{FromLua, IntoLua, Lua, MetaMethod, Table, UserData, Value}; #[derive(Clone, Copy, Default, FromLua)] pub struct Pad(ratatui::widgets::Padding); impl Deref for Pad { type Target = ratatui::widgets::Padding; fn deref(&self) -> &Self::Target { &self.0 } } impl From for Pad { fn from(pad: ratatui::widgets::Padding) -> Self { Self(pad) } } impl Pad { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, top, right, bottom, left): (Table, u16, u16, u16, u16)| { Ok(Self(ratatui::widgets::Padding::new(left, right, top, bottom))) })?; let pad = lua.create_table_from([ ( "left", lua.create_function(|_, left: u16| Ok(Self(ratatui::widgets::Padding::left(left))))?, ), ( "right", lua.create_function(|_, right: u16| Ok(Self(ratatui::widgets::Padding::right(right))))?, ), ("top", lua.create_function(|_, top: u16| Ok(Self(ratatui::widgets::Padding::top(top))))?), ( "bottom", lua .create_function(|_, bottom: u16| Ok(Self(ratatui::widgets::Padding::bottom(bottom))))?, ), ("x", lua.create_function(|_, x: u16| Ok(Self(ratatui::widgets::Padding::new(x, x, 0, 0))))?), ("y", lua.create_function(|_, y: u16| Ok(Self(ratatui::widgets::Padding::new(0, 0, y, y))))?), ( "xy", lua .create_function(|_, xy: u16| Ok(Self(ratatui::widgets::Padding::new(xy, xy, xy, xy))))?, ), ])?; pad.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; pad.into_lua(lua) } } impl UserData for Pad { fn add_fields>(fields: &mut F) { fields.add_field_method_get("left", |_, me| Ok(me.left)); fields.add_field_method_get("right", |_, me| Ok(me.right)); fields.add_field_method_get("top", |_, me| Ok(me.top)); fields.add_field_method_get("bottom", |_, me| Ok(me.bottom)); } } impl Add for Pad { type Output = Self; fn add(self, rhs: ratatui::widgets::Padding) -> Self::Output { Self(ratatui::widgets::Padding::new( self.left.saturating_add(rhs.left), self.right.saturating_add(rhs.right), self.top.saturating_add(rhs.top), self.bottom.saturating_add(rhs.bottom), )) } } impl AddAssign for Pad { fn add_assign(&mut self, rhs: ratatui::widgets::Padding) { *self = *self + rhs; } } ================================================ FILE: yazi-binding/src/elements/pos.rs ================================================ use std::{ops::Deref, str::FromStr}; use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, Value}; use super::Pad; const EXPECTED: &str = "expected a Pos"; #[derive(Clone, Copy, Default)] pub struct Pos { inner: yazi_config::popup::Position, pub(super) pad: Pad, } impl Deref for Pos { type Target = yazi_config::popup::Position; fn deref(&self) -> &Self::Target { &self.inner } } impl From for Pos { fn from(value: yazi_config::popup::Position) -> Self { Self { inner: value, ..Default::default() } } } impl From for yazi_config::popup::Position { fn from(value: Pos) -> Self { value.inner } } impl TryFrom
for Pos { type Error = mlua::Error; fn try_from(t: Table) -> Result { use yazi_config::popup::{Offset, Origin, Position}; Ok(Self::from(Position { origin: Origin::from_str(&t.raw_get::(1)?.to_str()?).into_lua_err()?, offset: Offset { x: t.raw_get("x").unwrap_or_default(), y: t.raw_get("y").unwrap_or_default(), width: t.raw_get("w").unwrap_or_default(), height: t.raw_get("h").unwrap_or_default(), }, })) } } impl FromLua for Pos { fn from_lua(value: Value, _: &Lua) -> mlua::Result { Ok(match value { Value::Table(tbl) => Self::try_from(tbl)?, Value::UserData(ud) => { if let Ok(pos) = ud.borrow() { *pos } else { Err(EXPECTED.into_lua_err())? } } _ => Err(EXPECTED.into_lua_err())?, }) } } impl Pos { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, t): (Table, Table)| Self::try_from(t))?; let position = lua.create_table()?; position.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; position.into_lua(lua) } pub fn with_height(mut self, height: u16) -> Self { self.inner.offset.height = height; self } } impl UserData for Pos { fn add_fields>(fields: &mut F) { // TODO: cache fields.add_field_method_get("1", |_, me| Ok(me.origin.to_string())); fields.add_field_method_get("x", |_, me| Ok(me.offset.x)); fields.add_field_method_get("y", |_, me| Ok(me.offset.y)); fields.add_field_method_get("w", |_, me| Ok(me.offset.width)); fields.add_field_method_get("h", |_, me| Ok(me.offset.height)); } fn add_methods>(methods: &mut M) { methods.add_function_mut("pad", |_, (ud, pad): (AnyUserData, Pad)| { ud.borrow_mut::()?.pad = pad; Ok(ud) }); } } ================================================ FILE: yazi-binding/src/elements/rect.rs ================================================ use std::ops::Deref; use mlua::{FromLua, IntoLua, Lua, MetaMethod, Table, UserData, Value}; use super::Pad; #[derive(Clone, Copy, Debug, Default, FromLua)] pub struct Rect(pub ratatui::layout::Rect); impl Deref for Rect { type Target = ratatui::layout::Rect; fn deref(&self) -> &Self::Target { &self.0 } } impl From for Rect { fn from(rect: ratatui::layout::Rect) -> Self { Self(rect) } } impl From for Rect { fn from(size: ratatui::layout::Size) -> Self { Self(ratatui::layout::Rect { x: 0, y: 0, width: size.width, height: size.height }) } } impl Rect { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, args): (Table, Table)| { Ok(Self(ratatui::layout::Rect { x: args.raw_get("x").unwrap_or_default(), y: args.raw_get("y").unwrap_or_default(), width: args.raw_get("w").unwrap_or_default(), height: args.raw_get("h").unwrap_or_default(), })) })?; let rect = lua.create_table()?; rect.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; rect.into_lua(lua) } pub(super) fn pad(self, pad: Pad) -> Self { let mut r = *self; r.x = r.x.saturating_add(pad.left); r.y = r.y.saturating_add(pad.top); r.width = r.width.saturating_sub(pad.left + pad.right); r.height = r.height.saturating_sub(pad.top + pad.bottom); Self(r) } } impl UserData for Rect { fn add_fields>(fields: &mut F) { fields.add_field_method_get("x", |_, me| Ok(me.x)); fields.add_field_method_get("y", |_, me| Ok(me.y)); fields.add_field_method_get("w", |_, me| Ok(me.width)); fields.add_field_method_get("h", |_, me| Ok(me.height)); fields.add_field_method_set("x", |_, me, x| Ok(me.0.x = x)); fields.add_field_method_set("y", |_, me, y| Ok(me.0.y = y)); fields.add_field_method_set("w", |_, me, w| Ok(me.0.width = w)); fields.add_field_method_set("h", |_, me, h| Ok(me.0.height = h)); fields.add_field_method_get("left", |_, me| Ok(me.left())); fields.add_field_method_get("right", |_, me| Ok(me.right())); fields.add_field_method_get("top", |_, me| Ok(me.top())); fields.add_field_method_get("bottom", |_, me| Ok(me.bottom())); } fn add_methods>(methods: &mut M) { methods.add_method("pad", |_, me, pad: Pad| Ok(me.pad(pad))); methods.add_method("contains", |_, me, Self(rect)| Ok(me.contains(rect.into()))); } } ================================================ FILE: yazi-binding/src/elements/renderable.rs ================================================ use std::any::TypeId; use mlua::{AnyUserData, ExternalError, FromLua, Lua, Value}; use ratatui::widgets::Widget; use super::{Bar, Border, Clear, Gauge, Line, List, Table, Text}; use crate::{Error, elements::Area}; #[derive(Clone, Debug)] pub enum Renderable { Line(Line), Text(Text), List(Box), Bar(Bar), Clear(Clear), Border(Border), Gauge(Box), Table(Box
), } impl Renderable { pub fn area(&self) -> Area { match self { Self::Line(line) => line.area, Self::Text(text) => text.area, Self::List(list) => list.area, Self::Bar(bar) => bar.area, Self::Clear(clear) => clear.area, Self::Border(border) => border.area, Self::Gauge(gauge) => gauge.area, Self::Table(table) => table.area, } } pub fn with_area(mut self, area: impl Into) -> Self { let area = area.into(); match &mut self { Self::Line(line) => line.area = area, Self::Text(text) => text.area = area, Self::List(list) => list.area = area, Self::Bar(bar) => bar.area = area, Self::Clear(clear) => clear.area = area, Self::Border(border) => border.area = area, Self::Gauge(gauge) => gauge.area = area, Self::Table(table) => table.area = area, } self } pub fn render_with(self, buf: &mut ratatui::buffer::Buffer, trans: T) where T: FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect, { let rect = self.area().transform(trans); self.render(rect, buf); } } impl TryFrom<&AnyUserData> for Renderable { type Error = mlua::Error; fn try_from(ud: &AnyUserData) -> Result { Ok(match ud.type_id() { Some(t) if t == TypeId::of::() => Self::Line(ud.take()?), Some(t) if t == TypeId::of::() => Self::Text(ud.take()?), Some(t) if t == TypeId::of::() => Self::List(Box::new(ud.take()?)), Some(t) if t == TypeId::of::() => Self::Bar(ud.take()?), Some(t) if t == TypeId::of::() => Self::Clear(ud.take()?), Some(t) if t == TypeId::of::() => Self::Border(ud.take()?), Some(t) if t == TypeId::of::() => Self::Gauge(Box::new(ud.take()?)), Some(t) if t == TypeId::of::
() => Self::Table(Box::new(ud.take()?)), _ => Err(format!("expected a renderable userdata, not: {ud:#?}").into_lua_err())?, }) } } impl From for Renderable { fn from(error: Error) -> Self { Self::Text(Text { inner: error.into_string().into(), wrap: ratatui::widgets::Wrap { trim: false }.into(), ..Default::default() }) } } impl Widget for Renderable { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { match self { Self::Line(line) => line.render(rect, buf), Self::Text(text) => text.render(rect, buf), Self::List(list) => list.render(rect, buf), Self::Bar(bar) => bar.render(rect, buf), Self::Clear(clear) => clear.render(rect, buf), Self::Border(border) => border.render(rect, buf), Self::Gauge(gauge) => gauge.render(rect, buf), Self::Table(table) => table.render(rect, buf), } } } impl Widget for &Renderable { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { match self { Renderable::Line(line) => line.render(rect, buf), Renderable::Text(text) => text.render(rect, buf), Renderable::List(list) => (&**list).render(rect, buf), Renderable::Bar(bar) => bar.render(rect, buf), Renderable::Clear(clear) => clear.render(rect, buf), Renderable::Border(border) => border.render(rect, buf), Renderable::Gauge(gauge) => (&**gauge).render(rect, buf), Renderable::Table(table) => (&**table).render(rect, buf), } } } impl FromLua for Renderable { fn from_lua(value: Value, _: &Lua) -> mlua::Result { match value { Value::UserData(ud) => Self::try_from(&ud), _ => Err(format!("expected a renderable userdata, not: {value:#?}").into_lua_err()), } } } ================================================ FILE: yazi-binding/src/elements/row.rs ================================================ use mlua::{AnyUserData, ExternalError, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, Value}; use super::Cell; const EXPECTED: &str = "expected a Row"; #[derive(Clone, Debug, Default)] pub struct Row { pub(super) cells: Vec, height: u16, top_margin: u16, bottom_margin: u16, style: ratatui::style::Style, } impl Row { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, cells): (Table, Vec)| { Ok(Self { cells, ..Default::default() }) })?; let row = lua.create_table()?; row.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; row.into_lua(lua) } } impl From for ratatui::widgets::Row<'static> { fn from(value: Row) -> Self { Self::new(value.cells) .height(value.height.max(1)) .top_margin(value.top_margin) .bottom_margin(value.bottom_margin) .style(value.style) } } impl FromLua for Row { fn from_lua(value: Value, _: &Lua) -> mlua::Result { Ok(match value { Value::UserData(ud) => { if let Ok(row) = ud.take() { row } else { Err(EXPECTED.into_lua_err())? } } _ => Err(EXPECTED.into_lua_err())?, }) } } impl UserData for Row { fn add_methods>(methods: &mut M) { crate::impl_style_method!(methods, style); methods.add_function_mut("height", |_, (ud, value): (AnyUserData, u16)| { ud.borrow_mut::()?.height = value; Ok(ud) }); methods.add_function_mut("margin_t", |_, (ud, value): (AnyUserData, u16)| { ud.borrow_mut::()?.top_margin = value; Ok(ud) }); methods.add_function_mut("margin_b", |_, (ud, value): (AnyUserData, u16)| { ud.borrow_mut::()?.bottom_margin = value; Ok(ud) }); } } ================================================ FILE: yazi-binding/src/elements/span.rs ================================================ use std::{borrow::Cow, ops::{Deref, DerefMut}}; use mlua::{AnyUserData, ExternalError, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value}; use unicode_width::UnicodeWidthChar; const EXPECTED: &str = "expected a string or Span"; pub struct Span(pub(super) ratatui::text::Span<'static>); impl Deref for Span { type Target = ratatui::text::Span<'static>; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for Span { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl Span { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, span): (Table, Self)| Ok(span))?; let span = lua.create_table()?; span.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; span.into_lua(lua) } pub(super) fn truncate(&mut self, max: usize) -> usize { if max < 1 { match &mut self.0.content { Cow::Borrowed(_) => self.0.content = Cow::Borrowed(""), Cow::Owned(s) => s.clear(), } return 0; } let mut adv = 0; let mut last; for (i, c) in self.0.content.char_indices() { (last, adv) = (adv, adv + c.width().unwrap_or(0)); if adv < max { continue; } else if adv == max && self.0.content[i..].chars().nth(1).is_none() { return max; } match &mut self.0.content { Cow::Borrowed(s) => self.0.content = format!("{}…", &s[..i]).into(), Cow::Owned(s) => { s.truncate(i); s.push('…'); } } return last + 1; } adv } } impl FromLua for Span { fn from_lua(value: Value, _: &Lua) -> mlua::Result { Ok(Self(match value { Value::String(s) => s.to_string_lossy().into(), Value::UserData(ud) => { if let Ok(Self(span)) = ud.take() { span } else { Err(EXPECTED.into_lua_err())? } } _ => Err(EXPECTED.into_lua_err())?, })) } } impl UserData for Span { fn add_methods>(methods: &mut M) { crate::impl_style_method!(methods, 0.style); crate::impl_style_shorthands!(methods, 0.style); methods.add_method("visible", |_, Self(me), ()| { Ok(me.content.chars().any(|c| c.width().unwrap_or(0) > 0)) }); methods.add_function_mut("truncate", |_, (ud, t): (AnyUserData, Table)| { ud.borrow_mut::()?.truncate(t.raw_get("max")?); Ok(ud) }); } } ================================================ FILE: yazi-binding/src/elements/table.rs ================================================ use mlua::{AnyUserData, IntoLua, Lua, MetaMethod, UserData, Value}; use ratatui::widgets::{StatefulWidget, Widget}; use super::{Area, Row}; use crate::{Style, elements::Constraint}; // --- Table #[derive(Clone, Debug, Default)] pub struct Table { pub area: Area, rows: Vec, header: Option>, footer: Option>, widths: Vec, column_spacing: u16, block: Option>, // TODO style: ratatui::style::Style, row_highlight_style: ratatui::style::Style, column_highlight_style: ratatui::style::Style, cell_highlight_style: ratatui::style::Style, highlight_symbol: ratatui::text::Text<'static>, // TODO highlight_spacing: ratatui::widgets::HighlightSpacing, // TODO flex: ratatui::layout::Flex, state: ratatui::widgets::TableState, } impl Table { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, rows): (mlua::Table, Vec)| { Ok(Self { rows, ..Default::default() }) })?; let table = lua.create_table()?; table.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; table.into_lua(lua) } pub fn selected_cell(&self) -> Option<&ratatui::text::Text<'_>> { let row = &self.rows[self.selected()?]; let col = self.state.selected_column()?; if row.cells.is_empty() { None } else { Some(&row.cells[col.min(row.cells.len() - 1)].text) } } pub fn len(&self) -> usize { self.rows.len() } pub fn select(&mut self, idx: Option) { self .state .select(idx.map(|i| if self.rows.is_empty() { 0 } else { i.min(self.rows.len() - 1) })); } pub fn selected(&self) -> Option { if self.rows.is_empty() { None } else { Some(self.state.selected()?.min(self.rows.len() - 1)) } } } impl TryFrom for Table { type Error = mlua::Error; fn try_from(value: AnyUserData) -> Result { value.take() } } impl Widget for Table { fn render(mut self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { let mut table = ratatui::widgets::Table::new(self.rows, self.widths) .column_spacing(self.column_spacing) .style(self.style) .row_highlight_style(self.row_highlight_style) .column_highlight_style(self.column_highlight_style) .cell_highlight_style(self.cell_highlight_style) .highlight_symbol(self.highlight_symbol) .highlight_spacing(self.highlight_spacing) .flex(self.flex); if let Some(header) = self.header { table = table.header(header); } if let Some(footer) = self.footer { table = table.footer(footer); } if let Some(block) = self.block { table = table.block(block); } StatefulWidget::render(table, rect, buf, &mut self.state); } } impl Widget for &Table { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { self.clone().render(rect, buf); } } impl UserData for Table { fn add_methods>(methods: &mut M) { crate::impl_area_method!(methods); methods.add_function_mut("header", |_, (ud, header): (AnyUserData, Row)| { ud.borrow_mut::()?.header = Some(header.into()); Ok(ud) }); methods.add_function_mut("footer", |_, (ud, footer): (AnyUserData, Row)| { ud.borrow_mut::()?.footer = Some(footer.into()); Ok(ud) }); methods.add_function_mut("widths", |_, (ud, widths): (AnyUserData, Vec)| { ud.borrow_mut::()?.widths = widths.into_iter().map(Into::into).collect(); Ok(ud) }); methods.add_function_mut("spacing", |_, (ud, spacing): (AnyUserData, u16)| { ud.borrow_mut::()?.column_spacing = spacing; Ok(ud) }); methods.add_function_mut("row", |_, (ud, idx): (AnyUserData, Option)| { ud.borrow_mut::()?.state.select(idx); Ok(ud) }); methods.add_function_mut("col", |_, (ud, idx): (AnyUserData, Option)| { ud.borrow_mut::()?.state.select_column(idx); Ok(ud) }); methods.add_function_mut("style", |_, (ud, style): (AnyUserData, Style)| { ud.borrow_mut::()?.style = style.0; Ok(ud) }); methods.add_function_mut("row_style", |_, (ud, style): (AnyUserData, Style)| { ud.borrow_mut::()?.row_highlight_style = style.0; Ok(ud) }); methods.add_function_mut("col_style", |_, (ud, style): (AnyUserData, Style)| { ud.borrow_mut::()?.column_highlight_style = style.0; Ok(ud) }); methods.add_function_mut("cell_style", |_, (ud, style): (AnyUserData, Style)| { ud.borrow_mut::()?.cell_highlight_style = style.0; Ok(ud) }); } } ================================================ FILE: yazi-binding/src/elements/text.rs ================================================ use std::{any::TypeId, mem}; use ansi_to_tui::IntoText; use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, Value}; use ratatui::widgets::Widget; use super::{Area, Line, Span, Wrap}; use crate::{Error, elements::Align}; const EXPECTED: &str = "expected a string, Line, Span, or a table of them"; #[derive(Clone, Debug, Default)] pub struct Text { pub area: Area, // TODO: block pub inner: ratatui::text::Text<'static>, pub wrap: Wrap, pub scroll: ratatui::layout::Position, } impl Text { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, text): (Table, Self)| Ok(text))?; let parse = lua.create_function(|_, code: mlua::String| { Ok(Self { inner: code.as_bytes().into_text().into_lua_err()?, ..Default::default() }) })?; let text = lua.create_table_from([("parse", parse)])?; text.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; text.into_lua(lua) } } impl TryFrom
for Text { type Error = mlua::Error; fn try_from(tb: Table) -> Result { let mut lines = Vec::with_capacity(tb.raw_len()); for v in tb.sequence_values() { lines.push(match v? { Value::String(s) => s.to_string_lossy().into(), Value::UserData(ud) => match ud.type_id() { Some(t) if t == TypeId::of::() => ud.take::()?.0.into(), Some(t) if t == TypeId::of::() => ud.take::()?.inner, Some(t) if t == TypeId::of::() => ud.take::()?.into_string().into(), _ => Err(EXPECTED.into_lua_err())?, }, _ => Err(EXPECTED.into_lua_err())?, }) } Ok(Self { inner: lines.into(), ..Default::default() }) } } impl From for ratatui::text::Text<'static> { fn from(value: Text) -> Self { value.inner } } impl From for ratatui::widgets::Paragraph<'static> { fn from(mut value: Text) -> Self { let align = value.inner.alignment.take(); let style = mem::take(&mut value.inner.style); let mut p = ratatui::widgets::Paragraph::new(value.inner).style(style); if let Some(align) = align { p = p.alignment(align); } if let Some(wrap) = value.wrap.0 { p = p.wrap(wrap); } p.scroll((value.scroll.y, value.scroll.x)) } } impl Widget for Text { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { if self.wrap.is_none() && self.scroll == Default::default() { self.inner.render(rect, buf); } else { ratatui::widgets::Paragraph::from(self).render(rect, buf); } } } impl Widget for &Text { fn render(self, rect: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) where Self: Sized, { if self.wrap.is_none() && self.scroll == Default::default() { (&self.inner).render(rect, buf); } else { ratatui::widgets::Paragraph::from(self.clone()).render(rect, buf); } } } impl FromLua for Text { fn from_lua(value: Value, _: &Lua) -> mlua::Result { let inner = match value { Value::Table(tb) => return Self::try_from(tb), Value::String(s) => s.to_string_lossy().into(), Value::UserData(ud) => match ud.type_id() { Some(t) if t == TypeId::of::() => ud.take::()?.inner.into(), Some(t) if t == TypeId::of::() => ud.take::()?.0.into(), Some(t) if t == TypeId::of::() => return ud.take(), Some(t) if t == TypeId::of::() => ud.take::()?.into_string().into(), _ => Err(EXPECTED.into_lua_err())?, }, _ => Err(EXPECTED.into_lua_err())?, }; Ok(Self { inner, ..Default::default() }) } } impl UserData for Text { fn add_methods>(methods: &mut M) { crate::impl_area_method!(methods); crate::impl_style_method!(methods, inner.style); crate::impl_style_shorthands!(methods, inner.style); methods.add_function_mut("align", |_, (ud, align): (AnyUserData, Align)| { ud.borrow_mut::()?.inner.alignment = Some(align.0); Ok(ud) }); methods.add_function_mut("wrap", |_, (ud, wrap): (AnyUserData, Wrap)| { ud.borrow_mut::()?.wrap = wrap; Ok(ud) }); methods.add_function_mut("scroll", |_, (ud, x, y): (AnyUserData, u16, u16)| { ud.borrow_mut::()?.scroll = ratatui::layout::Position { x, y }; Ok(ud) }); methods.add_method("max_width", |_, me, ()| { Ok(me.inner.lines.iter().take(me.area.size().height as usize).map(|l| l.width()).max()) }); } } ================================================ FILE: yazi-binding/src/elements/wrap.rs ================================================ use std::ops::Deref; use mlua::{FromLua, IntoLua, Lua, Value}; use yazi_config::preview::PreviewWrap; #[derive(Clone, Copy, Debug, Default)] pub struct Wrap(pub(super) Option); impl Deref for Wrap { type Target = Option; fn deref(&self) -> &Self::Target { &self.0 } } impl Wrap { pub fn compose(lua: &Lua) -> mlua::Result { lua.create_table_from([("NO", 0), ("YES", 1), ("TRIM", 2)])?.into_lua(lua) } } impl From for Option { fn from(value: Wrap) -> Self { value.0 } } impl From for Wrap { fn from(value: ratatui::widgets::Wrap) -> Self { Self(Some(value)) } } impl From for Wrap { fn from(value: PreviewWrap) -> Self { Self(match value { PreviewWrap::No => None, PreviewWrap::Yes => Some(ratatui::widgets::Wrap { trim: false }), }) } } impl FromLua for Wrap { fn from_lua(value: Value, _: &Lua) -> mlua::Result { let Value::Integer(n) = value else { return Err(mlua::Error::FromLuaConversionError { from: value.type_name(), to: "Wrap".to_string(), message: Some("expected an integer representation of a Wrap".to_string()), }); }; Ok(Self(match n { 0 => None, 1 => Some(ratatui::widgets::Wrap { trim: false }), 2 => Some(ratatui::widgets::Wrap { trim: true }), _ => Err(mlua::Error::FromLuaConversionError { from: value.type_name(), to: "Wrap".to_string(), message: Some("invalid value for Wrap".to_string()), })?, })) } } impl IntoLua for Wrap { fn into_lua(self, lua: &Lua) -> mlua::Result { match self.0 { None => 0.into_lua(lua), Some(ratatui::widgets::Wrap { trim: false }) => 1.into_lua(lua), Some(ratatui::widgets::Wrap { trim: true }) => 2.into_lua(lua), } } } ================================================ FILE: yazi-binding/src/error.rs ================================================ use std::{borrow::Cow, fmt::Display}; use mlua::{ExternalError, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, Value}; use yazi_codegen::FromLuaOwned; use yazi_shared::SStr; #[derive(FromLuaOwned)] pub enum Error { Io(std::io::Error), Fs(yazi_fs::error::Error), Serde(serde_json::Error), Custom(SStr), } impl Error { pub fn install(lua: &Lua) -> mlua::Result<()> { let custom = lua.create_function(|_, msg: String| Ok(Self::custom(msg)))?; let fs = lua.create_function(|_, value: Value| { Ok(Self::Fs(match value { Value::Table(t) => yazi_fs::error::Error::custom( &t.raw_get::("kind")?.to_str()?, t.raw_get("code")?, &t.raw_get::("message")?.to_str()?, )?, _ => Err("expected a table".into_lua_err())?, })) })?; lua.globals().raw_set("Error", lua.create_table_from([("custom", custom), ("fs", fs)])?) } pub fn custom(msg: impl Into) -> Self { Self::Custom(msg.into()) } pub fn into_string(self) -> SStr { match self { Self::Io(e) => Cow::Owned(e.to_string()), Self::Fs(e) => Cow::Owned(e.to_string()), Self::Serde(e) => Cow::Owned(e.to_string()), Self::Custom(s) => s, } } } impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Io(e) => write!(f, "{e}"), Self::Fs(e) => write!(f, "{e}"), Self::Serde(e) => write!(f, "{e}"), Self::Custom(s) => write!(f, "{s}"), } } } impl UserData for Error { fn add_fields>(fields: &mut F) { fields.add_field_method_get("code", |_, me| { Ok(match me { Self::Io(e) => e.raw_os_error(), Self::Fs(e) => e.raw_os_error(), _ => None, }) }); fields.add_field_method_get("kind", |_, me| { Ok(match me { Self::Io(e) => Some(yazi_fs::error::Error::from(e.kind()).kind_str()), Self::Fs(e) => Some(e.kind_str()), _ => None, }) }); } fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::ToString, |lua, me, ()| { Ok(match me { Self::Io(_) | Self::Fs(_) | Self::Serde(_) => lua.create_external_string(me.to_string()), Self::Custom(s) => lua.create_string(&**s), }) }); methods.add_meta_function(MetaMethod::Concat, |lua, (lhs, rhs): (Value, Value)| { match (lhs, rhs) { (Value::String(l), Value::UserData(r)) => { let r = r.borrow::()?; lua.create_external_string([&l.as_bytes(), r.to_string().as_bytes()].concat()) } (Value::UserData(l), Value::String(r)) => { let l = l.borrow::()?; lua.create_external_string([l.to_string().as_bytes(), &r.as_bytes()].concat()) } _ => Err("only string can be concatenated with Error".into_lua_err()), } }); } } ================================================ FILE: yazi-binding/src/fd.rs ================================================ use mlua::{IntoLuaMulti, UserData, UserDataMethods, Value}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::Error; pub struct Fd(pub yazi_vfs::provider::RwFile); impl UserData for Fd { fn add_methods>(methods: &mut M) { methods.add_async_method_mut("flush", |lua, mut me, ()| async move { match me.0.flush().await { Ok(()) => true.into_lua_multi(&lua), Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), } }); methods.add_async_method_mut("read", |lua, mut me, len: usize| async move { let mut buf = vec![0; len]; match me.0.read(&mut buf).await { Ok(n) => { buf.truncate(n); lua.create_external_string(buf)?.into_lua_multi(&lua) } Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); methods.add_async_method_mut("write_all", |lua, mut me, src: mlua::String| async move { match me.0.write_all(&src.as_bytes()).await { Ok(()) => true.into_lua_multi(&lua), Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), } }); } } ================================================ FILE: yazi-binding/src/file.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, ExternalError, FromLua, Lua, ObjectLike, Table, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; use crate::{Cha, Url, impl_file_fields, impl_file_methods}; pub type FileRef = UserDataRef; const EXPECTED: &str = "expected a table, File, or fs::File"; #[derive(Clone)] pub struct File { inner: yazi_fs::File, v_cha: Option, v_url: Option, v_link_to: Option, v_name: Option, v_path: Option, v_cache: Option, } impl Deref for File { type Target = yazi_fs::File; fn deref(&self) -> &Self::Target { &self.inner } } impl From for yazi_fs::File { fn from(value: File) -> Self { value.inner } } impl File { pub fn new(inner: impl Into) -> Self { Self { inner: inner.into(), v_cha: None, v_url: None, v_link_to: None, v_name: None, v_path: None, v_cache: None, } } pub fn install(lua: &Lua) -> mlua::Result<()> { lua.globals().raw_set("File", lua.create_function(|_, file: Self| Ok(file))?) } } impl TryFrom
for File { type Error = mlua::Error; fn try_from(value: Table) -> Result { Ok(Self::new(yazi_fs::File { url: value.raw_get::("url")?.into(), cha: *value.raw_get::("cha")?, ..Default::default() })) } } impl TryFrom for File { type Error = mlua::Error; fn try_from(value: AnyUserData) -> Result { Ok(if let Ok(me) = value.take::() { me } else { value.get("bare")? }) } } impl FromLua for File { fn from_lua(value: Value, _: &Lua) -> mlua::Result { match value { Value::Table(tbl) => Self::try_from(tbl), Value::UserData(ud) => Self::try_from(ud), _ => Err(EXPECTED.into_lua_err())?, } } } impl UserData for File { fn add_fields>(fields: &mut F) { impl_file_fields!(fields); } fn add_methods>(methods: &mut M) { impl_file_methods!(methods); methods.add_method("icon", |_, me, ()| { use crate::Icon; // TODO: use a cache Ok(yazi_config::THEME.icon.matches(me, false).map(Icon::from)) }); } } ================================================ FILE: yazi-binding/src/handle.rs ================================================ use mlua::{MultiValue, UserData, UserDataMethods}; use tokio::task::JoinHandle; pub enum Handle { AsyncFn(JoinHandle>), } impl UserData for Handle { fn add_methods>(methods: &mut M) { methods.add_method("abort", |_, me, ()| { Ok(match me { Self::AsyncFn(h) => h.abort(), }) }); } } ================================================ FILE: yazi-binding/src/icon.rs ================================================ use std::ops::Deref; use mlua::{UserData, UserDataFields, Value}; use crate::{Style, cached_field}; pub struct Icon { inner: &'static yazi_config::Icon, v_text: Option, v_style: Option, } impl Deref for Icon { type Target = yazi_config::Icon; fn deref(&self) -> &Self::Target { self.inner } } impl From<&'static yazi_config::Icon> for Icon { fn from(icon: &'static yazi_config::Icon) -> Self { Self { inner: icon, v_text: None, v_style: None } } } impl UserData for Icon { fn add_fields>(fields: &mut F) { cached_field!(fields, text, |lua, me| lua.create_string(&me.text)); cached_field!(fields, style, |_, me| Ok(Style::from(me.style))); } } ================================================ FILE: yazi-binding/src/id.rs ================================================ use std::ops::Deref; use mlua::{ExternalError, ExternalResult, FromLua, Lua, UserData, Value}; #[derive(Clone, Copy)] pub struct Id(pub yazi_shared::Id); impl Deref for Id { type Target = yazi_shared::Id; fn deref(&self) -> &Self::Target { &self.0 } } impl FromLua for Id { fn from_lua(value: Value, _: &Lua) -> mlua::Result { Ok(match value { Value::Integer(i) => Self(i.try_into().into_lua_err()?), Value::UserData(ud) => *ud.borrow::()?, _ => Err("expected integer or userdata".into_lua_err())?, }) } } impl UserData for Id { fn add_fields>(fields: &mut F) { fields.add_field_method_get("value", |_, me| Ok(me.0.get())); } } ================================================ FILE: yazi-binding/src/image.rs ================================================ use std::ops::Deref; use mlua::{MetaMethod, UserData}; pub struct ImageInfo(yazi_adapter::ImageInfo); impl Deref for ImageInfo { type Target = yazi_adapter::ImageInfo; fn deref(&self) -> &Self::Target { &self.0 } } impl From for ImageInfo { fn from(value: yazi_adapter::ImageInfo) -> Self { Self(value) } } impl UserData for ImageInfo { fn add_fields>(fields: &mut F) { fields.add_field_method_get("w", |_, me| Ok(me.width)); fields.add_field_method_get("h", |_, me| Ok(me.height)); fields.add_field_method_get("ori", |_, me| Ok(me.orientation.map(|o| o.to_exif()))); fields.add_field_method_get("format", |_, me| Ok(ImageFormat(me.format))); fields.add_field_method_get("color", |_, me| Ok(ImageColor(me.color))); } } // --- ImageFormat struct ImageFormat(yazi_adapter::ImageFormat); impl UserData for ImageFormat { fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::ToString, |_, me, ()| { use yazi_adapter::ImageFormat as F; Ok(match me.0 { F::Png => "PNG", F::Jpeg => "JPEG", F::Gif => "GIF", F::WebP => "WEBP", F::Pnm => "PNM", F::Tiff => "TIFF", F::Tga => "TGA", F::Dds => "DDS", F::Bmp => "BMP", F::Ico => "ICO", F::Hdr => "HDR", F::OpenExr => "OpenEXR", F::Farbfeld => "Farbfeld", F::Avif => "AVIF", F::Qoi => "QOI", _ => "Unknown", }) }); } } // --- ImageColor struct ImageColor(yazi_adapter::ImageColor); impl UserData for ImageColor { fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::ToString, |_, me, ()| { use yazi_adapter::ImageColor as C; Ok(match me.0 { C::L8 => "L8", C::La8 => "La8", C::Rgb8 => "Rgb8", C::Rgba8 => "Rgba8", C::L16 => "L16", C::La16 => "La16", C::Rgb16 => "Rgb16", C::Rgba16 => "Rgba16", C::Rgb32F => "Rgb32F", C::Rgba32F => "Rgba32F", _ => "Unknown", }) }); } } ================================================ FILE: yazi-binding/src/input.rs ================================================ use std::pin::Pin; use mlua::{UserData, UserDataMethods}; use tokio::pin; use tokio_stream::StreamExt; use yazi_widgets::input::InputEvent; pub struct InputRx> { inner: T, } impl> InputRx { pub fn new(inner: T) -> Self { Self { inner } } pub async fn consume(inner: T) -> (Option, u8) { pin!(inner); inner.next().await.map(Self::parse).unwrap_or((None, 0)) } fn parse(res: InputEvent) -> (Option, u8) { match res { InputEvent::Submit(s) => (Some(s), 1), InputEvent::Cancel(s) => (Some(s), 2), InputEvent::Type(s) => (Some(s), 3), _ => (None, 0), } } } impl + 'static> UserData for InputRx { fn add_methods>(methods: &mut M) { methods.add_async_method_mut("recv", |_, mut me, ()| async move { let mut inner = unsafe { Pin::new_unchecked(&mut me.inner) }; Ok(inner.next().await.map(Self::parse).unwrap_or((None, 0))) }); } } ================================================ FILE: yazi-binding/src/iter.rs ================================================ use mlua::{AnyUserData, ExternalError, FromLua, IntoLua, IntoLuaMulti, Lua, MetaMethod, UserData, UserDataMethods, UserDataRefMut, Value}; pub struct Iter, T> { iter: I, len: Option, count: usize, cache: Vec, } impl Iter where I: Iterator + 'static, T: IntoLua + 'static, { pub fn new(iter: I, len: Option) -> Self { Self { iter, len, count: 0, cache: vec![] } } } impl Iter where I: Iterator + 'static, T: FromLua + 'static, { pub fn into_iter(self, lua: &Lua) -> impl Iterator> { self .cache .into_iter() .map(|cached| T::from_lua(cached, lua)) .chain(self.iter.map(|rest| Ok(rest))) } } impl UserData for Iter where I: Iterator + 'static, T: IntoLua + 'static, { fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::Len, |_, me, ()| { if let Some(len) = me.len { Ok(len) } else { Err(format!("Length is unknown for {}", std::any::type_name::()).into_lua_err()) } }); methods.add_meta_function(MetaMethod::Pairs, |lua, ud: AnyUserData| { let iter = lua.create_function(|lua, mut me: UserDataRefMut| { if let Some(next) = me.cache.get(me.count).cloned() { me.count += 1; (me.count, next).into_lua_multi(lua) } else if let Some(next) = me.iter.next() { let value = next.into_lua(lua)?; me.cache.push(value.clone()); me.count += 1; (me.count, value).into_lua_multi(lua) } else { ().into_lua_multi(lua) } })?; ud.borrow_mut::()?.count = 0; Ok((iter, ud)) }); } } ================================================ FILE: yazi-binding/src/layer.rs ================================================ use mlua::{MetaMethod, UserData}; #[derive(Clone, Copy)] pub struct Layer(yazi_shared::Layer); impl From for Layer { fn from(event: yazi_shared::Layer) -> Self { Self(event) } } impl UserData for Layer { fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.0.to_string())); } } ================================================ FILE: yazi-binding/src/lib.rs ================================================ mod macros; yazi_macro::mod_pub!(elements); yazi_macro::mod_flat!(access calculator cha chan chord_cow color composer error fd file handle icon id image input iter layer mouse path permit range runtime scheme stage style url utils); ================================================ FILE: yazi-binding/src/macros.rs ================================================ #[macro_export] macro_rules! runtime { ($lua:ident) => {{ use mlua::ExternalError; $lua.app_data_ref::<$crate::Runtime>().ok_or_else(|| "Runtime not found".into_lua_err()) }}; } #[macro_export] macro_rules! runtime_mut { ($lua:ident) => {{ use mlua::ExternalError; $lua.app_data_mut::<$crate::Runtime>().ok_or_else(|| "Runtime not found".into_lua_err()) }}; } #[macro_export] macro_rules! runtime_scope { ($lua:ident, $id:expr, $block:expr) => {{ let mut f = || { let blocking = $crate::runtime_mut!($lua)?.critical_push($id, true); let result = (|| $block)(); $crate::runtime_mut!($lua)?.critical_pop(blocking)?; result }; f() }}; } #[macro_export] macro_rules! deprecate { ($lua:ident, $tt:tt) => {{ static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) { let id = match $crate::runtime!($lua)?.current()? { "init" => "`init.lua` config file", s => &format!("`{s}.yazi` plugin"), }; yazi_macro::emit!(Call( yazi_macro::relay!(app:deprecate).with("content", format!($tt, id)) )); } }}; } #[macro_export] macro_rules! cached_field { ($fields:ident, $key:ident, $value:expr) => { $fields.add_field_function_get(stringify!($key), |lua, ud| { use mlua::{Error::UserDataDestructed, IntoLua, Lua, Result, Value, Value::UserData}; ud.borrow_mut_scoped::>(|me| match paste::paste! { &me.[] } { Some(v) if !v.is_userdata() => Ok(v.clone()), Some(v @ UserData(ud)) if !matches!(ud.borrow::<()>(), Err(UserDataDestructed)) => { Ok(v.clone()) } _ => { let v = ($value as fn(&Lua, &Self) -> Result<_>)(lua, me)?.into_lua(lua)?; paste::paste! { me.[] = Some(v.clone()) }; Ok(v) } })? }); }; } #[macro_export] macro_rules! cached_field_mut { ($fields:ident, $key:ident, $value:expr) => { $fields.add_field_function_get(stringify!($key), |lua, ud| { use mlua::{Error::UserDataDestructed, IntoLua, Lua, Result, Value, Value::UserData}; ud.borrow_mut_scoped::>(|me| match paste::paste! { &me.[] } { Some(Ok(v)) if !v.is_userdata() => Ok(v.clone()), Some(Ok(v @ UserData(ud))) if !matches!(ud.borrow::<()>(), Err(UserDataDestructed)) => { Ok(v.clone()) } Some(Err(e)) => Err(e.clone()), _ => { let v = ($value as fn(&Lua, &mut Self) -> Result<_>)(lua, me).and_then(|v| v.into_lua(lua)); paste::paste! { me.[] = Some(v.clone()) }; v } })? }); }; } #[macro_export] macro_rules! impl_area_method { ($methods:ident) => { $methods.add_function_mut( "area", |lua, (ud, area): (mlua::AnyUserData, Option)| { use mlua::IntoLua; if let Some(v) = area { ud.borrow_mut::()?.area = $crate::elements::Area::try_from(v)?; ud.into_lua(lua) } else { ud.borrow::()?.area.into_lua(lua) } }, ); }; } #[macro_export] macro_rules! impl_style_method { ($methods:ident, $($field:tt).+) => { $methods.add_function_mut("style", |_, (ud, style): (mlua::AnyUserData, $crate::Style)| { ud.borrow_mut::()?.$($field).+ = style.0; Ok(ud) }); }; } #[macro_export] macro_rules! impl_style_shorthands { ($methods:ident, $($field:tt).+) => { $methods.add_function_mut("fg", |lua, (ud, value): (mlua::AnyUserData, mlua::Value)| { use mlua::FromLua; use ratatui::style::Modifier; let me = &mut ud.borrow_mut::()?.$($field).+; match value { mlua::Value::Boolean(true) if me.add_modifier.contains(Modifier::REVERSED) && !me.sub_modifier.contains(Modifier::REVERSED) => { me.bg.map($crate::Color).into_lua(lua) } mlua::Value::Nil | mlua::Value::Boolean(_) => { me.fg.map($crate::Color).into_lua(lua) } _ => { me.fg = Some($crate::Color::from_lua(value, lua)?.0); ud.into_lua(lua) } } }); $methods.add_function_mut("bg", |lua, (ud, value): (mlua::AnyUserData, mlua::Value)| { use mlua::FromLua; use ratatui::style::Modifier; let me = &mut ud.borrow_mut::()?.$($field).+; match value { mlua::Value::Boolean(true) if me.add_modifier.contains(Modifier::REVERSED) && !me.sub_modifier.contains(Modifier::REVERSED) => { me.fg.map($crate::Color).into_lua(lua) } mlua::Value::Nil | mlua::Value::Boolean(_) => { me.bg.map($crate::Color).into_lua(lua) } _ => { me.bg = Some($crate::Color::from_lua(value, lua)?.0); ud.into_lua(lua) } } }); $methods.add_function_mut("bold", |_, (ud, remove): (mlua::AnyUserData, bool)| { let me = &mut ud.borrow_mut::()?.$($field).+; if remove { *me = me.remove_modifier(ratatui::style::Modifier::BOLD); } else { *me = me.add_modifier(ratatui::style::Modifier::BOLD); } Ok(ud) }); $methods.add_function_mut("dim", |_, (ud, remove): (mlua::AnyUserData, bool)| { let me = &mut ud.borrow_mut::()?.$($field).+; if remove { *me = me.remove_modifier(ratatui::style::Modifier::DIM); } else { *me = me.add_modifier(ratatui::style::Modifier::DIM); } Ok(ud) }); $methods.add_function_mut("italic", |_, (ud, remove): (mlua::AnyUserData, bool)| { let me = &mut ud.borrow_mut::()?.$($field).+; if remove { *me = me.remove_modifier(ratatui::style::Modifier::ITALIC); } else { *me = me.add_modifier(ratatui::style::Modifier::ITALIC); } Ok(ud) }); $methods.add_function_mut("underline", |_, (ud, remove): (mlua::AnyUserData, bool)| { let me = &mut ud.borrow_mut::()?.$($field).+; if remove { *me = me.remove_modifier(ratatui::style::Modifier::UNDERLINED); } else { *me = me.add_modifier(ratatui::style::Modifier::UNDERLINED); } Ok(ud) }); $methods.add_function_mut("blink", |_, (ud, remove): (mlua::AnyUserData, bool)| { let me = &mut ud.borrow_mut::()?.$($field).+; if remove { *me = me.remove_modifier(ratatui::style::Modifier::SLOW_BLINK); } else { *me = me.add_modifier(ratatui::style::Modifier::SLOW_BLINK); } Ok(ud) }); $methods.add_function_mut("blink_rapid", |_, (ud, remove): (mlua::AnyUserData, bool)| { let me = &mut ud.borrow_mut::()?.$($field).+; if remove { *me = me.remove_modifier(ratatui::style::Modifier::RAPID_BLINK); } else { *me = me.add_modifier(ratatui::style::Modifier::RAPID_BLINK); } Ok(ud) }); $methods.add_function_mut("reverse", |_, (ud, remove): (mlua::AnyUserData, bool)| { let me = &mut ud.borrow_mut::()?.$($field).+; if remove { *me = me.remove_modifier(ratatui::style::Modifier::REVERSED); } else { *me = me.add_modifier(ratatui::style::Modifier::REVERSED); } Ok(ud) }); $methods.add_function_mut("hidden", |_, (ud, remove): (mlua::AnyUserData, bool)| { let me = &mut ud.borrow_mut::()?.$($field).+; if remove { *me = me.remove_modifier(ratatui::style::Modifier::HIDDEN); } else { *me = me.add_modifier(ratatui::style::Modifier::HIDDEN); } Ok(ud) }); $methods.add_function_mut("crossed", |_, (ud, remove): (mlua::AnyUserData, bool)| { let me = &mut ud.borrow_mut::()?.$($field).+; if remove { *me = me.remove_modifier(ratatui::style::Modifier::CROSSED_OUT); } else { *me = me.add_modifier(ratatui::style::Modifier::CROSSED_OUT); } Ok(ud) }); $methods.add_function_mut("reset", |_, ud: mlua::AnyUserData| { ud.borrow_mut::()?.$($field).+ = ratatui::style::Style::reset(); Ok(ud) }); }; } #[macro_export] macro_rules! impl_file_fields { ($fields:ident) => { $crate::cached_field!($fields, cha, |_, me| Ok($crate::Cha(me.cha))); $crate::cached_field!($fields, url, |_, me| Ok($crate::Url::new(me.url_owned()))); $crate::cached_field!($fields, link_to, |_, me| Ok(me.link_to.as_ref().map($crate::Path::new))); $crate::cached_field!($fields, name, |lua, me| { me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); $crate::cached_field!($fields, path, |_, me| { use yazi_fs::FsUrl; use yazi_shared::url::AsUrl; Ok($crate::Path::new(me.url.as_url().unified_path())) }); $crate::cached_field!($fields, cache, |_, me| { use yazi_fs::FsUrl; Ok(me.url.cache().map($crate::Path::new)) }); }; } #[macro_export] macro_rules! impl_file_methods { ($methods:ident) => { $methods.add_method("hash", |_, me, ()| { use yazi_fs::FsHash64; Ok(me.hash_u64()) }); }; } ================================================ FILE: yazi-binding/src/mouse.rs ================================================ use std::ops::Deref; use crossterm::event::MouseButton; use mlua::{UserData, UserDataFields}; #[derive(Clone, Copy)] pub struct MouseEvent(crossterm::event::MouseEvent); impl Deref for MouseEvent { type Target = crossterm::event::MouseEvent; fn deref(&self) -> &Self::Target { &self.0 } } impl From for MouseEvent { fn from(event: crossterm::event::MouseEvent) -> Self { Self(event) } } impl UserData for MouseEvent { fn add_fields>(fields: &mut F) { fields.add_field_method_get("x", |_, me| Ok(me.column)); fields.add_field_method_get("y", |_, me| Ok(me.row)); fields.add_field_method_get("is_left", |_, me| { use crossterm::event::MouseEventKind as K; Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Left)) }); fields.add_field_method_get("is_right", |_, me| { use crossterm::event::MouseEventKind as K; Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Right)) }); fields.add_field_method_get("is_middle", |_, me| { use crossterm::event::MouseEventKind as K; Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Middle)) }); } } ================================================ FILE: yazi-binding/src/path.rs ================================================ use std::ops::Deref; use mlua::{ExternalError, ExternalResult, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; use yazi_codegen::FromLuaOwned; use yazi_shared::{path::{PathBufDyn, PathLike, StripPrefixError}, strand::{AsStrand, Strand, StrandCow}}; use crate::cached_field; pub type PathRef = UserDataRef; #[derive(FromLuaOwned)] pub struct Path { inner: PathBufDyn, v_ext: Option, v_name: Option, v_parent: Option, v_stem: Option, } impl Deref for Path { type Target = PathBufDyn; fn deref(&self) -> &Self::Target { &self.inner } } impl From for PathBufDyn { fn from(value: Path) -> Self { value.inner } } impl AsStrand for Path { fn as_strand(&self) -> Strand<'_> { self.inner.as_strand() } } impl AsStrand for &Path { fn as_strand(&self) -> Strand<'_> { self.inner.as_strand() } } impl Path { pub fn new(path: impl Into) -> Self { Self { inner: path.into(), v_ext: None, v_name: None, v_parent: None, v_stem: None, } } pub fn install(lua: &Lua) -> mlua::Result<()> { lua.globals().raw_set( "Path", lua.create_table_from([( "os", lua.create_function(|_, s: mlua::String| { Ok(Self::new(s.as_bytes().as_strand().as_os_path().into_lua_err()?)) })?, )])?, ) } fn ends_with(&self, child: Value) -> mlua::Result { match child { Value::String(s) => { self.try_ends_with(StrandCow::with(self.kind(), &*s.as_bytes())?).into_lua_err() } Value::UserData(ud) => self.try_ends_with(&*ud.borrow::()?).into_lua_err(), _ => Err("must be a string or Path".into_lua_err())?, } } fn join(&self, other: Value) -> mlua::Result { Ok(Self::new(match other { Value::String(s) => { self.try_join(StrandCow::with(self.kind(), &*s.as_bytes())?).into_lua_err()? } Value::UserData(ref ud) => { let path = ud.borrow::()?; self.try_join(&*path).into_lua_err()? } _ => Err("must be a string or Path".into_lua_err())?, })) } fn starts_with(&self, base: Value) -> mlua::Result { match base { Value::String(s) => { self.try_starts_with(StrandCow::with(self.kind(), &*s.as_bytes())?).into_lua_err() } Value::UserData(ud) => self.try_starts_with(&*ud.borrow::()?).into_lua_err(), _ => Err("must be a string or Path".into_lua_err())?, } } fn strip_prefix(&self, base: Value) -> mlua::Result> { let strip = match base { Value::String(s) => self.try_strip_prefix(StrandCow::with(self.kind(), &*s.as_bytes())?), Value::UserData(ud) => self.try_strip_prefix(&*ud.borrow::()?), _ => Err("must be a string or Path".into_lua_err())?, }; Ok(match strip { Ok(p) => Some(Self::new(p)), Err(StripPrefixError::Exotic | StripPrefixError::NotPrefix) => None, Err(e @ StripPrefixError::WrongEncoding) => Err(e.into_lua_err())?, }) } } impl UserData for Path { fn add_fields>(fields: &mut F) { cached_field!(fields, ext, |lua, me| { me.ext().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, name, |lua, me| { me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, parent, |_, me| Ok(me.parent().map(Self::new))); cached_field!(fields, stem, |lua, me| { me.stem().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); fields.add_field_method_get("is_absolute", |_, me| Ok(me.is_absolute())); fields.add_field_method_get("has_root", |_, me| Ok(me.has_root())); } fn add_methods>(methods: &mut M) { methods.add_method("ends_with", |_, me, child: Value| me.ends_with(child)); methods.add_method("join", |_, me, other: Value| me.join(other)); methods.add_method("starts_with", |_, me, base: Value| me.starts_with(base)); methods.add_method("strip_prefix", |_, me, base: Value| me.strip_prefix(base)); methods.add_meta_method(MetaMethod::Concat, |lua, lhs, rhs: mlua::String| { lua.create_external_string([lhs.encoded_bytes(), &rhs.as_bytes()].concat()) }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: PathRef| Ok(me.inner == other.inner)); methods .add_meta_method(MetaMethod::ToString, |lua, me, ()| lua.create_string(me.encoded_bytes())); } } ================================================ FILE: yazi-binding/src/permit.rs ================================================ use std::{mem, ops::Deref}; use futures::{FutureExt, future::BoxFuture}; use mlua::{UserData, UserDataMethods}; use tokio::sync::SemaphorePermit; pub type PermitRef = mlua::UserDataRef; pub struct Permit { inner: Option>, destruct: Option>, } impl Deref for Permit { type Target = Option>; fn deref(&self) -> &Self::Target { &self.inner } } impl Permit { pub fn new(inner: SemaphorePermit<'static>, f: F) -> Self where F: Future + 'static + Send, { Self { inner: Some(inner), destruct: Some(f.boxed()) } } fn dropping(&mut self) -> impl Future + 'static { let inner = self.inner.take(); let destruct = self.destruct.take(); async move { if let Some(f) = destruct { f.await; } if let Some(p) = inner { mem::drop(p); } } } } impl Drop for Permit { fn drop(&mut self) { tokio::spawn(self.dropping()); } } impl UserData for Permit { fn add_methods>(methods: &mut M) { methods.add_async_method_mut("drop", |_, mut me, ()| async move { Ok(me.dropping().await) }); } } ================================================ FILE: yazi-binding/src/range.rs ================================================ use mlua::{IntoLua, Lua, Value}; pub struct Range(std::ops::Range); impl From> for Range { fn from(value: std::ops::Range) -> Self { Self(value) } } impl IntoLua for Range where T: IntoLua, { fn into_lua(self, lua: &Lua) -> mlua::Result { lua.create_sequence_from([self.0.start, self.0.end])?.into_lua(lua) } } ================================================ FILE: yazi-binding/src/runtime.rs ================================================ use std::{collections::VecDeque, mem}; use anyhow::{Context, Result}; use hashbrown::{HashMap, hash_map::EntryRef}; use mlua::Function; #[derive(Debug, Default)] pub struct Runtime { frames: VecDeque, blocks: HashMap>, pub blocking: bool, } #[derive(Debug)] pub struct RuntimeFrame { id: String, } impl Runtime { pub fn new_isolate(id: &str) -> Self { Self { frames: VecDeque::from([RuntimeFrame { id: id.to_owned() }]), ..Default::default() } } pub fn push(&mut self, id: &str) { self.frames.push_back(RuntimeFrame { id: id.to_owned() }); } pub fn pop(&mut self) -> Result { self.frames.pop_back().context("Runtime stack underflow") } pub fn critical_push(&mut self, id: &str, blocking: bool) -> bool { self.push(id); mem::replace(&mut self.blocking, blocking) } pub fn critical_pop(&mut self, blocking: bool) -> Result { self.blocking = blocking; self.pop() } pub fn current(&self) -> Result<&str> { self.frames.back().map(|f| f.id.as_str()).context("No current runtime frame") } pub fn current_owned(&self) -> Result { self.current().map(ToOwned::to_owned) } pub fn get_block(&self, id: &str, calls: usize) -> Option { self.blocks.get(id).and_then(|v| v.get(calls)).cloned() } pub fn put_block(&mut self, f: &Function) -> Option { let cur = self.frames.back().filter(|f| f.id != "init")?; Some(match self.blocks.entry_ref(&cur.id) { EntryRef::Occupied(mut oe) => { oe.get_mut().push(f.clone()); oe.get().len() - 1 } EntryRef::Vacant(ve) => { ve.insert(vec![f.clone()]); 0 } }) } } ================================================ FILE: yazi-binding/src/scheme.rs ================================================ use std::ops::Deref; use mlua::{UserData, UserDataFields, Value}; use yazi_fs::FsScheme; use yazi_shared::scheme::SchemeLike; use crate::{Path, cached_field}; pub struct Scheme { inner: yazi_shared::scheme::Scheme, v_kind: Option, v_cache: Option, } impl Deref for Scheme { type Target = yazi_shared::scheme::Scheme; fn deref(&self) -> &Self::Target { &self.inner } } impl Scheme { pub fn new(scheme: impl Into) -> Self { Self { inner: scheme.into(), v_kind: None, v_cache: None } } } impl UserData for Scheme { fn add_fields>(fields: &mut F) { cached_field!(fields, kind, |_, me| Ok(me.kind().as_str())); cached_field!(fields, cache, |_, me| Ok(me.cache().map(Path::new))); fields.add_field_method_get("is_virtual", |_, me| Ok(me.is_virtual())); } } ================================================ FILE: yazi-binding/src/stage.rs ================================================ use mlua::{IntoLuaMulti, MetaMethod, UserData, UserDataMethods}; pub struct FolderStage(yazi_fs::FolderStage); impl FolderStage { pub fn new(inner: yazi_fs::FolderStage) -> Self { Self(inner) } } impl UserData for FolderStage { fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::Call, |lua, me, ()| { use yazi_fs::FolderStage::*; match &me.0 { Loading => false.into_lua_multi(lua), Loaded => true.into_lua_multi(lua), Failed(e) => (true, crate::Error::Fs(e.clone())).into_lua_multi(lua), } }); } } ================================================ FILE: yazi-binding/src/style.rs ================================================ use mlua::{AnyUserData, ExternalError, FromLua, IntoLua, Lua, LuaSerdeExt, MetaMethod, Table, UserData, UserDataMethods, Value}; use crate::SER_OPT; #[derive(Clone, Copy, Default)] pub struct Style(pub ratatui::style::Style); impl Style { pub fn compose(lua: &Lua) -> mlua::Result { let new = lua.create_function(|_, (_, style): (Table, Self)| Ok(style))?; let style = lua.create_table()?; style.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?; style.into_lua(lua) } } impl From for Style { fn from(value: yazi_config::Style) -> Self { Self(value.into()) } } impl FromLua for Style { fn from_lua(value: Value, _: &Lua) -> mlua::Result { Ok(Self(match value { Value::Nil => Default::default(), Value::UserData(ud) => ud.borrow::()?.0, _ => Err("expected a Style or nil".into_lua_err())?, })) } } impl UserData for Style { fn add_methods>(methods: &mut M) { crate::impl_style_shorthands!(methods, 0); methods .add_method("raw", |lua, me, ()| lua.to_value_with(&yazi_config::Style::from(me.0), SER_OPT)); methods.add_function_mut("patch", |_, (ud, style): (AnyUserData, Self)| { let mut me = ud.borrow_mut::()?; me.0 = me.0.patch(style.0); Ok(ud) }) } } ================================================ FILE: yazi-binding/src/url.rs ================================================ use std::ops::Deref; use mlua::{AnyUserData, ExternalError, ExternalResult, IntoLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; use yazi_codegen::FromLuaOwned; use yazi_fs::{FsHash64, FsHash128, FsUrl}; use yazi_shared::{path::{PathLike, StripPrefixError}, scheme::{SchemeCow, SchemeLike}, strand::{StrandLike, ToStrand}, url::{AsUrl, UrlCow, UrlLike}}; use crate::{Path, Scheme, cached_field}; pub type UrlRef = UserDataRef; const EXPECTED: &str = "expected a string, Url, or Path"; #[derive(FromLuaOwned)] pub struct Url { inner: yazi_shared::url::UrlBuf, v_path: Option, v_name: Option, v_stem: Option, v_ext: Option, v_urn: Option, v_base: Option, v_parent: Option, v_scheme: Option, v_domain: Option, v_cache: Option, } impl Deref for Url { type Target = yazi_shared::url::UrlBuf; fn deref(&self) -> &Self::Target { &self.inner } } impl AsUrl for Url { fn as_url(&self) -> yazi_shared::url::Url<'_> { self.inner.as_url() } } impl AsUrl for &Url { fn as_url(&self) -> yazi_shared::url::Url<'_> { self.inner.as_url() } } impl From for yazi_shared::url::UrlBuf { fn from(value: Url) -> Self { value.inner } } impl From for yazi_shared::url::UrlBufCov { fn from(value: Url) -> Self { Self(value.inner) } } impl From for yazi_shared::url::UrlCow<'_> { fn from(value: Url) -> Self { value.inner.into() } } impl TryFrom<&[u8]> for Url { type Error = mlua::Error; fn try_from(value: &[u8]) -> mlua::Result { Ok(Self::new(UrlCow::try_from(value)?)) } } impl Url { pub fn new(url: impl Into) -> Self { Self { inner: url.into(), v_path: None, v_name: None, v_stem: None, v_ext: None, v_urn: None, v_base: None, v_parent: None, v_scheme: None, v_domain: None, v_cache: None, } } pub fn install(lua: &Lua) -> mlua::Result<()> { lua.globals().raw_set( "Url", lua.create_function(|_, value: Value| { Ok(match value { Value::String(s) => Self::try_from(&*s.as_bytes())?, Value::UserData(ud) => { if let Ok(url) = ud.borrow::() { Self::new(&url.inner) } else if let Ok(path) = ud.borrow::() { Self::new(path.as_os().into_lua_err()?) } else { Err(EXPECTED.into_lua_err())? } } _ => Err(EXPECTED.into_lua_err())?, }) })?, ) } fn ends_with(&self, child: Value) -> mlua::Result { match child { Value::String(s) => self.try_ends_with(UrlCow::try_from(&*s.as_bytes())?).into_lua_err(), Value::UserData(ud) => self.try_ends_with(&*ud.borrow::()?).into_lua_err(), _ => Err("must be a string or Url".into_lua_err())?, } } fn hash(&self, long: Option) -> mlua::Result { Ok(if long.unwrap_or(false) { format!("{:x}", self.hash_u128()) } else { format!("{:x}", self.hash_u64()) }) } fn join(&self, lua: &Lua, other: Value) -> mlua::Result { match other { Value::String(s) => { let b = s.as_bytes(); let (scheme, path) = SchemeCow::parse(&b)?; if scheme.covariant(self.scheme()) { Self::new(self.try_join(path).into_lua_err()?).into_lua(lua) } else { Self::new(UrlCow::try_from((scheme, path))?).into_lua(lua) } } Value::UserData(ref ud) => { let url = ud.borrow::()?; if url.scheme().covariant(self.scheme()) { Self::new(self.try_join(url.loc()).into_lua_err()?).into_lua(lua) } else { Ok(other) } } _ => Err("must be a string or Url".into_lua_err())?, } } fn starts_with(&self, base: Value) -> mlua::Result { match base { Value::String(s) => self.try_starts_with(UrlCow::try_from(&*s.as_bytes())?).into_lua_err(), Value::UserData(ud) => self.try_starts_with(&*ud.borrow::()?).into_lua_err(), _ => Err("must be a string or Url".into_lua_err())?, } } fn strip_prefix(&self, base: Value) -> mlua::Result> { let strip = match base { Value::String(s) => self.try_strip_prefix(UrlCow::try_from(&*s.as_bytes())?), Value::UserData(ud) => self.try_strip_prefix(&*ud.borrow::()?), _ => Err("must be a string or Url".into_lua_err())?, }; Ok(match strip { Ok(p) => Some(Path::new(p)), Err(StripPrefixError::Exotic | StripPrefixError::NotPrefix) => None, Err(e @ StripPrefixError::WrongEncoding) => Err(e.into_lua_err())?, }) } } impl UserData for Url { fn add_fields>(fields: &mut F) { cached_field!(fields, path, |_, me| Ok(Path::new(me.loc()))); cached_field!(fields, name, |lua, me| { me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, stem, |lua, me| { me.stem().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, ext, |lua, me| { me.ext().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, urn, |_, me| Ok(Path::new(me.urn()))); cached_field!(fields, base, |_, me| { Ok(Some(me.base()).filter(|u| !u.loc().is_empty()).map(Self::new)) }); cached_field!(fields, parent, |_, me| Ok(me.parent().map(Self::new))); cached_field!(fields, scheme, |_, me| Ok(Scheme::new(me.scheme()))); cached_field!(fields, domain, |lua, me| { me.scheme().domain().map(|s| lua.create_string(s)).transpose() }); cached_field!(fields, cache, |_, me| Ok(me.cache().map(Path::new))); fields.add_field_method_get("is_regular", |_, me| Ok(me.is_regular())); fields.add_field_method_get("is_search", |_, me| Ok(me.is_search())); fields.add_field_method_get("is_archive", |_, me| Ok(me.is_archive())); fields.add_field_method_get("is_absolute", |_, me| Ok(me.is_absolute())); fields.add_field_method_get("has_root", |_, me| Ok(me.has_root())); } fn add_methods>(methods: &mut M) { methods.add_method("ends_with", |_, me, child: Value| me.ends_with(child)); methods.add_method("hash", |_, me, long: Option| me.hash(long)); methods.add_method("join", |lua, me, other: Value| me.join(lua, other)); methods.add_method("starts_with", |_, me, base: Value| me.starts_with(base)); methods.add_method("strip_prefix", |_, me, base: Value| me.strip_prefix(base)); methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| { let url = ud.take::()?.inner.into_search(domain.to_str()?).into_lua_err()?; Ok(Self::new(url)) }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me.inner == other.inner)); methods.add_meta_method(MetaMethod::ToString, |lua, me, ()| { lua.create_string(me.to_strand().encoded_bytes()) }); methods.add_meta_method(MetaMethod::Concat, |lua, lhs, rhs: mlua::String| { lua.create_external_string([lhs.to_strand().encoded_bytes(), &rhs.as_bytes()].concat()) }); } } ================================================ FILE: yazi-binding/src/utils.rs ================================================ use mlua::{IntoLua, Lua, SerializeOptions, Table, Value}; pub const SER_OPT: SerializeOptions = SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false); pub fn get_metatable(lua: &Lua, value: impl IntoLua) -> mlua::Result
{ let (_, mt): (Value, Table) = unsafe { lua.exec_raw(value.into_lua(lua)?, |state| { mlua::ffi::lua_getmetatable(state, -1); }) }?; Ok(mt) } ================================================ FILE: yazi-boot/Cargo.toml ================================================ [package] name = "yazi-boot" description = "Yazi bootstrapper" version.workspace = true edition.workspace = true license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true rust-version.workspace = true [lints] workspace = true [dependencies] yazi-adapter = { path = "../yazi-adapter", version = "26.2.2" } yazi-config = { path = "../yazi-config", version = "26.2.2" } yazi-emulator = { path = "../yazi-emulator", version = "26.2.2" } yazi-fs = { path = "../yazi-fs", version = "26.2.2" } yazi-macro = { path = "../yazi-macro", version = "26.2.2" } yazi-shared = { path = "../yazi-shared", version = "26.2.2" } yazi-vfs = { path = "../yazi-vfs", version = "26.2.2" } # External dependencies clap = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } regex = { workspace = true } [build-dependencies] yazi-shared = { path = "../yazi-shared", version = "26.2.2" } # External dependencies clap = { workspace = true } clap_complete = "4.6.0" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.6.0" vergen-gitcl = { version = "9.1.0", features = [ "build", "rustc" ] } ================================================ FILE: yazi-boot/README.md ================================================ # yazi-boot This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. [source]: https://github.com/sxyazi/yazi ================================================ FILE: yazi-boot/build.rs ================================================ #[path = "src/args.rs"] mod args; use std::{env, error::Error}; use clap::CommandFactory; use clap_complete::{Shell, generate_to}; use vergen_gitcl::{BuildBuilder, Emitter, GitclBuilder, RustcBuilder}; fn main() -> Result<(), Box> { Emitter::default() .add_instructions(&BuildBuilder::default().build_date(true).build()?)? .add_instructions( &RustcBuilder::default() .commit_date(true) .commit_hash(true) .host_triple(true) .semver(true) .build()?, )? .add_instructions(&GitclBuilder::default().commit_date(true).sha(true).build()?)? .emit()?; if env::var_os("YAZI_GEN_COMPLETIONS").is_none() { return Ok(()); } let cmd = &mut args::Args::command(); let bin = "yazi"; let out = "completions"; std::fs::create_dir_all(out)?; for sh in [Shell::Bash, Shell::Fish, Shell::Zsh, Shell::Elvish, Shell::PowerShell] { generate_to(sh, cmd, bin, out)?; } generate_to(clap_complete_nushell::Nushell, cmd, bin, out)?; generate_to(clap_complete_fig::Fig, cmd, bin, out)?; Ok(()) } ================================================ FILE: yazi-boot/src/actions/actions.rs ================================================ use std::process; pub struct Actions; impl Actions { pub(crate) fn act(args: &crate::Args) { if args.debug { println!("{}", Self::debug().unwrap()); process::exit(0); } if args.version { println!("Yazi {}", Self::version()); process::exit(0); } if args.clear_cache { Self::clear_cache(); process::exit(0); } } } ================================================ FILE: yazi-boot/src/actions/clear_cache.rs ================================================ use yazi_config::YAZI; use yazi_fs::Xdg; use super::Actions; impl Actions { pub(super) fn clear_cache() { if YAZI.preview.cache_dir == *Xdg::cache_dir() { println!("Clearing cache directory: \n{:?}", YAZI.preview.cache_dir); std::fs::remove_dir_all(&YAZI.preview.cache_dir).unwrap(); } else { println!( "You've changed the default cache directory, for your data's safety, please clear it manually: \n{:?}", YAZI.preview.cache_dir ); } } } ================================================ FILE: yazi-boot/src/actions/debug.rs ================================================ use std::{env, ffi::OsStr, fmt::Write, path::Path}; use regex::Regex; use yazi_config::{THEME, YAZI}; use yazi_emulator::Mux; use yazi_fs::Xdg; use yazi_shared::timestamp_us; use super::Actions; impl Actions { pub(super) fn debug() -> Result { let mut s = String::new(); writeln!(s, "\nYazi")?; writeln!(s, " Version : {}", Self::version())?; writeln!(s, " Debug : {}", cfg!(debug_assertions))?; writeln!(s, " Triple : {}", Self::triple())?; writeln!(s, " Rustc : {}", Self::rustc())?; writeln!(s, " Backtrace: {:?}", env::var_os("RUST_BACKTRACE"))?; writeln!(s, "\nYa")?; writeln!(s, " Version: {}", Self::process_output("ya", "--version"))?; writeln!(s, "\nConfig")?; writeln!(s, " Init : {}", Self::config_state("init.lua"))?; writeln!(s, " Yazi : {}", Self::config_state("yazi.toml"))?; writeln!(s, " Keymap : {}", Self::config_state("keymap.toml"))?; writeln!(s, " Theme : {}", Self::config_state("theme.toml"))?; writeln!(s, " VFS : {}", Self::config_state("vfs.toml"))?; writeln!(s, " Package : {}", Self::config_state("package.toml"))?; writeln!(s, " Dark/light flavor: {:?} / {:?}", THEME.flavor.dark, THEME.flavor.light)?; writeln!(s, "\nEmulator")?; writeln!(s, " TERM : {:?}", env::var_os("TERM"))?; writeln!(s, " TERM_PROGRAM : {:?}", env::var_os("TERM_PROGRAM"))?; writeln!(s, " TERM_PROGRAM_VERSION: {:?}", env::var_os("TERM_PROGRAM_VERSION"))?; writeln!(s, " Brand.from_env : {:?}", yazi_emulator::Brand::from_env())?; writeln!(s, " Emulator.detect : {:?}", &*yazi_emulator::EMULATOR)?; writeln!(s, "\nAdapter")?; writeln!(s, " Adapter.matches : {:?}", yazi_adapter::ADAPTOR)?; writeln!(s, " Dimension.available: {:?}", yazi_emulator::Dimension::available())?; writeln!(s, "\nDesktop")?; writeln!(s, " XDG_SESSION_TYPE : {:?}", env::var_os("XDG_SESSION_TYPE"))?; writeln!(s, " WAYLAND_DISPLAY : {:?}", env::var_os("WAYLAND_DISPLAY"))?; writeln!(s, " DISPLAY : {:?}", env::var_os("DISPLAY"))?; writeln!(s, " SWAYSOCK : {:?}", env::var_os("SWAYSOCK"))?; #[rustfmt::skip] writeln!(s, " HYPRLAND_INSTANCE_SIGNATURE: {:?}", env::var_os("HYPRLAND_INSTANCE_SIGNATURE"))?; writeln!(s, " WAYFIRE_SOCKET : {:?}", env::var_os("WAYFIRE_SOCKET"))?; writeln!(s, "\nSSH")?; writeln!(s, " shared.in_ssh_connection: {}", yazi_shared::in_ssh_connection())?; writeln!(s, "\nWSL")?; writeln!(s, " WSL: {:?}", yazi_adapter::WSL)?; writeln!(s, "\nVariables")?; writeln!(s, " SHELL : {:?}", env::var_os("SHELL"))?; writeln!(s, " EDITOR : {:?}", env::var_os("EDITOR"))?; writeln!(s, " VISUAL : {:?}", env::var_os("VISUAL"))?; writeln!(s, " YAZI_FILE_ONE : {:?}", env::var_os("YAZI_FILE_ONE"))?; writeln!(s, " YAZI_CONFIG_HOME : {:?}", env::var_os("YAZI_CONFIG_HOME"))?; writeln!(s, " YAZI_ZOXIDE_OPTS : {:?}", env::var_os("YAZI_ZOXIDE_OPTS"))?; writeln!(s, " SSH_AUTH_SOCK : {:?}", env::var_os("SSH_AUTH_SOCK"))?; writeln!(s, " FZF_DEFAULT_OPTS : {:?}", env::var_os("FZF_DEFAULT_OPTS"))?; writeln!(s, " FZF_DEFAULT_COMMAND: {:?}", env::var_os("FZF_DEFAULT_COMMAND"))?; writeln!(s, "\nText Opener")?; writeln!( s, " default : {:?}", YAZI.opener.first(YAZI.open.all(Path::new("f75a.txt"), "text/plain")) )?; writeln!( s, " block-create: {:?}", YAZI.opener.block(YAZI.open.all(Path::new("bulk-create.txt"), "text/plain")) )?; writeln!( s, " block-rename: {:?}", YAZI.opener.block(YAZI.open.all(Path::new("bulk-rename.txt"), "text/plain")) )?; writeln!(s, "\nMultiplexers")?; writeln!(s, " TMUX : {}", yazi_emulator::TMUX)?; writeln!(s, " tmux version : {}", Self::process_output("tmux", "-V"))?; writeln!(s, " tmux build flags : enable-sixel={}", Mux::tmux_sixel_flag())?; writeln!(s, " ZELLIJ_SESSION_NAME: {:?}", env::var_os("ZELLIJ_SESSION_NAME"))?; writeln!(s, " Zellij version : {}", Self::process_output("zellij", "--version"))?; writeln!(s, "\nDependencies")?; #[rustfmt::skip] writeln!(s, " file : {}", Self::process_output(env::var_os("YAZI_FILE_ONE").unwrap_or("file".into()), "--version"))?; writeln!(s, " ueberzugpp : {}", Self::process_output("ueberzugpp", "--version"))?; #[rustfmt::skip] writeln!(s, " ffmpeg/ffprobe: {} / {}", Self::process_output("ffmpeg", "-version"), Self::process_output("ffprobe", "-version"))?; writeln!(s, " pdftoppm : {}", Self::process_output("pdftoppm", "--help"))?; writeln!(s, " magick : {}", Self::process_output("magick", "--version"))?; writeln!(s, " fzf : {}", Self::process_output("fzf", "--version"))?; #[rustfmt::skip] writeln!(s, " fd/fdfind : {} / {}", Self::process_output("fd", "--version"), Self::process_output("fdfind", "--version"))?; writeln!(s, " rg : {}", Self::process_output("rg", "--version"))?; writeln!(s, " chafa : {}", Self::process_output("chafa", "--version"))?; writeln!(s, " zoxide : {}", Self::process_output("zoxide", "--version"))?; #[rustfmt::skip] writeln!(s, " 7zz/7z : {} / {}", Self::process_output("7zz", "i"), Self::process_output("7z", "i"))?; writeln!(s, " resvg : {}", Self::process_output("resvg", "--version"))?; writeln!(s, " jq : {}", Self::process_output("jq", "--version"))?; writeln!(s, "\nClipboard")?; #[rustfmt::skip] writeln!(s, " wl-copy/paste: {} / {}", Self::process_output("wl-copy", "--version"), Self::process_output("wl-paste", "--version"))?; writeln!(s, " xclip : {}", Self::process_output("xclip", "-version"))?; writeln!(s, " xsel : {}", Self::process_output("xsel", "--version"))?; writeln!(s, "\nRoutine")?; writeln!(s, " `file -bL --mime-type`: {}", Self::file1_output())?; writeln!( s, "\n\nSee https://yazi-rs.github.io/docs/plugins/overview#debugging on how to enable logging or debug runtime errors." )?; Ok(s) } fn config_state(name: &str) -> String { let p = Xdg::config_dir().join(name); match std::fs::read_to_string(&p) { Ok(s) if s.is_empty() => format!("{} (empty)", p.display()), Ok(s) if s.trim().is_empty() => format!("{} (whitespaces)", p.display()), Ok(s) => format!("{} ({} chars)", p.display(), s.chars().count()), Err(e) => format!("{} ({e})", p.display()), } } fn process_output(name: impl AsRef, arg: impl AsRef) -> String { match std::process::Command::new(&name).arg(arg).output() { Ok(out) if out.status.success() => { let line = String::from_utf8_lossy(&if out.stdout.is_empty() { out.stderr } else { out.stdout }) .trim() .lines() .next() .unwrap_or_default() .to_owned(); if name.as_ref() == "ya" { line.trim_start_matches("Ya ").to_owned() } else { Regex::new(r"\d+\.\d+(\.\d+-\d+|\.\d+|\b)") .unwrap() .find(&line) .map(|m| m.as_str().to_owned()) .unwrap_or(line) } } Ok(out) => format!("{:?}, {:?}", out.status, String::from_utf8_lossy(&out.stderr)), Err(e) => format!("{e}"), } } fn file1_output() -> String { use std::io::Write; let p = env::temp_dir().join(format!(".yazi-debug-{}.tmp", timestamp_us())); std::fs::File::create_new(&p).map(|mut f| f.write_all(b"Hello, World!")).ok(); let program = env::var_os("YAZI_FILE_ONE").unwrap_or("file".into()); match std::process::Command::new(program).args(["-bL", "--mime-type"]).arg(&p).output() { Ok(out) => { String::from_utf8_lossy(&out.stdout).trim().lines().next().unwrap_or_default().to_owned() } Err(e) => format!("{e}"), } } } ================================================ FILE: yazi-boot/src/actions/mod.rs ================================================ yazi_macro::mod_flat!(actions clear_cache debug rustc triple version); ================================================ FILE: yazi-boot/src/actions/rustc.rs ================================================ use super::Actions; impl Actions { pub(super) fn rustc() -> String { format!( "{} ({} {})", env!("VERGEN_RUSTC_SEMVER"), &env!("VERGEN_RUSTC_COMMIT_HASH")[..8], env!("VERGEN_RUSTC_COMMIT_DATE") ) } } ================================================ FILE: yazi-boot/src/actions/triple.rs ================================================ use super::Actions; impl Actions { pub(super) fn triple() -> String { format!( "{} ({}-{})", env!("VERGEN_RUSTC_HOST_TRIPLE"), std::env::consts::OS, std::env::consts::ARCH ) } } ================================================ FILE: yazi-boot/src/actions/version.rs ================================================ use super::Actions; impl Actions { pub fn version() -> &'static str { concat!( env!("CARGO_PKG_VERSION"), " (", env!("VERGEN_GIT_SHA"), " ", env!("VERGEN_BUILD_DATE"), ")" ) } } ================================================ FILE: yazi-boot/src/args.rs ================================================ use std::path::PathBuf; use clap::Parser; use yazi_shared::{Id, url::UrlBuf}; #[derive(Debug, Default, Parser)] #[command(name = "yazi")] #[command(after_help = "See https://yazi-rs.github.io/docs/quick-start for a quick starter.")] pub struct Args { /// Set the current working entry #[arg(index = 1, num_args = 1..=9)] pub entries: Vec, /// Write the cwd on exit to this file #[arg(long)] pub cwd_file: Option, /// Write the selected files to this file on open fired #[arg(long)] pub chooser_file: Option, /// Clear the cache directory #[arg(long)] pub clear_cache: bool, /// Use the specified client ID, must be a globally unique number #[arg(long)] pub client_id: Option, /// Report the specified local events to stdout #[arg(long)] pub local_events: Option, /// Report the specified remote events to stdout #[arg(long)] pub remote_events: Option, /// Print debug information #[arg(long)] pub debug: bool, /// Print version #[arg(short = 'V', long)] pub version: bool, } ================================================ FILE: yazi-boot/src/boot.rs ================================================ use std::path::PathBuf; use futures::executor::block_on; use hashbrown::HashSet; use yazi_fs::{CWD, Xdg, path::clean_url}; use yazi_shared::{strand::StrandBuf, url::{UrlBuf, UrlLike}}; use yazi_vfs::provider; #[derive(Debug, Default)] pub struct Boot { pub cwds: Vec, pub files: Vec, pub local_events: HashSet, pub remote_events: HashSet, pub config_dir: PathBuf, pub flavor_dir: PathBuf, pub plugin_dir: PathBuf, pub state_dir: PathBuf, } impl Boot { async fn parse_entries(entries: &[UrlBuf]) -> (Vec, Vec) { if entries.is_empty() { return (vec![CWD.load().as_ref().clone()], vec![Default::default()]); } async fn go(entry: &UrlBuf) -> (UrlBuf, StrandBuf) { let mut entry = clean_url(entry); if let Ok(u) = provider::absolute(&entry).await && u.is_owned() { entry = u.into_owned(); } let Some((parent, child)) = entry.pair() else { return (entry, Default::default()); }; if provider::metadata(&entry).await.is_ok_and(|m| m.is_file()) { (parent.into(), child.into()) } else { (entry, Default::default()) } } futures::future::join_all(entries.iter().map(go)).await.into_iter().unzip() } } impl From<&crate::Args> for Boot { fn from(args: &crate::Args) -> Self { let config_dir = Xdg::config_dir(); let (cwds, files) = block_on(Self::parse_entries(&args.entries)); let local_events = args .local_events .as_ref() .map(|s| s.split(',').map(|s| s.to_owned()).collect()) .unwrap_or_default(); let remote_events = args .remote_events .as_ref() .map(|s| s.split(',').map(|s| s.to_owned()).collect()) .unwrap_or_default(); Self { cwds, files, local_events, remote_events, flavor_dir: config_dir.join("flavors"), plugin_dir: config_dir.join("plugins"), config_dir, state_dir: Xdg::state_dir(), } } } ================================================ FILE: yazi-boot/src/lib.rs ================================================ yazi_macro::mod_pub!(actions); yazi_macro::mod_flat!(args boot); use clap::Parser; use yazi_shared::RoCell; pub static ARGS: RoCell = RoCell::new(); pub static BOOT: RoCell = RoCell::new(); pub fn init() { ARGS.with(<_>::parse); BOOT.init(<_>::from(&*ARGS)); actions::Actions::act(&ARGS); } pub fn init_default() { ARGS.with(<_>::default); BOOT.with(<_>::default); } ================================================ FILE: yazi-build/Cargo.toml ================================================ [package] name = "yazi-build" description = "Yazi build system" version.workspace = true edition.workspace = true license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true rust-version.workspace = true [lints] workspace = true [profile.release] codegen-units = 1 lto = true panic = "abort" strip = true [build-dependencies] yazi-tty = { path = "../yazi-tty", version = "26.2.2" } [[bin]] name = "yazi-build" path = "src/main.rs" ================================================ FILE: yazi-build/README.md ================================================ # yazi-build This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. [source]: https://github.com/sxyazi/yazi ================================================ FILE: yazi-build/build.rs ================================================ use std::{env, error::Error, io::{BufRead, BufReader, Read, Write}, process::{Command, Stdio}, thread}; use yazi_tty::TTY; fn main() -> Result<(), Box> { yazi_tty::init(); let manifest = env::var_os("CARGO_MANIFEST_DIR").unwrap().to_string_lossy().replace(r"\", "/"); let crates = if manifest.contains("/git/checkouts/yazi-") { &["--git", "https://github.com/sxyazi/yazi.git", "yazi-fm", "yazi-cli"] } else if manifest.contains("/registry/src/index.crates.io-") { &["yazi-fm", "yazi-cli"][..] } else { return Ok(()); }; let target = env::var("TARGET").unwrap(); let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); unsafe { env::set_var("CARGO_TARGET_DIR", "target"); env::set_var("VERGEN_GIT_SHA", "Crates.io"); env::set_var("YAZI_CRATE_BUILD", "1"); env::set_var("JEMALLOC_SYS_WITH_LG_PAGE", "16"); env::set_var("JEMALLOC_SYS_WITH_MALLOC_CONF", "narenas:1"); env::set_var( "MACOSX_DEPLOYMENT_TARGET", if target == "aarch64-apple-darwin" { "11.0" } else { "10.12" }, ); if target == "aarch64-apple-darwin" { env::set_var("RUSTFLAGS", "-Ctarget-cpu=apple-m1"); } }; let profile = if target_os == "windows" { &["--profile", "release-windows"][..] } else { &[] }; let mut child = Command::new(env::var_os("CARGO").unwrap()) .args(["install", "--force", "--locked"]) .args(profile) .args(crates) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; let out = flash(child.stdout.take().unwrap()); let err = flash(child.stderr.take().unwrap()); child.wait()?; out.join().ok(); err.join().ok(); Ok(()) } fn flash(src: R) -> thread::JoinHandle<()> { thread::spawn(move || { let reader = BufReader::new(src); for part in reader.split(b'\n') { match part { Ok(mut bytes) => { bytes.push(b'\n'); let mut out = TTY.lockout(); out.write_all(&bytes).ok(); out.flush().ok(); } Err(_) => break, } } }) } ================================================ FILE: yazi-build/src/main.rs ================================================ fn main() { println!("See https://yazi-rs.github.io/docs/installation#crates on how to install Yazi."); } ================================================ FILE: yazi-cli/Cargo.toml ================================================ [package] name = "yazi-cli" description = "Yazi command-line interface" version.workspace = true edition.workspace = true license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true rust-version.workspace = true [lints] workspace = true [profile.release] codegen-units = 1 lto = true panic = "abort" strip = true [profile.release-windows] inherits = "release" panic = "unwind" [dependencies] yazi-boot = { path = "../yazi-boot", version = "26.2.2" } yazi-dds = { path = "../yazi-dds", version = "26.2.2" } yazi-fs = { path = "../yazi-fs", version = "26.2.2" } yazi-macro = { path = "../yazi-macro", version = "26.2.2" } yazi-shared = { path = "../yazi-shared", version = "26.2.2" } yazi-shim = { path = "../yazi-shim", version = "26.2.2" } # External dependencies anyhow = { workspace = true } clap = { workspace = true } crossterm = { workspace = true } hashbrown = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } toml = { workspace = true } twox-hash = { workspace = true } [build-dependencies] yazi-shared = { path = "../yazi-shared", version = "26.2.2" } # External build dependencies anyhow = { workspace = true } clap = { workspace = true } clap_complete = "4.6.0" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.6.0" serde = { workspace = true } serde_json = { workspace = true } vergen-gitcl = { version = "9.1.0", features = [ "build" ] } [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } [[bin]] name = "ya" path = "src/main.rs" [package.metadata.binstall] pkg-url = "{ repo }/releases/download/v{ version }/yazi-{ target }{ archive-suffix }" bin-dir = "yazi-{ target }/{ bin }{ binary-ext }" ================================================ FILE: yazi-cli/README.md ================================================ # yazi-cli This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. [source]: https://github.com/sxyazi/yazi ================================================ FILE: yazi-cli/build.rs ================================================ #[path = "src/args.rs"] mod args; use std::{env, error::Error}; use clap::CommandFactory; use clap_complete::{Shell, generate_to}; use vergen_gitcl::{BuildBuilder, Emitter, GitclBuilder}; fn main() -> Result<(), Box> { let manifest = env::var_os("CARGO_MANIFEST_DIR").unwrap().to_string_lossy().replace(r"\", "/"); if env::var_os("YAZI_CRATE_BUILD").is_none() && (manifest.contains("/git/checkouts/yazi-") || manifest.contains("/registry/src/index.crates.io-")) { panic!( "Due to Cargo's limitations, the `yazi-fm` and `yazi-cli` crates on crates.io must be built with `cargo install --force yazi-build`" ); } generate() } fn generate() -> Result<(), Box> { Emitter::default() .add_instructions(&BuildBuilder::default().build_date(true).build()?)? .add_instructions(&GitclBuilder::default().commit_date(true).sha(true).build()?)? .emit()?; if env::var_os("YAZI_GEN_COMPLETIONS").is_none() { return Ok(()); } let cmd = &mut args::Args::command(); let bin = "ya"; let out = "completions"; std::fs::create_dir_all(out)?; for sh in [Shell::Bash, Shell::Fish, Shell::Zsh, Shell::Elvish, Shell::PowerShell] { generate_to(sh, cmd, bin, out)?; } generate_to(clap_complete_nushell::Nushell, cmd, bin, out)?; generate_to(clap_complete_fig::Fig, cmd, bin, out)?; Ok(()) } ================================================ FILE: yazi-cli/src/args.rs ================================================ use std::{borrow::Cow, ffi::OsString}; use anyhow::{Result, bail}; use clap::{Parser, Subcommand}; use yazi_shared::Id; #[derive(Parser)] #[command(name = "Ya", about, long_about = None)] pub(super) struct Args { #[command(subcommand)] pub(super) command: Command, /// Print version #[arg(short = 'V', long)] pub(super) version: bool, } #[derive(Subcommand)] pub(super) enum Command { /// Emit an action to be executed by the current instance. Emit(CommandEmit), /// Emit an action to be executed by the specified instance. EmitTo(CommandEmitTo), /// Execute an action on the current instance and print its result. Exec(CommandExec), /// Manage packages. #[command(subcommand)] Pkg(CommandPkg), /// Publish a message to the current instance. Pub(CommandPub), /// Publish a message to the specified instance. PubTo(CommandPubTo), /// Subscribe to messages from all remote instances. Sub(CommandSub), } #[derive(clap::Args)] pub(super) struct CommandEmit { /// Name of the action. pub(super) name: String, /// Arguments of the action. #[arg(allow_hyphen_values = true, trailing_var_arg = true)] pub(super) args: Vec, } #[derive(clap::Args)] pub(super) struct CommandEmitTo { /// Receiver ID. pub(super) receiver: Id, /// Name of the action. pub(super) name: String, /// Arguments of the action. #[arg(allow_hyphen_values = true, trailing_var_arg = true)] pub(super) args: Vec, } #[derive(clap::Args)] pub(super) struct CommandExec { /// Name of the action. pub(super) name: String, /// Arguments of the action. #[arg(allow_hyphen_values = true, trailing_var_arg = true)] pub(super) args: Vec, } #[derive(Subcommand)] pub(super) enum CommandPkg { /// Add packages. #[command(arg_required_else_help = true)] Add { /// Packages to add. #[arg(index = 1, num_args = 1..)] ids: Vec, }, /// Delete packages. #[command(arg_required_else_help = true)] Delete { /// Packages to delete. #[arg(index = 1, num_args = 1..)] ids: Vec, }, /// Install all packages. Install, /// List all packages. List, /// Upgrade all packages. Upgrade { /// Packages to upgrade, upgrade all if unspecified. #[arg(index = 1, num_args = 0..)] ids: Vec, }, } #[derive(clap::Args)] pub(super) struct CommandPub { /// Kind of message. #[arg(index = 1)] pub(super) kind: String, /// Send the message with a string body. #[arg(long)] pub(super) str: Option, /// Send the message with a JSON body. #[arg(long)] pub(super) json: Option, /// Send the message as a list of strings. #[arg(long, num_args = 0..)] pub(super) list: Vec, } impl CommandPub { #[allow(dead_code)] pub(super) fn receiver() -> Result { if let Some(s) = std::env::var("YAZI_PID").ok().filter(|s| !s.is_empty()) { Ok(s.parse()?) } else { bail!("No `YAZI_ID` environment variable found.") } } } #[derive(clap::Args)] pub(super) struct CommandPubTo { /// Receiver ID. #[arg(index = 1)] pub(super) receiver: Id, /// Kind of message. #[arg(index = 2)] pub(super) kind: String, /// Send the message with a string body. #[arg(long)] pub(super) str: Option, /// Send the message with a JSON body. #[arg(long)] pub(super) json: Option, /// Send the message as a list of strings. #[arg(long, num_args = 0..)] pub(super) list: Vec, } #[derive(clap::Args)] pub(super) struct CommandSub { /// Kind of messages to subscribe to, separated by commas if multiple. #[arg(index = 1)] pub(super) kinds: String, } // --- Macros macro_rules! impl_emit_body { ($name:ident) => { impl $name { #[allow(dead_code)] pub(super) fn body(self) -> Result { #[derive(serde::Serialize)] #[serde(untagged)] enum Elem { Name(String), Arg(Vec), } let action: Vec<_> = [Elem::Name(self.name)] .into_iter() .chain(self.args.into_iter().map(|s| Elem::Arg(s.into_encoded_bytes()))) .collect(); Ok(serde_json::to_string(&action)?) } } }; } macro_rules! impl_exec_body { ($name:ident) => { impl $name { #[allow(dead_code)] pub(super) fn body(self, reply_to: Id) -> Result { #[derive(serde::Serialize)] #[serde(untagged)] enum Elem { Id(Id), Name(String), Arg(Vec), } let action: Vec<_> = [Elem::Id(reply_to), Elem::Name(self.name)] .into_iter() .chain(self.args.into_iter().map(|s| Elem::Arg(s.into_encoded_bytes()))) .collect(); Ok(serde_json::to_string(&action)?) } } }; } macro_rules! impl_pub_body { ($name:ident) => { impl $name { #[allow(dead_code)] pub(super) fn body(&self) -> Result> { Ok(if let Some(json) = &self.json { json.into() } else if let Some(str) = &self.str { serde_json::to_string(str)?.into() } else if !self.list.is_empty() { serde_json::to_string(&self.list)?.into() } else { "".into() }) } } }; } impl_emit_body!(CommandEmit); impl_emit_body!(CommandEmitTo); impl_exec_body!(CommandExec); impl_pub_body!(CommandPub); impl_pub_body!(CommandPubTo); ================================================ FILE: yazi-cli/src/dds/draw.rs ================================================ use anyhow::{Context, Result}; use hashbrown::HashSet; use tokio::{io::AsyncWriteExt, time}; use yazi_dds::{ClientReader, Payload, Stream, ember::EmberHi}; use yazi_macro::try_format; use crate::dds::Dds; impl Dds { /// Connect to an existing server and listen in on the messages that are being /// sent by other yazi instances: /// - If no server is running, fail right away; /// - If a server is closed, attempt to reconnect forever. pub(crate) async fn draw(kinds: HashSet<&str>) -> Result<()> { async fn make(kinds: &HashSet<&str>) -> Result { let (lines, mut writer) = Stream::connect().await?; let hi = Payload::new(EmberHi::borrowed(kinds.iter().copied())); writer.write_all(try_format!("{hi}\n")?.as_bytes()).await?; writer.flush().await?; Ok(lines) } let mut lines = make(&kinds).await.context("No running Yazi instance found")?; loop { match lines.next_line().await? { Some(s) => { let kind = s.split(',').next(); if matches!(kind, Some(kind) if kinds.contains(kind)) { println!("{s}"); } } None => loop { time::sleep(time::Duration::from_secs(1)).await; if let Ok(new) = make(&kinds).await { lines = new; break; } }, } } } } ================================================ FILE: yazi-cli/src/dds/exec.rs ================================================ use std::str::FromStr; use anyhow::{Result, bail}; use serde::Deserialize; use tokio::io::AsyncWriteExt; use yazi_dds::{ID, Payload, Stream, ember::{Ember, EmberHi}}; use yazi_macro::try_format; use yazi_shared::{Id, data::Data}; use crate::{CommandExec, CommandPub, dds::Dds}; impl Dds { pub(crate) async fn exec(cmd: CommandExec) -> anyhow::Result { let receiver = CommandPub::receiver()?; let req = cmd.body(*yazi_dds::ID)?; let resp = Self::ask("dds-exec", receiver, &req, "dds-exec-result").await?; #[derive(Deserialize)] struct Body { ok: bool, #[serde(default)] value: Data, #[serde(default)] error: String, } let body = Body::deserialize(&resp)?; if body.ok { Ok(body.value) } else if !body.error.is_empty() { bail!("{}", body.error) } else { bail!("Unknown error") } } /// Send one custom message and wait for a matching custom reply. async fn ask(kind: &str, receiver: Id, body: &str, reply_kind: &str) -> Result { Ember::validate(kind)?; Ember::validate(reply_kind)?; let payload = try_format!( "{}\n{kind},{receiver},{ID},{body}\n", Payload::new(EmberHi::borrowed([reply_kind])), )?; let (mut lines, mut writer) = Stream::connect().await?; writer.write_all(payload.as_bytes()).await?; writer.flush().await?; drop(writer); while let Ok(Some(line)) = lines.next_line().await { match line.split(',').next() { Some("hey") => { if let Ok(Ember::Hey(hey)) = Payload::from_str(&line).map(|p| p.body) { Self::ensure_version(Some(&hey.version))?; Self::ensure_ability(&hey.peers, kind, receiver)?; } } Some(kind) if kind == reply_kind => match Payload::from_str(&line)?.body { Ember::Custom(body) => return Ok(body.data), _ => bail!("Expected custom payload of kind `{reply_kind}`"), }, _ => {} } } bail!("Connection closed before receiving reply") } } ================================================ FILE: yazi-cli/src/dds/mod.rs ================================================ yazi_macro::mod_flat!(draw exec shot); pub(crate) struct Dds; ================================================ FILE: yazi-cli/src/dds/shot.rs ================================================ use std::str::FromStr; use anyhow::{Result, bail}; use hashbrown::HashMap; use tokio::io::AsyncWriteExt; use yazi_dds::{ID, Payload, Peer, Stream, ember::{Ember, EmberBye, EmberHi}}; use yazi_macro::try_format; use yazi_shared::Id; use crate::dds::Dds; impl Dds { /// Connect to an existing server to send a single message. pub(crate) async fn shot(kind: &str, receiver: Id, body: &str) -> Result<()> { Ember::validate(kind)?; let payload = try_format!( "{}\n{kind},{receiver},{ID},{body}\n{}\n", Payload::new(EmberHi::borrowed([])), Payload::new(EmberBye::borrowed()) )?; let (mut lines, mut writer) = Stream::connect().await?; writer.write_all(payload.as_bytes()).await?; writer.flush().await?; drop(writer); let (mut peers, mut version) = Default::default(); while let Ok(Some(line)) = lines.next_line().await { match line.split(',').next() { Some("hey") => { if let Ok(Ember::Hey(hey)) = Payload::from_str(&line).map(|p| p.body) { (peers, version) = (hey.peers, Some(hey.version)); } } Some("bye") => break, _ => {} } } Self::ensure_version(version.as_deref())?; Self::ensure_ability(&peers, kind, receiver)?; Ok(()) } pub(super) fn ensure_version(version: Option<&str>) -> Result<()> { if version.as_deref() != Some(EmberHi::version()) { bail!( "Incompatible version (Ya {}, Yazi {}). Restart all `ya` and `yazi` processes if you upgrade either one.", EmberHi::version(), version.as_deref().unwrap_or("Unknown") ); } Ok(()) } pub(super) fn ensure_ability(peers: &HashMap, kind: &str, receiver: Id) -> Result<()> { match (receiver, peers.get(&receiver).map(|p| p.able(kind))) { // Send to all receivers (Id(0), _) if peers.is_empty() => { bail!("No receiver found. Check if any receivers are running.") } (Id(0), _) if peers.values().all(|p| !p.able(kind)) => { bail!("No receiver has the ability to receive `{kind}` messages.") } (Id(0), _) => Ok(()), // Send to a specific receiver (_, Some(true)) => Ok(()), (_, Some(false)) => { bail!("Receiver `{receiver}` does not have the ability to receive `{kind}` messages.") } (_, None) => bail!("Receiver `{receiver}` not found. Check if the receiver is running."), } } } ================================================ FILE: yazi-cli/src/main.rs ================================================ yazi_macro::mod_pub!(dds package shared); yazi_macro::mod_flat!(args); use std::process::ExitCode; use clap::Parser; use yazi_macro::{errln, outln}; use yazi_shared::LOCAL_SET; #[tokio::main] async fn main() -> ExitCode { yazi_shared::init(); yazi_fs::init(); match LOCAL_SET.run_until(run()).await { Ok(()) => ExitCode::SUCCESS, Err(e) => { for cause in e.chain() { if let Some(ioerr) = cause.downcast_ref::() && ioerr.kind() == std::io::ErrorKind::BrokenPipe { return ExitCode::from(0); } } errln!("{e:#}").ok(); ExitCode::FAILURE } } } async fn run() -> anyhow::Result<()> { if std::env::args_os().nth(1).is_some_and(|s| s == "-V" || s == "--version") { outln!( "Ya {} ({} {})", env!("CARGO_PKG_VERSION"), env!("VERGEN_GIT_SHA"), env!("VERGEN_BUILD_DATE") )?; return Ok(()); } match Args::parse().command { Command::Emit(cmd) => { yazi_boot::init_default(); yazi_dds::init(); if let Err(e) = dds::Dds::shot("dds-emit", CommandPub::receiver()?, &cmd.body()?).await { errln!("Cannot emit command: {e}")?; std::process::exit(1); } } Command::EmitTo(cmd) => { yazi_boot::init_default(); yazi_dds::init(); if let Err(e) = dds::Dds::shot("dds-emit", cmd.receiver, &cmd.body()?).await { errln!("Cannot emit command: {e}")?; std::process::exit(1); } } Command::Exec(cmd) => { yazi_boot::init_default(); yazi_dds::init(); match dds::Dds::exec(cmd).await { Ok(data) => outln!("{}", serde_json::to_string(&data)?)?, Err(e) => { errln!("Cannot execute command: {e}")?; std::process::exit(1); } } } Command::Pkg(cmd) => { package::init()?; let mut pkg = package::Package::load().await?; match cmd { CommandPkg::Add { ids } => pkg.add_many(&ids).await?, CommandPkg::Delete { ids } => pkg.delete_many(&ids).await?, CommandPkg::Install => pkg.install().await?, CommandPkg::List => pkg.print()?, CommandPkg::Upgrade { ids } => pkg.upgrade_many(&ids).await?, } } Command::Pub(cmd) => { yazi_boot::init_default(); yazi_dds::init(); if let Err(e) = dds::Dds::shot(&cmd.kind, CommandPub::receiver()?, &cmd.body()?).await { errln!("Cannot send message: {e}")?; std::process::exit(1); } } Command::PubTo(cmd) => { yazi_boot::init_default(); yazi_dds::init(); if let Err(e) = dds::Dds::shot(&cmd.kind, cmd.receiver, &cmd.body()?).await { errln!("Cannot send message: {e}")?; std::process::exit(1); } } Command::Sub(cmd) => { yazi_boot::init_default(); yazi_dds::init(); dds::Dds::draw(cmd.kinds.split(',').collect()).await?; tokio::signal::ctrl_c().await?; } } Ok(()) } ================================================ FILE: yazi-cli/src/package/add.rs ================================================ use anyhow::Result; use super::{Dependency, Git}; use crate::shared::must_exists; impl Dependency { pub(super) async fn add(&mut self) -> Result<()> { self.header("Upgrading package `{name}`")?; let path = self.local(); if must_exists(&path).await { Git::pull(&path).await?; } else { Git::clone(&self.remote(), &path).await?; }; self.deploy().await?; self.rev = Git::revision(&path).await?; Ok(()) } } ================================================ FILE: yazi-cli/src/package/delete.rs ================================================ use anyhow::{Context, Result}; use yazi_fs::{ok_or_not_found, provider::{Provider, local::Local}}; use yazi_macro::outln; use super::Dependency; use crate::shared::{maybe_exists, remove_sealed}; impl Dependency { pub(super) async fn delete(&self) -> Result<()> { self.header("Deleting package `{name}`")?; let dir = self.target(); if !maybe_exists(&dir).await { return Ok(outln!("Not found, skipping")?); } self.hash_check().await?; self.delete_assets().await?; self.delete_sources().await?; Ok(()) } pub(super) async fn delete_assets(&self) -> Result<()> { let assets = self.target().join("assets"); match tokio::fs::read_dir(&assets).await { Ok(mut it) => { while let Some(entry) = it.next_entry().await? { remove_sealed(&entry.path()) .await .with_context(|| format!("failed to remove `{}`", entry.path().display()))?; } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} Err(e) => Err(e).context(format!("failed to read `{}`", assets.display()))?, }; Local::regular(&assets).remove_dir_clean().await; Ok(()) } pub(super) async fn delete_sources(&self) -> Result<()> { let dir = self.target(); let files = if self.is_flavor { Self::flavor_files() } else { Self::plugin_files(&dir).await? }; for path in files.iter().map(|s| dir.join(s)) { ok_or_not_found(remove_sealed(&path).await) .with_context(|| format!("failed to delete `{}`", path.display()))?; } if ok_or_not_found(Local::regular(&dir).remove_dir().await).is_ok() { outln!("Done!")?; } else { outln!( "Done! For safety, user data has been preserved, please manually delete them within: {}", dir.display() )?; } Ok(()) } } ================================================ FILE: yazi-cli/src/package/dependency.rs ================================================ use std::{env, io::{self, BufWriter}, path::{Path, PathBuf}, str::FromStr}; use anyhow::{Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use twox_hash::XxHash3_128; use yazi_fs::Xdg; use yazi_macro::ok_or_not_found; use yazi_shared::BytesExt; #[derive(Clone, Default)] pub(crate) struct Dependency { pub(crate) r#use: String, // owner/repo:child pub(crate) name: String, // child.yazi pub(crate) parent: String, // owner/repo pub(crate) child: String, // child.yazi pub(crate) rev: String, pub(crate) hash: String, pub(super) is_flavor: bool, } impl Dependency { pub(super) fn local(&self) -> PathBuf { Xdg::state_dir() .join("packages") .join(format!("{:x}", XxHash3_128::oneshot(self.remote().as_bytes()))) } pub(super) fn remote(&self) -> String { // Support more Git hosting services in the future format!("https://github.com/{}.git", self.parent) } pub(super) fn target(&self) -> PathBuf { if self.is_flavor { Xdg::config_dir().join(format!("flavors/{}", self.name)) } else { Xdg::config_dir().join(format!("plugins/{}", self.name)) } } pub(super) fn identical(&self, other: &Self) -> bool { self.parent == other.parent && self.child == other.child } pub(super) fn header(&self, s: &str) -> Result<()> { use std::io::IsTerminal; use crossterm::style::{Attribute, Print, SetAttributes}; use yazi_shim::crossterm::If; let ansi = env::var_os("YA_FORCE_ANSI").is_some_and(|v| v == "1") || io::stdout().is_terminal(); crossterm::execute!( BufWriter::new(io::stdout()), Print("\n"), If(ansi, SetAttributes(Attribute::Reverse.into())), If(ansi, SetAttributes(Attribute::Bold.into())), Print(" "), Print(s.replacen("{name}", &self.name, 1)), Print(" "), If(ansi, SetAttributes(Attribute::Reset.into())), Print("\n\n"), )?; Ok(()) } pub(super) async fn plugin_files(dir: &Path) -> io::Result> { let mut it = ok_or_not_found!(tokio::fs::read_dir(dir).await, return Ok(vec![])); let mut files: Vec = ["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect(); while let Some(entry) = it.next_entry().await? { if let Ok(name) = entry.file_name().into_string() && let Some(stripped) = name.strip_suffix(".lua") && stripped != "main" && stripped.as_bytes().kebab_cased() { files.push(name); } } files.sort_unstable(); Ok(files) } pub(super) fn flavor_files() -> Vec { ["LICENSE", "LICENSE-tmtheme", "README.md", "flavor.toml", "preview.png", "tmtheme.xml"] .into_iter() .map(Into::into) .collect() } } impl FromStr for Dependency { type Err = anyhow::Error; fn from_str(s: &str) -> std::result::Result { let mut parts = s.splitn(2, ':'); let Some(parent) = parts.next() else { bail!("Package URL cannot be empty") }; let child = parts.next().unwrap_or_default(); let Some((_, repo)) = parent.split_once('/') else { bail!("Package URL `{parent}` must be in the format `owner/repository`") }; let name = if child.is_empty() { repo } else { child }; if !name.as_bytes().kebab_cased() { bail!("Package name `{name}` must be in kebab-case") } Ok(Self { r#use: s.to_owned(), name: format!("{name}.yazi"), parent: format!("{parent}{}", if child.is_empty() { ".yazi" } else { "" }), child: if child.is_empty() { String::new() } else { format!("{child}.yazi") }, ..Default::default() }) } } impl<'de> Deserialize<'de> for Dependency { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] struct Shadow { r#use: String, #[serde(default)] rev: String, #[serde(default)] hash: String, } let outer = Shadow::deserialize(deserializer)?; Ok(Self { rev: outer.rev, hash: outer.hash, ..Self::from_str(&outer.r#use).map_err(serde::de::Error::custom)? }) } } impl Serialize for Dependency { fn serialize(&self, serializer: S) -> Result where S: Serializer, { #[derive(Serialize)] struct Shadow<'a> { r#use: &'a str, rev: &'a str, hash: &'a str, } Shadow { r#use: &self.r#use, rev: &self.rev, hash: &self.hash }.serialize(serializer) } } ================================================ FILE: yazi-cli/src/package/deploy.rs ================================================ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use yazi_fs::provider::{Provider, local::Local}; use yazi_macro::outln; use super::Dependency; use crate::shared::{copy_and_seal, maybe_exists}; impl Dependency { pub(super) async fn deploy(&mut self) -> Result<()> { let from = self.local().join(&self.child); self.header("Deploying package `{name}`")?; self.is_flavor = maybe_exists(&from.join("flavor.toml")).await; let to = self.target(); let exists = maybe_exists(&to).await; if exists { self.hash_check().await?; } Local::regular(&to).create_dir_all().await?; self.delete_assets().await?; let res1 = Self::deploy_assets(from.join("assets"), to.join("assets")).await; let res2 = Self::deploy_sources(&from, &to, self.is_flavor).await; if !exists && (res2.is_err() || res1.is_err()) { self.delete_assets().await?; self.delete_sources().await?; } Local::regular(&to).remove_dir_clean().await; self.hash = self.hash().await?; res2?; res1?; outln!("Done!")?; Ok(()) } async fn deploy_assets(from: PathBuf, to: PathBuf) -> Result<()> { match tokio::fs::read_dir(&from).await { Ok(mut it) => { Local::regular(&to).create_dir_all().await?; while let Some(entry) = it.next_entry().await? { let (src, dist) = (entry.path(), to.join(entry.file_name())); copy_and_seal(&src, &dist).await.with_context(|| { format!("failed to copy `{}` to `{}`", src.display(), dist.display()) })?; } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} Err(e) => Err(e).context(format!("failed to read `{}`", from.display()))?, } Ok(()) } async fn deploy_sources(from: &Path, to: &Path, is_flavor: bool) -> Result<()> { let files = if is_flavor { Self::flavor_files() } else { Self::plugin_files(from).await? }; for file in files { let (from, to) = (from.join(&file), to.join(&file)); copy_and_seal(&from, &to) .await .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))?; } Ok(()) } } ================================================ FILE: yazi-cli/src/package/git.rs ================================================ use std::path::Path; use anyhow::{Context, Result, bail}; use tokio::process::Command; use yazi_shared::strip_trailing_newline; pub(super) struct Git; impl Git { pub(super) async fn clone(url: &str, path: &Path) -> Result<()> { Self::exec(|c| c.args(["clone", url]).arg(path)).await } pub(super) async fn fetch(path: &Path) -> Result<()> { Self::exec(|c| c.arg("fetch").current_dir(path)).await } pub(super) async fn checkout(path: &Path, rev: &str) -> Result<()> { Self::exec(|c| c.args(["checkout", rev, "--force"]).current_dir(path)).await } pub(super) async fn pull(path: &Path) -> Result<()> { Self::fetch(path).await?; Self::checkout(path, "origin/HEAD").await?; Ok(()) } pub(super) async fn revision(path: &Path) -> Result { let output = Command::new("git") .args(["rev-parse", "--short", "HEAD"]) .current_dir(path) .output() .await .context("Failed to get current revision")?; if !output.status.success() { bail!("Getting revision failed: {}", output.status); } Ok(strip_trailing_newline( String::from_utf8(output.stdout).context("Failed to parse revision")?, )) } async fn exec(f: impl FnOnce(&mut Command) -> &mut Command) -> Result<()> { let status = f(Command::new("git").args([ "-c", "advice.detachedHead=false", "-c", "checkout.defaultRemote=origin", "-c", "clone.defaultRemoteName=origin", ])) .status() .await .context("Failed to execute `git` command")?; if !status.success() { bail!("`git` command failed: {status}"); } Ok(()) } } ================================================ FILE: yazi-cli/src/package/hash.rs ================================================ use anyhow::{Context, Result, bail}; use twox_hash::XxHash3_128; use yazi_fs::provider::local::Local; use yazi_macro::ok_or_not_found; use super::Dependency; impl Dependency { pub(crate) async fn hash(&self) -> Result { let dir = self.target(); let files = if self.is_flavor { Self::flavor_files() } else { Self::plugin_files(&dir).await? }; let mut h = XxHash3_128::new(); for file in files { h.write(file.as_bytes()); h.write(b"VpvFw9Atb7cWGOdqhZCra634CcJJRlsRl72RbZeV0vpG1\0"); h.write(&ok_or_not_found!(Local::regular(&dir.join(file)).read().await)); } let mut assets = vec![]; match tokio::fs::read_dir(dir.join("assets")).await { Ok(mut it) => { while let Some(entry) = it.next_entry().await? { let Ok(name) = entry.file_name().into_string() else { bail!("asset path is not valid UTF-8: {}", entry.path().display()); }; assets.push((name, Local::regular(&entry.path()).read().await?)); } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} Err(e) => Err(e).context(format!("failed to read `{}`", dir.join("assets").display()))?, } assets.sort_unstable_by(|(a, _), (b, _)| a.cmp(b)); for (name, data) in assets { h.write(name.as_bytes()); h.write(b"pQU2in0xcsu97Y77Nuq2LnT8mczMlFj22idcYRmMrglqU\0"); h.write(&data); } Ok(format!("{:x}", h.finish_128())) } pub(super) async fn hash_check(&self) -> Result<()> { if self.hash != self.hash().await? { bail!( "You have modified the contents of the `{}` {}. For safety, the operation has been aborted. Please manually delete it from `{}` and re-run the command.", self.name, if self.is_flavor { "flavor" } else { "plugin" }, self.target().display() ); } Ok(()) } } ================================================ FILE: yazi-cli/src/package/install.rs ================================================ use anyhow::Result; use super::{Dependency, Git}; use crate::shared::must_exists; impl Dependency { pub(super) async fn install(&mut self) -> Result<()> { self.header("Fetching package `{name}`")?; let path = self.local(); if must_exists(&path).await { Git::fetch(&path).await?; } else { Git::clone(&self.remote(), &path).await?; }; if !self.rev.is_empty() { Git::checkout(&path, self.rev.trim_start_matches('=')).await?; } self.deploy().await?; if self.rev.is_empty() { self.rev = Git::revision(&path).await?; } Ok(()) } } ================================================ FILE: yazi-cli/src/package/mod.rs ================================================ yazi_macro::mod_flat!(add delete dependency deploy git hash install package upgrade); use anyhow::Context; use yazi_fs::Xdg; pub(super) fn init() -> anyhow::Result<()> { let packages_dir = Xdg::state_dir().join("packages"); std::fs::create_dir_all(&packages_dir) .with_context(|| format!("failed to create packages directory: {packages_dir:?}"))?; let config_dir = Xdg::config_dir(); std::fs::create_dir_all(&config_dir) .with_context(|| format!("failed to create config directory: {config_dir:?}"))?; Ok(()) } ================================================ FILE: yazi-cli/src/package/package.rs ================================================ use std::{path::PathBuf, str::FromStr}; use anyhow::{Context, Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use yazi_fs::{Xdg, provider::{Provider, local::Local}}; use yazi_macro::{ok_or_not_found, outln}; use super::Dependency; #[derive(Default)] pub(crate) struct Package { pub(crate) plugins: Vec, pub(crate) flavors: Vec, } impl Package { pub(crate) async fn load() -> Result { let s = ok_or_not_found!(Local::regular(&Self::toml()).read_to_string().await); Ok(toml::from_str(&s)?) } pub(crate) async fn add_many(&mut self, uses: &[String]) -> Result<()> { for u in uses { let r = self.add(u).await; self.save().await?; r?; } Ok(()) } pub(crate) async fn delete_many(&mut self, uses: &[String]) -> Result<()> { for u in uses { let r = self.delete(u).await; self.save().await?; r?; } Ok(()) } pub(crate) async fn install(&mut self) -> Result<()> { macro_rules! go { ($dep:expr) => { let r = $dep.install().await; self.save().await?; r?; }; } for i in 0..self.plugins.len() { go!(self.plugins[i]); } for i in 0..self.flavors.len() { go!(self.flavors[i]); } Ok(()) } pub(crate) async fn upgrade_many(&mut self, uses: &[String]) -> Result<()> { macro_rules! go { ($dep:expr) => { if uses.is_empty() || uses.contains(&$dep.r#use) { let r = $dep.upgrade().await; self.save().await?; r?; } }; } for i in 0..self.plugins.len() { go!(self.plugins[i]); } for i in 0..self.flavors.len() { go!(self.flavors[i]); } Ok(()) } pub(crate) fn print(&self) -> Result<()> { outln!("Plugins:")?; for d in &self.plugins { if d.rev.is_empty() { outln!("\t{}", d.r#use)?; } else { outln!("\t{} ({})", d.r#use, d.rev)?; } } outln!("Flavors:")?; for d in &self.flavors { if d.rev.is_empty() { outln!("\t{}", d.r#use)?; } else { outln!("\t{} ({})", d.r#use, d.rev)?; } } Ok(()) } async fn add(&mut self, r#use: &str) -> Result<()> { let mut dep = Dependency::from_str(r#use)?; if let Some(d) = self.identical(&dep) { bail!( "{} `{}` already exists in package.toml", if d.is_flavor { "Flavor" } else { "Plugin" }, dep.name ) } dep.add().await?; if dep.is_flavor { self.flavors.push(dep); } else { self.plugins.push(dep); } Ok(()) } async fn delete(&mut self, r#use: &str) -> Result<()> { let Some(dep) = self.identical(&Dependency::from_str(r#use)?).cloned() else { bail!("`{}` was not found in package.toml", r#use) }; dep.delete().await?; if dep.is_flavor { self.flavors.retain(|d| !d.identical(&dep)); } else { self.plugins.retain(|d| !d.identical(&dep)); } Ok(()) } async fn save(&self) -> Result<()> { let s = toml::to_string_pretty(self)?; Local::regular(&Self::toml()).write(s).await.context("Failed to write package.toml") } fn toml() -> PathBuf { Xdg::config_dir().join("package.toml") } fn identical(&self, other: &Dependency) -> Option<&Dependency> { self.plugins.iter().chain(&self.flavors).find(|d| d.identical(other)) } } impl<'de> Deserialize<'de> for Package { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] struct Outer { #[serde(default)] plugin: Shadow, #[serde(default)] flavor: Shadow, } #[derive(Default, Deserialize)] struct Shadow { deps: Vec, } let mut outer = Outer::deserialize(deserializer)?; outer.flavor.deps.iter_mut().for_each(|d| d.is_flavor = true); Ok(Self { plugins: outer.plugin.deps, flavors: outer.flavor.deps }) } } impl Serialize for Package { fn serialize(&self, serializer: S) -> Result where S: Serializer, { #[derive(Serialize)] struct Outer<'a> { plugin: Shadow<'a>, flavor: Shadow<'a>, } #[derive(Serialize)] struct Shadow<'a> { deps: &'a [Dependency], } Outer { plugin: Shadow { deps: &self.plugins }, flavor: Shadow { deps: &self.flavors } } .serialize(serializer) } } ================================================ FILE: yazi-cli/src/package/upgrade.rs ================================================ use anyhow::Result; use super::Dependency; impl Dependency { pub(super) async fn upgrade(&mut self) -> Result<()> { if self.rev.starts_with('=') { Ok(()) } else { self.add().await } } } ================================================ FILE: yazi-cli/src/shared/mod.rs ================================================ yazi_macro::mod_flat!(shared); ================================================ FILE: yazi-cli/src/shared/shared.rs ================================================ use std::{io, path::Path}; use tokio::io::AsyncWriteExt; use yazi_fs::provider::{FileBuilder, Provider, local::{Gate, Local}}; use yazi_macro::ok_or_not_found; #[inline] pub async fn must_exists(path: impl AsRef) -> bool { Local::regular(&path).symlink_metadata().await.is_ok() } #[inline] pub async fn maybe_exists(path: impl AsRef) -> bool { match Local::regular(&path).symlink_metadata().await { Ok(_) => true, Err(e) => e.kind() != std::io::ErrorKind::NotFound, } } pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> { let b = Local::regular(from).read().await?; ok_or_not_found!(remove_sealed(to).await); let mut file = Gate::default().create_new(true).write(true).truncate(true).open(to).await?; file.write_all(&b).await?; let mut perm = file.metadata().await?.permissions(); perm.set_readonly(true); file.set_permissions(perm).await?; Ok(()) } pub async fn remove_sealed(p: &Path) -> io::Result<()> { #[cfg(windows)] { let mut perm = tokio::fs::metadata(p).await?.permissions(); perm.set_readonly(false); tokio::fs::set_permissions(p, perm).await?; } Local::regular(p).remove_file().await } ================================================ FILE: yazi-codegen/Cargo.toml ================================================ [package] name = "yazi-codegen" description = "Yazi code generator" version.workspace = true edition.workspace = true license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true rust-version.workspace = true [lints] workspace = true [lib] proc-macro = true [dependencies] # External dependencies syn = { version = "2.0.117", features = [ "full" ] } quote = "1.0.45" ================================================ FILE: yazi-codegen/README.md ================================================ # yazi-codegen This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. [source]: https://github.com/sxyazi/yazi ================================================ FILE: yazi-codegen/src/lib.rs ================================================ use proc_macro::TokenStream; use quote::quote; use syn::{Data, DeriveInput, Fields, parse_macro_input}; #[proc_macro_derive(DeserializeOver)] pub fn deserialize_over(input: TokenStream) -> TokenStream { let DeriveInput { ident, .. } = parse_macro_input!(input as DeriveInput); quote! { impl #ident { pub(crate) fn deserialize_over(self, input: &str) -> Result { crate::error_with_input(self.deserialize_over_with(toml::de::DeTable::parse(input)?), input) } } } .into() } #[proc_macro_derive(DeserializeOver1)] pub fn deserialize_over1(input: TokenStream) -> TokenStream { let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput); let assignments = match data { Data::Struct(struct_) => match struct_.fields { Fields::Named(fields) => { let mut assignments = Vec::with_capacity(fields.named.len()); for field in fields.named { let field_ident = &field.ident; let field_name = field_ident.as_ref().unwrap().to_string(); assignments.push(quote! { if let Some(value) = table.remove(#field_name) { if !matches!(value.get_ref(), toml::de::DeValue::Table(_)) { _ = toml::Table::deserialize(value.into_deserializer())?; return Err(serde::de::Error::custom(format!("expected top-level `{}` to be a TOML table", #field_name))); } let span = value.span(); if let toml::de::DeValue::Table(table) = value.into_inner() { self.#field_ident = self.#field_ident.deserialize_over_with(toml::Spanned::new(span, table))?; } } }); } assignments } _ => panic!("DeserializeOver1 only supports structs with named fields"), }, _ => panic!("DeserializeOver1 only supports structs"), }; quote! { impl #ident { pub(crate) fn deserialize_over_with<'de>(mut self, table: toml::Spanned>) -> Result { use serde::{Deserialize, de::IntoDeserializer}; let mut table = table.into_inner(); #(#assignments)* Ok(self) } } } .into() } #[proc_macro_derive(DeserializeOver2)] pub fn deserialize_over2(input: TokenStream) -> TokenStream { let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput); let assignments = match data { Data::Struct(struct_) => match struct_.fields { Fields::Named(fields) => { let mut assignments = Vec::with_capacity(fields.named.len()); for field in fields.named { let field_ident = field.ident; let field_name = field_ident.as_ref().unwrap().to_string(); assignments.push(quote! { if let Some(value) = table.remove(#field_name) { self.#field_ident = <_>::deserialize(value.into_deserializer())?; } }); } assignments } _ => panic!("DeserializeOver2 only supports structs with named fields"), }, _ => panic!("DeserializeOver2 only supports structs"), }; quote! { impl #ident { pub(crate) fn deserialize_over_with<'de>(mut self, table: toml::Spanned>) -> Result { use serde::{Deserialize, de::IntoDeserializer}; let mut table = table.into_inner(); #(#assignments)* Ok(self) } } } .into() } #[proc_macro_derive(FromLuaOwned)] pub fn from_lua(input: TokenStream) -> TokenStream { let DeriveInput { ident, generics, .. } = parse_macro_input!(input as DeriveInput); let ident_str = ident.to_string(); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); quote! { impl #impl_generics ::mlua::FromLua for #ident #ty_generics #where_clause { #[inline] fn from_lua(value: ::mlua::Value, _: &::mlua::Lua) -> ::mlua::Result { match value { ::mlua::Value::UserData(ud) => ud.take::(), _ => Err(::mlua::Error::FromLuaConversionError { from: value.type_name(), to: #ident_str.to_owned(), message: None, }), } } } } .into() } ================================================ FILE: yazi-config/Cargo.toml ================================================ [package] name = "yazi-config" description = "Yazi configuration file parser" version.workspace = true edition.workspace = true license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true rust-version.workspace = true [lints] workspace = true [dependencies] yazi-codegen = { path = "../yazi-codegen", version = "26.2.2" } yazi-fs = { path = "../yazi-fs", version = "26.2.2" } yazi-macro = { path = "../yazi-macro", version = "26.2.2" } yazi-shared = { path = "../yazi-shared", version = "26.2.2" } yazi-tty = { path = "../yazi-tty", version = "26.2.2" } # External dependencies anyhow = { workspace = true } bitflags = { workspace = true } crossterm = { workspace = true } globset = { workspace = true } hashbrown = { workspace = true } indexmap = { workspace = true } ratatui = { workspace = true } regex = { workspace = true } serde = { workspace = true } tokio = { workspace = true } toml = { workspace = true } tracing = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } ================================================ FILE: yazi-config/README.md ================================================ # yazi-config This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. [source]: https://github.com/sxyazi/yazi ================================================ FILE: yazi-config/preset/README.md ================================================ # Default Configuration > [!IMPORTANT] > If you're using a stable release of Yazi instead of the newest nightly build, make sure you're checking these files out from [the `shipped` tag][shipped], not the newest `main` branch. This directory contains the default configuration files for Yazi: - [`yazi-default.toml`][yazi-default]: General configuration - [`keymap-default.toml`][keymap-default]: Keybindings configuration - [`theme-dark.toml`][theme-dark]: Dark color scheme (loaded when your terminal is in dark mode) - [`theme-light.toml`][theme-light]: Light color scheme (loaded when your terminal is in light mode) These files are already included with Yazi when you install the release, so you don't need to manually download or copy them to your Yazi configuration directory. However, if you want to customize certain configurations: - Create a `yazi.toml` in your config directory to override certain settings in [`yazi-default.toml`][yazi-default], so either: - `~/.config/yazi/yazi.toml` on Unix-like systems - `%AppData%\yazi\config\yazi.toml` on Windows - Create a `keymap.toml` in your config directory to override certain settings in [`keymap-default.toml`][keymap-default], so either: - `~/.config/yazi/keymap.toml` on Unix-like systems - `%AppData%\yazi\config\keymap.toml` on Windows - Create a `theme.toml` in your config directory to override certain settings in [`theme-light.toml`][theme-light] and [`theme-dark.toml`][theme-dark], so either: - `~/.config/yazi/theme.toml` on Unix-like systems - `%AppData%\yazi\config\theme.toml` on Windows For the user's `theme.toml` file, you can only apply the same color scheme to both the light and dark themes. If you want more granular control over colors, specify two different flavors for light and dark modes under the `[flavor]` section of your `theme.toml`, and override them in your respective flavor instead. [shipped]: https://github.com/sxyazi/yazi/tree/shipped [yazi-default]: yazi-default.toml [keymap-default]: keymap-default.toml [theme-dark]: theme-dark.toml [theme-light]: theme-light.toml ## Learn more - [Configuration documentation](https://yazi-rs.github.io/docs/configuration/overview) - [Flavors documentation](https://yazi-rs.github.io/docs/flavors/overview) ================================================ FILE: yazi-config/preset/keymap-default.toml ================================================ # A TOML linter such as https://taplo.tamasfe.dev/ can use this schema to validate your config. # If you encounter any issues, please make an issue at https://github.com/yazi-rs/schemas. "$schema" = "https://yazi-rs.github.io/schemas/keymap.json" [mgr] keymap = [ { on = "", run = "escape", desc = "Exit visual mode, clear selection, or cancel search" }, { on = "", run = "escape", desc = "Exit visual mode, clear selection, or cancel search" }, { on = "q", run = "quit", desc = "Quit the process" }, { on = "Q", run = "quit --no-cwd-file", desc = "Quit without outputting cwd-file" }, { on = "", run = "close", desc = "Close the current tab, or quit if it's last" }, { on = "", run = "suspend", desc = "Suspend the process" }, # Hopping { on = "k", run = "arrow prev", desc = "Previous file" }, { on = "j", run = "arrow next", desc = "Next file" }, { on = "", run = "arrow prev", desc = "Previous file" }, { on = "", run = "arrow next", desc = "Next file" }, { on = "", run = "arrow -50%", desc = "Move cursor up half page" }, { on = "", run = "arrow 50%", desc = "Move cursor down half page" }, { on = "", run = "arrow -100%", desc = "Move cursor up one page" }, { on = "", run = "arrow 100%", desc = "Move cursor down one page" }, { on = "", run = "arrow -50%", desc = "Move cursor up half page" }, { on = "", run = "arrow 50%", desc = "Move cursor down half page" }, { on = "", run = "arrow -100%", desc = "Move cursor up one page" }, { on = "", run = "arrow 100%", desc = "Move cursor down one page" }, { on = [ "g", "g" ], run = "arrow top", desc = "Go to top" }, { on = "G", run = "arrow bot", desc = "Go to bottom" }, # Navigation { on = "h", run = "leave", desc = "Back to the parent directory" }, { on = "l", run = "enter", desc = "Enter the child directory" }, { on = "", run = "leave", desc = "Back to the parent directory" }, { on = "", run = "enter", desc = "Enter the child directory" }, { on = "H", run = "back", desc = "Back to previous directory" }, { on = "L", run = "forward", desc = "Forward to next directory" }, # Toggle { on = "", run = [ "toggle", "arrow 1" ], desc = "Toggle the current selection state" }, { on = "", run = "toggle_all --state=on", desc = "Select all files" }, { on = "", run = "toggle_all", desc = "Invert selection of all files" }, # Visual mode { on = "v", run = "visual_mode", desc = "Enter visual mode (selection mode)" }, { on = "V", run = "visual_mode --unset", desc = "Enter visual mode (unset mode)" }, # Seeking { on = "K", run = "seek -5", desc = "Seek up 5 units in the preview" }, { on = "J", run = "seek 5", desc = "Seek down 5 units in the preview" }, # Spotting { on = "", run = "spot", desc = "Spot hovered file" }, # Operation { on = "o", run = "open", desc = "Open selected files" }, { on = "O", run = "open --interactive", desc = "Open selected files interactively" }, { on = "", run = "open", desc = "Open selected files" }, { on = "", run = "open --interactive", desc = "Open selected files interactively" }, { on = "y", run = "yank", desc = "Yank selected files (copy)" }, { on = "x", run = "yank --cut", desc = "Yank selected files (cut)" }, { on = "p", run = "paste", desc = "Paste yanked files" }, { on = "P", run = "paste --force", desc = "Paste yanked files (overwrite if the destination exists)" }, { on = "-", run = "link", desc = "Symlink the absolute path of yanked files" }, { on = "_", run = "link --relative", desc = "Symlink the relative path of yanked files" }, { on = "", run = "hardlink", desc = "Hardlink yanked files" }, { on = "Y", run = "unyank", desc = "Cancel the yank status" }, { on = "X", run = "unyank", desc = "Cancel the yank status" }, { on = "d", run = "remove", desc = "Trash selected files" }, { on = "D", run = "remove --permanently", desc = "Permanently delete selected files" }, { on = "a", run = "create", desc = "Create a file (ends with / for directories)" }, { on = "r", run = "rename --cursor=before_ext", desc = "Rename selected file(s)" }, { on = ";", run = "shell --interactive", desc = "Run a shell command" }, { on = ":", run = "shell --block --interactive", desc = "Run a shell command (block until finishes)" }, { on = ".", run = "hidden toggle", desc = "Toggle the visibility of hidden files" }, { on = "s", run = "search --via=fd", desc = "Search files by name via fd" }, { on = "S", run = "search --via=rg", desc = "Search files by content via ripgrep" }, { on = "", run = "escape --search", desc = "Cancel the ongoing search" }, { on = "z", run = "plugin fzf", desc = "Jump to a file/directory via fzf" }, { on = "Z", run = "plugin zoxide", desc = "Jump to a directory via zoxide" }, # Linemode { on = [ "m", "s" ], run = "linemode size", desc = "Linemode: size" }, { on = [ "m", "p" ], run = "linemode permissions", desc = "Linemode: permissions" }, { on = [ "m", "b" ], run = "linemode btime", desc = "Linemode: btime" }, { on = [ "m", "m" ], run = "linemode mtime", desc = "Linemode: mtime" }, { on = [ "m", "o" ], run = "linemode owner", desc = "Linemode: owner" }, { on = [ "m", "n" ], run = "linemode none", desc = "Linemode: none" }, # Copy { on = [ "c", "c" ], run = "copy path", desc = "Copy file URL" }, { on = [ "c", "d" ], run = "copy dirname", desc = "Copy directory URL" }, { on = [ "c", "f" ], run = "copy filename", desc = "Copy filename" }, { on = [ "c", "n" ], run = "copy name_without_ext", desc = "Copy filename without extension" }, # Filter { on = "f", run = "filter --smart", desc = "Filter files" }, # Find { on = "/", run = "find --smart", desc = "Find next file" }, { on = "?", run = "find --previous --smart", desc = "Find previous file" }, { on = "n", run = "find_arrow", desc = "Next found" }, { on = "N", run = "find_arrow --previous", desc = "Previous found" }, # Sorting { on = [ ",", "m" ], run = [ "sort mtime --reverse=no", "linemode mtime" ], desc = "Sort by modified time" }, { on = [ ",", "M" ], run = [ "sort mtime --reverse=yes", "linemode mtime" ], desc = "Sort by modified time (reverse)" }, { on = [ ",", "b" ], run = [ "sort btime --reverse=no", "linemode btime" ], desc = "Sort by birth time" }, { on = [ ",", "B" ], run = [ "sort btime --reverse=yes", "linemode btime" ], desc = "Sort by birth time (reverse)" }, { on = [ ",", "e" ], run = "sort extension --reverse=no", desc = "Sort by extension" }, { on = [ ",", "E" ], run = "sort extension --reverse=yes", desc = "Sort by extension (reverse)" }, { on = [ ",", "a" ], run = "sort alphabetical --reverse=no", desc = "Sort alphabetically" }, { on = [ ",", "A" ], run = "sort alphabetical --reverse=yes", desc = "Sort alphabetically (reverse)" }, { on = [ ",", "n" ], run = "sort natural --reverse=no", desc = "Sort naturally" }, { on = [ ",", "N" ], run = "sort natural --reverse=yes", desc = "Sort naturally (reverse)" }, { on = [ ",", "s" ], run = [ "sort size --reverse=no", "linemode size" ], desc = "Sort by size" }, { on = [ ",", "S" ], run = [ "sort size --reverse=yes", "linemode size" ], desc = "Sort by size (reverse)" }, { on = [ ",", "r" ], run = "sort random --reverse=no", desc = "Sort randomly" }, # Goto { on = [ "g", "h" ], run = "cd ~", desc = "Go home" }, { on = [ "g", "c" ], run = "cd ~/.config", desc = "Go ~/.config" }, { on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Go ~/Downloads" }, { on = [ "g", "" ], run = "cd --interactive", desc = "Jump interactively" }, { on = [ "g", "f" ], run = "follow", desc = "Follow hovered symlink" }, # Tabs { on = [ "t", "t" ], run = "tab_create --current", desc = "Create a new tab in CWD" }, { on = [ "t", "r" ], run = "tab_rename --interactive", desc = "Rename current tab" }, { on = "1", run = "tab_switch 0", desc = "Switch to first tab" }, { on = "2", run = "tab_switch 1", desc = "Switch to second tab" }, { on = "3", run = "tab_switch 2", desc = "Switch to third tab" }, { on = "4", run = "tab_switch 3", desc = "Switch to fourth tab" }, { on = "5", run = "tab_switch 4", desc = "Switch to fifth tab" }, { on = "6", run = "tab_switch 5", desc = "Switch to sixth tab" }, { on = "7", run = "tab_switch 6", desc = "Switch to seventh tab" }, { on = "8", run = "tab_switch 7", desc = "Switch to eighth tab" }, { on = "9", run = "tab_switch 8", desc = "Switch to ninth tab" }, { on = "[", run = "tab_switch -1 --relative", desc = "Switch to previous tab" }, { on = "]", run = "tab_switch 1 --relative", desc = "Switch to next tab" }, { on = "{", run = "tab_swap -1", desc = "Swap current tab with previous tab" }, { on = "}", run = "tab_swap 1", desc = "Swap current tab with next tab" }, # Tasks { on = "w", run = "tasks:show", desc = "Show task manager" }, # Help { on = "~", run = "help", desc = "Open help" }, { on = "", run = "help", desc = "Open help" }, ] [tasks] keymap = [ { on = "", run = "close", desc = "Close task manager" }, { on = "", run = "close", desc = "Close task manager" }, { on = "", run = "close", desc = "Close task manager" }, { on = "w", run = "close", desc = "Close task manager" }, { on = "k", run = "arrow prev", desc = "Previous task" }, { on = "j", run = "arrow next", desc = "Next task" }, { on = "", run = "arrow prev", desc = "Previous task" }, { on = "", run = "arrow next", desc = "Next task" }, { on = "", run = "inspect", desc = "Inspect the task" }, { on = "x", run = "cancel", desc = "Cancel the task" }, # Help { on = "~", run = "help", desc = "Open help" }, { on = "", run = "help", desc = "Open help" }, ] [spot] keymap = [ { on = "", run = "close", desc = "Close the spot" }, { on = "", run = "close", desc = "Close the spot" }, { on = "", run = "close", desc = "Close the spot" }, { on = "", run = "close", desc = "Close the spot" }, { on = "k", run = "arrow prev", desc = "Previous line" }, { on = "j", run = "arrow next", desc = "Next line" }, { on = "h", run = "swipe prev", desc = "Swipe to previous file" }, { on = "l", run = "swipe next", desc = "Swipe to next file" }, { on = "", run = "arrow prev", desc = "Previous line" }, { on = "", run = "arrow next", desc = "Next line" }, { on = "", run = "swipe prev", desc = "Swipe to previous file" }, { on = "", run = "swipe next", desc = "Swipe to next file" }, # Copy { on = [ "c", "c" ], run = "copy cell", desc = "Copy selected cell" }, # Help { on = "~", run = "help", desc = "Open help" }, { on = "", run = "help", desc = "Open help" }, ] [pick] keymap = [ { on = "", run = "close", desc = "Cancel pick" }, { on = "", run = "close", desc = "Cancel pick" }, { on = "", run = "close", desc = "Cancel pick" }, { on = "", run = "close --submit", desc = "Submit the pick" }, { on = "k", run = "arrow prev", desc = "Previous option" }, { on = "j", run = "arrow next", desc = "Next option" }, { on = "", run = "arrow prev", desc = "Previous option" }, { on = "", run = "arrow next", desc = "Next option" }, # Help { on = "~", run = "help", desc = "Open help" }, { on = "", run = "help", desc = "Open help" }, ] [input] keymap = [ { on = "", run = "close", desc = "Cancel input" }, { on = "", run = "close --submit", desc = "Submit input" }, { on = "", run = "escape", desc = "Back to normal mode, or cancel input" }, { on = "", run = "escape", desc = "Back to normal mode, or cancel input" }, # Mode { on = "i", run = "insert", desc = "Enter insert mode" }, { on = "I", run = [ "move first-char", "insert" ], desc = "Move to the BOL, and enter insert mode" }, { on = "a", run = "insert --append", desc = "Enter append mode" }, { on = "A", run = [ "move eol", "insert --append" ], desc = "Move to the EOL, and enter append mode" }, { on = "v", run = "visual", desc = "Enter visual mode" }, { on = "r", run = "replace", desc = "Replace a single character" }, # Selection { on = "V", run = [ "move bol", "visual", "move eol" ], desc = "Select from BOL to EOL" }, { on = "", run = [ "move eol", "visual", "move bol" ], desc = "Select from EOL to BOL" }, { on = "", run = [ "move bol", "visual", "move eol" ], desc = "Select from BOL to EOL" }, # Character-wise movement { on = "h", run = "move -1", desc = "Move back a character" }, { on = "l", run = "move 1", desc = "Move forward a character" }, { on = "", run = "move -1", desc = "Move back a character" }, { on = "", run = "move 1", desc = "Move forward a character" }, { on = "", run = "move -1", desc = "Move back a character" }, { on = "", run = "move 1", desc = "Move forward a character" }, # Word-wise movement { on = "b", run = "backward", desc = "Move back to the start of the current or previous word" }, { on = "B", run = "backward --far", desc = "Move back to the start of the current or previous WORD" }, { on = "w", run = "forward", desc = "Move forward to the start of the next word" }, { on = "W", run = "forward --far", desc = "Move forward to the start of the next WORD" }, { on = "e", run = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, { on = "E", run = "forward --far --end-of-word", desc = "Move forward to the end of the current or next WORD" }, { on = "", run = "backward", desc = "Move back to the start of the current or previous word" }, { on = "", run = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, { on = "", run = "backward", desc = "Move back to the start of the current or previous word" }, { on = "", run = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, # Line-wise movement { on = "0", run = "move bol", desc = "Move to the BOL" }, { on = "$", run = "move eol", desc = "Move to the EOL" }, { on = "_", run = "move first-char", desc = "Move to the first non-whitespace character" }, { on = "^", run = "move first-char", desc = "Move to the first non-whitespace character" }, { on = "", run = "move bol", desc = "Move to the BOL" }, { on = "", run = "move eol", desc = "Move to the EOL" }, { on = "", run = "move bol", desc = "Move to the BOL" }, { on = "", run = "move eol", desc = "Move to the EOL" }, # Delete { on = "", run = "backspace", desc = "Delete the character before the cursor" }, { on = "", run = "backspace --under", desc = "Delete the character under the cursor" }, { on = "", run = "backspace", desc = "Delete the character before the cursor" }, { on = "", run = "backspace --under", desc = "Delete the character under the cursor" }, # Kill { on = "", run = "kill bol", desc = "Kill backwards to the BOL" }, { on = "", run = "kill eol", desc = "Kill forwards to the EOL" }, { on = "", run = "kill backward", desc = "Kill backwards to the start of the current word" }, { on = "", run = "kill forward", desc = "Kill forwards to the end of the current word" }, { on = "", run = "kill backward", desc = "Kill backwards to the start of the current word" }, { on = "", run = "kill forward", desc = "Kill forwards to the end of the current word" }, # Cut/Yank/Paste { on = "d", run = "delete --cut", desc = "Cut selected characters" }, { on = "D", run = [ "delete --cut", "move eol" ], desc = "Cut until EOL" }, { on = "c", run = "delete --cut --insert", desc = "Cut selected characters, and enter insert mode" }, { on = "C", run = [ "delete --cut --insert", "move eol" ], desc = "Cut until EOL, and enter insert mode" }, { on = "s", run = [ "delete --cut --insert", "move 1" ], desc = "Cut current character, and enter insert mode" }, { on = "S", run = [ "move bol", "delete --cut --insert", "move eol" ], desc = "Cut from BOL until EOL, and enter insert mode" }, { on = "x", run = [ "delete --cut", "move 1 --in-operating" ], desc = "Cut current character" }, { on = "y", run = "yank", desc = "Copy selected characters" }, { on = "p", run = "paste", desc = "Paste copied characters after the cursor" }, { on = "P", run = "paste --before", desc = "Paste copied characters before the cursor" }, # Undo/Redo/Casefy { on = "u", run = [ "undo", "casefy lower" ], desc = "Undo, or lowercase if in visual mode" }, { on = "U", run = "casefy upper", desc = "Uppercase" }, { on = "", run = "redo", desc = "Redo the last operation" }, # Help { on = "~", run = "help", desc = "Open help" }, { on = "", run = "help", desc = "Open help" }, ] [confirm] keymap = [ { on = "", run = "close", desc = "Cancel the confirm" }, { on = "", run = "close", desc = "Cancel the confirm" }, { on = "", run = "close", desc = "Cancel the confirm" }, { on = "", run = "close --submit", desc = "Submit the confirm" }, { on = "n", run = "close", desc = "Cancel the confirm" }, { on = "y", run = "close --submit", desc = "Submit the confirm" }, { on = "k", run = "arrow prev", desc = "Previous line" }, { on = "j", run = "arrow next", desc = "Next line" }, { on = "", run = "arrow prev", desc = "Previous line" }, { on = "", run = "arrow next", desc = "Next line" }, # Help { on = "~", run = "help", desc = "Open help" }, { on = "", run = "help", desc = "Open help" }, ] [cmp] keymap = [ { on = "", run = "close", desc = "Cancel completion" }, { on = "", run = "close --submit", desc = "Submit the completion" }, { on = "", run = [ "close --submit", "input:close --submit" ], desc = "Complete and submit the input" }, { on = "", run = "arrow prev", desc = "Previous item" }, { on = "", run = "arrow next", desc = "Next item" }, { on = "", run = "arrow prev", desc = "Previous item" }, { on = "", run = "arrow next", desc = "Next item" }, { on = "", run = "arrow prev", desc = "Previous item" }, { on = "", run = "arrow next", desc = "Next item" }, # Help { on = "~", run = "help", desc = "Open help" }, { on = "", run = "help", desc = "Open help" }, ] [help] keymap = [ { on = "", run = "escape", desc = "Clear the filter, or hide the help" }, { on = "", run = "escape", desc = "Clear the filter, or hide the help" }, { on = "", run = "close", desc = "Hide the help" }, # Navigation { on = "k", run = "arrow prev", desc = "Previous line" }, { on = "j", run = "arrow next", desc = "Next line" }, { on = "", run = "arrow prev", desc = "Previous line" }, { on = "", run = "arrow next", desc = "Next line" }, # Filtering { on = "f", run = "filter", desc = "Filter help items" }, ] ================================================ FILE: yazi-config/preset/theme-dark.toml ================================================ # If the user's terminal is in dark mode, Yazi will load `theme-dark.toml` on startup; otherwise, `theme-light.toml`. # You can override any parts of them that are not related to the dark/light mode in your own `theme.toml`. # If you want to dynamically override their content based on dark/light mode, you can specify two different flavors # for dark and light modes under `[flavor]`, and do so in those flavors instead. "$schema" = "https://yazi-rs.github.io/schemas/theme.json" # vim:fileencoding=utf-8:foldmethod=marker # : Flavor {{{ [flavor] dark = "" light = "" # : }}} # : App {{{ [app] overall = {} # : }}} # : Manager {{{ [mgr] cwd = { fg = "cyan" } # Find find_keyword = { fg = "yellow", bold = true, italic = true, underline = true } find_position = { fg = "magenta", bg = "reset", bold = true, italic = true } # Symlink symlink_target = { italic = true } # Marker marker_copied = { fg = "lightgreen", bg = "lightgreen" } marker_cut = { fg = "lightred", bg = "lightred" } marker_marked = { fg = "lightcyan", bg = "lightcyan" } marker_selected = { fg = "lightyellow", bg = "lightyellow" } marker_symbol = "│" # Count count_copied = { fg = "white", bg = "green" } count_cut = { fg = "white", bg = "red" } count_selected = { fg = "black", bg = "yellow" } # Border border_symbol = "│" border_style = { fg = "gray" } # Highlighting syntect_theme = "" # : }}} # : Tabs {{{ [tabs] active = { bg = "blue", bold = true } inactive = { fg = "blue", bg = "gray" } # Separator sep_inner = { open = "", close = "" } sep_outer = { open = "", close = "" } # : }}} # : Mode {{{ [mode] normal_main = { bg = "blue", bold = true } normal_alt = { fg = "blue", bg = "gray" } # Select mode select_main = { bg = "red", bold = true } select_alt = { fg = "red", bg = "gray" } # Unset mode unset_main = { bg = "red", bold = true } unset_alt = { fg = "red", bg = "gray" } # : }}} # : Indicator of hovered file {{{ [indicator] parent = { reversed = true } current = { reversed = true } preview = { underline = true } padding = { open = "", close = "" } # : }}} # : Status bar {{{ [status] overall = {} sep_left = { open = "", close = "" } sep_right = { open = "", close = "" } # Permissions perm_sep = { fg = "darkgray" } perm_type = { fg = "green" } perm_read = { fg = "yellow" } perm_write = { fg = "red" } perm_exec = { fg = "cyan" } # Progress progress_label = { bold = true } progress_normal = { fg = "green", bg = "black" } progress_error = { fg = "yellow", bg = "red" } # : }}} # : Which {{{ [which] cols = 3 mask = { bg = "black" } cand = { fg = "lightcyan" } rest = { fg = "darkgray" } desc = { fg = "lightmagenta" } separator = "  " separator_style = { fg = "darkgray" } # : }}} # : Confirmation {{{ [confirm] border = { fg = "blue" } title = { fg = "blue" } body = {} list = {} btn_yes = { reversed = true } btn_no = {} btn_labels = [ " [Y]es ", " (N)o " ] # : }}} # : Spotter {{{ [spot] border = { fg = "blue" } title = { fg = "blue" } # Table tbl_col = { fg = "blue" } tbl_cell = { fg = "yellow", reversed = true } # : }}} # : Notification {{{ [notify] title_info = { fg = "green" } title_warn = { fg = "yellow" } title_error = { fg = "red" } # Icons icon_info = "" icon_warn = "" icon_error = "" # : }}} # : Picker {{{ [pick] border = { fg = "blue" } active = { fg = "magenta", bold = true } inactive = {} # : }}} # : Input {{{ [input] border = { fg = "blue" } title = {} value = {} selected = { reversed = true } # : }}} # : Completion {{{ [cmp] border = { fg = "blue" } active = { reversed = true } inactive = {} # Icons icon_file = "" icon_folder = "" icon_command = "" # : }}} # : Task manager {{{ [tasks] border = { fg = "blue" } title = {} hovered = { fg = "magenta", bold = true } # : }}} # : Help menu {{{ [help] on = { fg = "cyan" } run = { fg = "magenta" } desc = {} hovered = { reversed = true, bold = true } footer = { fg = "black", bg = "white" } # : }}} # : File-specific styles {{{ [filetype] rules = [ # Image { mime = "image/*", fg = "yellow" }, # Media { mime = "{audio,video}/*", fg = "magenta" }, # Archive { mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", fg = "red" }, # Document { mime = "application/{pdf,doc,rtf}", fg = "cyan" }, # Virtual file system { mime = "vfs/{absent,stale}", fg = "gray" }, # Special file { url = "*", is = "orphan", bg = "red" }, { url = "*", is = "exec" , fg = "green" }, # Dummy file { url = "*", is = "dummy", bg = "red" }, { url = "*/", is = "dummy", bg = "red" }, # Fallback { url = "*/", fg = "blue" } ] # : }}} # : Icons {{{ [icon] globs = [] dirs = [ { name = ".config", text = "", fg = "#ff9800" }, { name = ".git", text = "", fg = "#00bcd4" }, { name = ".github", text = "", fg = "#03a9f4" }, { name = ".npm", text = "", fg = "#03a9f4" }, { name = "Desktop", text = "", fg = "#00bcd4" }, { name = "Development", text = "", fg = "#00bcd4" }, { name = "Documents", text = "", fg = "#00bcd4" }, { name = "Downloads", text = "", fg = "#00bcd4" }, { name = "Library", text = "", fg = "#00bcd4" }, { name = "Movies", text = "", fg = "#00bcd4" }, { name = "Music", text = "", fg = "#00bcd4" }, { name = "Pictures", text = "", fg = "#00bcd4" }, { name = "Public", text = "", fg = "#00bcd4" }, { name = "Videos", text = "", fg = "#00bcd4" }, ] files = [ { name = ".babelrc", text = "", fg = "#cbcb41" }, { name = ".bash_profile", text = "", fg = "#89e051" }, { name = ".bashrc", text = "", fg = "#89e051" }, { name = ".clang-format", text = "", fg = "#6d8086" }, { name = ".clang-tidy", text = "", fg = "#6d8086" }, { name = ".codespellrc", text = "󰓆", fg = "#35da60" }, { name = ".condarc", text = "", fg = "#43b02a" }, { name = ".dockerignore", text = "󰡨", fg = "#458ee6" }, { name = ".ds_store", text = "", fg = "#41535b" }, { name = ".editorconfig", text = "", fg = "#fff2f2" }, { name = ".env", text = "", fg = "#faf743" }, { name = ".eslintignore", text = "", fg = "#4b32c3" }, { name = ".eslintrc", text = "", fg = "#4b32c3" }, { name = ".git-blame-ignore-revs", text = "", fg = "#f54d27" }, { name = ".gitattributes", text = "", fg = "#f54d27" }, { name = ".gitconfig", text = "", fg = "#f54d27" }, { name = ".gitignore", text = "", fg = "#f54d27" }, { name = ".gitlab-ci.yml", text = "", fg = "#e24329" }, { name = ".gitmodules", text = "", fg = "#f54d27" }, { name = ".gtkrc-2.0", text = "", fg = "#ffffff" }, { name = ".gvimrc", text = "", fg = "#019833" }, { name = ".justfile", text = "", fg = "#6d8086" }, { name = ".luacheckrc", text = "", fg = "#00a2ff" }, { name = ".luaurc", text = "", fg = "#00a2ff" }, { name = ".mailmap", text = "󰊢", fg = "#f54d27" }, { name = ".nanorc", text = "", fg = "#440077" }, { name = ".npmignore", text = "", fg = "#e8274b" }, { name = ".npmrc", text = "", fg = "#e8274b" }, { name = ".nuxtrc", text = "󱄆", fg = "#00c58e" }, { name = ".nvmrc", text = "", fg = "#5fa04e" }, { name = ".pnpmfile.cjs", text = "", fg = "#f9ad02" }, { name = ".pre-commit-config.yaml", text = "󰛢", fg = "#f8b424" }, { name = ".prettierignore", text = "", fg = "#4285f4" }, { name = ".prettierrc", text = "", fg = "#4285f4" }, { name = ".prettierrc.cjs", text = "", fg = "#4285f4" }, { name = ".prettierrc.js", text = "", fg = "#4285f4" }, { name = ".prettierrc.json", text = "", fg = "#4285f4" }, { name = ".prettierrc.json5", text = "", fg = "#4285f4" }, { name = ".prettierrc.mjs", text = "", fg = "#4285f4" }, { name = ".prettierrc.toml", text = "", fg = "#4285f4" }, { name = ".prettierrc.yaml", text = "", fg = "#4285f4" }, { name = ".prettierrc.yml", text = "", fg = "#4285f4" }, { name = ".pylintrc", text = "", fg = "#6d8086" }, { name = ".settings.json", text = "", fg = "#854cc7" }, { name = ".SRCINFO", text = "󰣇", fg = "#0f94d2" }, { name = ".vimrc", text = "", fg = "#019833" }, { name = ".Xauthority", text = "", fg = "#e54d18" }, { name = ".xinitrc", text = "", fg = "#e54d18" }, { name = ".Xresources", text = "", fg = "#e54d18" }, { name = ".xsession", text = "", fg = "#e54d18" }, { name = ".zprofile", text = "", fg = "#89e051" }, { name = ".zshenv", text = "", fg = "#89e051" }, { name = ".zshrc", text = "", fg = "#89e051" }, { name = "_gvimrc", text = "", fg = "#019833" }, { name = "_vimrc", text = "", fg = "#019833" }, { name = "AUTHORS", text = "", fg = "#a172ff" }, { name = "AUTHORS.txt", text = "", fg = "#a172ff" }, { name = "brewfile", text = "", fg = "#701516" }, { name = "bspwmrc", text = "", fg = "#2f2f2f" }, { name = "build", text = "", fg = "#89e051" }, { name = "build.gradle", text = "", fg = "#005f87" }, { name = "build.zig.zon", text = "", fg = "#f69a1b" }, { name = "bun.lock", text = "", fg = "#eadcd1" }, { name = "bun.lockb", text = "", fg = "#eadcd1" }, { name = "cantorrc", text = "", fg = "#1c99f3" }, { name = "checkhealth", text = "󰓙", fg = "#75b4fb" }, { name = "cmakelists.txt", text = "", fg = "#dce3eb" }, { name = "code_of_conduct", text = "", fg = "#e41662" }, { name = "code_of_conduct.md", text = "", fg = "#e41662" }, { name = "commit_editmsg", text = "", fg = "#f54d27" }, { name = "commitlint.config.js", text = "󰜘", fg = "#2b9689" }, { name = "commitlint.config.ts", text = "󰜘", fg = "#2b9689" }, { name = "compose.yaml", text = "󰡨", fg = "#458ee6" }, { name = "compose.yml", text = "󰡨", fg = "#458ee6" }, { name = "config", text = "", fg = "#6d8086" }, { name = "containerfile", text = "󰡨", fg = "#458ee6" }, { name = "copying", text = "", fg = "#cbcb41" }, { name = "copying.lesser", text = "", fg = "#cbcb41" }, { name = "Directory.Build.props", text = "", fg = "#00a2ff" }, { name = "Directory.Build.targets", text = "", fg = "#00a2ff" }, { name = "Directory.Packages.props", text = "", fg = "#00a2ff" }, { name = "docker-compose.yaml", text = "󰡨", fg = "#458ee6" }, { name = "docker-compose.yml", text = "󰡨", fg = "#458ee6" }, { name = "dockerfile", text = "󰡨", fg = "#458ee6" }, { name = "eslint.config.cjs", text = "", fg = "#4b32c3" }, { name = "eslint.config.js", text = "", fg = "#4b32c3" }, { name = "eslint.config.mjs", text = "", fg = "#4b32c3" }, { name = "eslint.config.ts", text = "", fg = "#4b32c3" }, { name = "ext_typoscript_setup.txt", text = "", fg = "#ff8700" }, { name = "favicon.ico", text = "", fg = "#cbcb41" }, { name = "fp-info-cache", text = "", fg = "#ffffff" }, { name = "fp-lib-table", text = "", fg = "#ffffff" }, { name = "FreeCAD.conf", text = "", fg = "#cb333b" }, { name = "Gemfile", text = "", fg = "#701516" }, { name = "gnumakefile", text = "", fg = "#6d8086" }, { name = "go.mod", text = "", fg = "#00add8" }, { name = "go.sum", text = "", fg = "#00add8" }, { name = "go.work", text = "", fg = "#00add8" }, { name = "gradle-wrapper.properties", text = "", fg = "#005f87" }, { name = "gradle.properties", text = "", fg = "#005f87" }, { name = "gradlew", text = "", fg = "#005f87" }, { name = "groovy", text = "", fg = "#4a687c" }, { name = "gruntfile.babel.js", text = "", fg = "#e37933" }, { name = "gruntfile.coffee", text = "", fg = "#e37933" }, { name = "gruntfile.js", text = "", fg = "#e37933" }, { name = "gruntfile.ts", text = "", fg = "#e37933" }, { name = "gtkrc", text = "", fg = "#ffffff" }, { name = "gulpfile.babel.js", text = "", fg = "#cc3e44" }, { name = "gulpfile.coffee", text = "", fg = "#cc3e44" }, { name = "gulpfile.js", text = "", fg = "#cc3e44" }, { name = "gulpfile.ts", text = "", fg = "#cc3e44" }, { name = "hypridle.conf", text = "", fg = "#00aaae" }, { name = "hyprland.conf", text = "", fg = "#00aaae" }, { name = "hyprlandd.conf", text = "", fg = "#00aaae" }, { name = "hyprlock.conf", text = "", fg = "#00aaae" }, { name = "hyprpaper.conf", text = "", fg = "#00aaae" }, { name = "hyprsunset.conf", text = "", fg = "#00aaae" }, { name = "i18n.config.js", text = "󰗊", fg = "#7986cb" }, { name = "i18n.config.ts", text = "󰗊", fg = "#7986cb" }, { name = "i3blocks.conf", text = "", fg = "#e8ebee" }, { name = "i3status.conf", text = "", fg = "#e8ebee" }, { name = "index.theme", text = "", fg = "#2db96f" }, { name = "ionic.config.json", text = "", fg = "#4f8ff7" }, { name = "Jenkinsfile", text = "", fg = "#d33833" }, { name = "justfile", text = "", fg = "#6d8086" }, { name = "kalgebrarc", text = "", fg = "#1c99f3" }, { name = "kdeglobals", text = "", fg = "#1c99f3" }, { name = "kdenlive-layoutsrc", text = "", fg = "#83b8f2" }, { name = "kdenliverc", text = "", fg = "#83b8f2" }, { name = "kritadisplayrc", text = "", fg = "#f245fb" }, { name = "kritarc", text = "", fg = "#f245fb" }, { name = "license", text = "", fg = "#d0bf41" }, { name = "license.md", text = "", fg = "#d0bf41" }, { name = "lxde-rc.xml", text = "", fg = "#909090" }, { name = "lxqt.conf", text = "", fg = "#0192d3" }, { name = "makefile", text = "", fg = "#6d8086" }, { name = "mix.lock", text = "", fg = "#a074c4" }, { name = "mpv.conf", text = "", fg = "#3b1342" }, { name = "next.config.cjs", text = "", fg = "#ffffff" }, { name = "next.config.js", text = "", fg = "#ffffff" }, { name = "next.config.ts", text = "", fg = "#ffffff" }, { name = "node_modules", text = "", fg = "#e8274b" }, { name = "nuxt.config.cjs", text = "󱄆", fg = "#00c58e" }, { name = "nuxt.config.js", text = "󱄆", fg = "#00c58e" }, { name = "nuxt.config.mjs", text = "󱄆", fg = "#00c58e" }, { name = "nuxt.config.ts", text = "󱄆", fg = "#00c58e" }, { name = "package-lock.json", text = "", fg = "#7a0d21" }, { name = "package.json", text = "", fg = "#e8274b" }, { name = "PKGBUILD", text = "", fg = "#0f94d2" }, { name = "platformio.ini", text = "", fg = "#f6822b" }, { name = "playwright.config.cjs", text = "", fg = "#2fad33" }, { name = "playwright.config.cts", text = "", fg = "#2fad33" }, { name = "playwright.config.js", text = "", fg = "#2fad33" }, { name = "playwright.config.mjs", text = "", fg = "#2fad33" }, { name = "playwright.config.mts", text = "", fg = "#2fad33" }, { name = "playwright.config.ts", text = "", fg = "#2fad33" }, { name = "pnpm-lock.yaml", text = "", fg = "#f9ad02" }, { name = "pnpm-workspace.yaml", text = "", fg = "#f9ad02" }, { name = "pom.xml", text = "", fg = "#7a0d21" }, { name = "prettier.config.cjs", text = "", fg = "#4285f4" }, { name = "prettier.config.js", text = "", fg = "#4285f4" }, { name = "prettier.config.mjs", text = "", fg = "#4285f4" }, { name = "prettier.config.ts", text = "", fg = "#4285f4" }, { name = "prisma.config.mts", text = "", fg = "#5a67d8" }, { name = "prisma.config.ts", text = "", fg = "#5a67d8" }, { name = "procfile", text = "", fg = "#a074c4" }, { name = "PrusaSlicer.ini", text = "", fg = "#ec6b23" }, { name = "PrusaSlicerGcodeViewer.ini", text = "", fg = "#ec6b23" }, { name = "py.typed", text = "", fg = "#ffbc03" }, { name = "QtProject.conf", text = "", fg = "#40cd52" }, { name = "rakefile", text = "", fg = "#701516" }, { name = "readme", text = "󰂺", fg = "#ededed" }, { name = "readme.md", text = "󰂺", fg = "#ededed" }, { name = "rmd", text = "", fg = "#519aba" }, { name = "robots.txt", text = "󰚩", fg = "#5d7096" }, { name = "security", text = "󰒃", fg = "#bec4c9" }, { name = "security.md", text = "󰒃", fg = "#bec4c9" }, { name = "settings.gradle", text = "", fg = "#005f87" }, { name = "svelte.config.js", text = "", fg = "#ff3e00" }, { name = "sxhkdrc", text = "", fg = "#2f2f2f" }, { name = "sym-lib-table", text = "", fg = "#ffffff" }, { name = "tailwind.config.js", text = "󱏿", fg = "#20c2e3" }, { name = "tailwind.config.mjs", text = "󱏿", fg = "#20c2e3" }, { name = "tailwind.config.ts", text = "󱏿", fg = "#20c2e3" }, { name = "tmux.conf", text = "", fg = "#14ba19" }, { name = "tmux.conf.local", text = "", fg = "#14ba19" }, { name = "tsconfig.json", text = "", fg = "#519aba" }, { name = "unlicense", text = "", fg = "#d0bf41" }, { name = "vagrantfile", text = "", fg = "#1563ff" }, { name = "vercel.json", text = "", fg = "#ffffff" }, { name = "vite.config.cjs", text = "", fg = "#ffa800" }, { name = "vite.config.cts", text = "", fg = "#ffa800" }, { name = "vite.config.js", text = "", fg = "#ffa800" }, { name = "vite.config.mjs", text = "", fg = "#ffa800" }, { name = "vite.config.mts", text = "", fg = "#ffa800" }, { name = "vite.config.ts", text = "", fg = "#ffa800" }, { name = "vitest.config.cjs", text = "", fg = "#739b1b" }, { name = "vitest.config.cts", text = "", fg = "#739b1b" }, { name = "vitest.config.js", text = "", fg = "#739b1b" }, { name = "vitest.config.mjs", text = "", fg = "#739b1b" }, { name = "vitest.config.mts", text = "", fg = "#739b1b" }, { name = "vitest.config.ts", text = "", fg = "#739b1b" }, { name = "vlcrc", text = "󰕼", fg = "#ee7a00" }, { name = "webpack", text = "󰜫", fg = "#519aba" }, { name = "weston.ini", text = "", fg = "#ffbb01" }, { name = "workspace", text = "", fg = "#89e051" }, { name = "wrangler.jsonc", text = "", fg = "#f48120" }, { name = "wrangler.toml", text = "", fg = "#f48120" }, { name = "xdph.conf", text = "", fg = "#00aaae" }, { name = "xmobarrc", text = "", fg = "#fd4d5d" }, { name = "xmobarrc.hs", text = "", fg = "#fd4d5d" }, { name = "xmonad.hs", text = "", fg = "#fd4d5d" }, { name = "xorg.conf", text = "", fg = "#e54d18" }, { name = "xsettingsd.conf", text = "", fg = "#e54d18" }, ] exts = [ { name = "3gp", text = "", fg = "#fd971f" }, { name = "3mf", text = "󰆧", fg = "#888888" }, { name = "7z", text = "", fg = "#eca517" }, { name = "a", text = "", fg = "#dcddd6" }, { name = "aac", text = "", fg = "#00afff" }, { name = "ada", text = "", fg = "#599eff" }, { name = "adb", text = "", fg = "#599eff" }, { name = "ads", text = "", fg = "#a074c4" }, { name = "ai", text = "", fg = "#cbcb41" }, { name = "aif", text = "", fg = "#00afff" }, { name = "aiff", text = "", fg = "#00afff" }, { name = "android", text = "", fg = "#34a853" }, { name = "ape", text = "", fg = "#00afff" }, { name = "apk", text = "", fg = "#34a853" }, { name = "apl", text = "", fg = "#24a148" }, { name = "app", text = "", fg = "#9f0500" }, { name = "applescript", text = "", fg = "#6d8085" }, { name = "asc", text = "󰦝", fg = "#576d7f" }, { name = "asm", text = "", fg = "#0091bd" }, { name = "ass", text = "󰨖", fg = "#ffb713" }, { name = "astro", text = "", fg = "#e23f67" }, { name = "avif", text = "", fg = "#a074c4" }, { name = "awk", text = "", fg = "#4d5a5e" }, { name = "azcli", text = "", fg = "#0078d4" }, { name = "bak", text = "󰁯", fg = "#6d8086" }, { name = "bash", text = "", fg = "#89e051" }, { name = "bat", text = "", fg = "#c1f12e" }, { name = "bazel", text = "", fg = "#89e051" }, { name = "bib", text = "󱉟", fg = "#cbcb41" }, { name = "bicep", text = "", fg = "#519aba" }, { name = "bicepparam", text = "", fg = "#9f74b3" }, { name = "bin", text = "", fg = "#9f0500" }, { name = "blade.php", text = "", fg = "#f05340" }, { name = "blend", text = "󰂫", fg = "#ea7600" }, { name = "blp", text = "󰺾", fg = "#5796e2" }, { name = "bmp", text = "", fg = "#a074c4" }, { name = "bqn", text = "", fg = "#24a148" }, { name = "brep", text = "󰻫", fg = "#839463" }, { name = "bz", text = "", fg = "#eca517" }, { name = "bz2", text = "", fg = "#eca517" }, { name = "bz3", text = "", fg = "#eca517" }, { name = "bzl", text = "", fg = "#89e051" }, { name = "c", text = "", fg = "#599eff" }, { name = "c++", text = "", fg = "#f34b7d" }, { name = "cache", text = "", fg = "#ffffff" }, { name = "cast", text = "", fg = "#fd971f" }, { name = "cbl", text = "", fg = "#005ca5" }, { name = "cc", text = "", fg = "#f34b7d" }, { name = "ccm", text = "", fg = "#f34b7d" }, { name = "cfc", text = "", fg = "#01a4ba" }, { name = "cfg", text = "", fg = "#6d8086" }, { name = "cfm", text = "", fg = "#01a4ba" }, { name = "cjs", text = "", fg = "#cbcb41" }, { name = "clj", text = "", fg = "#8dc149" }, { name = "cljc", text = "", fg = "#8dc149" }, { name = "cljd", text = "", fg = "#519aba" }, { name = "cljs", text = "", fg = "#519aba" }, { name = "cmake", text = "", fg = "#dce3eb" }, { name = "cob", text = "", fg = "#005ca5" }, { name = "cobol", text = "", fg = "#005ca5" }, { name = "coffee", text = "", fg = "#cbcb41" }, { name = "conda", text = "", fg = "#43b02a" }, { name = "conf", text = "", fg = "#6d8086" }, { name = "config.ru", text = "", fg = "#701516" }, { name = "cow", text = "󰆚", fg = "#965824" }, { name = "cp", text = "", fg = "#519aba" }, { name = "cpp", text = "", fg = "#519aba" }, { name = "cppm", text = "", fg = "#519aba" }, { name = "cpy", text = "", fg = "#005ca5" }, { name = "cr", text = "", fg = "#c8c8c8" }, { name = "crdownload", text = "", fg = "#44cda8" }, { name = "cs", text = "󰌛", fg = "#596706" }, { name = "csh", text = "", fg = "#4d5a5e" }, { name = "cshtml", text = "󱦗", fg = "#512bd4" }, { name = "cson", text = "", fg = "#cbcb41" }, { name = "csproj", text = "󰪮", fg = "#512bd4" }, { name = "css", text = "", fg = "#663399" }, { name = "csv", text = "", fg = "#89e051" }, { name = "cts", text = "", fg = "#519aba" }, { name = "cu", text = "", fg = "#89e051" }, { name = "cue", text = "󰲹", fg = "#ed95ae" }, { name = "cuh", text = "", fg = "#a074c4" }, { name = "cxx", text = "", fg = "#519aba" }, { name = "cxxm", text = "", fg = "#519aba" }, { name = "d", text = "", fg = "#b03931" }, { name = "d.ts", text = "", fg = "#d59855" }, { name = "dart", text = "", fg = "#03589c" }, { name = "db", text = "", fg = "#dad8d8" }, { name = "dconf", text = "", fg = "#ffffff" }, { name = "desktop", text = "", fg = "#563d7c" }, { name = "diff", text = "", fg = "#41535b" }, { name = "dll", text = "", fg = "#4d2c0b" }, { name = "doc", text = "󰈬", fg = "#185abd" }, { name = "Dockerfile", text = "󰡨", fg = "#458ee6" }, { name = "dockerignore", text = "󰡨", fg = "#458ee6" }, { name = "docx", text = "󰈬", fg = "#185abd" }, { name = "dot", text = "󱁉", fg = "#30638e" }, { name = "download", text = "", fg = "#44cda8" }, { name = "drl", text = "", fg = "#ffafaf" }, { name = "dropbox", text = "", fg = "#0061fe" }, { name = "dump", text = "", fg = "#dad8d8" }, { name = "dwg", text = "󰻫", fg = "#839463" }, { name = "dxf", text = "󰻫", fg = "#839463" }, { name = "ebook", text = "", fg = "#eab16d" }, { name = "ebuild", text = "", fg = "#4c416e" }, { name = "edn", text = "", fg = "#519aba" }, { name = "eex", text = "", fg = "#a074c4" }, { name = "ejs", text = "", fg = "#cbcb41" }, { name = "el", text = "", fg = "#8172be" }, { name = "elc", text = "", fg = "#8172be" }, { name = "elf", text = "", fg = "#9f0500" }, { name = "elm", text = "", fg = "#519aba" }, { name = "eln", text = "", fg = "#8172be" }, { name = "env", text = "", fg = "#faf743" }, { name = "eot", text = "", fg = "#ececec" }, { name = "epp", text = "", fg = "#ffa61a" }, { name = "epub", text = "", fg = "#eab16d" }, { name = "erb", text = "", fg = "#701516" }, { name = "erl", text = "", fg = "#b83998" }, { name = "ex", text = "", fg = "#a074c4" }, { name = "exe", text = "", fg = "#9f0500" }, { name = "exs", text = "", fg = "#a074c4" }, { name = "f#", text = "", fg = "#519aba" }, { name = "f3d", text = "󰻫", fg = "#839463" }, { name = "f90", text = "󱈚", fg = "#734f96" }, { name = "fbx", text = "󰆧", fg = "#888888" }, { name = "fcbak", text = "", fg = "#cb333b" }, { name = "fcmacro", text = "", fg = "#cb333b" }, { name = "fcmat", text = "", fg = "#cb333b" }, { name = "fcparam", text = "", fg = "#cb333b" }, { name = "fcscript", text = "", fg = "#cb333b" }, { name = "fcstd", text = "", fg = "#cb333b" }, { name = "fcstd1", text = "", fg = "#cb333b" }, { name = "fctb", text = "", fg = "#cb333b" }, { name = "fctl", text = "", fg = "#cb333b" }, { name = "fdmdownload", text = "", fg = "#44cda8" }, { name = "feature", text = "", fg = "#00a818" }, { name = "fish", text = "", fg = "#4d5a5e" }, { name = "flac", text = "", fg = "#0075aa" }, { name = "flc", text = "", fg = "#ececec" }, { name = "flf", text = "", fg = "#ececec" }, { name = "fnl", text = "", fg = "#fff3d7" }, { name = "fodg", text = "", fg = "#fffb57" }, { name = "fodp", text = "", fg = "#fe9c45" }, { name = "fods", text = "", fg = "#78fc4e" }, { name = "fodt", text = "", fg = "#2dcbfd" }, { name = "frag", text = "", fg = "#5586a6" }, { name = "fs", text = "", fg = "#519aba" }, { name = "fsi", text = "", fg = "#519aba" }, { name = "fsscript", text = "", fg = "#519aba" }, { name = "fsx", text = "", fg = "#519aba" }, { name = "gcode", text = "󰐫", fg = "#1471ad" }, { name = "gd", text = "", fg = "#6d8086" }, { name = "gemspec", text = "", fg = "#701516" }, { name = "geom", text = "", fg = "#5586a6" }, { name = "gif", text = "", fg = "#a074c4" }, { name = "git", text = "", fg = "#f14c28" }, { name = "glb", text = "", fg = "#ffb13b" }, { name = "gleam", text = "", fg = "#ffaff3" }, { name = "glsl", text = "", fg = "#5586a6" }, { name = "gnumakefile", text = "", fg = "#6d8086" }, { name = "go", text = "", fg = "#00add8" }, { name = "godot", text = "", fg = "#6d8086" }, { name = "gpr", text = "", fg = "#6d8086" }, { name = "gql", text = "", fg = "#e535ab" }, { name = "gradle", text = "", fg = "#005f87" }, { name = "graphql", text = "", fg = "#e535ab" }, { name = "gresource", text = "", fg = "#ffffff" }, { name = "gv", text = "󱁉", fg = "#30638e" }, { name = "gz", text = "", fg = "#eca517" }, { name = "h", text = "", fg = "#a074c4" }, { name = "haml", text = "", fg = "#eaeae1" }, { name = "hbs", text = "", fg = "#f0772b" }, { name = "heex", text = "", fg = "#a074c4" }, { name = "hex", text = "", fg = "#2e63ff" }, { name = "hh", text = "", fg = "#a074c4" }, { name = "hpp", text = "", fg = "#a074c4" }, { name = "hrl", text = "", fg = "#b83998" }, { name = "hs", text = "", fg = "#a074c4" }, { name = "htm", text = "", fg = "#e34c26" }, { name = "html", text = "", fg = "#e44d26" }, { name = "http", text = "", fg = "#008ec7" }, { name = "huff", text = "󰡘", fg = "#4242c7" }, { name = "hurl", text = "", fg = "#ff0288" }, { name = "hx", text = "", fg = "#ea8220" }, { name = "hxx", text = "", fg = "#a074c4" }, { name = "ical", text = "", fg = "#2b2e83" }, { name = "icalendar", text = "", fg = "#2b2e83" }, { name = "ico", text = "", fg = "#cbcb41" }, { name = "ics", text = "", fg = "#2b2e83" }, { name = "ifb", text = "", fg = "#2b2e83" }, { name = "ifc", text = "󰻫", fg = "#839463" }, { name = "ige", text = "󰻫", fg = "#839463" }, { name = "iges", text = "󰻫", fg = "#839463" }, { name = "igs", text = "󰻫", fg = "#839463" }, { name = "image", text = "", fg = "#d0bec8" }, { name = "img", text = "", fg = "#d0bec8" }, { name = "import", text = "", fg = "#ececec" }, { name = "info", text = "", fg = "#ffffcd" }, { name = "ini", text = "", fg = "#6d8086" }, { name = "ino", text = "", fg = "#56b6c2" }, { name = "ipynb", text = "", fg = "#f57d01" }, { name = "iso", text = "", fg = "#d0bec8" }, { name = "ixx", text = "", fg = "#519aba" }, { name = "jar", text = "", fg = "#ffaf67" }, { name = "java", text = "", fg = "#cc3e44" }, { name = "jl", text = "", fg = "#a270ba" }, { name = "jpeg", text = "", fg = "#a074c4" }, { name = "jpg", text = "", fg = "#a074c4" }, { name = "js", text = "", fg = "#cbcb41" }, { name = "json", text = "", fg = "#cbcb41" }, { name = "json5", text = "", fg = "#cbcb41" }, { name = "jsonc", text = "", fg = "#cbcb41" }, { name = "jsx", text = "", fg = "#20c2e3" }, { name = "jwmrc", text = "", fg = "#0078cd" }, { name = "jxl", text = "", fg = "#a074c4" }, { name = "kbx", text = "󰯄", fg = "#737672" }, { name = "kdb", text = "", fg = "#529b34" }, { name = "kdbx", text = "", fg = "#529b34" }, { name = "kdenlive", text = "", fg = "#83b8f2" }, { name = "kdenlivetitle", text = "", fg = "#83b8f2" }, { name = "kicad_dru", text = "", fg = "#ffffff" }, { name = "kicad_mod", text = "", fg = "#ffffff" }, { name = "kicad_pcb", text = "", fg = "#ffffff" }, { name = "kicad_prl", text = "", fg = "#ffffff" }, { name = "kicad_pro", text = "", fg = "#ffffff" }, { name = "kicad_sch", text = "", fg = "#ffffff" }, { name = "kicad_sym", text = "", fg = "#ffffff" }, { name = "kicad_wks", text = "", fg = "#ffffff" }, { name = "ko", text = "", fg = "#dcddd6" }, { name = "kpp", text = "", fg = "#f245fb" }, { name = "kra", text = "", fg = "#f245fb" }, { name = "krz", text = "", fg = "#f245fb" }, { name = "ksh", text = "", fg = "#4d5a5e" }, { name = "kt", text = "", fg = "#7f52ff" }, { name = "kts", text = "", fg = "#7f52ff" }, { name = "lck", text = "", fg = "#bbbbbb" }, { name = "leex", text = "", fg = "#a074c4" }, { name = "less", text = "", fg = "#563d7c" }, { name = "lff", text = "", fg = "#ececec" }, { name = "lhs", text = "", fg = "#a074c4" }, { name = "lib", text = "", fg = "#4d2c0b" }, { name = "license", text = "", fg = "#cbcb41" }, { name = "liquid", text = "", fg = "#95bf47" }, { name = "lock", text = "", fg = "#bbbbbb" }, { name = "log", text = "󰌱", fg = "#dddddd" }, { name = "lrc", text = "󰨖", fg = "#ffb713" }, { name = "lua", text = "", fg = "#51a0cf" }, { name = "luac", text = "", fg = "#51a0cf" }, { name = "luau", text = "", fg = "#00a2ff" }, { name = "m", text = "", fg = "#599eff" }, { name = "m3u", text = "󰲹", fg = "#ed95ae" }, { name = "m3u8", text = "󰲹", fg = "#ed95ae" }, { name = "m4a", text = "", fg = "#00afff" }, { name = "m4v", text = "", fg = "#fd971f" }, { name = "magnet", text = "", fg = "#a51b16" }, { name = "makefile", text = "", fg = "#6d8086" }, { name = "markdown", text = "", fg = "#dddddd" }, { name = "material", text = "", fg = "#b83998" }, { name = "md", text = "", fg = "#dddddd" }, { name = "md5", text = "󰕥", fg = "#8c86af" }, { name = "mdx", text = "", fg = "#519aba" }, { name = "mint", text = "󰌪", fg = "#87c095" }, { name = "mjs", text = "", fg = "#f1e05a" }, { name = "mk", text = "", fg = "#6d8086" }, { name = "mkv", text = "", fg = "#fd971f" }, { name = "ml", text = "", fg = "#e37933" }, { name = "mli", text = "", fg = "#e37933" }, { name = "mm", text = "", fg = "#519aba" }, { name = "mo", text = "", fg = "#9772fb" }, { name = "mobi", text = "", fg = "#eab16d" }, { name = "mojo", text = "", fg = "#ff4c1f" }, { name = "mov", text = "", fg = "#fd971f" }, { name = "mp3", text = "", fg = "#00afff" }, { name = "mp4", text = "", fg = "#fd971f" }, { name = "mpp", text = "", fg = "#519aba" }, { name = "msf", text = "", fg = "#137be1" }, { name = "mts", text = "", fg = "#519aba" }, { name = "mustache", text = "", fg = "#e37933" }, { name = "nfo", text = "", fg = "#ffffcd" }, { name = "nim", text = "", fg = "#f3d400" }, { name = "nix", text = "", fg = "#7ebae4" }, { name = "norg", text = "", fg = "#4878be" }, { name = "nswag", text = "", fg = "#85ea2d" }, { name = "nu", text = "", fg = "#3aa675" }, { name = "o", text = "", fg = "#9f0500" }, { name = "obj", text = "󰆧", fg = "#888888" }, { name = "odf", text = "", fg = "#ff5a96" }, { name = "odg", text = "", fg = "#fffb57" }, { name = "odin", text = "󰟢", fg = "#3882d2" }, { name = "odp", text = "", fg = "#fe9c45" }, { name = "ods", text = "", fg = "#78fc4e" }, { name = "odt", text = "", fg = "#2dcbfd" }, { name = "oga", text = "", fg = "#0075aa" }, { name = "ogg", text = "", fg = "#0075aa" }, { name = "ogv", text = "", fg = "#fd971f" }, { name = "ogx", text = "", fg = "#fd971f" }, { name = "opus", text = "", fg = "#0075aa" }, { name = "org", text = "", fg = "#77aa99" }, { name = "otf", text = "", fg = "#ececec" }, { name = "out", text = "", fg = "#9f0500" }, { name = "part", text = "", fg = "#44cda8" }, { name = "patch", text = "", fg = "#41535b" }, { name = "pck", text = "", fg = "#6d8086" }, { name = "pcm", text = "", fg = "#0075aa" }, { name = "pdf", text = "", fg = "#b30b00" }, { name = "php", text = "", fg = "#a074c4" }, { name = "pl", text = "", fg = "#519aba" }, { name = "pls", text = "󰲹", fg = "#ed95ae" }, { name = "ply", text = "󰆧", fg = "#888888" }, { name = "pm", text = "", fg = "#519aba" }, { name = "png", text = "", fg = "#a074c4" }, { name = "po", text = "", fg = "#2596be" }, { name = "pot", text = "", fg = "#2596be" }, { name = "pp", text = "", fg = "#ffa61a" }, { name = "ppt", text = "󰈧", fg = "#cb4a32" }, { name = "pptx", text = "󰈧", fg = "#cb4a32" }, { name = "prisma", text = "", fg = "#5a67d8" }, { name = "pro", text = "", fg = "#e4b854" }, { name = "ps1", text = "󰨊", fg = "#4273ca" }, { name = "psb", text = "", fg = "#519aba" }, { name = "psd", text = "", fg = "#519aba" }, { name = "psd1", text = "󰨊", fg = "#6975c4" }, { name = "psm1", text = "󰨊", fg = "#6975c4" }, { name = "pub", text = "󰷖", fg = "#e3c58e" }, { name = "pxd", text = "", fg = "#5aa7e4" }, { name = "pxi", text = "", fg = "#5aa7e4" }, { name = "py", text = "", fg = "#ffbc03" }, { name = "pyc", text = "", fg = "#ffe291" }, { name = "pyd", text = "", fg = "#ffe291" }, { name = "pyi", text = "", fg = "#ffbc03" }, { name = "pyo", text = "", fg = "#ffe291" }, { name = "pyw", text = "", fg = "#5aa7e4" }, { name = "pyx", text = "", fg = "#5aa7e4" }, { name = "qm", text = "", fg = "#2596be" }, { name = "qml", text = "", fg = "#40cd52" }, { name = "qrc", text = "", fg = "#40cd52" }, { name = "qss", text = "", fg = "#40cd52" }, { name = "query", text = "", fg = "#90a850" }, { name = "R", text = "󰟔", fg = "#2266ba" }, { name = "r", text = "󰟔", fg = "#2266ba" }, { name = "rake", text = "", fg = "#701516" }, { name = "rar", text = "", fg = "#eca517" }, { name = "rasi", text = "", fg = "#cbcb41" }, { name = "razor", text = "󱦘", fg = "#512bd4" }, { name = "rb", text = "", fg = "#701516" }, { name = "res", text = "", fg = "#cc3e44" }, { name = "resi", text = "", fg = "#f55385" }, { name = "rlib", text = "", fg = "#dea584" }, { name = "rmd", text = "", fg = "#519aba" }, { name = "rproj", text = "󰗆", fg = "#358a5b" }, { name = "rs", text = "", fg = "#dea584" }, { name = "rss", text = "", fg = "#fb9d3b" }, { name = "s", text = "", fg = "#0071c5" }, { name = "sass", text = "", fg = "#f55385" }, { name = "sbt", text = "", fg = "#cc3e44" }, { name = "sc", text = "", fg = "#cc3e44" }, { name = "scad", text = "", fg = "#f9d72c" }, { name = "scala", text = "", fg = "#cc3e44" }, { name = "scm", text = "󰘧", fg = "#eeeeee" }, { name = "scss", text = "", fg = "#f55385" }, { name = "sh", text = "", fg = "#4d5a5e" }, { name = "sha1", text = "󰕥", fg = "#8c86af" }, { name = "sha224", text = "󰕥", fg = "#8c86af" }, { name = "sha256", text = "󰕥", fg = "#8c86af" }, { name = "sha384", text = "󰕥", fg = "#8c86af" }, { name = "sha512", text = "󰕥", fg = "#8c86af" }, { name = "sig", text = "󰘧", fg = "#e37933" }, { name = "signature", text = "󰘧", fg = "#e37933" }, { name = "skp", text = "󰻫", fg = "#839463" }, { name = "sldasm", text = "󰻫", fg = "#839463" }, { name = "sldprt", text = "󰻫", fg = "#839463" }, { name = "slim", text = "", fg = "#e34c26" }, { name = "sln", text = "", fg = "#854cc7" }, { name = "slnx", text = "", fg = "#854cc7" }, { name = "slvs", text = "󰻫", fg = "#839463" }, { name = "sml", text = "󰘧", fg = "#e37933" }, { name = "so", text = "", fg = "#dcddd6" }, { name = "sol", text = "", fg = "#519aba" }, { name = "spec.js", text = "", fg = "#cbcb41" }, { name = "spec.jsx", text = "", fg = "#20c2e3" }, { name = "spec.ts", text = "", fg = "#519aba" }, { name = "spec.tsx", text = "", fg = "#1354bf" }, { name = "spx", text = "", fg = "#0075aa" }, { name = "sql", text = "", fg = "#dad8d8" }, { name = "sqlite", text = "", fg = "#dad8d8" }, { name = "sqlite3", text = "", fg = "#dad8d8" }, { name = "srt", text = "󰨖", fg = "#ffb713" }, { name = "ssa", text = "󰨖", fg = "#ffb713" }, { name = "ste", text = "󰻫", fg = "#839463" }, { name = "step", text = "󰻫", fg = "#839463" }, { name = "stl", text = "󰆧", fg = "#888888" }, { name = "stories.js", text = "", fg = "#ff4785" }, { name = "stories.jsx", text = "", fg = "#ff4785" }, { name = "stories.mjs", text = "", fg = "#ff4785" }, { name = "stories.svelte", text = "", fg = "#ff4785" }, { name = "stories.ts", text = "", fg = "#ff4785" }, { name = "stories.tsx", text = "", fg = "#ff4785" }, { name = "stories.vue", text = "", fg = "#ff4785" }, { name = "stp", text = "󰻫", fg = "#839463" }, { name = "strings", text = "", fg = "#2596be" }, { name = "styl", text = "", fg = "#8dc149" }, { name = "sub", text = "󰨖", fg = "#ffb713" }, { name = "sublime", text = "", fg = "#e37933" }, { name = "suo", text = "", fg = "#854cc7" }, { name = "sv", text = "󰍛", fg = "#019833" }, { name = "svelte", text = "", fg = "#ff3e00" }, { name = "svg", text = "󰜡", fg = "#ffb13b" }, { name = "svgz", text = "󰜡", fg = "#ffb13b" }, { name = "svh", text = "󰍛", fg = "#019833" }, { name = "swift", text = "", fg = "#e37933" }, { name = "t", text = "", fg = "#519aba" }, { name = "tbc", text = "󰛓", fg = "#1e5cb3" }, { name = "tcl", text = "󰛓", fg = "#1e5cb3" }, { name = "templ", text = "", fg = "#dbbd30" }, { name = "terminal", text = "", fg = "#31b53e" }, { name = "test.js", text = "", fg = "#cbcb41" }, { name = "test.jsx", text = "", fg = "#20c2e3" }, { name = "test.ts", text = "", fg = "#519aba" }, { name = "test.tsx", text = "", fg = "#1354bf" }, { name = "tex", text = "", fg = "#3d6117" }, { name = "tf", text = "", fg = "#5f43e9" }, { name = "tfvars", text = "", fg = "#5f43e9" }, { name = "tgz", text = "", fg = "#eca517" }, { name = "tmpl", text = "", fg = "#dbbd30" }, { name = "tmux", text = "", fg = "#14ba19" }, { name = "toml", text = "", fg = "#9c4221" }, { name = "torrent", text = "", fg = "#44cda8" }, { name = "tres", text = "", fg = "#6d8086" }, { name = "ts", text = "", fg = "#519aba" }, { name = "tscn", text = "", fg = "#6d8086" }, { name = "tsconfig", text = "", fg = "#ff8700" }, { name = "tsx", text = "", fg = "#1354bf" }, { name = "ttf", text = "", fg = "#ececec" }, { name = "twig", text = "", fg = "#8dc149" }, { name = "txt", text = "󰈙", fg = "#89e051" }, { name = "txz", text = "", fg = "#eca517" }, { name = "typ", text = "", fg = "#0dbcc0" }, { name = "typoscript", text = "", fg = "#ff8700" }, { name = "ui", text = "", fg = "#015bf0" }, { name = "v", text = "󰍛", fg = "#019833" }, { name = "vala", text = "", fg = "#7b3db9" }, { name = "vert", text = "", fg = "#5586a6" }, { name = "vh", text = "󰍛", fg = "#019833" }, { name = "vhd", text = "󰍛", fg = "#019833" }, { name = "vhdl", text = "󰍛", fg = "#019833" }, { name = "vi", text = "", fg = "#fec60a" }, { name = "vim", text = "", fg = "#019833" }, { name = "vsh", text = "", fg = "#5d87bf" }, { name = "vsix", text = "", fg = "#854cc7" }, { name = "vue", text = "", fg = "#8dc149" }, { name = "wasm", text = "", fg = "#5c4cdb" }, { name = "wav", text = "", fg = "#00afff" }, { name = "webm", text = "", fg = "#fd971f" }, { name = "webmanifest", text = "", fg = "#f1e05a" }, { name = "webp", text = "", fg = "#a074c4" }, { name = "webpack", text = "󰜫", fg = "#519aba" }, { name = "wma", text = "", fg = "#00afff" }, { name = "wmv", text = "", fg = "#fd971f" }, { name = "woff", text = "", fg = "#ececec" }, { name = "woff2", text = "", fg = "#ececec" }, { name = "wrl", text = "󰆧", fg = "#888888" }, { name = "wrz", text = "󰆧", fg = "#888888" }, { name = "wv", text = "", fg = "#00afff" }, { name = "wvc", text = "", fg = "#00afff" }, { name = "x", text = "", fg = "#599eff" }, { name = "xaml", text = "󰙳", fg = "#512bd4" }, { name = "xcf", text = "", fg = "#635b46" }, { name = "xcplayground", text = "", fg = "#e37933" }, { name = "xcstrings", text = "", fg = "#2596be" }, { name = "xls", text = "󰈛", fg = "#207245" }, { name = "xlsx", text = "󰈛", fg = "#207245" }, { name = "xm", text = "", fg = "#519aba" }, { name = "xml", text = "󰗀", fg = "#e37933" }, { name = "xpi", text = "", fg = "#ff1b01" }, { name = "xslt", text = "󰗀", fg = "#33a9dc" }, { name = "xul", text = "", fg = "#e37933" }, { name = "xz", text = "", fg = "#eca517" }, { name = "yaml", text = "", fg = "#6d8086" }, { name = "yml", text = "", fg = "#6d8086" }, { name = "zig", text = "", fg = "#f69a1b" }, { name = "zip", text = "", fg = "#eca517" }, { name = "zsh", text = "", fg = "#89e051" }, { name = "zst", text = "", fg = "#eca517" }, { name = "🔥", text = "", fg = "#ff4c1f" }, ] conds = [ # Special files { if = "orphan", text = "", fg = "#ffffff" }, { if = "link", text = "", fg = "#9e9e9e" }, { if = "block", text = "", fg = "#cddc39" }, { if = "char", text = "", fg = "#cddc39" }, { if = "fifo", text = "", fg = "#cddc39" }, { if = "sock", text = "", fg = "#cddc39" }, { if = "sticky", text = "", fg = "#cddc39" }, { if = "dummy", text = "", fg = "#f44336" }, # Fallback { if = "dir & hovered", text = "", fg = "#03a9f4" }, { if = "dir", text = "", fg = "#03a9f4" }, { if = "exec", text = "", fg = "#8bc34a" }, { if = "!dir", text = "", fg = "#ffffff" }, ] # : }}} ================================================ FILE: yazi-config/preset/theme-light.toml ================================================ # If the user's terminal is in dark mode, Yazi will load `theme-dark.toml` on startup; otherwise, `theme-light.toml`. # You can override any parts of them that are not related to the dark/light mode in your own `theme.toml`. # If you want to dynamically override their content based on dark/light mode, you can specify two different flavors # for dark and light modes under `[flavor]`, and do so in those flavors instead. "$schema" = "https://yazi-rs.github.io/schemas/theme.json" # vim:fileencoding=utf-8:foldmethod=marker # : Flavor {{{ [flavor] dark = "" light = "" # : }}} # : App {{{ [app] overall = {} # : }}} # : Manager {{{ [mgr] cwd = { fg = "cyan" } # Find find_keyword = { fg = "yellow", bold = true, italic = true, underline = true } find_position = { fg = "magenta", bg = "reset", bold = true, italic = true } # Symlink symlink_target = { italic = true } # Marker marker_copied = { fg = "lightgreen", bg = "lightgreen" } marker_cut = { fg = "lightred", bg = "lightred" } marker_marked = { fg = "lightcyan", bg = "lightcyan" } marker_selected = { fg = "lightyellow", bg = "lightyellow" } marker_symbol = "│" # Count count_copied = { fg = "white", bg = "green" } count_cut = { fg = "white", bg = "red" } count_selected = { fg = "white", bg = "yellow" } # Border border_symbol = "│" border_style = { fg = "gray" } # Highlighting syntect_theme = "" # : }}} # : Tabs {{{ [tabs] active = { bg = "blue", bold = true } inactive = { fg = "blue", bg = "gray" } # Separator sep_inner = { open = "", close = "" } sep_outer = { open = "", close = "" } # : }}} # : Mode {{{ [mode] normal_main = { bg = "blue", bold = true } normal_alt = { fg = "blue", bg = "gray" } # Select mode select_main = { bg = "red", bold = true } select_alt = { fg = "red", bg = "gray" } # Unset mode unset_main = { bg = "red", bold = true } unset_alt = { fg = "red", bg = "gray" } # : }}} # : Indicator of hovered file {{{ [indicator] parent = { reversed = true } current = { reversed = true } preview = { underline = true } padding = { open = "", close = "" } # : }}} # : Status bar {{{ [status] overall = {} sep_left = { open = "", close = "" } sep_right = { open = "", close = "" } # Permissions perm_sep = { fg = "darkgray" } perm_type = { fg = "green" } perm_read = { fg = "yellow" } perm_write = { fg = "red" } perm_exec = { fg = "cyan" } # Progress progress_label = { bold = true } progress_normal = { fg = "green", bg = "gray" } progress_error = { fg = "yellow", bg = "red" } # : }}} # : Which {{{ [which] cols = 3 mask = { bg = "black" } cand = { fg = "lightcyan" } rest = { fg = "darkgray" } desc = { fg = "lightmagenta" } separator = "  " separator_style = { fg = "darkgray" } # : }}} # : Confirmation {{{ [confirm] border = { fg = "blue" } title = { fg = "blue" } body = {} list = {} btn_yes = { reversed = true } btn_no = {} btn_labels = [ " [Y]es ", " (N)o " ] # : }}} # : Spotter {{{ [spot] border = { fg = "blue" } title = { fg = "blue" } # Table tbl_col = { fg = "blue" } tbl_cell = { fg = "yellow", reversed = true } # : }}} # : Notification {{{ [notify] title_info = { fg = "green" } title_warn = { fg = "yellow" } title_error = { fg = "red" } # Icons icon_info = "" icon_warn = "" icon_error = "" # : }}} # : Picker {{{ [pick] border = { fg = "blue" } active = { fg = "magenta", bold = true } inactive = {} # : }}} # : Input {{{ [input] border = { fg = "blue" } title = {} value = {} selected = { reversed = true } # : }}} # : Completion {{{ [cmp] border = { fg = "blue" } active = { reversed = true } inactive = {} # Icons icon_file = "" icon_folder = "" icon_command = "" # : }}} # : Task manager {{{ [tasks] border = { fg = "blue" } title = {} hovered = { fg = "magenta", bold = true } # : }}} # : Help menu {{{ [help] on = { fg = "cyan" } run = { fg = "magenta" } desc = {} hovered = { reversed = true, bold = true } footer = { fg = "black", bg = "white" } # : }}} # : File-specific styles {{{ [filetype] rules = [ # Image { mime = "image/*", fg = "yellow" }, # Media { mime = "{audio,video}/*", fg = "magenta" }, # Archive { mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", fg = "red" }, # Document { mime = "application/{pdf,doc,rtf}", fg = "cyan" }, # Virtual file system { mime = "vfs/{absent,stale}", fg = "darkgray" }, # Special file { url = "*", is = "orphan", bg = "red" }, { url = "*", is = "exec" , fg = "green" }, # Dummy file { url = "*", is = "dummy", bg = "red" }, { url = "*/", is = "dummy", bg = "red" }, # Fallback { url = "*/", fg = "blue" } ] # : }}} # : Icons {{{ [icon] globs = [] dirs = [ { name = ".config", text = "", fg = "#ff9800" }, { name = ".git", text = "", fg = "#009688" }, { name = ".github", text = "", fg = "#03a9f4" }, { name = ".npm", text = "", fg = "#03a9f4" }, { name = "Desktop", text = "", fg = "#009688" }, { name = "Development", text = "", fg = "#009688" }, { name = "Documents", text = "", fg = "#009688" }, { name = "Downloads", text = "", fg = "#009688" }, { name = "Library", text = "", fg = "#009688" }, { name = "Movies", text = "", fg = "#009688" }, { name = "Music", text = "", fg = "#009688" }, { name = "Pictures", text = "", fg = "#009688" }, { name = "Public", text = "", fg = "#009688" }, { name = "Videos", text = "", fg = "#009688" }, ] files = [ { name = ".babelrc", text = "", fg = "#666620" }, { name = ".bash_profile", text = "", fg = "#447028" }, { name = ".bashrc", text = "", fg = "#447028" }, { name = ".clang-format", text = "", fg = "#526064" }, { name = ".clang-tidy", text = "", fg = "#526064" }, { name = ".codespellrc", text = "󰓆", fg = "#239140" }, { name = ".condarc", text = "", fg = "#2d751c" }, { name = ".dockerignore", text = "󰡨", fg = "#2e5f99" }, { name = ".ds_store", text = "", fg = "#41535b" }, { name = ".editorconfig", text = "", fg = "#333030" }, { name = ".env", text = "", fg = "#32310d" }, { name = ".eslintignore", text = "", fg = "#4b32c3" }, { name = ".eslintrc", text = "", fg = "#4b32c3" }, { name = ".git-blame-ignore-revs", text = "", fg = "#b83a1d" }, { name = ".gitattributes", text = "", fg = "#b83a1d" }, { name = ".gitconfig", text = "", fg = "#b83a1d" }, { name = ".gitignore", text = "", fg = "#b83a1d" }, { name = ".gitlab-ci.yml", text = "", fg = "#aa321f" }, { name = ".gitmodules", text = "", fg = "#b83a1d" }, { name = ".gtkrc-2.0", text = "", fg = "#333333" }, { name = ".gvimrc", text = "", fg = "#017226" }, { name = ".justfile", text = "", fg = "#526064" }, { name = ".luacheckrc", text = "", fg = "#007abf" }, { name = ".luaurc", text = "", fg = "#007abf" }, { name = ".mailmap", text = "󰊢", fg = "#b83a1d" }, { name = ".nanorc", text = "", fg = "#440077" }, { name = ".npmignore", text = "", fg = "#ae1d38" }, { name = ".npmrc", text = "", fg = "#ae1d38" }, { name = ".nuxtrc", text = "󱄆", fg = "#00835f" }, { name = ".nvmrc", text = "", fg = "#3f6b34" }, { name = ".pnpmfile.cjs", text = "", fg = "#7c5601" }, { name = ".pre-commit-config.yaml", text = "󰛢", fg = "#7c5a12" }, { name = ".prettierignore", text = "", fg = "#3264b7" }, { name = ".prettierrc", text = "", fg = "#3264b7" }, { name = ".prettierrc.cjs", text = "", fg = "#3264b7" }, { name = ".prettierrc.js", text = "", fg = "#3264b7" }, { name = ".prettierrc.json", text = "", fg = "#3264b7" }, { name = ".prettierrc.json5", text = "", fg = "#3264b7" }, { name = ".prettierrc.mjs", text = "", fg = "#3264b7" }, { name = ".prettierrc.toml", text = "", fg = "#3264b7" }, { name = ".prettierrc.yaml", text = "", fg = "#3264b7" }, { name = ".prettierrc.yml", text = "", fg = "#3264b7" }, { name = ".pylintrc", text = "", fg = "#526064" }, { name = ".settings.json", text = "", fg = "#643995" }, { name = ".SRCINFO", text = "󰣇", fg = "#0b6f9e" }, { name = ".vimrc", text = "", fg = "#017226" }, { name = ".Xauthority", text = "", fg = "#ac3a12" }, { name = ".xinitrc", text = "", fg = "#ac3a12" }, { name = ".Xresources", text = "", fg = "#ac3a12" }, { name = ".xsession", text = "", fg = "#ac3a12" }, { name = ".zprofile", text = "", fg = "#447028" }, { name = ".zshenv", text = "", fg = "#447028" }, { name = ".zshrc", text = "", fg = "#447028" }, { name = "_gvimrc", text = "", fg = "#017226" }, { name = "_vimrc", text = "", fg = "#017226" }, { name = "AUTHORS", text = "", fg = "#6b4caa" }, { name = "AUTHORS.txt", text = "", fg = "#6b4caa" }, { name = "brewfile", text = "", fg = "#701516" }, { name = "bspwmrc", text = "", fg = "#2f2f2f" }, { name = "build", text = "", fg = "#447028" }, { name = "build.gradle", text = "", fg = "#005f87" }, { name = "build.zig.zon", text = "", fg = "#7b4d0e" }, { name = "bun.lock", text = "", fg = "#4e4946" }, { name = "bun.lockb", text = "", fg = "#4e4946" }, { name = "cantorrc", text = "", fg = "#1573b6" }, { name = "checkhealth", text = "󰓙", fg = "#3a5a7e" }, { name = "cmakelists.txt", text = "", fg = "#2c2d2f" }, { name = "code_of_conduct", text = "", fg = "#ab104a" }, { name = "code_of_conduct.md", text = "", fg = "#ab104a" }, { name = "commit_editmsg", text = "", fg = "#b83a1d" }, { name = "commitlint.config.js", text = "󰜘", fg = "#207067" }, { name = "commitlint.config.ts", text = "󰜘", fg = "#207067" }, { name = "compose.yaml", text = "󰡨", fg = "#2e5f99" }, { name = "compose.yml", text = "󰡨", fg = "#2e5f99" }, { name = "config", text = "", fg = "#526064" }, { name = "containerfile", text = "󰡨", fg = "#2e5f99" }, { name = "copying", text = "", fg = "#666620" }, { name = "copying.lesser", text = "", fg = "#666620" }, { name = "Directory.Build.props", text = "", fg = "#007abf" }, { name = "Directory.Build.targets", text = "", fg = "#007abf" }, { name = "Directory.Packages.props", text = "", fg = "#007abf" }, { name = "docker-compose.yaml", text = "󰡨", fg = "#2e5f99" }, { name = "docker-compose.yml", text = "󰡨", fg = "#2e5f99" }, { name = "dockerfile", text = "󰡨", fg = "#2e5f99" }, { name = "eslint.config.cjs", text = "", fg = "#4b32c3" }, { name = "eslint.config.js", text = "", fg = "#4b32c3" }, { name = "eslint.config.mjs", text = "", fg = "#4b32c3" }, { name = "eslint.config.ts", text = "", fg = "#4b32c3" }, { name = "ext_typoscript_setup.txt", text = "", fg = "#aa5a00" }, { name = "favicon.ico", text = "", fg = "#666620" }, { name = "fp-info-cache", text = "", fg = "#333333" }, { name = "fp-lib-table", text = "", fg = "#333333" }, { name = "FreeCAD.conf", text = "", fg = "#98262c" }, { name = "Gemfile", text = "", fg = "#701516" }, { name = "gnumakefile", text = "", fg = "#526064" }, { name = "go.mod", text = "", fg = "#0082a2" }, { name = "go.sum", text = "", fg = "#0082a2" }, { name = "go.work", text = "", fg = "#0082a2" }, { name = "gradle-wrapper.properties", text = "", fg = "#005f87" }, { name = "gradle.properties", text = "", fg = "#005f87" }, { name = "gradlew", text = "", fg = "#005f87" }, { name = "groovy", text = "", fg = "#384e5d" }, { name = "gruntfile.babel.js", text = "", fg = "#975122" }, { name = "gruntfile.coffee", text = "", fg = "#975122" }, { name = "gruntfile.js", text = "", fg = "#975122" }, { name = "gruntfile.ts", text = "", fg = "#975122" }, { name = "gtkrc", text = "", fg = "#333333" }, { name = "gulpfile.babel.js", text = "", fg = "#992e33" }, { name = "gulpfile.coffee", text = "", fg = "#992e33" }, { name = "gulpfile.js", text = "", fg = "#992e33" }, { name = "gulpfile.ts", text = "", fg = "#992e33" }, { name = "hypridle.conf", text = "", fg = "#008082" }, { name = "hyprland.conf", text = "", fg = "#008082" }, { name = "hyprlandd.conf", text = "", fg = "#008082" }, { name = "hyprlock.conf", text = "", fg = "#008082" }, { name = "hyprpaper.conf", text = "", fg = "#008082" }, { name = "hyprsunset.conf", text = "", fg = "#008082" }, { name = "i18n.config.js", text = "󰗊", fg = "#515987" }, { name = "i18n.config.ts", text = "󰗊", fg = "#515987" }, { name = "i3blocks.conf", text = "", fg = "#2e2f30" }, { name = "i3status.conf", text = "", fg = "#2e2f30" }, { name = "index.theme", text = "", fg = "#1e7b4a" }, { name = "ionic.config.json", text = "", fg = "#355fa5" }, { name = "Jenkinsfile", text = "", fg = "#9e2a26" }, { name = "justfile", text = "", fg = "#526064" }, { name = "kalgebrarc", text = "", fg = "#1573b6" }, { name = "kdeglobals", text = "", fg = "#1573b6" }, { name = "kdenlive-layoutsrc", text = "", fg = "#425c79" }, { name = "kdenliverc", text = "", fg = "#425c79" }, { name = "kritadisplayrc", text = "", fg = "#a12ea7" }, { name = "kritarc", text = "", fg = "#a12ea7" }, { name = "license", text = "", fg = "#686020" }, { name = "license.md", text = "", fg = "#686020" }, { name = "lxde-rc.xml", text = "", fg = "#606060" }, { name = "lxqt.conf", text = "", fg = "#016e9e" }, { name = "makefile", text = "", fg = "#526064" }, { name = "mix.lock", text = "", fg = "#6b4d83" }, { name = "mpv.conf", text = "", fg = "#3b1342" }, { name = "next.config.cjs", text = "", fg = "#333333" }, { name = "next.config.js", text = "", fg = "#333333" }, { name = "next.config.ts", text = "", fg = "#333333" }, { name = "node_modules", text = "", fg = "#ae1d38" }, { name = "nuxt.config.cjs", text = "󱄆", fg = "#00835f" }, { name = "nuxt.config.js", text = "󱄆", fg = "#00835f" }, { name = "nuxt.config.mjs", text = "󱄆", fg = "#00835f" }, { name = "nuxt.config.ts", text = "󱄆", fg = "#00835f" }, { name = "package-lock.json", text = "", fg = "#7a0d21" }, { name = "package.json", text = "", fg = "#ae1d38" }, { name = "PKGBUILD", text = "", fg = "#0b6f9e" }, { name = "platformio.ini", text = "", fg = "#a4571d" }, { name = "playwright.config.cjs", text = "", fg = "#238226" }, { name = "playwright.config.cts", text = "", fg = "#238226" }, { name = "playwright.config.js", text = "", fg = "#238226" }, { name = "playwright.config.mjs", text = "", fg = "#238226" }, { name = "playwright.config.mts", text = "", fg = "#238226" }, { name = "playwright.config.ts", text = "", fg = "#238226" }, { name = "pnpm-lock.yaml", text = "", fg = "#7c5601" }, { name = "pnpm-workspace.yaml", text = "", fg = "#7c5601" }, { name = "pom.xml", text = "", fg = "#7a0d21" }, { name = "prettier.config.cjs", text = "", fg = "#3264b7" }, { name = "prettier.config.js", text = "", fg = "#3264b7" }, { name = "prettier.config.mjs", text = "", fg = "#3264b7" }, { name = "prettier.config.ts", text = "", fg = "#3264b7" }, { name = "prisma.config.mts", text = "", fg = "#444da2" }, { name = "prisma.config.ts", text = "", fg = "#444da2" }, { name = "procfile", text = "", fg = "#6b4d83" }, { name = "PrusaSlicer.ini", text = "", fg = "#9d4717" }, { name = "PrusaSlicerGcodeViewer.ini", text = "", fg = "#9d4717" }, { name = "py.typed", text = "", fg = "#805e02" }, { name = "QtProject.conf", text = "", fg = "#2b8937" }, { name = "rakefile", text = "", fg = "#701516" }, { name = "readme", text = "󰂺", fg = "#2f2f2f" }, { name = "readme.md", text = "󰂺", fg = "#2f2f2f" }, { name = "rmd", text = "", fg = "#36677c" }, { name = "robots.txt", text = "󰚩", fg = "#465470" }, { name = "security", text = "󰒃", fg = "#3f4143" }, { name = "security.md", text = "󰒃", fg = "#3f4143" }, { name = "settings.gradle", text = "", fg = "#005f87" }, { name = "svelte.config.js", text = "", fg = "#bf2e00" }, { name = "sxhkdrc", text = "", fg = "#2f2f2f" }, { name = "sym-lib-table", text = "", fg = "#333333" }, { name = "tailwind.config.js", text = "󱏿", fg = "#158197" }, { name = "tailwind.config.mjs", text = "󱏿", fg = "#158197" }, { name = "tailwind.config.ts", text = "󱏿", fg = "#158197" }, { name = "tmux.conf", text = "", fg = "#0f8c13" }, { name = "tmux.conf.local", text = "", fg = "#0f8c13" }, { name = "tsconfig.json", text = "", fg = "#36677c" }, { name = "unlicense", text = "", fg = "#686020" }, { name = "vagrantfile", text = "", fg = "#104abf" }, { name = "vercel.json", text = "", fg = "#333333" }, { name = "vite.config.cjs", text = "", fg = "#805400" }, { name = "vite.config.cts", text = "", fg = "#805400" }, { name = "vite.config.js", text = "", fg = "#805400" }, { name = "vite.config.mjs", text = "", fg = "#805400" }, { name = "vite.config.mts", text = "", fg = "#805400" }, { name = "vite.config.ts", text = "", fg = "#805400" }, { name = "vitest.config.cjs", text = "", fg = "#4d6712" }, { name = "vitest.config.cts", text = "", fg = "#4d6712" }, { name = "vitest.config.js", text = "", fg = "#4d6712" }, { name = "vitest.config.mjs", text = "", fg = "#4d6712" }, { name = "vitest.config.mts", text = "", fg = "#4d6712" }, { name = "vitest.config.ts", text = "", fg = "#4d6712" }, { name = "vlcrc", text = "󰕼", fg = "#9f5100" }, { name = "webpack", text = "󰜫", fg = "#36677c" }, { name = "weston.ini", text = "", fg = "#805e00" }, { name = "workspace", text = "", fg = "#447028" }, { name = "wrangler.jsonc", text = "", fg = "#a35615" }, { name = "wrangler.toml", text = "", fg = "#a35615" }, { name = "xdph.conf", text = "", fg = "#008082" }, { name = "xmobarrc", text = "", fg = "#a9333e" }, { name = "xmobarrc.hs", text = "", fg = "#a9333e" }, { name = "xmonad.hs", text = "", fg = "#a9333e" }, { name = "xorg.conf", text = "", fg = "#ac3a12" }, { name = "xsettingsd.conf", text = "", fg = "#ac3a12" }, ] exts = [ { name = "3gp", text = "", fg = "#7e4c10" }, { name = "3mf", text = "󰆧", fg = "#5b5b5b" }, { name = "7z", text = "", fg = "#76520c" }, { name = "a", text = "", fg = "#494a47" }, { name = "aac", text = "", fg = "#0075aa" }, { name = "ada", text = "", fg = "#3b69aa" }, { name = "adb", text = "", fg = "#3b69aa" }, { name = "ads", text = "", fg = "#6b4d83" }, { name = "ai", text = "", fg = "#666620" }, { name = "aif", text = "", fg = "#0075aa" }, { name = "aiff", text = "", fg = "#0075aa" }, { name = "android", text = "", fg = "#277e3e" }, { name = "ape", text = "", fg = "#0075aa" }, { name = "apk", text = "", fg = "#277e3e" }, { name = "apl", text = "", fg = "#1b7936" }, { name = "app", text = "", fg = "#9f0500" }, { name = "applescript", text = "", fg = "#526064" }, { name = "asc", text = "󰦝", fg = "#41525f" }, { name = "asm", text = "", fg = "#006d8e" }, { name = "ass", text = "󰨖", fg = "#805c0a" }, { name = "astro", text = "", fg = "#aa2f4d" }, { name = "avif", text = "", fg = "#6b4d83" }, { name = "awk", text = "", fg = "#3a4446" }, { name = "azcli", text = "", fg = "#005a9f" }, { name = "bak", text = "󰁯", fg = "#526064" }, { name = "bash", text = "", fg = "#447028" }, { name = "bat", text = "", fg = "#40500f" }, { name = "bazel", text = "", fg = "#447028" }, { name = "bib", text = "󱉟", fg = "#666620" }, { name = "bicep", text = "", fg = "#36677c" }, { name = "bicepparam", text = "", fg = "#6a4d77" }, { name = "bin", text = "", fg = "#9f0500" }, { name = "blade.php", text = "", fg = "#a0372b" }, { name = "blend", text = "󰂫", fg = "#9c4f00" }, { name = "blp", text = "󰺾", fg = "#3a6497" }, { name = "bmp", text = "", fg = "#6b4d83" }, { name = "bqn", text = "", fg = "#1b7936" }, { name = "brep", text = "󰻫", fg = "#576342" }, { name = "bz", text = "", fg = "#76520c" }, { name = "bz2", text = "", fg = "#76520c" }, { name = "bz3", text = "", fg = "#76520c" }, { name = "bzl", text = "", fg = "#447028" }, { name = "c", text = "", fg = "#3b69aa" }, { name = "c++", text = "", fg = "#a23253" }, { name = "cache", text = "", fg = "#333333" }, { name = "cast", text = "", fg = "#7e4c10" }, { name = "cbl", text = "", fg = "#005ca5" }, { name = "cc", text = "", fg = "#a23253" }, { name = "ccm", text = "", fg = "#a23253" }, { name = "cfc", text = "", fg = "#017b8c" }, { name = "cfg", text = "", fg = "#526064" }, { name = "cfm", text = "", fg = "#017b8c" }, { name = "cjs", text = "", fg = "#666620" }, { name = "clj", text = "", fg = "#466024" }, { name = "cljc", text = "", fg = "#466024" }, { name = "cljd", text = "", fg = "#36677c" }, { name = "cljs", text = "", fg = "#36677c" }, { name = "cmake", text = "", fg = "#2c2d2f" }, { name = "cob", text = "", fg = "#005ca5" }, { name = "cobol", text = "", fg = "#005ca5" }, { name = "coffee", text = "", fg = "#666620" }, { name = "conda", text = "", fg = "#2d751c" }, { name = "conf", text = "", fg = "#526064" }, { name = "config.ru", text = "", fg = "#701516" }, { name = "cow", text = "󰆚", fg = "#70421b" }, { name = "cp", text = "", fg = "#36677c" }, { name = "cpp", text = "", fg = "#36677c" }, { name = "cppm", text = "", fg = "#36677c" }, { name = "cpy", text = "", fg = "#005ca5" }, { name = "cr", text = "", fg = "#434343" }, { name = "crdownload", text = "", fg = "#226654" }, { name = "cs", text = "󰌛", fg = "#434d04" }, { name = "csh", text = "", fg = "#3a4446" }, { name = "cshtml", text = "󱦗", fg = "#512bd4" }, { name = "cson", text = "", fg = "#666620" }, { name = "csproj", text = "󰪮", fg = "#512bd4" }, { name = "css", text = "", fg = "#663399" }, { name = "csv", text = "", fg = "#447028" }, { name = "cts", text = "", fg = "#36677c" }, { name = "cu", text = "", fg = "#447028" }, { name = "cue", text = "󰲹", fg = "#764a57" }, { name = "cuh", text = "", fg = "#6b4d83" }, { name = "cxx", text = "", fg = "#36677c" }, { name = "cxxm", text = "", fg = "#36677c" }, { name = "d", text = "", fg = "#842b25" }, { name = "d.ts", text = "", fg = "#6a4c2a" }, { name = "dart", text = "", fg = "#03589c" }, { name = "db", text = "", fg = "#494848" }, { name = "dconf", text = "", fg = "#333333" }, { name = "desktop", text = "", fg = "#563d7c" }, { name = "diff", text = "", fg = "#41535b" }, { name = "dll", text = "", fg = "#4d2c0b" }, { name = "doc", text = "󰈬", fg = "#185abd" }, { name = "Dockerfile", text = "󰡨", fg = "#2e5f99" }, { name = "dockerignore", text = "󰡨", fg = "#2e5f99" }, { name = "docx", text = "󰈬", fg = "#185abd" }, { name = "dot", text = "󱁉", fg = "#244a6a" }, { name = "download", text = "", fg = "#226654" }, { name = "drl", text = "", fg = "#553a3a" }, { name = "dropbox", text = "", fg = "#0049be" }, { name = "dump", text = "", fg = "#494848" }, { name = "dwg", text = "󰻫", fg = "#576342" }, { name = "dxf", text = "󰻫", fg = "#576342" }, { name = "ebook", text = "", fg = "#755836" }, { name = "ebuild", text = "", fg = "#4c416e" }, { name = "edn", text = "", fg = "#36677c" }, { name = "eex", text = "", fg = "#6b4d83" }, { name = "ejs", text = "", fg = "#666620" }, { name = "el", text = "", fg = "#61568e" }, { name = "elc", text = "", fg = "#61568e" }, { name = "elf", text = "", fg = "#9f0500" }, { name = "elm", text = "", fg = "#36677c" }, { name = "eln", text = "", fg = "#61568e" }, { name = "env", text = "", fg = "#32310d" }, { name = "eot", text = "", fg = "#2f2f2f" }, { name = "epp", text = "", fg = "#80530d" }, { name = "epub", text = "", fg = "#755836" }, { name = "erb", text = "", fg = "#701516" }, { name = "erl", text = "", fg = "#8a2b72" }, { name = "ex", text = "", fg = "#6b4d83" }, { name = "exe", text = "", fg = "#9f0500" }, { name = "exs", text = "", fg = "#6b4d83" }, { name = "f#", text = "", fg = "#36677c" }, { name = "f3d", text = "󰻫", fg = "#576342" }, { name = "f90", text = "󱈚", fg = "#563b70" }, { name = "fbx", text = "󰆧", fg = "#5b5b5b" }, { name = "fcbak", text = "", fg = "#98262c" }, { name = "fcmacro", text = "", fg = "#98262c" }, { name = "fcmat", text = "", fg = "#98262c" }, { name = "fcparam", text = "", fg = "#98262c" }, { name = "fcscript", text = "", fg = "#98262c" }, { name = "fcstd", text = "", fg = "#98262c" }, { name = "fcstd1", text = "", fg = "#98262c" }, { name = "fctb", text = "", fg = "#98262c" }, { name = "fctl", text = "", fg = "#98262c" }, { name = "fdmdownload", text = "", fg = "#226654" }, { name = "feature", text = "", fg = "#007e12" }, { name = "fish", text = "", fg = "#3a4446" }, { name = "flac", text = "", fg = "#005880" }, { name = "flc", text = "", fg = "#2f2f2f" }, { name = "flf", text = "", fg = "#2f2f2f" }, { name = "fnl", text = "", fg = "#33312b" }, { name = "fodg", text = "", fg = "#333211" }, { name = "fodp", text = "", fg = "#7f4e22" }, { name = "fods", text = "", fg = "#28541a" }, { name = "fodt", text = "", fg = "#16667e" }, { name = "frag", text = "", fg = "#40647c" }, { name = "fs", text = "", fg = "#36677c" }, { name = "fsi", text = "", fg = "#36677c" }, { name = "fsscript", text = "", fg = "#36677c" }, { name = "fsx", text = "", fg = "#36677c" }, { name = "gcode", text = "󰐫", fg = "#0f5582" }, { name = "gd", text = "", fg = "#526064" }, { name = "gemspec", text = "", fg = "#701516" }, { name = "geom", text = "", fg = "#40647c" }, { name = "gif", text = "", fg = "#6b4d83" }, { name = "git", text = "", fg = "#b5391e" }, { name = "glb", text = "", fg = "#80581e" }, { name = "gleam", text = "", fg = "#553a51" }, { name = "glsl", text = "", fg = "#40647c" }, { name = "gnumakefile", text = "", fg = "#526064" }, { name = "go", text = "", fg = "#0082a2" }, { name = "godot", text = "", fg = "#526064" }, { name = "gpr", text = "", fg = "#526064" }, { name = "gql", text = "", fg = "#ac2880" }, { name = "gradle", text = "", fg = "#005f87" }, { name = "graphql", text = "", fg = "#ac2880" }, { name = "gresource", text = "", fg = "#333333" }, { name = "gv", text = "󱁉", fg = "#244a6a" }, { name = "gz", text = "", fg = "#76520c" }, { name = "h", text = "", fg = "#6b4d83" }, { name = "haml", text = "", fg = "#2f2f2d" }, { name = "hbs", text = "", fg = "#a04f1d" }, { name = "heex", text = "", fg = "#6b4d83" }, { name = "hex", text = "", fg = "#224abf" }, { name = "hh", text = "", fg = "#6b4d83" }, { name = "hpp", text = "", fg = "#6b4d83" }, { name = "hrl", text = "", fg = "#8a2b72" }, { name = "hs", text = "", fg = "#6b4d83" }, { name = "htm", text = "", fg = "#aa391c" }, { name = "html", text = "", fg = "#ab3a1c" }, { name = "http", text = "", fg = "#006a95" }, { name = "huff", text = "󰡘", fg = "#4242c7" }, { name = "hurl", text = "", fg = "#bf0266" }, { name = "hx", text = "", fg = "#9c5715" }, { name = "hxx", text = "", fg = "#6b4d83" }, { name = "ical", text = "", fg = "#2b2e83" }, { name = "icalendar", text = "", fg = "#2b2e83" }, { name = "ico", text = "", fg = "#666620" }, { name = "ics", text = "", fg = "#2b2e83" }, { name = "ifb", text = "", fg = "#2b2e83" }, { name = "ifc", text = "󰻫", fg = "#576342" }, { name = "ige", text = "󰻫", fg = "#576342" }, { name = "iges", text = "󰻫", fg = "#576342" }, { name = "igs", text = "󰻫", fg = "#576342" }, { name = "image", text = "", fg = "#453f43" }, { name = "img", text = "", fg = "#453f43" }, { name = "import", text = "", fg = "#2f2f2f" }, { name = "info", text = "", fg = "#333329" }, { name = "ini", text = "", fg = "#526064" }, { name = "ino", text = "", fg = "#397981" }, { name = "ipynb", text = "", fg = "#a35301" }, { name = "iso", text = "", fg = "#453f43" }, { name = "ixx", text = "", fg = "#36677c" }, { name = "jar", text = "", fg = "#805834" }, { name = "java", text = "", fg = "#992e33" }, { name = "jl", text = "", fg = "#6c4b7c" }, { name = "jpeg", text = "", fg = "#6b4d83" }, { name = "jpg", text = "", fg = "#6b4d83" }, { name = "js", text = "", fg = "#666620" }, { name = "json", text = "", fg = "#666620" }, { name = "json5", text = "", fg = "#666620" }, { name = "jsonc", text = "", fg = "#666620" }, { name = "jsx", text = "", fg = "#158197" }, { name = "jwmrc", text = "", fg = "#005a9a" }, { name = "jxl", text = "", fg = "#6b4d83" }, { name = "kbx", text = "󰯄", fg = "#565856" }, { name = "kdb", text = "", fg = "#3e7427" }, { name = "kdbx", text = "", fg = "#3e7427" }, { name = "kdenlive", text = "", fg = "#425c79" }, { name = "kdenlivetitle", text = "", fg = "#425c79" }, { name = "kicad_dru", text = "", fg = "#333333" }, { name = "kicad_mod", text = "", fg = "#333333" }, { name = "kicad_pcb", text = "", fg = "#333333" }, { name = "kicad_prl", text = "", fg = "#333333" }, { name = "kicad_pro", text = "", fg = "#333333" }, { name = "kicad_sch", text = "", fg = "#333333" }, { name = "kicad_sym", text = "", fg = "#333333" }, { name = "kicad_wks", text = "", fg = "#333333" }, { name = "ko", text = "", fg = "#494a47" }, { name = "kpp", text = "", fg = "#a12ea7" }, { name = "kra", text = "", fg = "#a12ea7" }, { name = "krz", text = "", fg = "#a12ea7" }, { name = "ksh", text = "", fg = "#3a4446" }, { name = "kt", text = "", fg = "#5f3ebf" }, { name = "kts", text = "", fg = "#5f3ebf" }, { name = "lck", text = "", fg = "#5e5e5e" }, { name = "leex", text = "", fg = "#6b4d83" }, { name = "less", text = "", fg = "#563d7c" }, { name = "lff", text = "", fg = "#2f2f2f" }, { name = "lhs", text = "", fg = "#6b4d83" }, { name = "lib", text = "", fg = "#4d2c0b" }, { name = "license", text = "", fg = "#666620" }, { name = "liquid", text = "", fg = "#4a6024" }, { name = "lock", text = "", fg = "#5e5e5e" }, { name = "log", text = "󰌱", fg = "#4a4a4a" }, { name = "lrc", text = "󰨖", fg = "#805c0a" }, { name = "lua", text = "", fg = "#366b8a" }, { name = "luac", text = "", fg = "#366b8a" }, { name = "luau", text = "", fg = "#007abf" }, { name = "m", text = "", fg = "#3b69aa" }, { name = "m3u", text = "󰲹", fg = "#764a57" }, { name = "m3u8", text = "󰲹", fg = "#764a57" }, { name = "m4a", text = "", fg = "#0075aa" }, { name = "m4v", text = "", fg = "#7e4c10" }, { name = "magnet", text = "", fg = "#a51b16" }, { name = "makefile", text = "", fg = "#526064" }, { name = "markdown", text = "", fg = "#4a4a4a" }, { name = "material", text = "", fg = "#8a2b72" }, { name = "md", text = "", fg = "#4a4a4a" }, { name = "md5", text = "󰕥", fg = "#5d5975" }, { name = "mdx", text = "", fg = "#36677c" }, { name = "mint", text = "󰌪", fg = "#44604a" }, { name = "mjs", text = "", fg = "#504b1e" }, { name = "mk", text = "", fg = "#526064" }, { name = "mkv", text = "", fg = "#7e4c10" }, { name = "ml", text = "", fg = "#975122" }, { name = "mli", text = "", fg = "#975122" }, { name = "mm", text = "", fg = "#36677c" }, { name = "mo", text = "", fg = "#654ca7" }, { name = "mobi", text = "", fg = "#755836" }, { name = "mojo", text = "", fg = "#bf3917" }, { name = "mov", text = "", fg = "#7e4c10" }, { name = "mp3", text = "", fg = "#0075aa" }, { name = "mp4", text = "", fg = "#7e4c10" }, { name = "mpp", text = "", fg = "#36677c" }, { name = "msf", text = "", fg = "#0e5ca9" }, { name = "mts", text = "", fg = "#36677c" }, { name = "mustache", text = "", fg = "#975122" }, { name = "nfo", text = "", fg = "#333329" }, { name = "nim", text = "", fg = "#514700" }, { name = "nix", text = "", fg = "#3f5d72" }, { name = "norg", text = "", fg = "#365a8e" }, { name = "nswag", text = "", fg = "#427516" }, { name = "nu", text = "", fg = "#276f4e" }, { name = "o", text = "", fg = "#9f0500" }, { name = "obj", text = "󰆧", fg = "#5b5b5b" }, { name = "odf", text = "", fg = "#aa3c64" }, { name = "odg", text = "", fg = "#333211" }, { name = "odin", text = "󰟢", fg = "#2a629e" }, { name = "odp", text = "", fg = "#7f4e22" }, { name = "ods", text = "", fg = "#28541a" }, { name = "odt", text = "", fg = "#16667e" }, { name = "oga", text = "", fg = "#005880" }, { name = "ogg", text = "", fg = "#005880" }, { name = "ogv", text = "", fg = "#7e4c10" }, { name = "ogx", text = "", fg = "#7e4c10" }, { name = "opus", text = "", fg = "#005880" }, { name = "org", text = "", fg = "#4f7166" }, { name = "otf", text = "", fg = "#2f2f2f" }, { name = "out", text = "", fg = "#9f0500" }, { name = "part", text = "", fg = "#226654" }, { name = "patch", text = "", fg = "#41535b" }, { name = "pck", text = "", fg = "#526064" }, { name = "pcm", text = "", fg = "#005880" }, { name = "pdf", text = "", fg = "#b30b00" }, { name = "php", text = "", fg = "#6b4d83" }, { name = "pl", text = "", fg = "#36677c" }, { name = "pls", text = "󰲹", fg = "#764a57" }, { name = "ply", text = "󰆧", fg = "#5b5b5b" }, { name = "pm", text = "", fg = "#36677c" }, { name = "png", text = "", fg = "#6b4d83" }, { name = "po", text = "", fg = "#1c708e" }, { name = "pot", text = "", fg = "#1c708e" }, { name = "pp", text = "", fg = "#80530d" }, { name = "ppt", text = "󰈧", fg = "#983826" }, { name = "pptx", text = "󰈧", fg = "#983826" }, { name = "prisma", text = "", fg = "#444da2" }, { name = "pro", text = "", fg = "#725c2a" }, { name = "ps1", text = "󰨊", fg = "#325698" }, { name = "psb", text = "", fg = "#36677c" }, { name = "psd", text = "", fg = "#36677c" }, { name = "psd1", text = "󰨊", fg = "#4f5893" }, { name = "psm1", text = "󰨊", fg = "#4f5893" }, { name = "pub", text = "󰷖", fg = "#4c422f" }, { name = "pxd", text = "", fg = "#3c6f98" }, { name = "pxi", text = "", fg = "#3c6f98" }, { name = "py", text = "", fg = "#805e02" }, { name = "pyc", text = "", fg = "#332d1d" }, { name = "pyd", text = "", fg = "#332d1d" }, { name = "pyi", text = "", fg = "#805e02" }, { name = "pyo", text = "", fg = "#332d1d" }, { name = "pyw", text = "", fg = "#3c6f98" }, { name = "pyx", text = "", fg = "#3c6f98" }, { name = "qm", text = "", fg = "#1c708e" }, { name = "qml", text = "", fg = "#2b8937" }, { name = "qrc", text = "", fg = "#2b8937" }, { name = "qss", text = "", fg = "#2b8937" }, { name = "query", text = "", fg = "#607035" }, { name = "R", text = "󰟔", fg = "#1a4c8c" }, { name = "r", text = "󰟔", fg = "#1a4c8c" }, { name = "rake", text = "", fg = "#701516" }, { name = "rar", text = "", fg = "#76520c" }, { name = "rasi", text = "", fg = "#666620" }, { name = "razor", text = "󱦘", fg = "#512bd4" }, { name = "rb", text = "", fg = "#701516" }, { name = "res", text = "", fg = "#992e33" }, { name = "resi", text = "", fg = "#a33759" }, { name = "rlib", text = "", fg = "#6f5242" }, { name = "rmd", text = "", fg = "#36677c" }, { name = "rproj", text = "󰗆", fg = "#286844" }, { name = "rs", text = "", fg = "#6f5242" }, { name = "rss", text = "", fg = "#7e4e1e" }, { name = "s", text = "", fg = "#005594" }, { name = "sass", text = "", fg = "#a33759" }, { name = "sbt", text = "", fg = "#992e33" }, { name = "sc", text = "", fg = "#992e33" }, { name = "scad", text = "", fg = "#53480f" }, { name = "scala", text = "", fg = "#992e33" }, { name = "scm", text = "󰘧", fg = "#303030" }, { name = "scss", text = "", fg = "#a33759" }, { name = "sh", text = "", fg = "#3a4446" }, { name = "sha1", text = "󰕥", fg = "#5d5975" }, { name = "sha224", text = "󰕥", fg = "#5d5975" }, { name = "sha256", text = "󰕥", fg = "#5d5975" }, { name = "sha384", text = "󰕥", fg = "#5d5975" }, { name = "sha512", text = "󰕥", fg = "#5d5975" }, { name = "sig", text = "󰘧", fg = "#975122" }, { name = "signature", text = "󰘧", fg = "#975122" }, { name = "skp", text = "󰻫", fg = "#576342" }, { name = "sldasm", text = "󰻫", fg = "#576342" }, { name = "sldprt", text = "󰻫", fg = "#576342" }, { name = "slim", text = "", fg = "#aa391c" }, { name = "sln", text = "", fg = "#643995" }, { name = "slnx", text = "", fg = "#643995" }, { name = "slvs", text = "󰻫", fg = "#576342" }, { name = "sml", text = "󰘧", fg = "#975122" }, { name = "so", text = "", fg = "#494a47" }, { name = "sol", text = "", fg = "#36677c" }, { name = "spec.js", text = "", fg = "#666620" }, { name = "spec.jsx", text = "", fg = "#158197" }, { name = "spec.ts", text = "", fg = "#36677c" }, { name = "spec.tsx", text = "", fg = "#1354bf" }, { name = "spx", text = "", fg = "#005880" }, { name = "sql", text = "", fg = "#494848" }, { name = "sqlite", text = "", fg = "#494848" }, { name = "sqlite3", text = "", fg = "#494848" }, { name = "srt", text = "󰨖", fg = "#805c0a" }, { name = "ssa", text = "󰨖", fg = "#805c0a" }, { name = "ste", text = "󰻫", fg = "#576342" }, { name = "step", text = "󰻫", fg = "#576342" }, { name = "stl", text = "󰆧", fg = "#5b5b5b" }, { name = "stories.js", text = "", fg = "#aa2f59" }, { name = "stories.jsx", text = "", fg = "#aa2f59" }, { name = "stories.mjs", text = "", fg = "#aa2f59" }, { name = "stories.svelte", text = "", fg = "#aa2f59" }, { name = "stories.ts", text = "", fg = "#aa2f59" }, { name = "stories.tsx", text = "", fg = "#aa2f59" }, { name = "stories.vue", text = "", fg = "#aa2f59" }, { name = "stp", text = "󰻫", fg = "#576342" }, { name = "strings", text = "", fg = "#1c708e" }, { name = "styl", text = "", fg = "#466024" }, { name = "sub", text = "󰨖", fg = "#805c0a" }, { name = "sublime", text = "", fg = "#975122" }, { name = "suo", text = "", fg = "#643995" }, { name = "sv", text = "󰍛", fg = "#017226" }, { name = "svelte", text = "", fg = "#bf2e00" }, { name = "svg", text = "󰜡", fg = "#80581e" }, { name = "svgz", text = "󰜡", fg = "#80581e" }, { name = "svh", text = "󰍛", fg = "#017226" }, { name = "swift", text = "", fg = "#975122" }, { name = "t", text = "", fg = "#36677c" }, { name = "tbc", text = "󰛓", fg = "#1e5cb3" }, { name = "tcl", text = "󰛓", fg = "#1e5cb3" }, { name = "templ", text = "", fg = "#6e5e18" }, { name = "terminal", text = "", fg = "#217929" }, { name = "test.js", text = "", fg = "#666620" }, { name = "test.jsx", text = "", fg = "#158197" }, { name = "test.ts", text = "", fg = "#36677c" }, { name = "test.tsx", text = "", fg = "#1354bf" }, { name = "tex", text = "", fg = "#3d6117" }, { name = "tf", text = "", fg = "#4732af" }, { name = "tfvars", text = "", fg = "#4732af" }, { name = "tgz", text = "", fg = "#76520c" }, { name = "tmpl", text = "", fg = "#6e5e18" }, { name = "tmux", text = "", fg = "#0f8c13" }, { name = "toml", text = "", fg = "#753219" }, { name = "torrent", text = "", fg = "#226654" }, { name = "tres", text = "", fg = "#526064" }, { name = "ts", text = "", fg = "#36677c" }, { name = "tscn", text = "", fg = "#526064" }, { name = "tsconfig", text = "", fg = "#aa5a00" }, { name = "tsx", text = "", fg = "#1354bf" }, { name = "ttf", text = "", fg = "#2f2f2f" }, { name = "twig", text = "", fg = "#466024" }, { name = "txt", text = "󰈙", fg = "#447028" }, { name = "txz", text = "", fg = "#76520c" }, { name = "typ", text = "", fg = "#097d80" }, { name = "typoscript", text = "", fg = "#aa5a00" }, { name = "ui", text = "", fg = "#015bf0" }, { name = "v", text = "󰍛", fg = "#017226" }, { name = "vala", text = "", fg = "#5c2e8b" }, { name = "vert", text = "", fg = "#40647c" }, { name = "vh", text = "󰍛", fg = "#017226" }, { name = "vhd", text = "󰍛", fg = "#017226" }, { name = "vhdl", text = "󰍛", fg = "#017226" }, { name = "vi", text = "", fg = "#554203" }, { name = "vim", text = "", fg = "#017226" }, { name = "vsh", text = "", fg = "#3e5a7f" }, { name = "vsix", text = "", fg = "#643995" }, { name = "vue", text = "", fg = "#466024" }, { name = "wasm", text = "", fg = "#4539a4" }, { name = "wav", text = "", fg = "#0075aa" }, { name = "webm", text = "", fg = "#7e4c10" }, { name = "webmanifest", text = "", fg = "#504b1e" }, { name = "webp", text = "", fg = "#6b4d83" }, { name = "webpack", text = "󰜫", fg = "#36677c" }, { name = "wma", text = "", fg = "#0075aa" }, { name = "wmv", text = "", fg = "#7e4c10" }, { name = "woff", text = "", fg = "#2f2f2f" }, { name = "woff2", text = "", fg = "#2f2f2f" }, { name = "wrl", text = "󰆧", fg = "#5b5b5b" }, { name = "wrz", text = "󰆧", fg = "#5b5b5b" }, { name = "wv", text = "", fg = "#0075aa" }, { name = "wvc", text = "", fg = "#0075aa" }, { name = "x", text = "", fg = "#3b69aa" }, { name = "xaml", text = "󰙳", fg = "#512bd4" }, { name = "xcf", text = "", fg = "#4a4434" }, { name = "xcplayground", text = "", fg = "#975122" }, { name = "xcstrings", text = "", fg = "#1c708e" }, { name = "xls", text = "󰈛", fg = "#207245" }, { name = "xlsx", text = "󰈛", fg = "#207245" }, { name = "xm", text = "", fg = "#36677c" }, { name = "xml", text = "󰗀", fg = "#975122" }, { name = "xpi", text = "", fg = "#bf1401" }, { name = "xslt", text = "󰗀", fg = "#227193" }, { name = "xul", text = "", fg = "#975122" }, { name = "xz", text = "", fg = "#76520c" }, { name = "yaml", text = "", fg = "#526064" }, { name = "yml", text = "", fg = "#526064" }, { name = "zig", text = "", fg = "#7b4d0e" }, { name = "zip", text = "", fg = "#76520c" }, { name = "zsh", text = "", fg = "#447028" }, { name = "zst", text = "", fg = "#76520c" }, { name = "🔥", text = "", fg = "#bf3917" }, ] conds = [ # Special files { if = "orphan", text = "", fg = "#000000" }, { if = "link", text = "", fg = "#9e9e9e" }, { if = "block", text = "", fg = "#ffc107" }, { if = "char", text = "", fg = "#ffc107" }, { if = "fifo", text = "", fg = "#ffc107" }, { if = "sock", text = "", fg = "#ffc107" }, { if = "sticky", text = "", fg = "#ffc107" }, { if = "dummy", text = "", fg = "#f44336" }, # Fallback { if = "dir & hovered", text = "", fg = "#03a9f4" }, { if = "dir", text = "", fg = "#03a9f4" }, { if = "exec", text = "", fg = "#8bc34a" }, { if = "!dir", text = "", fg = "#000000" }, ] # : }}} ================================================ FILE: yazi-config/preset/vfs-default.toml ================================================ [services] ================================================ FILE: yazi-config/preset/yazi-default.toml ================================================ # A TOML linter such as https://taplo.tamasfe.dev/ can use this schema to validate your config. # If you encounter any issues, please make an issue at https://github.com/yazi-rs/schemas. "$schema" = "https://yazi-rs.github.io/schemas/yazi.json" [mgr] ratio = [ 1, 4, 3 ] sort_by = "alphabetical" sort_sensitive = false sort_reverse = false sort_dir_first = true sort_translit = false sort_fallback = "alphabetical" linemode = "none" show_hidden = false show_symlink = true scrolloff = 5 mouse_events = [ "click", "scroll" ] [preview] wrap = "no" tab_size = 2 max_width = 600 max_height = 900 cache_dir = "" image_delay = 30 image_filter = "triangle" image_quality = 75 ueberzug_scale = 1 ueberzug_offset = [ 0, 0, 0, 0 ] [opener] edit = [ { run = "${EDITOR:-vi} %s", desc = "$EDITOR", for = "unix", block = true }, { run = "code %s", desc = "code", for = "windows", orphan = true }, { run = "code -w %s", desc = "code (block)", for = "windows", block = true }, ] play = [ { run = "xdg-open %s1", desc = "Play", for = "linux", orphan = true }, { run = "open %s", desc = "Play", for = "macos" }, { run = 'start "" %s1', desc = "Play", for = "windows", orphan = true }, { run = "termux-open %s1", desc = "Play", for = "android" }, { run = "mediainfo %s1; echo 'Press enter to exit'; read _", block = true, desc = "Show media info", for = "unix" }, { run = "mediainfo %s1 & pause", block = true, desc = "Show media info", for = "windows" }, ] open = [ { run = "xdg-open %s1", desc = "Open", for = "linux" }, { run = "open %s", desc = "Open", for = "macos" }, { run = 'start "" %s1', desc = "Open", for = "windows", orphan = true }, { run = "termux-open %s1", desc = "Open", for = "android" }, ] reveal = [ { run = "xdg-open %d1", desc = "Reveal", for = "linux" }, { run = "open -R %s1", desc = "Reveal", for = "macos" }, { run = "explorer /select,%s1", desc = "Reveal", for = "windows", orphan = true }, { run = "termux-open %d1", desc = "Reveal", for = "android" }, { run = "clear; exiftool %s1; echo 'Press enter to exit'; read _", desc = "Show EXIF", for = "unix", block = true }, ] extract = [ { run = "ya pub extract --list %s", desc = "Extract here" }, ] download = [ { run = "ya emit download --open %S", desc = "Download and open" }, { run = "ya emit download %S", desc = "Download" }, ] [open] rules = [ # Folder { url = "*/", use = [ "edit", "open", "reveal" ] }, # Text { mime = "text/*", use = [ "edit", "reveal" ] }, # Image { mime = "image/*", use = [ "open", "reveal" ] }, # Media { mime = "{audio,video}/*", use = [ "play", "reveal" ] }, # Code { mime = "application/{json,ndjson,javascript,wine-extension-ini}", use = [ "edit", "reveal" ] }, # Archive { mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", use = [ "extract", "reveal" ] }, # Empty file { mime = "inode/empty", use = [ "edit", "reveal" ] }, # Virtual file system { mime = "vfs/{absent,stale}", use = "download" }, # Fallback { url = "*", use = [ "open", "reveal" ] }, ] [tasks] file_workers = 3 plugin_workers = 5 fetch_workers = 5 preload_workers = 2 process_workers = 5 bizarre_retry = 3 image_alloc = 536870912 # 512MB image_bound = [ 10000, 10000 ] suppress_preload = false [plugin] fetchers = [ # Mimetype { id = "mime", url = "*/", run = "mime.dir", prio = "high" }, { id = "mime", url = "local://*", run = "mime.local", prio = "high" }, { id = "mime", url = "remote://*", run = "mime.remote", prio = "high" }, ] spotters = [ # Multi-file { mime = "multi/*", run = "multi" }, # Folder { url = "*/", run = "folder" }, # Code { mime = "text/*", run = "code" }, { mime = "application/{mbox,javascript,wine-extension-ini}", run = "code" }, # Image { mime = "image/{avif,hei?,jxl}", run = "magick" }, { mime = "image/svg+xml", run = "svg" }, { mime = "image/*", run = "image" }, # Video { mime = "video/*", run = "video" }, # Virtual file system { mime = "vfs/*", run = "vfs" }, # Error { mime = "null/*", run = "null" }, # Fallback { url = "*", run = "file" }, ] preloaders = [ # Image { mime = "image/{avif,hei?,jxl}", run = "magick" }, { mime = "image/svg+xml", run = "svg" }, { mime = "image/*", run = "image" }, # Video { mime = "video/*", run = "video" }, # PDF { mime = "application/pdf", run = "pdf" }, # Font { mime = "font/*", run = "font" }, { mime = "application/ms-opentype", run = "font" }, ] previewers = [ { url = "*/", run = "folder" }, # Code { mime = "text/*", run = "code" }, { mime = "application/{mbox,javascript,wine-extension-ini}", run = "code" }, # JSON { mime = "application/{json,ndjson}", run = "json" }, # Image { mime = "image/{avif,hei?,jxl}", run = "magick" }, { mime = "image/svg+xml", run = "svg" }, { mime = "image/*", run = "image" }, # Video { mime = "video/*", run = "video" }, # PDF { mime = "application/pdf", run = "pdf" }, # Archive { mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", run = "archive" }, { mime = "application/{debian*-package,redhat-package-manager,rpm,android.package-archive}", run = "archive" }, { url = "*.{AppImage,appimage}", run = "archive" }, # Virtual Disk / Disk Image { mime = "application/{iso9660-image,qemu-disk,ms-wim,apple-diskimage}", run = "archive" }, { mime = "application/virtualbox-{vhd,vhdx}", run = "archive" }, { url = "*.{img,fat,ext,ext2,ext3,ext4,squashfs,ntfs,hfs,hfsx}", run = "archive" }, # Font { mime = "font/*", run = "font" }, { mime = "application/ms-opentype", run = "font" }, # Empty file { mime = "inode/empty", run = "empty" }, # Virtual file system { mime = "vfs/*", run = "vfs" }, # Error { mime = "null/*", run = "null" }, # Fallback { url = "*", run = "file" }, ] [input] cursor_blink = false # cd cd_title = "Change directory:" cd_origin = "top-center" cd_offset = [ 0, 2, 50, 3 ] # create create_title = [ "Create:", "Create (dir):" ] create_origin = "top-center" create_offset = [ 0, 2, 50, 3 ] # rename rename_title = "Rename:" rename_origin = "hovered" rename_offset = [ 0, 1, 50, 3 ] # filter filter_title = "Filter:" filter_origin = "top-center" filter_offset = [ 0, 2, 50, 3 ] # find find_title = [ "Find next:", "Find previous:" ] find_origin = "top-center" find_offset = [ 0, 2, 50, 3 ] # search search_title = "Search via {n}:" search_origin = "top-center" search_offset = [ 0, 2, 50, 3 ] # shell shell_title = [ "Shell:", "Shell (block):" ] shell_origin = "top-center" shell_offset = [ 0, 2, 50, 3 ] [confirm] # trash trash_title = "Trash {n} selected file{s}?" trash_origin = "center" trash_offset = [ 0, 0, 70, 20 ] # delete delete_title = "Permanently delete {n} selected file{s}?" delete_origin = "center" delete_offset = [ 0, 0, 70, 20 ] # overwrite overwrite_title = "Overwrite file?" overwrite_body = "Will overwrite the following file:" overwrite_origin = "center" overwrite_offset = [ 0, 0, 50, 15 ] # quit quit_title = "Quit?" quit_body = "There are unfinished tasks, quit anyway?\n(Open task manager with default key 'w')" quit_origin = "center" quit_offset = [ 0, 0, 50, 15 ] [pick] open_title = "Open with:" open_origin = "hovered" open_offset = [ 0, 1, 50, 7 ] [which] sort_by = "none" sort_sensitive = false sort_reverse = false sort_translit = false ================================================ FILE: yazi-config/src/icon.rs ================================================ use crate::Style; #[derive(Clone, Debug)] pub struct Icon { pub text: String, pub style: Style, } ================================================ FILE: yazi-config/src/keymap/chord.rs ================================================ use std::{borrow::Cow, hash::{Hash, Hasher}, sync::OnceLock}; use anyhow::Result; use regex::Regex; use serde::Deserialize; use yazi_shared::{Layer, Source, event::Action}; use super::Key; static RE: OnceLock = OnceLock::new(); #[derive(Clone, Debug, Default, Deserialize)] pub struct Chord { #[serde(deserialize_with = "super::deserialize_on")] pub on: Vec, #[serde(deserialize_with = "super::deserialize_run")] pub run: Vec, pub desc: Option, pub r#for: Option, } impl PartialEq for Chord { fn eq(&self, other: &Self) -> bool { self.on == other.on } } impl Eq for Chord {} impl Hash for Chord { fn hash(&self, state: &mut H) { self.on.hash(state) } } impl Chord { pub fn on(&self) -> String { self.on.iter().map(ToString::to_string).collect() } pub fn run(&self) -> String { RE.get_or_init(|| Regex::new(r"\s+").unwrap()) .replace_all(&self.run.iter().map(|c| c.to_string()).collect::>().join("; "), " ") .into_owned() } pub fn desc(&self) -> Option> { self.desc.as_ref().map(|s| RE.get_or_init(|| Regex::new(r"\s+").unwrap()).replace_all(s, " ")) } pub fn desc_or_run(&self) -> Cow<'_, str> { self.desc().unwrap_or_else(|| self.run().into()) } pub fn contains(&self, s: &str) -> bool { let s = s.to_lowercase(); self.desc().map(|d| d.to_lowercase().contains(&s)) == Some(true) || self.run().to_lowercase().contains(&s) || self.on().to_lowercase().contains(&s) } #[inline] pub(super) fn noop(&self) -> bool { self.run.len() == 1 && self.run[0].name == "noop" && self.run[0].args.is_empty() } } impl Chord { pub(super) fn reshape(mut self, layer: Layer) -> Result { for action in &mut self.run { action.source = Source::Key; if action.layer == Default::default() { action.layer = layer; } } Ok(self) } } ================================================ FILE: yazi-config/src/keymap/cow.rs ================================================ use std::ops::Deref; use yazi_shared::event::ActionCow; use super::Chord; #[derive(Clone, Debug)] pub enum ChordCow { Owned(Chord), Borrowed(&'static Chord), } impl From for ChordCow { fn from(c: Chord) -> Self { Self::Owned(c) } } impl From<&'static Chord> for ChordCow { fn from(c: &'static Chord) -> Self { Self::Borrowed(c) } } impl Deref for ChordCow { type Target = Chord; fn deref(&self) -> &Self::Target { match self { Self::Owned(c) => c, Self::Borrowed(c) => c, } } } impl Default for ChordCow { fn default() -> Self { const C: &Chord = &Chord { on: vec![], run: vec![], desc: None, r#for: None }; Self::Borrowed(C) } } impl ChordCow { pub fn into_seq(self) -> Vec { match self { Self::Owned(c) => c.run.into_iter().rev().map(Into::into).collect(), Self::Borrowed(c) => c.run.iter().rev().map(Into::into).collect(), } } } ================================================ FILE: yazi-config/src/keymap/deserializers.rs ================================================ use std::{fmt, str::FromStr}; use anyhow::Result; use serde::{Deserializer, de::{self, Visitor}}; use yazi_shared::event::Action; use crate::keymap::Key; pub(super) fn deserialize_on<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { struct OnVisitor; impl<'de> Visitor<'de> for OnVisitor { type Value = Vec; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a `on` string or array of strings within keymap.toml") } fn visit_seq(self, mut seq: A) -> Result where A: de::SeqAccess<'de>, { let mut keys = Vec::with_capacity(seq.size_hint().unwrap_or(0)); while let Some(value) = &seq.next_element::()? { keys.push(Key::from_str(value).map_err(de::Error::custom)?); } if keys.is_empty() { return Err(de::Error::custom("`on` within keymap.toml cannot be empty")); } Ok(keys) } fn visit_str(self, value: &str) -> Result where E: de::Error, { Ok(vec![Key::from_str(value).map_err(de::Error::custom)?]) } } deserializer.deserialize_any(OnVisitor) } pub(super) fn deserialize_run<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { struct RunVisitor; impl<'de> Visitor<'de> for RunVisitor { type Value = Vec; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a `run` string or array of strings within keymap.toml") } fn visit_seq(self, mut seq: A) -> Result where A: de::SeqAccess<'de>, { let mut actions = Vec::with_capacity(seq.size_hint().unwrap_or(0)); while let Some(value) = &seq.next_element::()? { actions.push(Action::from_str(value).map_err(de::Error::custom)?); } if actions.is_empty() { return Err(de::Error::custom("`run` within keymap.toml cannot be empty")); } Ok(actions) } fn visit_str(self, value: &str) -> Result where E: de::Error, { Ok(vec![Action::from_str(value).map_err(de::Error::custom)?]) } } deserializer.deserialize_any(RunVisitor) } ================================================ FILE: yazi-config/src/keymap/key.rs ================================================ use std::{fmt::{Display, Write}, str::FromStr}; use anyhow::bail; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Key { pub code: KeyCode, pub shift: bool, pub ctrl: bool, pub alt: bool, pub super_: bool, } impl Key { #[inline] pub fn plain(&self) -> Option { match self.code { KeyCode::Char(c) if !self.ctrl && !self.alt && !self.super_ => Some(c), _ => None, } } } impl Default for Key { fn default() -> Self { Self { code: KeyCode::Null, shift: false, ctrl: false, alt: false, super_: false } } } impl From for Key { fn from(value: KeyEvent) -> Self { // For alphabet: // Unix : => Char("A") + SHIFT // Windows : => Char("A") + SHIFT // // For non-alphabet: // Unix : => Char("~") + NULL // Windows : => Char("~") + SHIFT // // So we detect `Char("~") + SHIFT`, and change it to `Char("~") + NULL` // for consistent behavior between OSs. let shift = match (value.code, value.modifiers) { (KeyCode::Char(c), _) => c.is_ascii_uppercase(), (KeyCode::BackTab, _) => false, (_, m) => m.contains(KeyModifiers::SHIFT), }; Self { code: value.code, shift, ctrl: value.modifiers.contains(KeyModifiers::CONTROL), alt: value.modifiers.contains(KeyModifiers::ALT), super_: value.modifiers.contains(KeyModifiers::SUPER), } } } impl FromStr for Key { type Err = anyhow::Error; fn from_str(s: &str) -> Result { if s.is_empty() { bail!("empty key") } let mut key = Self::default(); if !s.starts_with('<') || !s.ends_with('>') { key.code = KeyCode::Char(s.chars().next().unwrap()); key.shift = matches!(key.code, KeyCode::Char(c) if c.is_ascii_uppercase()); return Ok(key); } let mut it = s[1..s.len() - 1].split_inclusive('-').peekable(); while let Some(next) = it.next() { match next.to_ascii_lowercase().as_str() { "s-" => key.shift = true, "c-" => key.ctrl = true, "a-" => key.alt = true, "d-" => key.super_ = true, "space" => key.code = KeyCode::Char(' '), "backspace" => key.code = KeyCode::Backspace, "enter" => key.code = KeyCode::Enter, "left" => key.code = KeyCode::Left, "right" => key.code = KeyCode::Right, "up" => key.code = KeyCode::Up, "down" => key.code = KeyCode::Down, "home" => key.code = KeyCode::Home, "end" => key.code = KeyCode::End, "pageup" => key.code = KeyCode::PageUp, "pagedown" => key.code = KeyCode::PageDown, "tab" => key.code = KeyCode::Tab, "backtab" => key.code = KeyCode::BackTab, "delete" => key.code = KeyCode::Delete, "insert" => key.code = KeyCode::Insert, "f1" => key.code = KeyCode::F(1), "f2" => key.code = KeyCode::F(2), "f3" => key.code = KeyCode::F(3), "f4" => key.code = KeyCode::F(4), "f5" => key.code = KeyCode::F(5), "f6" => key.code = KeyCode::F(6), "f7" => key.code = KeyCode::F(7), "f8" => key.code = KeyCode::F(8), "f9" => key.code = KeyCode::F(9), "f10" => key.code = KeyCode::F(10), "f11" => key.code = KeyCode::F(11), "f12" => key.code = KeyCode::F(12), "f13" => key.code = KeyCode::F(13), "f14" => key.code = KeyCode::F(14), "f15" => key.code = KeyCode::F(15), "f16" => key.code = KeyCode::F(16), "f17" => key.code = KeyCode::F(17), "f18" => key.code = KeyCode::F(18), "f19" => key.code = KeyCode::F(19), "esc" => key.code = KeyCode::Esc, _ => match next { s if it.peek().is_none() => { let c = s.chars().next().unwrap(); key.shift |= c.is_ascii_uppercase(); key.code = KeyCode::Char(if key.shift { c.to_ascii_uppercase() } else { c }); } s => bail!("unknown key: {s}"), }, } } if key.code == KeyCode::Null { bail!("empty key") } Ok(key) } } impl Display for Key { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(c) = self.plain() { return if c == ' ' { write!(f, "") } else { f.write_char(c) }; } write!(f, "<")?; if self.super_ { write!(f, "D-")?; } if self.ctrl { write!(f, "C-")?; } if self.alt { write!(f, "A-")?; } if self.shift && !matches!(self.code, KeyCode::Char(_)) { write!(f, "S-")?; } let code = match self.code { KeyCode::Backspace => "Backspace", KeyCode::Enter => "Enter", KeyCode::Left => "Left", KeyCode::Right => "Right", KeyCode::Up => "Up", KeyCode::Down => "Down", KeyCode::Home => "Home", KeyCode::End => "End", KeyCode::PageUp => "PageUp", KeyCode::PageDown => "PageDown", KeyCode::Tab => "Tab", KeyCode::BackTab => "BackTab", KeyCode::Delete => "Delete", KeyCode::Insert => "Insert", KeyCode::F(1) => "F1", KeyCode::F(2) => "F2", KeyCode::F(3) => "F3", KeyCode::F(4) => "F4", KeyCode::F(5) => "F5", KeyCode::F(6) => "F6", KeyCode::F(7) => "F7", KeyCode::F(8) => "F8", KeyCode::F(9) => "F9", KeyCode::F(10) => "F10", KeyCode::F(11) => "F11", KeyCode::F(12) => "F12", KeyCode::F(13) => "F13", KeyCode::F(14) => "F14", KeyCode::F(15) => "F15", KeyCode::F(16) => "F16", KeyCode::F(17) => "F17", KeyCode::F(18) => "F18", KeyCode::F(19) => "F19", KeyCode::Esc => "Esc", KeyCode::Char(' ') => "Space", KeyCode::Char(c) => { f.write_char(c)?; "" } _ => "Unknown", }; write!(f, "{code}>") } } ================================================ FILE: yazi-config/src/keymap/keymap.rs ================================================ use anyhow::{Context, Result}; use serde::Deserialize; use yazi_codegen::{DeserializeOver, DeserializeOver1}; use yazi_fs::{Xdg, ok_or_not_found}; use yazi_shared::Layer; use super::{Chord, KeymapRules}; #[derive(Deserialize, DeserializeOver, DeserializeOver1)] pub struct Keymap { pub mgr: KeymapRules, pub tasks: KeymapRules, pub spot: KeymapRules, pub pick: KeymapRules, pub input: KeymapRules, pub confirm: KeymapRules, pub help: KeymapRules, pub cmp: KeymapRules, } impl Keymap { pub fn get(&self, layer: Layer) -> &[Chord] { match layer { Layer::App => &[], Layer::Mgr => &self.mgr, Layer::Tasks => &self.tasks, Layer::Spot => &self.spot, Layer::Pick => &self.pick, Layer::Input => &self.input, Layer::Confirm => &self.confirm, Layer::Help => &self.help, Layer::Cmp => &self.cmp, Layer::Which => &[], Layer::Notify => &[], } } } impl Keymap { pub(crate) fn read() -> Result { let p = Xdg::config_dir().join("keymap.toml"); ok_or_not_found(std::fs::read_to_string(&p)) .with_context(|| format!("Failed to read keymap {p:?}")) } pub(crate) fn reshape(self) -> Result { Ok(Self { mgr: self.mgr.reshape(Layer::Mgr)?, tasks: self.tasks.reshape(Layer::Tasks)?, spot: self.spot.reshape(Layer::Spot)?, pick: self.pick.reshape(Layer::Pick)?, input: self.input.reshape(Layer::Input)?, confirm: self.confirm.reshape(Layer::Confirm)?, help: self.help.reshape(Layer::Help)?, cmp: self.cmp.reshape(Layer::Cmp)?, }) } } ================================================ FILE: yazi-config/src/keymap/mod.rs ================================================ yazi_macro::mod_flat!(chord cow deserializers key keymap rules); ================================================ FILE: yazi-config/src/keymap/rules.rs ================================================ use std::ops::Deref; use anyhow::Result; use hashbrown::HashSet; use serde::Deserialize; use yazi_codegen::DeserializeOver2; use yazi_shared::Layer; use super::Chord; use crate::{Preset, check_for, keymap::Key}; #[derive(Default, Deserialize, DeserializeOver2)] pub struct KeymapRules { pub keymap: Vec, #[serde(default)] prepend_keymap: Vec, #[serde(default)] append_keymap: Vec, } impl Deref for KeymapRules { type Target = Vec; fn deref(&self) -> &Self::Target { &self.keymap } } impl KeymapRules { pub(crate) fn reshape(self, layer: Layer) -> Result { #[inline] fn on(Chord { on, .. }: &Chord) -> [Key; 2] { [on.first().copied().unwrap_or_default(), on.get(1).copied().unwrap_or_default()] } let a_seen: HashSet<_> = self.prepend_keymap.iter().map(on).collect(); let b_seen: HashSet<_> = self.keymap.iter().map(on).collect(); let keymap = Preset::mix( self.prepend_keymap, self.keymap.into_iter().filter(|v| !a_seen.contains(&on(v))), self.append_keymap.into_iter().filter(|v| !b_seen.contains(&on(v))), ) .map(|mut chord| (chord.r#for.take(), chord)) .filter(|(r#for, chord)| !chord.noop() && check_for(r#for.as_deref())) .map(|(_, chord)| chord.reshape(layer)) .collect::>()?; Ok(Self { keymap, ..Default::default() }) } } ================================================ FILE: yazi-config/src/layout.rs ================================================ use ratatui::layout::Rect; #[derive(Clone, Copy, Default, Eq, PartialEq)] pub struct Layout { pub current: Rect, pub preview: Rect, pub progress: Rect, } impl Layout { pub const fn default() -> Self { Self { current: Rect::ZERO, preview: Rect::ZERO, progress: Rect::ZERO } } pub const fn folder_limit(self) -> usize { self.current.height as _ } } ================================================ FILE: yazi-config/src/lib.rs ================================================ yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which vfs); yazi_macro::mod_flat!(icon layout pattern platform preset priority style utils yazi); use std::io::{Read, Write}; use yazi_shared::{RoCell, SyncCell}; use yazi_tty::TTY; pub static YAZI: RoCell = RoCell::new(); pub static KEYMAP: RoCell = RoCell::new(); pub static THEME: RoCell = RoCell::new(); pub static LAYOUT: SyncCell = SyncCell::new(Layout::default()); pub fn init() -> anyhow::Result<()> { if let Err(e) = try_init(true) { wait_for_key(e)?; try_init(false)?; } Ok(()) } fn try_init(merge: bool) -> anyhow::Result<()> { let mut yazi = Preset::yazi()?; let mut keymap = Preset::keymap()?; if merge { yazi = yazi.deserialize_over(&yazi::Yazi::read()?)?; keymap = keymap.deserialize_over(&keymap::Keymap::read()?)?; } YAZI.init(yazi.reshape()?); KEYMAP.init(keymap.reshape()?); Ok(()) } pub fn init_flavor(light: bool) -> anyhow::Result<()> { if let Err(e) = try_init_flavor(light, true) { wait_for_key(e)?; try_init_flavor(light, false)?; } Ok(()) } fn try_init_flavor(light: bool, merge: bool) -> anyhow::Result<()> { let mut preset = Preset::theme(light)?; if merge { let theme_str = theme::Theme::read()?; let theme = toml::de::DeTable::parse(&theme_str)?; let flavor_str = theme::Flavor::from_theme(&theme, &theme_str)?.read(light)?; preset = preset.deserialize_over(&flavor_str)?; preset = error_with_input(preset.deserialize_over_with(theme), &theme_str)?; } THEME.init(preset.reshape(light)?); Ok(()) } fn wait_for_key(e: anyhow::Error) -> anyhow::Result<()> { let stdout = &mut *TTY.lockout(); writeln!(stdout, "{e}")?; if let Some(src) = e.source() { writeln!(stdout, "\nCaused by:\n{src}")?; } use crossterm::style::{Attribute, Print, SetAttributes}; crossterm::execute!( stdout, SetAttributes(Attribute::Reverse.into()), SetAttributes(Attribute::Bold.into()), Print("Press to continue with preset settings..."), SetAttributes(Attribute::Reset.into()), Print("\n"), )?; TTY.reader().read_exact(&mut [0])?; Ok(()) } pub(crate) fn error_with_input( result: Result, input: &str, ) -> Result { result.map_err(|mut err| { err.set_input(Some(input)); err }) } ================================================ FILE: yazi-config/src/mgr/mgr.rs ================================================ use anyhow::{Result, bail}; use serde::Deserialize; use yazi_codegen::DeserializeOver2; use yazi_fs::{SortBy, SortFallback}; use yazi_shared::SyncCell; use super::{MgrRatio, MouseEvents}; #[derive(Debug, Deserialize, DeserializeOver2)] pub struct Mgr { pub ratio: SyncCell, // Sorting pub sort_by: SyncCell, pub sort_sensitive: SyncCell, pub sort_reverse: SyncCell, pub sort_dir_first: SyncCell, pub sort_translit: SyncCell, pub sort_fallback: SyncCell, // Display pub linemode: String, pub show_hidden: SyncCell, pub show_symlink: SyncCell, pub scrolloff: SyncCell, pub mouse_events: SyncCell, } impl Mgr { pub(crate) fn reshape(self) -> Result { if self.linemode.is_empty() || self.linemode.len() > 20 { bail!("[mgr].linemode must be between 1 and 20 characters."); } Ok(self) } } ================================================ FILE: yazi-config/src/mgr/mod.rs ================================================ yazi_macro::mod_flat!(mgr mouse ratio); ================================================ FILE: yazi-config/src/mgr/mouse.rs ================================================ use anyhow::{Result, bail}; use bitflags::bitflags; use crossterm::event::MouseEventKind; use serde::{Deserialize, Serialize}; bitflags! { #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(try_from = "Vec", into = "Vec")] pub struct MouseEvents: u8 { const CLICK = 0b00001; const SCROLL = 0b00010; const TOUCH = 0b00100; const MOVE = 0b01000; const DRAG = 0b10000; } } impl MouseEvents { pub const fn draggable(self) -> bool { self.contains(Self::DRAG) } } impl TryFrom> for MouseEvents { type Error = anyhow::Error; fn try_from(value: Vec) -> Result { value.into_iter().try_fold(Self::empty(), |aac, s| { Ok(match s.as_str() { "click" => aac | Self::CLICK, "scroll" => aac | Self::SCROLL, "touch" => aac | Self::TOUCH, "move" => aac | Self::MOVE, "drag" => aac | Self::DRAG, _ => bail!("Invalid mouse event: {s}"), }) }) } } impl From for Vec { fn from(value: MouseEvents) -> Self { let events = [ (MouseEvents::CLICK, "click"), (MouseEvents::SCROLL, "scroll"), (MouseEvents::TOUCH, "touch"), (MouseEvents::MOVE, "move"), (MouseEvents::DRAG, "drag"), ]; events.into_iter().filter(|v| value.contains(v.0)).map(|v| v.1.to_owned()).collect() } } impl From for MouseEvents { fn from(value: crossterm::event::MouseEventKind) -> Self { match value { MouseEventKind::Down(_) | MouseEventKind::Up(_) => Self::CLICK, MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => Self::SCROLL, MouseEventKind::ScrollLeft | MouseEventKind::ScrollRight => Self::TOUCH, MouseEventKind::Moved => Self::MOVE, MouseEventKind::Drag(_) => Self::DRAG, } } } ================================================ FILE: yazi-config/src/mgr/ratio.rs ================================================ use anyhow::bail; use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(try_from = "[u16; 3]")] pub struct MgrRatio { pub parent: u16, pub current: u16, pub preview: u16, pub all: u16, } impl TryFrom<[u16; 3]> for MgrRatio { type Error = anyhow::Error; fn try_from(ratio: [u16; 3]) -> 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], }) } } ================================================ FILE: yazi-config/src/open/mod.rs ================================================ yazi_macro::mod_flat!(open rule); ================================================ FILE: yazi-config/src/open/open.rs ================================================ use std::ops::Deref; use anyhow::Result; use indexmap::IndexSet; use serde::Deserialize; use yazi_codegen::DeserializeOver2; use yazi_shared::url::AsUrl; use crate::{Preset, open::OpenRule}; #[derive(Default, Deserialize, DeserializeOver2)] pub struct Open { rules: Vec, #[serde(default)] prepend_rules: Vec, #[serde(default)] append_rules: Vec, } impl Deref for Open { type Target = Vec; fn deref(&self) -> &Self::Target { &self.rules } } impl Open { pub fn all<'a, 'b, U, M>(&'a self, url: U, mime: M) -> impl Iterator + 'b where 'a: 'b, U: AsUrl + 'b, M: AsRef + 'b, { let is_dir = mime.as_ref().starts_with("folder/"); self .rules .iter() .find(move |&r| { r.mime.as_ref().is_some_and(|p| p.match_mime(&mime)) || r.url.as_ref().is_some_and(|p| p.match_url(url.as_url(), is_dir)) }) .into_iter() .flat_map(|r| &r.r#use) .map(String::as_str) } pub fn common<'a, 'b, U, M>(&'a self, targets: &'b [(U, M)]) -> IndexSet<&'a str> where &'b U: AsUrl, M: AsRef, { let each: Vec> = targets .iter() .map(|(u, m)| self.all(u, m).collect::>()) .filter(|s| !s.is_empty()) .collect(); let mut flat: IndexSet<_> = each.iter().flatten().copied().collect(); flat.retain(|use_| each.iter().all(|e| e.contains(use_))); flat } } impl Open { pub(crate) fn reshape(self) -> Result { let any_file = self.append_rules.iter().any(|r| r.any_file()); let any_dir = self.append_rules.iter().any(|r| r.any_dir()); let it = self.rules.into_iter().filter(|r| !(any_file && r.any_file() || any_dir && r.any_dir())); Ok(Self { rules: Preset::mix(self.prepend_rules, it, self.append_rules).collect(), ..Default::default() }) } } ================================================ FILE: yazi-config/src/open/rule.rs ================================================ use std::fmt; use serde::{Deserialize, Deserializer, de::{self, Visitor}}; use crate::pattern::Pattern; #[derive(Debug, Deserialize)] pub struct OpenRule { pub url: Option, pub mime: Option, #[serde(deserialize_with = "OpenRule::deserialize")] pub r#use: Vec, } impl OpenRule { #[inline] pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) } #[inline] pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) } } impl OpenRule { fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { struct UseVisitor; impl<'de> Visitor<'de> for UseVisitor { type Value = Vec; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string, or array of strings") } fn visit_seq(self, mut seq: A) -> Result where A: de::SeqAccess<'de>, { let mut uses = Vec::with_capacity(seq.size_hint().unwrap_or(0)); while let Some(use_) = seq.next_element::()? { uses.push(use_); } Ok(uses) } fn visit_str(self, value: &str) -> Result where E: de::Error, { Ok(vec![value.to_owned()]) } fn visit_string(self, v: String) -> Result where E: de::Error, { Ok(vec![v]) } } deserializer.deserialize_any(UseVisitor) } } ================================================ FILE: yazi-config/src/opener/mod.rs ================================================ yazi_macro::mod_flat!(opener rule); ================================================ FILE: yazi-config/src/opener/opener.rs ================================================ use std::{mem, ops::Deref}; use anyhow::Result; use hashbrown::HashMap; use indexmap::IndexSet; use serde::{Deserialize, de::IntoDeserializer}; use toml::{Spanned, de::DeTable}; use yazi_codegen::DeserializeOver; use super::OpenerRule; use crate::check_for; #[derive(Debug, Deserialize, DeserializeOver)] pub struct Opener(HashMap>); impl Deref for Opener { type Target = HashMap>; fn deref(&self) -> &Self::Target { &self.0 } } impl Opener { pub fn all<'a, I>(&self, uses: I) -> impl Iterator where I: Iterator, { uses.flat_map(|use_| self.get(use_)).flatten() } pub fn first<'a, I>(&self, uses: I) -> Option<&OpenerRule> where I: Iterator, { uses.flat_map(|use_| self.get(use_)).flatten().next() } pub fn block<'a, I>(&self, uses: I) -> Option<&OpenerRule> where I: Iterator, { uses.flat_map(|use_| self.get(use_)).flatten().find(|&o| o.block) } } impl Opener { pub(crate) fn reshape(mut self) -> Result { for rules in self.0.values_mut() { *rules = mem::take(rules) .into_iter() .map(|mut r| (r.r#for.take(), r)) .filter(|(r#for, _)| check_for(r#for.as_deref())) .map(|(_, r)| r.reshape()) .collect::>>()? .into_iter() .collect(); } Ok(self) } pub(crate) fn deserialize_over_with<'de>( mut self, table: Spanned>, ) -> Result { for (key, value) in table.into_inner() { self.0.insert(key.into_inner().into_owned(), <_>::deserialize(value.into_deserializer())?); } Ok(self) } } ================================================ FILE: yazi-config/src/opener/rule.rs ================================================ use anyhow::{Result, bail}; use serde::Deserialize; use yazi_fs::Splatter; #[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct OpenerRule { pub run: String, #[serde(default)] pub block: bool, #[serde(default)] pub orphan: bool, #[serde(default)] pub desc: String, pub r#for: Option, #[serde(skip)] pub spread: bool, } impl OpenerRule { pub fn desc(&self) -> String { if !self.desc.is_empty() { self.desc.clone() } else if let Some(first) = self.run.split_whitespace().next() { first.to_owned() } else { String::new() } } } impl OpenerRule { pub(super) fn reshape(mut self) -> Result { if self.run.is_empty() { bail!("[open].rules.*.run cannot be empty."); } #[cfg(unix)] { self.spread = Splatter::<()>::spread(&self.run) || self.run.contains("$@") || self.run.contains("$*"); } #[cfg(windows)] { self.spread = Splatter::<()>::spread(&self.run) || self.run.contains("%*"); } Ok(self) } } ================================================ FILE: yazi-config/src/pattern.rs ================================================ use std::{fmt::Debug, str::FromStr}; use anyhow::{Result, bail}; use globset::{Candidate, GlobBuilder}; use serde::Deserialize; use yazi_shared::{scheme::SchemeKind, url::AsUrl}; #[derive(Deserialize)] #[serde(try_from = "String")] pub struct Pattern { inner: globset::GlobMatcher, scheme: PatternScheme, pub is_dir: bool, is_star: bool, #[cfg(windows)] sep_lit: bool, } impl Debug for Pattern { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Pattern") .field("regex", &self.inner.glob().regex()) .field("scheme", &self.scheme) .field("is_dir", &self.is_dir) .field("is_star", &self.is_star) .finish() } } impl Pattern { pub fn match_url(&self, url: impl AsUrl, is_dir: bool) -> bool { let url = url.as_url(); if is_dir != self.is_dir { return false; } else if !self.scheme.matches(url.kind()) { return false; } else if self.is_star { return true; } #[cfg(unix)] { self.inner.is_match_candidate(&Candidate::from_bytes(url.loc().encoded_bytes())) } #[cfg(windows)] if self.sep_lit { use yazi_shared::strand::{AsStrand, StrandLike}; self.inner.is_match_candidate(&Candidate::from_bytes( url.loc().as_strand().backslash_to_slash().encoded_bytes(), )) } else { self.inner.is_match_candidate(&Candidate::from_bytes(url.loc().encoded_bytes())) } } pub fn match_mime(&self, mime: impl AsRef) -> bool { self.is_star || (!mime.as_ref().is_empty() && self.inner.is_match(mime.as_ref())) } #[inline] pub fn any_file(&self) -> bool { self.is_star && !self.is_dir } #[inline] pub fn any_dir(&self) -> bool { self.is_star && self.is_dir } } impl FromStr for Pattern { type Err = anyhow::Error; fn from_str(s: &str) -> Result { // Trim leading case-sensitive indicator let a = s.trim_start_matches(r"\s"); // Parse the URL scheme if present let (scheme, skip) = PatternScheme::parse(a)?; let b = &a[skip..]; // Trim the ending slash which indicates a directory let c = b.trim_end_matches('/'); // Check whether it's a filename pattern or a full path pattern let sep_lit = c.contains('/'); let inner = GlobBuilder::new(c) .case_insensitive(a.len() == s.len()) .literal_separator(sep_lit) .backslash_escape(false) .empty_alternates(true) .build()? .compile_matcher(); Ok(Self { inner, scheme, is_dir: c.len() < b.len(), is_star: c == "*", #[cfg(windows)] sep_lit, }) } } impl TryFrom for Pattern { type Error = anyhow::Error; fn try_from(s: String) -> Result { Self::from_str(s.as_str()) } } // --- Scheme #[derive(Clone, Copy, Debug)] enum PatternScheme { Any, Local, Remote, Virtual, Regular, Search, Archive, Sftp, } impl PatternScheme { fn parse(s: &str) -> Result<(Self, usize)> { let Some((protocol, _)) = s.split_once("://") else { return Ok((Self::Any, 0)); }; let scheme = match protocol { "*" => Self::Any, "local" => Self::Local, "remote" => Self::Remote, "virtual" => Self::Virtual, "regular" => Self::Regular, "search" => Self::Search, "archive" => Self::Archive, "sftp" => Self::Sftp, "" => bail!("Invalid URL pattern: protocol is empty"), _ => bail!("Unknown protocol in URL pattern: {protocol}"), }; Ok((scheme, protocol.len() + 3)) } #[inline] fn matches(self, kind: SchemeKind) -> bool { use SchemeKind as K; match (self, kind) { (Self::Any, _) => true, (Self::Local, s) => s.is_local(), (Self::Remote, s) => s.is_remote(), (Self::Virtual, s) => s.is_virtual(), (Self::Regular, K::Regular) => true, (Self::Search, K::Search) => true, (Self::Archive, K::Archive) => true, (Self::Sftp, K::Sftp) => true, _ => false, } } } // --- Tests #[cfg(test)] mod tests { use yazi_shared::url::UrlCow; use super::*; fn matches(glob: &str, url: &str) -> bool { Pattern::from_str(glob).unwrap().match_url(UrlCow::try_from(url).unwrap(), false) } #[cfg(unix)] #[test] fn test_unix() { // Wildcard assert!(matches("*", "/foo")); assert!(matches("*", "/foo/bar")); assert!(matches("**", "foo")); assert!(matches("**", "/foo")); assert!(matches("**", "/foo/bar")); // Filename assert!(matches("*.md", "foo.md")); assert!(matches("*.md", "/foo.md")); assert!(matches("*.md", "/foo/bar.md")); // 1-star assert!(matches("/*", "/foo")); assert!(matches("/*/*.md", "/foo/bar.md")); // 2-star assert!(matches("/**", "/foo")); assert!(matches("/**", "/foo/bar")); assert!(matches("**/**", "/foo")); assert!(matches("**/**", "/foo/bar")); assert!(matches("/**/*", "/foo")); assert!(matches("/**/*", "/foo/bar")); // Failures assert!(!matches("/*/*", "/foo")); assert!(!matches("/*/*.md", "/foo.md")); assert!(!matches("/*", "/foo/bar")); assert!(!matches("/*.md", "/foo/bar.md")); } #[cfg(windows)] #[test] fn test_windows() { // Wildcard assert!(matches("*", r#"C:\foo"#)); assert!(matches("*", r#"C:\foo\bar"#)); assert!(matches("**", r#"foo"#)); assert!(matches("**", r#"C:\foo"#)); assert!(matches("**", r#"C:\foo\bar"#)); // Filename assert!(matches("*.md", r#"foo.md"#)); assert!(matches("*.md", r#"C:\foo.md"#)); assert!(matches("*.md", r#"C:\foo\bar.md"#)); // 1-star assert!(matches(r#"C:/*"#, r#"C:\foo"#)); assert!(matches(r#"C:/*/*.md"#, r#"C:\foo\bar.md"#)); // 2-star assert!(matches(r#"C:/**"#, r#"C:\foo"#)); assert!(matches(r#"C:/**"#, r#"C:\foo\bar"#)); assert!(matches(r#"**/**"#, r#"C:\foo"#)); assert!(matches(r#"**/**"#, r#"C:\foo\bar"#)); assert!(matches(r#"C:/**/*"#, r#"C:\foo"#)); assert!(matches(r#"C:/**/*"#, r#"C:\foo\bar"#)); // Drive letter assert!(matches(r#"*:/*"#, r#"C:\foo"#)); assert!(matches(r#"*:/**/*.md"#, r#"C:\foo\bar.md"#)); // Failures assert!(!matches(r#"C:/*/*"#, r#"C:\foo"#)); assert!(!matches(r#"C:/*/*.md"#, r#"C:\foo.md"#)); assert!(!matches(r#"C:/*"#, r#"C:\foo\bar"#)); assert!(!matches(r#"C:/*.md"#, r#"C:\foo\bar.md"#)); } } ================================================ FILE: yazi-config/src/platform.rs ================================================ #[inline] pub(crate) fn check_for(r#for: Option<&str>) -> bool { match r#for.as_ref().map(|s| s.as_ref()) { Some("unix") if cfg!(unix) => true, Some(os) if os == std::env::consts::OS => true, Some(_) => false, None => true, } } ================================================ FILE: yazi-config/src/plugin/fetcher.rs ================================================ use serde::Deserialize; use yazi_fs::File; use yazi_shared::event::Action; use crate::{Pattern, Priority}; #[derive(Debug, Deserialize)] pub struct Fetcher { #[serde(skip)] pub idx: u8, pub id: String, pub url: Option, pub mime: Option, pub run: Action, #[serde(default)] pub prio: Priority, } impl Fetcher { #[inline] pub fn matches(&self, file: &File, mime: &str) -> bool { self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) || self.url.as_ref().is_some_and(|p| p.match_url(&file.url, file.is_dir())) } } ================================================ FILE: yazi-config/src/plugin/mod.rs ================================================ yazi_macro::mod_flat!(fetcher plugin preloader previewer spotter); pub const MAX_FETCHERS: u8 = 16; pub const MAX_PRELOADERS: u8 = 16; ================================================ FILE: yazi-config/src/plugin/plugin.rs ================================================ use anyhow::{Result, bail}; use hashbrown::HashSet; use serde::Deserialize; use tracing::warn; use yazi_codegen::DeserializeOver2; use yazi_fs::File; use super::{Fetcher, Preloader, Previewer, Spotter}; use crate::{Preset, plugin::{MAX_FETCHERS, MAX_PRELOADERS}}; #[derive(Default, Deserialize, DeserializeOver2)] pub struct Plugin { pub fetchers: Vec, #[serde(default)] prepend_fetchers: Vec, #[serde(default)] append_fetchers: Vec, pub spotters: Vec, #[serde(default)] prepend_spotters: Vec, #[serde(default)] append_spotters: Vec, pub preloaders: Vec, #[serde(default)] prepend_preloaders: Vec, #[serde(default)] append_preloaders: Vec, pub previewers: Vec, #[serde(default)] prepend_previewers: Vec, #[serde(default)] append_previewers: Vec, } impl Plugin { pub fn fetchers<'a, 'b: 'a>( &'b self, file: &'a File, mime: &'a str, ) -> impl Iterator + 'a { let mut seen = HashSet::new(); self.fetchers.iter().filter(move |&f| { if seen.contains(&f.id) || !f.matches(file, mime) { return false; } seen.insert(&f.id); true }) } pub fn mime_fetchers(&self, files: Vec) -> impl Iterator)> { let mut tasks: [Vec<_>; MAX_FETCHERS as usize] = Default::default(); for f in files { let found = self.fetchers.iter().find(|&g| g.id == "mime" && g.matches(&f, "")); if let Some(g) = found { tasks[g.idx as usize].push(f); } else { warn!("No mime fetcher for {f:?}"); } } tasks.into_iter().enumerate().filter_map(|(i, tasks)| { if tasks.is_empty() { None } else { Some((&self.fetchers[i], tasks)) } }) } pub fn spotter(&self, file: &File, mime: &str) -> Option<&Spotter> { self.spotters.iter().find(|&p| p.matches(file, mime)) } pub fn preloaders<'a, 'b: 'a>( &'b self, file: &'a File, mime: &'a str, ) -> impl Iterator + 'a { let mut next = true; self.preloaders.iter().filter(move |&p| { if !next || !p.matches(file, mime) { return false; } next = p.next; true }) } pub fn previewer(&self, file: &File, mime: &str) -> Option<&Previewer> { self.previewers.iter().find(|&p| p.matches(file, mime)) } } impl Plugin { // TODO: remove .retain() and .collect() pub(crate) fn reshape(mut self) -> Result { if self.append_spotters.iter().any(|r| r.any_file()) { self.spotters.retain(|r| !r.any_file()); } if self.append_spotters.iter().any(|r| r.any_dir()) { self.spotters.retain(|r| !r.any_dir()); } if self.append_previewers.iter().any(|r| r.any_file()) { self.previewers.retain(|r| !r.any_file()); } if self.append_previewers.iter().any(|r| r.any_dir()) { self.previewers.retain(|r| !r.any_dir()); } self.fetchers = Preset::mix(self.prepend_fetchers, self.fetchers, self.append_fetchers).collect(); self.spotters = Preset::mix(self.prepend_spotters, self.spotters, self.append_spotters).collect(); self.preloaders = Preset::mix(self.prepend_preloaders, self.preloaders, self.append_preloaders).collect(); self.previewers = Preset::mix(self.prepend_previewers, self.previewers, self.append_previewers).collect(); if self.fetchers.len() > MAX_FETCHERS as usize { bail!("Fetchers exceed the limit of {MAX_FETCHERS}"); } else if self.preloaders.len() > MAX_PRELOADERS as usize { bail!("Preloaders exceed the limit of {MAX_PRELOADERS}"); } for (i, p) in self.fetchers.iter_mut().enumerate() { p.idx = i as u8; } for (i, p) in self.preloaders.iter_mut().enumerate() { p.idx = i as u8; } Ok(Self { fetchers: self.fetchers, spotters: self.spotters, preloaders: self.preloaders, previewers: self.previewers, ..Default::default() }) } } ================================================ FILE: yazi-config/src/plugin/preloader.rs ================================================ use serde::Deserialize; use yazi_fs::File; use yazi_shared::event::Action; use crate::{Pattern, Priority}; #[derive(Debug, Deserialize)] pub struct Preloader { #[serde(skip)] pub idx: u8, pub url: Option, pub mime: Option, pub run: Action, #[serde(default)] pub next: bool, #[serde(default)] pub prio: Priority, } impl Preloader { #[inline] pub fn matches(&self, file: &File, mime: &str) -> bool { self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) || self.url.as_ref().is_some_and(|p| p.match_url(&file.url, file.is_dir())) } } ================================================ FILE: yazi-config/src/plugin/previewer.rs ================================================ use serde::Deserialize; use yazi_fs::File; use yazi_shared::event::Action; use crate::Pattern; #[derive(Debug, Deserialize)] pub struct Previewer { pub url: Option, pub mime: Option, pub run: Action, } impl Previewer { #[inline] pub fn matches(&self, file: &File, mime: &str) -> bool { self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) || self.url.as_ref().is_some_and(|p| p.match_url(&file.url, file.is_dir())) } #[inline] pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) } #[inline] pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) } } ================================================ FILE: yazi-config/src/plugin/spotter.rs ================================================ use serde::Deserialize; use yazi_fs::File; use yazi_shared::event::Action; use crate::Pattern; #[derive(Debug, Deserialize)] pub struct Spotter { pub url: Option, pub mime: Option, pub run: Action, } impl Spotter { #[inline] pub fn matches(&self, file: &File, mime: &str) -> bool { self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) || self.url.as_ref().is_some_and(|p| p.match_url(&file.url, file.is_dir())) } #[inline] pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) } #[inline] pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) } } ================================================ FILE: yazi-config/src/popup/confirm.rs ================================================ use serde::Deserialize; use yazi_codegen::DeserializeOver2; use super::{Offset, Origin}; use crate::popup::Position; #[derive(Deserialize, DeserializeOver2)] pub struct Confirm { // trash pub trash_title: String, pub trash_origin: Origin, pub trash_offset: Offset, // delete pub delete_title: String, pub delete_origin: Origin, pub delete_offset: Offset, // overwrite pub overwrite_title: String, pub overwrite_body: String, pub overwrite_origin: Origin, pub overwrite_offset: Offset, // quit pub quit_title: String, pub quit_body: String, pub quit_origin: Origin, pub quit_offset: Offset, } impl Confirm { pub const fn border(&self) -> u16 { 2 } pub const fn trash_position(&self) -> Position { Position::new(self.trash_origin, self.trash_offset) } pub const fn delete_position(&self) -> Position { Position::new(self.delete_origin, self.delete_offset) } pub const fn overwrite_position(&self) -> Position { Position::new(self.overwrite_origin, self.overwrite_offset) } pub const fn quit_position(&self) -> Position { Position::new(self.quit_origin, self.quit_offset) } } ================================================ FILE: yazi-config/src/popup/input.rs ================================================ use serde::Deserialize; use yazi_codegen::DeserializeOver2; use super::{Offset, Origin}; #[derive(Deserialize, DeserializeOver2)] pub struct Input { pub cursor_blink: bool, // cd pub cd_title: String, pub cd_origin: Origin, pub cd_offset: Offset, // create pub create_title: [String; 2], pub create_origin: Origin, pub create_offset: Offset, // rename pub rename_title: String, pub rename_origin: Origin, pub rename_offset: Offset, // filter pub filter_title: String, pub filter_origin: Origin, pub filter_offset: Offset, // find pub find_title: [String; 2], pub find_origin: Origin, pub find_offset: Offset, // search pub search_title: String, pub search_origin: Origin, pub search_offset: Offset, // shell pub shell_title: [String; 2], pub shell_origin: Origin, pub shell_offset: Offset, } impl Input { pub const fn border(&self) -> u16 { 2 } } ================================================ FILE: yazi-config/src/popup/mod.rs ================================================ yazi_macro::mod_flat!(confirm input offset options origin pick position); ================================================ FILE: yazi-config/src/popup/offset.rs ================================================ use anyhow::bail; use serde::Deserialize; #[derive(Clone, Copy, Debug, Default, Deserialize)] #[serde(try_from = "[i16; 4]")] pub struct Offset { pub x: i16, pub y: i16, pub width: u16, pub height: u16, } impl TryFrom<[i16; 4]> for Offset { type Error = anyhow::Error; fn try_from(values: [i16; 4]) -> Result { if values.len() != 4 { bail!("offset must have 4 values: {:?}", values); } if values[2] < 0 || values[3] < 0 { bail!("offset width and height must be positive: {:?}", values); } if values[3] < 3 { bail!("offset height must be at least 3: {:?}", values); } Ok(Self { x: values[0], y: values[1], width: values[2] as u16, height: values[3] as u16, }) } } impl Offset { #[inline] pub fn line() -> Self { Self { x: 0, y: 0, width: u16::MAX, height: 1 } } } ================================================ FILE: yazi-config/src/popup/options.rs ================================================ use ratatui::{text::{Line, Text}, widgets::{Paragraph, Wrap}}; use yazi_shared::{scheme::Encode as EncodeScheme, strand::ToStrand, url::{Url, UrlBuf}}; use super::{Offset, Position}; use crate::{YAZI, popup::Origin}; #[derive(Clone, Debug, Default)] pub struct InputCfg { pub title: String, pub value: String, pub cursor: Option, pub obscure: bool, pub position: Position, pub realtime: bool, pub completion: bool, } #[derive(Clone, Debug, Default)] pub struct PickCfg { pub title: String, pub items: Vec, pub position: Position, } #[derive(Clone, Debug, Default)] pub struct ConfirmCfg { pub position: Position, pub title: Line<'static>, pub body: Paragraph<'static>, pub list: Paragraph<'static>, } impl InputCfg { pub fn cd(cwd: Url) -> Self { Self { title: YAZI.input.cd_title.clone(), value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() }, position: Position::new(YAZI.input.cd_origin, YAZI.input.cd_offset), completion: true, ..Default::default() } } pub fn create(dir: bool) -> Self { Self { title: YAZI.input.create_title[dir as usize].clone(), position: Position::new(YAZI.input.create_origin, YAZI.input.create_offset), ..Default::default() } } pub fn rename() -> Self { Self { title: YAZI.input.rename_title.clone(), position: Position::new(YAZI.input.rename_origin, YAZI.input.rename_offset), ..Default::default() } } pub fn filter() -> Self { Self { title: YAZI.input.filter_title.clone(), position: Position::new(YAZI.input.filter_origin, YAZI.input.filter_offset), realtime: true, ..Default::default() } } pub fn find(prev: bool) -> Self { Self { title: YAZI.input.find_title[prev as usize].clone(), position: Position::new(YAZI.input.find_origin, YAZI.input.find_offset), realtime: true, ..Default::default() } } pub fn search(name: &str) -> Self { Self { title: YAZI.input.search_title.replace("{n}", name), position: Position::new(YAZI.input.search_origin, YAZI.input.search_offset), ..Default::default() } } pub fn shell(block: bool) -> Self { Self { title: YAZI.input.shell_title[block as usize].clone(), position: Position::new(YAZI.input.shell_origin, YAZI.input.shell_offset), ..Default::default() } } pub fn tab_rename() -> Self { Self { title: "Rename tab:".to_owned(), position: Position::new(Origin::TopCenter, Offset { x: 0, y: 2, width: 50, height: 3, }), ..Default::default() } } #[inline] pub fn with_value(mut self, value: impl Into) -> Self { self.value = value.into(); self } #[inline] pub fn with_cursor(mut self, cursor: Option) -> Self { self.cursor = cursor; self } } impl ConfirmCfg { fn new( title: String, position: Position, body: Option>, list: Option>, ) -> Self { Self { position, title: Line::raw(title), body: body.map(|b| Paragraph::new(b).wrap(Wrap { trim: false })).unwrap_or_default(), list: list.map(|l| Paragraph::new(l).wrap(Wrap { trim: false })).unwrap_or_default(), } } pub fn trash(urls: &[yazi_shared::url::UrlBuf]) -> Self { Self::new( Self::replace_number(&YAZI.confirm.trash_title, urls.len()), YAZI.confirm.trash_position(), None, Self::truncate_list(urls, urls.len(), 100), ) } pub fn delete(urls: &[yazi_shared::url::UrlBuf]) -> Self { Self::new( Self::replace_number(&YAZI.confirm.delete_title, urls.len()), YAZI.confirm.delete_position(), None, Self::truncate_list(urls, urls.len(), 100), ) } pub fn overwrite(url: &UrlBuf) -> Self { Self::new( YAZI.confirm.overwrite_title.clone(), YAZI.confirm.overwrite_position(), Some(Text::raw(&YAZI.confirm.overwrite_body)), Some(url.to_strand().into_string_lossy().into()), ) } pub fn quit(len: usize, names: Vec) -> Self { Self::new( Self::replace_number(&YAZI.confirm.quit_title, len), YAZI.confirm.quit_position(), Some(Text::raw(&YAZI.confirm.quit_body)), Self::truncate_list(names, len, 10), ) } fn replace_number(tpl: &str, n: usize) -> String { tpl.replace("{n}", &n.to_string()).replace("{s}", if n > 1 { "s" } else { "" }) } fn truncate_list(it: I, len: usize, max: usize) -> Option> where I: IntoIterator, I::Item: ToStrand, { let mut lines = Vec::with_capacity(len.min(max + 1)); for (i, s) in it.into_iter().enumerate() { if i >= max { lines.push(format!("... and {} more", len - max)); break; } lines.push(s.to_strand().into_string_lossy()); } Some(Text::from_iter(lines)) } } impl PickCfg { fn max_height(len: usize) -> u16 { YAZI.pick.open_offset.height.min(YAZI.pick.border().saturating_add(len as u16)) } pub fn open(items: Vec) -> Self { let max_height = Self::max_height(items.len()); Self { title: YAZI.pick.open_title.clone(), items, position: Position::new(YAZI.pick.open_origin, Offset { height: max_height, ..YAZI.pick.open_offset }), } } } ================================================ FILE: yazi-config/src/popup/origin.rs ================================================ use std::{fmt::Display, str::FromStr}; use serde::Deserialize; #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum Origin { #[default] TopLeft, TopCenter, TopRight, BottomLeft, BottomCenter, BottomRight, Center, Hovered, } impl Display for Origin { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::TopLeft => "top-left", Self::TopCenter => "top-center", Self::TopRight => "top-right", Self::BottomLeft => "bottom-left", Self::BottomCenter => "bottom-center", Self::BottomRight => "bottom-right", Self::Center => "center", Self::Hovered => "hovered", }) } } impl FromStr for Origin { type Err = serde::de::value::Error; fn from_str(s: &str) -> Result { Self::deserialize(serde::de::value::StrDeserializer::new(s)) } } ================================================ FILE: yazi-config/src/popup/pick.rs ================================================ use serde::Deserialize; use yazi_codegen::DeserializeOver2; use super::{Offset, Origin}; #[derive(Deserialize, DeserializeOver2)] pub struct Pick { // open pub open_title: String, pub open_origin: Origin, pub open_offset: Offset, } impl Pick { pub const fn border(&self) -> u16 { 2 } } ================================================ FILE: yazi-config/src/popup/position.rs ================================================ use crossterm::terminal::WindowSize; use ratatui::layout::Rect; use super::{Offset, Origin}; #[derive(Clone, Copy, Debug, Default)] pub struct Position { pub origin: Origin, pub offset: Offset, } impl Position { pub const fn new(origin: Origin, offset: Offset) -> Self { Self { origin, offset } } pub fn rect(&self, WindowSize { columns, rows, .. }: WindowSize) -> Rect { use Origin::*; let Offset { x, y, width, height } = self.offset; let max_x = columns.saturating_sub(width); let new_x = match self.origin { TopLeft | BottomLeft => x.clamp(0, max_x as i16) as u16, TopCenter | BottomCenter | Center => { (columns / 2).saturating_sub(width / 2).saturating_add_signed(x).clamp(0, max_x) } TopRight | BottomRight => max_x.saturating_add_signed(x).clamp(0, max_x), Hovered => unreachable!(), }; let max_y = rows.saturating_sub(height); let new_y = match self.origin { TopLeft | TopCenter | TopRight => y.clamp(0, max_y as i16) as u16, Center => (rows / 2).saturating_sub(height / 2).saturating_add_signed(y).clamp(0, max_y), BottomLeft | BottomCenter | BottomRight => max_y.saturating_add_signed(y).clamp(0, max_y), Hovered => unreachable!(), }; Rect { x: new_x, y: new_y, width: width.min(columns.saturating_sub(new_x)), height: height.min(rows.saturating_sub(new_y)), } } pub fn sticky(WindowSize { columns, rows, .. }: WindowSize, base: Rect, offset: Offset) -> Rect { let Offset { x, y, width, height } = offset; let above = base.y.saturating_add(base.height).saturating_add(height).saturating_add_signed(y) > rows; let new_x = base.x.saturating_add_signed(x).clamp(0, columns.saturating_sub(width)); let new_y = if above { base.y.saturating_sub(height.saturating_sub(y.unsigned_abs())) } else { base.y.saturating_add(base.height).saturating_add_signed(y) }; Rect { x: new_x, y: new_y, width: width.min(columns.saturating_sub(new_x)), height: height.min(rows.saturating_sub(new_y)), } } } ================================================ FILE: yazi-config/src/preset.rs ================================================ use crate::{Yazi, keymap::Keymap, theme::Theme, vfs::Vfs}; pub(crate) struct Preset; impl Preset { pub(super) fn yazi() -> Result { toml::from_str(&yazi_macro::config_preset!("yazi")) } pub(super) fn keymap() -> Result { toml::from_str(&yazi_macro::config_preset!("keymap")) } pub(super) fn theme(light: bool) -> Result { toml::from_str(&if light { yazi_macro::theme_preset!("light") } else { yazi_macro::theme_preset!("dark") }) } pub(super) fn vfs() -> Result { toml::from_str(&yazi_macro::config_preset!("vfs")) } #[inline] pub(crate) fn mix(a: A, b: B, c: C) -> impl Iterator where A: IntoIterator, B: IntoIterator, C: IntoIterator, { a.into_iter().chain(b).chain(c) } } ================================================ FILE: yazi-config/src/preview/mod.rs ================================================ yazi_macro::mod_flat!(preview wrap); ================================================ FILE: yazi-config/src/preview/preview.rs ================================================ use std::path::PathBuf; use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; use yazi_codegen::DeserializeOver2; use yazi_fs::Xdg; use yazi_shared::{SStr, timestamp_us}; use super::PreviewWrap; use crate::normalize_path; #[derive(Debug, Deserialize, DeserializeOver2, Serialize)] pub struct Preview { pub wrap: PreviewWrap, pub tab_size: u8, pub max_width: u16, pub max_height: u16, pub cache_dir: PathBuf, pub image_delay: u8, pub image_filter: String, pub image_quality: u8, pub ueberzug_scale: f32, pub ueberzug_offset: (f32, f32, f32, f32), } impl Preview { pub fn tmpfile(&self, prefix: &str) -> PathBuf { self.cache_dir.join(format!("{prefix}-{}", timestamp_us())) } pub fn indent(&self) -> SStr { #[rustfmt::skip] const TABS: &[&str] = &["", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "]; if let Some(&s) = TABS.get(self.tab_size as usize) { s.into() } else { " ".repeat(self.tab_size as usize).into() } } } impl Preview { pub(crate) fn reshape(mut self) -> Result { if self.image_delay > 100 { bail!("[preview].image_delay must be between 0 and 100."); } else if self.image_quality < 50 || self.image_quality > 90 { bail!("[preview].image_quality must be between 50 and 90."); } self.cache_dir = if self.cache_dir.as_os_str().is_empty() { Xdg::cache_dir().to_owned() } else if let Some(p) = normalize_path(self.cache_dir) { p } else { bail!("[preview].cache_dir must be either empty or an absolute path."); }; std::fs::create_dir_all(&self.cache_dir) .context(format!("Failed to create cache directory: {}", self.cache_dir.display()))?; Ok(self) } } ================================================ FILE: yazi-config/src/preview/wrap.rs ================================================ use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "kebab-case")] pub enum PreviewWrap { No, Yes, } impl From for Option { fn from(wrap: PreviewWrap) -> Self { match wrap { PreviewWrap::No => None, PreviewWrap::Yes => Some(ratatui::widgets::Wrap { trim: false }), } } } ================================================ FILE: yazi-config/src/priority.rs ================================================ use serde::Deserialize; #[derive(Clone, Copy, Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Priority { Low = 0, #[default] Normal = 1, High = 2, } ================================================ FILE: yazi-config/src/style.rs ================================================ use ratatui::style::Color; use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)] pub struct Style { pub fg: Option, pub bg: Option, pub bold: Option, pub dim: Option, pub italic: Option, pub underline: Option, pub blink: Option, pub blink_rapid: Option, pub reversed: Option, pub hidden: Option, pub crossed: Option, } impl From for Style { #[rustfmt::skip] fn from(value: ratatui::style::Style) -> Self { use ratatui::style::Modifier as M; let sub = value.sub_modifier; let all = value.add_modifier - sub; Self { fg: value.fg, bg: value.bg, bold: if all.contains(M::BOLD) { Some(true) } else if sub.contains(M::BOLD) { Some(false) } else { None }, dim: if all.contains(M::DIM) { Some(true) } else if sub.contains(M::DIM) { Some(false) } else { None }, italic: if all.contains(M::ITALIC) { Some(true) } else if sub.contains(M::ITALIC) { Some(false) } else { None }, underline: if all.contains(M::UNDERLINED) { Some(true) } else if sub.contains(M::UNDERLINED) { Some(false) } else { None }, blink: if all.contains(M::SLOW_BLINK) { Some(true) } else if sub.contains(M::SLOW_BLINK) { Some(false) } else { None }, blink_rapid: if all.contains(M::RAPID_BLINK) { Some(true) } else if sub.contains(M::RAPID_BLINK) { Some(false) } else { None }, reversed: if all.contains(M::REVERSED) { Some(true) } else if sub.contains(M::REVERSED) { Some(false) } else { None }, hidden: if all.contains(M::HIDDEN) { Some(true) } else if sub.contains(M::HIDDEN) { Some(false) } else { None }, crossed: if all.contains(M::CROSSED_OUT) { Some(true) } else if sub.contains(M::CROSSED_OUT) { Some(false) } else { None }, } } } impl From