Repository: eza-community/eza Branch: main Commit: 744d1664ce30 Files: 744 Total size: 1.5 MB Directory structure: gitextract_t9nlug2r/ ├── .config/ │ ├── cliff.toml │ └── treefmt.nix ├── .envrc ├── .git-blame-ignore-revs ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── compilation_error.md │ │ ├── config.yml │ │ ├── feature_request.md │ │ └── question.md │ ├── PULL_REQUEST_TEMPLATE/ │ │ └── pull_request_template.md │ ├── dependabot.yml │ └── workflows/ │ ├── apt.yml │ ├── unit-tests.yml │ ├── update-dependencies.sh │ ├── update-dependencies.yaml │ └── winget.yml ├── .gitignore ├── .pre-commit-config-non-nix.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── INSTALL.md ├── LICENSE.txt ├── LICENSES/ │ ├── CC-BY-4.0.txt │ ├── EUPL-1.2.txt │ └── MIT.txt ├── README.md ├── REUSE.toml ├── SECURITY.md ├── TESTING.md ├── benches/ │ └── my_benchmark.rs ├── build.rs ├── completions/ │ ├── bash/ │ │ └── eza │ ├── fish/ │ │ └── eza.fish │ ├── nush/ │ │ └── eza.nu │ ├── pwsh/ │ │ └── _eza.ps1 │ └── zsh/ │ └── _eza ├── deb.asc ├── deny.toml ├── devtools/ │ ├── deb-package.sh │ ├── dir-generator.sh │ ├── generate-timestamp-test-dir.sh │ └── generate-trycmd-test.sh ├── docs/ │ ├── tapes/ │ │ └── demo.tape │ └── theme.yml ├── flake.nix ├── justfile ├── man/ │ ├── eza.1.md │ ├── eza_colors-explanation.5.md │ └── eza_colors.5.md ├── nix/ │ ├── eza.nix │ └── trycmd.nix ├── powertest.yaml ├── rust-toolchain.toml ├── snap/ │ └── snapcraft.yaml ├── src/ │ ├── fs/ │ │ ├── dir.rs │ │ ├── dir_action.rs │ │ ├── feature/ │ │ │ ├── git.rs │ │ │ ├── mod.rs │ │ │ └── xattr.rs │ │ ├── fields.rs │ │ ├── file.rs │ │ ├── filter.rs │ │ ├── mod.rs │ │ ├── mounts/ │ │ │ ├── linux.rs │ │ │ ├── macos.rs │ │ │ └── mod.rs │ │ └── recursive_size.rs │ ├── info/ │ │ ├── filetype.rs │ │ ├── mod.rs │ │ └── sources.rs │ ├── lib.rs │ ├── logger.rs │ ├── main.rs │ ├── options/ │ │ ├── config.rs │ │ ├── dir_action.rs │ │ ├── error.rs │ │ ├── file_name.rs │ │ ├── filter.rs │ │ ├── mod.rs │ │ ├── parser.rs │ │ ├── stdin.rs │ │ ├── theme.rs │ │ ├── vars.rs │ │ └── view.rs │ ├── output/ │ │ ├── cell.rs │ │ ├── color_scale.rs │ │ ├── details.rs │ │ ├── escape.rs │ │ ├── file_name.rs │ │ ├── grid.rs │ │ ├── grid_details.rs │ │ ├── icons.rs │ │ ├── lines.rs │ │ ├── mod.rs │ │ ├── render/ │ │ │ ├── blocks.rs │ │ │ ├── filetype.rs │ │ │ ├── flags.rs │ │ │ ├── flags_bsd.rs │ │ │ ├── flags_windows.rs │ │ │ ├── git.rs │ │ │ ├── groups.rs │ │ │ ├── inode.rs │ │ │ ├── links.rs │ │ │ ├── mod.rs │ │ │ ├── octal.rs │ │ │ ├── permissions.rs │ │ │ ├── permissions_unix.rs │ │ │ ├── permissions_windows.rs │ │ │ ├── securityctx.rs │ │ │ ├── size.rs │ │ │ ├── times.rs │ │ │ └── users.rs │ │ ├── table.rs │ │ ├── time.rs │ │ └── tree.rs │ └── theme/ │ ├── default_theme.rs │ ├── lsc.rs │ ├── mod.rs │ └── ui_styles.rs └── tests/ ├── cli_tests.rs ├── cmd/ │ ├── absolute_file_all.stderr │ ├── absolute_file_all.stdout │ ├── absolute_file_all.toml │ ├── absolute_recurse_unix.stderr │ ├── absolute_recurse_unix.stdout │ ├── absolute_recurse_unix.toml │ ├── absolute_unix.stderr │ ├── absolute_unix.stdout │ ├── absolute_unix.toml │ ├── basic_all.stderr │ ├── basic_all.stdout │ ├── basic_all.toml │ ├── classify-hyperlink-width-50_nix_local.stderr │ ├── classify-hyperlink-width-50_nix_local.stdout │ ├── follow-symlinks_unix.stderr │ ├── follow-symlinks_unix.stdout │ ├── follow-symlinks_unix.toml │ ├── icons_all.stderr │ ├── icons_all.stdout │ ├── icons_all.toml │ ├── long_icons_always.stderr │ ├── long_icons_always.stdout │ ├── long_icons_always.toml │ ├── long_windows.stderr │ ├── long_windows.stdout │ ├── long_windows.toml │ ├── tree_unix.stderr │ ├── tree_unix.stdout │ └── tree_unix.toml ├── gen/ │ ├── inexistant_file_unix.stderr │ ├── inexistant_file_unix.stdout │ ├── inexistant_file_unix.toml │ ├── long_all_nix.stderr │ ├── long_all_nix.stdout │ ├── long_all_nix.toml │ ├── long_binary_bytes_unix.stderr │ ├── long_binary_bytes_unix.stdout │ ├── long_binary_bytes_unix.toml │ ├── long_blocksize_nix.stderr │ ├── long_blocksize_nix.stdout │ ├── long_blocksize_nix.toml │ ├── long_extended_nix.stderr │ ├── long_extended_nix.stdout │ ├── long_extended_nix.toml │ ├── long_file_size_unix.stderr │ ├── long_file_size_unix.stdout │ ├── long_file_size_unix.toml │ ├── long_git_nix.stderr │ ├── long_git_nix.stdout │ ├── long_git_nix.toml │ ├── long_git_repos_nix.stderr │ ├── long_git_repos_nix.stdout │ ├── long_git_repos_nix.toml │ ├── long_git_repos_no_status_nix.stderr │ ├── long_git_repos_no_status_nix.stdout │ ├── long_git_repos_no_status_nix.toml │ ├── long_grid_nix.stderr │ ├── long_grid_nix.stdout │ ├── long_grid_nix.toml │ ├── long_header_nix.stderr │ ├── long_header_nix.stdout │ ├── long_header_nix.toml │ ├── long_header_unix.stderr │ ├── long_header_unix.stdout │ ├── long_header_unix.toml │ ├── long_icons_nix.stderr │ ├── long_icons_nix.stdout │ ├── long_icons_nix.toml │ ├── long_links_recurse_unix.stderr │ ├── long_links_recurse_unix.stdout │ ├── long_links_recurse_unix.toml │ ├── long_nix.stderr │ ├── long_nix.stdout │ ├── long_nix.toml │ ├── long_octal_nix.stderr │ ├── long_octal_nix.stdout │ ├── long_octal_nix.toml │ ├── long_recurse_with_level_unix.stderr │ ├── long_recurse_with_level_unix.stdout │ ├── long_recurse_with_level_unix.toml │ ├── long_time_style_custom_non_recent_and_recent_nix.stderr │ ├── long_time_style_custom_non_recent_and_recent_nix.stdout │ ├── long_time_style_custom_non_recent_and_recent_nix.toml │ ├── long_time_style_custom_non_recent_empty_nix.stdout │ ├── long_time_style_custom_non_recent_empty_nix.toml │ ├── long_time_style_custom_non_recent_none_nix.stdout │ ├── long_time_style_custom_non_recent_none_nix.toml │ ├── long_time_style_custom_non_recent_only_nix.stderr │ ├── long_time_style_custom_non_recent_only_nix.stdout │ ├── long_time_style_custom_non_recent_only_nix.toml │ ├── long_time_style_custom_recent_empty_nix.stdout │ ├── long_time_style_relative_nix.stderr │ ├── long_time_style_relative_nix.stdout │ ├── long_time_style_relative_nix.toml │ ├── long_unix.stderr │ ├── long_unix.stdout │ ├── long_unix.toml │ ├── only_dir_recursive_long_unix.stderr │ ├── only_dir_recursive_long_unix.stdout │ ├── only_dir_recursive_long_unix.toml │ ├── only_dir_recursive_unix.stderr │ ├── only_dir_recursive_unix.stdout │ ├── only_dir_recursive_unix.toml │ ├── only_dir_unix.stderr │ ├── only_dir_unix.stdout │ ├── only_dir_unix.toml │ ├── only_file_unix.stderr │ ├── only_file_unix.stdout │ ├── only_file_unix.toml │ ├── ptest_19a666ddec321ae2.stderr │ ├── ptest_19a666ddec321ae2.stdout │ ├── ptest_2dbc2104ddb934cd.stderr │ ├── ptest_2dbc2104ddb934cd.stdout │ ├── ptest_4b30f7de50929327.stderr │ ├── ptest_4b30f7de50929327.stdout │ ├── ptest_4b7165c936488151.stderr │ ├── ptest_4b7165c936488151.stdout │ ├── ptest_82a40fefd797654.stderr │ ├── ptest_82a40fefd797654.stdout │ ├── ptest_a920233617a69b55.stderr │ ├── ptest_a920233617a69b55.stdout │ ├── ptest_f90d48e69d70b41.stderr │ ├── ptest_f90d48e69d70b41.stdout │ ├── recursive_long_unix.stderr │ ├── recursive_long_unix.stdout │ ├── recursive_long_unix.toml │ ├── recursive_unix.stderr │ ├── recursive_unix.stdout │ ├── recursive_unix.toml │ ├── tree_long_unix.stderr │ ├── tree_long_unix.stdout │ ├── tree_long_unix.toml │ ├── tree_unix.stderr │ ├── tree_unix.stdout │ └── tree_unix.toml ├── itest/ │ ├── a │ ├── b │ ├── c │ ├── d │ ├── e │ ├── exa/ │ │ └── sssssssssssssssssssssssssggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggsssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss/ │ │ └── Makefile │ ├── f │ ├── g │ ├── h │ ├── i │ ├── j │ ├── k │ ├── l │ ├── m │ ├── n │ ├── o │ ├── p │ ├── q │ └── vagrant/ │ ├── debug/ │ │ ├── .gitignore │ │ └── a │ ├── dev/ │ │ └── main.bf │ └── log/ │ └── run/ │ ├── run.log.text │ └── sps.log.text └── ptests/ ├── ptest_109771b99ff4bc2c.stderr ├── ptest_109771b99ff4bc2c.stdout ├── ptest_109771b99ff4bc2c.toml ├── ptest_10c50228fc1c6107.stderr ├── ptest_10c50228fc1c6107.stdout ├── ptest_10c50228fc1c6107.toml ├── ptest_1128756497bbcbce.stderr ├── ptest_1128756497bbcbce.stdout ├── ptest_1128756497bbcbce.toml ├── ptest_13234446d830648.stderr ├── ptest_13234446d830648.stdout ├── ptest_13234446d830648.toml ├── ptest_132ac9f159c3473e.stderr ├── ptest_132ac9f159c3473e.stdout ├── ptest_132ac9f159c3473e.toml ├── ptest_13c76530584f9804.stderr ├── ptest_13c76530584f9804.stdout ├── ptest_13c76530584f9804.toml ├── ptest_14c5df14f800bc28.stderr ├── ptest_14c5df14f800bc28.stdout ├── ptest_14c5df14f800bc28.toml ├── ptest_182085f8602ed2aa.stderr ├── ptest_182085f8602ed2aa.stdout ├── ptest_182085f8602ed2aa.toml ├── ptest_1889daad10054484.stderr ├── ptest_1889daad10054484.stdout ├── ptest_1889daad10054484.toml ├── ptest_19cf21a43f373b6.stderr ├── ptest_19cf21a43f373b6.stdout ├── ptest_19cf21a43f373b6.toml ├── ptest_1ab8279d631495b0.stderr ├── ptest_1ab8279d631495b0.stdout ├── ptest_1ab8279d631495b0.toml ├── ptest_1c1df011089efa5f.stderr ├── ptest_1c1df011089efa5f.stdout ├── ptest_1c1df011089efa5f.toml ├── ptest_1c990fbac951fdaf.stderr ├── ptest_1c990fbac951fdaf.stdout ├── ptest_1c990fbac951fdaf.toml ├── ptest_1e61d8c305ff6c48.stderr ├── ptest_1e61d8c305ff6c48.stdout ├── ptest_1e61d8c305ff6c48.toml ├── ptest_1ec7257c14635d03.stderr ├── ptest_1ec7257c14635d03.stdout ├── ptest_1ec7257c14635d03.toml ├── ptest_2092f19a0d4ea1a2.stderr ├── ptest_2092f19a0d4ea1a2.stdout ├── ptest_2092f19a0d4ea1a2.toml ├── ptest_2176b1b061cb7da2.stderr ├── ptest_2176b1b061cb7da2.stdout ├── ptest_2176b1b061cb7da2.toml ├── ptest_219303a91ed6056a.stderr ├── ptest_219303a91ed6056a.stdout ├── ptest_219303a91ed6056a.toml ├── ptest_219f7c8dfa0d0323.stderr ├── ptest_219f7c8dfa0d0323.stdout ├── ptest_219f7c8dfa0d0323.toml ├── ptest_2439b7d68089135b.stderr ├── ptest_2439b7d68089135b.stdout ├── ptest_2439b7d68089135b.toml ├── ptest_247e5acfcf9ba3a8.stderr ├── ptest_247e5acfcf9ba3a8.stdout ├── ptest_247e5acfcf9ba3a8.toml ├── ptest_283667fe5d9c3015.stderr ├── ptest_283667fe5d9c3015.stdout ├── ptest_283667fe5d9c3015.toml ├── ptest_2ba3ba45a3b3cc94.stderr ├── ptest_2ba3ba45a3b3cc94.stdout ├── ptest_2ba3ba45a3b3cc94.toml ├── ptest_2cccf6c2af490359.stderr ├── ptest_2cccf6c2af490359.stdout ├── ptest_2cccf6c2af490359.toml ├── ptest_2dcffc4f000d924e.stderr ├── ptest_2dcffc4f000d924e.stdout ├── ptest_2dcffc4f000d924e.toml ├── ptest_32870705e39ad648.stderr ├── ptest_32870705e39ad648.stdout ├── ptest_32870705e39ad648.toml ├── ptest_32e159e8f043025.stderr ├── ptest_32e159e8f043025.stdout ├── ptest_32e159e8f043025.toml ├── ptest_33a4632005ea6e7f.stderr ├── ptest_33a4632005ea6e7f.stdout ├── ptest_33a4632005ea6e7f.toml ├── ptest_3528aa3d0794ba5b.stderr ├── ptest_3528aa3d0794ba5b.stdout ├── ptest_3528aa3d0794ba5b.toml ├── ptest_36127172d1c48ad1.stderr ├── ptest_36127172d1c48ad1.stdout ├── ptest_36127172d1c48ad1.toml ├── ptest_365b1525fed70635.stderr ├── ptest_365b1525fed70635.stdout ├── ptest_365b1525fed70635.toml ├── ptest_36ff749946aa7b76.stderr ├── ptest_36ff749946aa7b76.stdout ├── ptest_36ff749946aa7b76.toml ├── ptest_38c83409ee57e2af.stderr ├── ptest_38c83409ee57e2af.stdout ├── ptest_38c83409ee57e2af.toml ├── ptest_391fb71023fbe78f.stderr ├── ptest_391fb71023fbe78f.stdout ├── ptest_391fb71023fbe78f.toml ├── ptest_39a4ecaff3909961.stderr ├── ptest_39a4ecaff3909961.stdout ├── ptest_39a4ecaff3909961.toml ├── ptest_39b4326562c3f75f.stderr ├── ptest_39b4326562c3f75f.stdout ├── ptest_39b4326562c3f75f.toml ├── ptest_3a8ed471ff49be49.stderr ├── ptest_3a8ed471ff49be49.stdout ├── ptest_3a8ed471ff49be49.toml ├── ptest_3b5b6fd2802ba8f9.stderr ├── ptest_3b5b6fd2802ba8f9.stdout ├── ptest_3b5b6fd2802ba8f9.toml ├── ptest_3bed5866e590e62a.stderr ├── ptest_3bed5866e590e62a.stdout ├── ptest_3bed5866e590e62a.toml ├── ptest_3f0c3a89d3fcf7d3.stderr ├── ptest_3f0c3a89d3fcf7d3.stdout ├── ptest_3f0c3a89d3fcf7d3.toml ├── ptest_3fe14fdeb5bf19de.stderr ├── ptest_3fe14fdeb5bf19de.stdout ├── ptest_3fe14fdeb5bf19de.toml ├── ptest_404fbc2fe3e5c85.stderr ├── ptest_404fbc2fe3e5c85.stdout ├── ptest_404fbc2fe3e5c85.toml ├── ptest_458d0cef9ea1a5b9.stderr ├── ptest_458d0cef9ea1a5b9.stdout ├── ptest_458d0cef9ea1a5b9.toml ├── ptest_469e79a86c2c874f.stderr ├── ptest_469e79a86c2c874f.stdout ├── ptest_469e79a86c2c874f.toml ├── ptest_4728f2d14d31f2ff.stderr ├── ptest_4728f2d14d31f2ff.stdout ├── ptest_4728f2d14d31f2ff.toml ├── ptest_4805a91da5df26.stderr ├── ptest_4805a91da5df26.stdout ├── ptest_4805a91da5df26.toml ├── ptest_4974d70325cb7550.stderr ├── ptest_4974d70325cb7550.stdout ├── ptest_4974d70325cb7550.toml ├── ptest_4b0ed60c44c669f.stderr ├── ptest_4b0ed60c44c669f.stdout ├── ptest_4b0ed60c44c669f.toml ├── ptest_4b30f7de50929327.stderr ├── ptest_4b30f7de50929327.stdout ├── ptest_4b30f7de50929327.toml ├── ptest_4b538407f6a872e8.stderr ├── ptest_4b538407f6a872e8.stdout ├── ptest_4b538407f6a872e8.toml ├── ptest_4b7165c936488151.stderr ├── ptest_4b7165c936488151.stdout ├── ptest_4b7165c936488151.toml ├── ptest_4e899e9b065acc8f.stderr ├── ptest_4e899e9b065acc8f.stdout ├── ptest_4e899e9b065acc8f.toml ├── ptest_4fd72fa9235ffc80.stderr ├── ptest_4fd72fa9235ffc80.stdout ├── ptest_4fd72fa9235ffc80.toml ├── ptest_4ff72fb4d6bc110e.stderr ├── ptest_4ff72fb4d6bc110e.stdout ├── ptest_4ff72fb4d6bc110e.toml ├── ptest_514bf873279385ba.stderr ├── ptest_514bf873279385ba.stdout ├── ptest_514bf873279385ba.toml ├── ptest_52961e9e4d3030fc.stderr ├── ptest_52961e9e4d3030fc.stdout ├── ptest_52961e9e4d3030fc.toml ├── ptest_55383760e4618c41.stderr ├── ptest_55383760e4618c41.stdout ├── ptest_55383760e4618c41.toml ├── ptest_56d755ade90650de.stderr ├── ptest_56d755ade90650de.stdout ├── ptest_56d755ade90650de.toml ├── ptest_5709b91eb3610886.stderr ├── ptest_5709b91eb3610886.stdout ├── ptest_5709b91eb3610886.toml ├── ptest_57a5aac99c0c821c.stderr ├── ptest_57a5aac99c0c821c.stdout ├── ptest_57a5aac99c0c821c.toml ├── ptest_581533c37ac03853.stderr ├── ptest_581533c37ac03853.stdout ├── ptest_581533c37ac03853.toml ├── ptest_585bdfd3218af4a0.stderr ├── ptest_585bdfd3218af4a0.stdout ├── ptest_585bdfd3218af4a0.toml ├── ptest_5ba3cfebbb42c1f9.stderr ├── ptest_5ba3cfebbb42c1f9.stdout ├── ptest_5ba3cfebbb42c1f9.toml ├── ptest_5bf846977eb5a96e.stderr ├── ptest_5bf846977eb5a96e.stdout ├── ptest_5bf846977eb5a96e.toml ├── ptest_5c5f8c58460e0026.stderr ├── ptest_5c5f8c58460e0026.stdout ├── ptest_5c5f8c58460e0026.toml ├── ptest_5d72b8a5ba66436b.stderr ├── ptest_5d72b8a5ba66436b.stdout ├── ptest_5d72b8a5ba66436b.toml ├── ptest_5eac3027be1d2909.stderr ├── ptest_5eac3027be1d2909.stdout ├── ptest_5eac3027be1d2909.toml ├── ptest_607792764dd84355.stderr ├── ptest_607792764dd84355.stdout ├── ptest_607792764dd84355.toml ├── ptest_62034c92edbb1244.stderr ├── ptest_62034c92edbb1244.stdout ├── ptest_62034c92edbb1244.toml ├── ptest_631e7c0eadc876e3.stderr ├── ptest_631e7c0eadc876e3.stdout ├── ptest_631e7c0eadc876e3.toml ├── ptest_66a65e6644555c05.stderr ├── ptest_66a65e6644555c05.stdout ├── ptest_66a65e6644555c05.toml ├── ptest_6796295d6420d03a.stderr ├── ptest_6796295d6420d03a.stdout ├── ptest_6796295d6420d03a.toml ├── ptest_69fb8da018a73af3.stderr ├── ptest_69fb8da018a73af3.stdout ├── ptest_69fb8da018a73af3.toml ├── ptest_6abf99c853aede16.stderr ├── ptest_6abf99c853aede16.stdout ├── ptest_6abf99c853aede16.toml ├── ptest_6d64a7584b621832.stderr ├── ptest_6d64a7584b621832.stdout ├── ptest_6d64a7584b621832.toml ├── ptest_6e6893c2c2254622.stderr ├── ptest_6e6893c2c2254622.stdout ├── ptest_6e6893c2c2254622.toml ├── ptest_6eabf01c1fcde44e.stderr ├── ptest_6eabf01c1fcde44e.stdout ├── ptest_6eabf01c1fcde44e.toml ├── ptest_7212f6096d7b2f6c.stderr ├── ptest_7212f6096d7b2f6c.stdout ├── ptest_7212f6096d7b2f6c.toml ├── ptest_74825603975f419c.stderr ├── ptest_74825603975f419c.stdout ├── ptest_74825603975f419c.toml ├── ptest_767aca4b412a3f3e.stderr ├── ptest_767aca4b412a3f3e.stdout ├── ptest_767aca4b412a3f3e.toml ├── ptest_79116932e9ca8b26.stderr ├── ptest_79116932e9ca8b26.stdout ├── ptest_79116932e9ca8b26.toml ├── ptest_7a449fd794fd3993.stderr ├── ptest_7a449fd794fd3993.stdout ├── ptest_7a449fd794fd3993.toml ├── ptest_7b9a096392df2be1.stderr ├── ptest_7b9a096392df2be1.stdout ├── ptest_7b9a096392df2be1.toml ├── ptest_7cad7af690128a39.stderr ├── ptest_7cad7af690128a39.stdout ├── ptest_7cad7af690128a39.toml ├── ptest_7d1cd636566df8cd.stderr ├── ptest_7d1cd636566df8cd.stdout ├── ptest_7d1cd636566df8cd.toml ├── ptest_80cd40f7a3947055.stderr ├── ptest_80cd40f7a3947055.stdout ├── ptest_80cd40f7a3947055.toml ├── ptest_818bdf865ff2e514.stderr ├── ptest_818bdf865ff2e514.stdout ├── ptest_818bdf865ff2e514.toml ├── ptest_825e60e73630f857.stderr ├── ptest_825e60e73630f857.stdout ├── ptest_825e60e73630f857.toml ├── ptest_83b42c3f144d78ba.stderr ├── ptest_83b42c3f144d78ba.stdout ├── ptest_83b42c3f144d78ba.toml ├── ptest_86f826124c5b4511.stderr ├── ptest_86f826124c5b4511.stdout ├── ptest_86f826124c5b4511.toml ├── ptest_8822968d21a463f5.stderr ├── ptest_8822968d21a463f5.stdout ├── ptest_8822968d21a463f5.toml ├── ptest_88f8528f0c592965.stderr ├── ptest_88f8528f0c592965.stdout ├── ptest_88f8528f0c592965.toml ├── ptest_89146337fb6b0967.stderr ├── ptest_89146337fb6b0967.stdout ├── ptest_89146337fb6b0967.toml ├── ptest_8becd3030ba5621c.stderr ├── ptest_8becd3030ba5621c.stdout ├── ptest_8becd3030ba5621c.toml ├── ptest_8cd9b0ae2930d704.stderr ├── ptest_8cd9b0ae2930d704.stdout ├── ptest_8cd9b0ae2930d704.toml ├── ptest_91d7b6efe549ede0.stderr ├── ptest_91d7b6efe549ede0.stdout ├── ptest_91d7b6efe549ede0.toml ├── ptest_91e89705f7a716a7.stderr ├── ptest_91e89705f7a716a7.stdout ├── ptest_91e89705f7a716a7.toml ├── ptest_94d98e7060506df0.stderr ├── ptest_94d98e7060506df0.stdout ├── ptest_94d98e7060506df0.toml ├── ptest_94ed50412bfc177f.stderr ├── ptest_94ed50412bfc177f.stdout ├── ptest_94ed50412bfc177f.toml ├── ptest_97958c59351ef010.stderr ├── ptest_97958c59351ef010.stdout ├── ptest_97958c59351ef010.toml ├── ptest_98d345bf337daf3f.stderr ├── ptest_98d345bf337daf3f.stdout ├── ptest_98d345bf337daf3f.toml ├── ptest_98e04e3185e9174c.stderr ├── ptest_98e04e3185e9174c.stdout ├── ptest_98e04e3185e9174c.toml ├── ptest_992337433f8c6594.stderr ├── ptest_992337433f8c6594.stdout ├── ptest_992337433f8c6594.toml ├── ptest_9ad652110670db05.stderr ├── ptest_9ad652110670db05.stdout ├── ptest_9ad652110670db05.toml ├── ptest_9b2a49fcb2a74cc7.stderr ├── ptest_9b2a49fcb2a74cc7.stdout ├── ptest_9b2a49fcb2a74cc7.toml ├── ptest_9c1d803a17fd05c9.stderr ├── ptest_9c1d803a17fd05c9.stdout ├── ptest_9c1d803a17fd05c9.toml ├── ptest_9d319b2ce5ecd989.stderr ├── ptest_9d319b2ce5ecd989.stdout ├── ptest_9d319b2ce5ecd989.toml ├── ptest_9ece18efb453f48d.stderr ├── ptest_9ece18efb453f48d.stdout ├── ptest_9ece18efb453f48d.toml ├── ptest_a139598e95d06a4b.stderr ├── ptest_a139598e95d06a4b.stdout ├── ptest_a139598e95d06a4b.toml ├── ptest_a689ab7558716dda.stderr ├── ptest_a689ab7558716dda.stdout ├── ptest_a689ab7558716dda.toml ├── ptest_a6bbf53a066c588e.stderr ├── ptest_a6bbf53a066c588e.stdout ├── ptest_a6bbf53a066c588e.toml ├── ptest_a71aaa46984bd23.stderr ├── ptest_a71aaa46984bd23.stdout ├── ptest_a71aaa46984bd23.toml ├── ptest_a78bf581d9095079.stderr ├── ptest_a78bf581d9095079.stdout ├── ptest_a78bf581d9095079.toml ├── ptest_a82ad7ec2e961f84.stderr ├── ptest_a82ad7ec2e961f84.stdout ├── ptest_a82ad7ec2e961f84.toml ├── ptest_a8c541050a307891.stderr ├── ptest_a8c541050a307891.stdout ├── ptest_a8c541050a307891.toml ├── ptest_aba07307b0f70090.stderr ├── ptest_aba07307b0f70090.stdout ├── ptest_aba07307b0f70090.toml ├── ptest_abc83ec759ddab6.stderr ├── ptest_abc83ec759ddab6.stdout ├── ptest_abc83ec759ddab6.toml ├── ptest_add56bbfa6f842d3.stderr ├── ptest_add56bbfa6f842d3.stdout ├── ptest_add56bbfa6f842d3.toml ├── ptest_af29d370729835d8.stderr ├── ptest_af29d370729835d8.stdout ├── ptest_af29d370729835d8.toml ├── ptest_b1cc7b6966ce28b3.stderr ├── ptest_b1cc7b6966ce28b3.stdout ├── ptest_b1cc7b6966ce28b3.toml ├── ptest_b20ec649e4099d19.stderr ├── ptest_b20ec649e4099d19.stdout ├── ptest_b20ec649e4099d19.toml ├── ptest_b4bb07ce512e09ba.stderr ├── ptest_b4bb07ce512e09ba.stdout ├── ptest_b4bb07ce512e09ba.toml ├── ptest_b86c3dd42089b8ae.stderr ├── ptest_b86c3dd42089b8ae.stdout ├── ptest_b86c3dd42089b8ae.toml ├── ptest_b8bfd122ac3a6a7f.stderr ├── ptest_b8bfd122ac3a6a7f.stdout ├── ptest_b8bfd122ac3a6a7f.toml ├── ptest_b9480d5b12bc2158.stderr ├── ptest_b9480d5b12bc2158.stdout ├── ptest_b9480d5b12bc2158.toml ├── ptest_bb087081fad71387.stderr ├── ptest_bb087081fad71387.stdout ├── ptest_bb087081fad71387.toml ├── ptest_bbfc26a93fbe15a7.stderr ├── ptest_bbfc26a93fbe15a7.stdout ├── ptest_bbfc26a93fbe15a7.toml ├── ptest_bc0980c681a1bf6e.stderr ├── ptest_bc0980c681a1bf6e.stdout ├── ptest_bc0980c681a1bf6e.toml ├── ptest_bc3ef3722b915c0a.stderr ├── ptest_bc3ef3722b915c0a.stdout ├── ptest_bc3ef3722b915c0a.toml ├── ptest_bda5e8cec0adaa3d.stderr ├── ptest_bda5e8cec0adaa3d.stdout ├── ptest_bda5e8cec0adaa3d.toml ├── ptest_c0b6a5f5211e052e.stderr ├── ptest_c0b6a5f5211e052e.stdout ├── ptest_c0b6a5f5211e052e.toml ├── ptest_c15429df79ff8a02.stderr ├── ptest_c15429df79ff8a02.stdout ├── ptest_c15429df79ff8a02.toml ├── ptest_c320d2ec8a647bec.stderr ├── ptest_c320d2ec8a647bec.stdout ├── ptest_c320d2ec8a647bec.toml ├── ptest_c5a06187ebc81e63.stderr ├── ptest_c5a06187ebc81e63.stdout ├── ptest_c5a06187ebc81e63.toml ├── ptest_caa4e824b02fa569.stderr ├── ptest_caa4e824b02fa569.stdout ├── ptest_caa4e824b02fa569.toml ├── ptest_cb765650e293bae4.stderr ├── ptest_cb765650e293bae4.stdout ├── ptest_cb765650e293bae4.toml ├── ptest_cf65adc4fe95ba6.stderr ├── ptest_cf65adc4fe95ba6.stdout ├── ptest_cf65adc4fe95ba6.toml ├── ptest_d1571a37ac82a799.stderr ├── ptest_d1571a37ac82a799.stdout ├── ptest_d1571a37ac82a799.toml ├── ptest_d604d143ec99b7fa.stderr ├── ptest_d604d143ec99b7fa.stdout ├── ptest_d604d143ec99b7fa.toml ├── ptest_d7c710df474a3770.stderr ├── ptest_d7c710df474a3770.stdout ├── ptest_d7c710df474a3770.toml ├── ptest_dbc0c8e8c3f9b147.stderr ├── ptest_dbc0c8e8c3f9b147.stdout ├── ptest_dbc0c8e8c3f9b147.toml ├── ptest_dc6b5f21bb23c27.stderr ├── ptest_dc6b5f21bb23c27.stdout ├── ptest_dc6b5f21bb23c27.toml ├── ptest_ddcc15459b874630.stderr ├── ptest_ddcc15459b874630.stdout ├── ptest_ddcc15459b874630.toml ├── ptest_ded586a10b97281e.stderr ├── ptest_ded586a10b97281e.stdout ├── ptest_ded586a10b97281e.toml ├── ptest_e4cf2a922377bee.stderr ├── ptest_e4cf2a922377bee.stdout ├── ptest_e4cf2a922377bee.toml ├── ptest_ee83bc6dc4504743.stderr ├── ptest_ee83bc6dc4504743.stdout ├── ptest_ee83bc6dc4504743.toml ├── ptest_f21d9a563d3e0f10.stderr ├── ptest_f21d9a563d3e0f10.stdout ├── ptest_f21d9a563d3e0f10.toml ├── ptest_f261ab10a0ea20f.stderr ├── ptest_f261ab10a0ea20f.stdout ├── ptest_f261ab10a0ea20f.toml ├── ptest_f2ea3bfaf96e639.stderr ├── ptest_f2ea3bfaf96e639.stdout ├── ptest_f2ea3bfaf96e639.toml ├── ptest_f33fd54762597c23.stderr ├── ptest_f33fd54762597c23.stdout ├── ptest_f33fd54762597c23.toml ├── ptest_f641eabbfb35f76f.stderr ├── ptest_f641eabbfb35f76f.stdout ├── ptest_f641eabbfb35f76f.toml ├── ptest_f79a790e130be075.stderr ├── ptest_f79a790e130be075.stdout ├── ptest_f79a790e130be075.toml ├── ptest_f90d48e69d70b41.stderr ├── ptest_f90d48e69d70b41.stdout ├── ptest_f90d48e69d70b41.toml ├── ptest_ffbdb9af2de10fa.stderr ├── ptest_ffbdb9af2de10fa.stdout └── ptest_ffbdb9af2de10fa.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .config/cliff.toml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 # git-cliff ~ default configuration file # https://git-cliff.org/docs/configuration # # Lines starting with "#" are comments. # Configuration options are organized into tables and keys. # See documentation for more information on available options. [changelog] # changelog header header = """ # Changelog\n """ # template for the changelog body # https://tera.netlify.app/docs body = """ {% if version %}\ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} {% else %}\ ## [unreleased] {% endif %}\ {% for group, commits in commits | group_by(attribute="group") %} ### {{ group | upper_first }} {% for commit in commits %} - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ {% endfor %} {% endfor %}\n """ # remove the leading and trailing whitespace from the template trim = true # changelog footer footer = """ """ [git] # parse the commits based on https://www.conventionalcommits.org conventional_commits = true # filter out the commits that are not conventional filter_unconventional = true # process each line of a commit as an individual commit split_commits = false # regex for preprocessing the commit messages commit_preprocessors = [ # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"}, # replace issue numbers ] # regex for parsing and grouping commits commit_parsers = [ { message = "^feat", group = "Features" }, { message = "^fix", group = "Bug Fixes" }, { message = "^docs", group = "Documentation" }, { message = "^perf", group = "Performance" }, { message = "^refactor", group = "Refactor" }, { message = "^style", group = "Styling" }, { message = "^test", group = "Testing" }, { message = "^chore\\(release\\): prepare for", skip = true }, { message = "^chore", group = "Miscellaneous Tasks" }, { body = ".*security", group = "Security" }, ] # protect breaking changes from being skipped due to matching a skipping commit_parser protect_breaking_commits = false # filter out the commits that are not matched by commit parsers filter_commits = false # glob pattern for matching git tags tag_pattern = "v[0-9]*" # regex for skipping tags skip_tags = "v0.1.0-beta.1" # regex for ignoring tags ignore_tags = "" # sort the tags topologically topo_order = false # sort the commits inside sections by oldest/newest order sort_commits = "oldest" # limit the number of commits included in the changelog. # limit_commits = 42 ================================================ FILE: .config/treefmt.nix ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 { projectRootFile = "Cargo.toml"; programs = { nixfmt.enable = true; # nix statix.enable = true; # nix static analysis deadnix.enable = true; # find dead nix code # TODO https://github.com/numtide/treefmt-nix/issues/343 #rustfmt.enable = true; # rust shellcheck.enable = true; # bash/shell taplo.enable = true; # toml yamlfmt.enable = true; # yaml }; settings = { formatter = { shellcheck = { includes = [ "*.sh" "./completions/bash/eza" ]; excludes = [ ".envrc" ]; }; taplo.excludes = [ "tests/ptests/*.toml" ]; yamlfmt.excludes = [ "./powertest.yaml" ]; }; }; } ================================================ FILE: .envrc ================================================ if has nix; then use flake . fi ================================================ FILE: .git-blame-ignore-revs ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 # This file contains a list of commits that are not likely what you # are looking for in a blame, such as mass reformatting or renaming. # You can set this file as a default ignore file for blame by running # the following command. # # $ git config blame.ignoreRevsFile .git-blame-ignore-revs # # To temporarily not use this file add # --ignore-revs-file="" # to your blame command. # # The ignoreRevsFile can't be set globally due to blame failing if the file isn't present. # To not have to set the option in every repository it is needed in, # save the following script in your path with the name "git-bblame" # now you can run # $ git bblame $FILE # to use the .git-blame-ignore-revs file if it is present. # # #!/usr/bin/env bash # repo_root=$(git rev-parse --show-toplevel) # if [[ -e $repo_root/.git-blame-ignore-revs ]]; then # git blame --ignore-revs-file="$repo_root/.git-blame-ignore-revs" $@ # else # git blame $@ # fi # treewide rustfmt https://github.com/eza-community/eza/pull/405 0e06409b07f060e3afe1c099c4c54e6504847ee0 # initial powertests https://github.com/eza-community/eza/pull/644/commits 2273e29bc006baeb76795ae40c7b8b76f61c6f26 4f949fc9bbb1e387b489ad841af56e7be448bef3 # Rustfmt related commits 29d009a439f43190ec617080e1afd96773264f81 be0c2d1ba1feebc9ed7b2f24b80619dbf8fdf143 313e602cde89b2ec8089b7588f57f4601579a249 2704e0a8499802b4b79f120349ac3bd41e02c208 c86edc54435a57466204bf41a0166c7b6f687aeb # Rust edition 2024 37ec91db827823cf32068ab930ffc83b790800b6 ================================================ FILE: .github/CODEOWNERS ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 # Lines starting with a '#' are comments. # Each line is a file pattern followed by one or more owners. # These owners will be the default owners for everything in the repository. # * @global-owner1 @global-owner2 # The 'docs/*' pattern will match files like # 'docs/getting-started.md' but not further nested files like # 'docs/build-app/troubleshooting.md'. # docs/* @doc-owner-team # You can also use email addresses if the user isn't on GitHub. # *.py admin@example.com # You can use a '*' at the end of a pattern to match all files # of a particular type. # *.* @all-file-types-owner # Order is important. The last matching pattern has the most precedence. # This means if a pull request touches both *.js and *.css files, # it will only request a review from @js-owner, not @css-owner. # *.js @js-owner # *.css @css-owner ================================================ FILE: .github/FUNDING.yml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 github: cafkafk ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Report a crash, runtime error, or invalid output in eza title: 'bug: ' labels: errors assignees: '' --- If eza does something unexpected, or its output looks wrong, or it displays an error on the screen, or if it outright crashes, then please include the following information in your report: - The version of eza being used (`eza --version`) - The command-line arguments you are using - Your shell and terminal - Your operating system and hardware platform If it’s a crash, please include the full text of the crash that gets printed to the screen. If you’re seeing unexpected behaviour, a screenshot of the issue will help a lot. --- ================================================ FILE: .github/ISSUE_TEMPLATE/compilation_error.md ================================================ --- name: Compilation error about: Report a problem compiling eza title: 'ci: ' labels: '' assignees: '' --- If eza fails to compile, or if there is a problem during the build process, then please include the following information in your report: - The exact eza commit you are building (`git rev-parse --short HEAD`) - The version of rustc you are compiling it with (`rustc --version`) - Your operating system and hardware platform - The Rust build target (the _exact_ output of `rustc --print cfg`) If you are seeing compilation errors, please include the output of the build process. --- ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 # # SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors # SPDX-FileCopyrightText: 2014 Benjamin Sago # SPDX-License-Identifier: MIT blank_issues_enabled: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Request a feature or enhancement to eza title: 'feat: ' labels: '' assignees: '' --- ================================================ FILE: .github/ISSUE_TEMPLATE/question.md ================================================ --- name: Question about: Ask a question about eza title: '' labels: question assignees: '' --- This should be posted in Q&A in discussions ================================================ FILE: .github/PULL_REQUEST_TEMPLATE/pull_request_template.md ================================================ ##### Description ##### How Has This Been Tested? ================================================ FILE: .github/dependabot.yml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 # # SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors # SPDX-FileCopyrightText: 2014 Benjamin Sago # SPDX-License-Identifier: MIT # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" - package-ecosystem: "github-actions" # Workflow files stored in the # default location of `.github/workflows` directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/apt.yml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 # # SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors # SPDX-FileCopyrightText: 2014 Benjamin Sago # SPDX-License-Identifier: MIT name: Apt Installation on: schedule: - cron: '0 0 * * *' push: branches: [main] paths: - 'deb.asc' - '.github/workflows/apt.yml' pull_request: branches: [main] paths: - 'deb.asc' - '.github/workflows/apt.yml' concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} cancel-in-progress: true jobs: apt_installation: runs-on: ubuntu-latest steps: - name: Install eza via apt repo run: | wget -qO- https://raw.githubusercontent.com/eza-community/eza/main/deb.asc | sudo tee /etc/apt/trusted.gpg.d/gierens.asc && \ echo "deb http://deb.gierens.de stable main" | sudo tee /etc/apt/sources.list.d/gierens.list && \ sudo apt update && \ sudo apt install -y eza - name: Run eza run: eza - name: Open man page run: man eza | cat ================================================ FILE: .github/workflows/unit-tests.yml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 name: Unit tests on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} cancel-in-progress: true env: CARGO_TERM_COLOR: always RUSTFLAGS: --deny warnings msrv: '1.90' jobs: security_audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: taiki-e/install-action@cargo-deny - name: Scan for vulnerabilities run: cargo deny check check_if_pr: runs-on: ubuntu-latest outputs: is_pr: ${{ steps.check.outputs.is_pr }} steps: - name: Check if it's a PR id: check run: | if [ "${{ github.event_name }}" == "pull_request" ]; then echo "is_pr=true" >> $GITHUB_OUTPUT else echo "is_pr=false" >> $GITHUB_OUTPUT fi no-merge-commits: needs: check_if_pr if: needs.check_if_pr.outputs.is_pr == 'true' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Run test uses: NexusPHP/no-merge-commits@v2.2.1 with: token: ${{ secrets.GITHUB_TOKEN }} conventional: needs: [check_if_pr, no-merge-commits] name: Conventional Commits runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: webiny/action-conventional-commits@v1.3.0 unit-tests: needs: conventional runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.rust == 'nightly' }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] rust: [msrv, stable, beta, nightly] steps: - name: Checkout repository uses: actions/checkout@v6 - run: rustup toolchain install ${{ matrix.rust == 'msrv' && env.msrv || matrix.rust }} --profile minimal - uses: Swatinem/rust-cache@v2 - name: Install cargo-hack uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 command: cargo install cargo-hack --locked - name: Run rustfmt checks run: cargo fmt --check - name: Run clippy lints if: ${{ matrix.os != 'windows-latest' }} run: cargo clippy -- -D warnings - name: Run unit tests run: cargo hack test unit-tests-freebsd: needs: conventional runs-on: ubuntu-22.04 timeout-minutes: 20 continue-on-error: ${{ matrix.rust == 'nightly' }} strategy: matrix: rust: [msrv, stable, beta, nightly] steps: - uses: actions/checkout@v6 with: submodules: recursive - name: Compile uses: vmactions/freebsd-vm@v1 with: release: '15.0' usesh: true prepare: | pkg install -y curl git curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain ${{ matrix.rust == 'msrv' && env.msrv || matrix.rust }} --profile minimal -y . ~/.cargo/env cargo install cargo-hack git config --global --add safe.directory /home/runner/work/eza/eza run: | set -e . ~/.cargo/env export CARGO_TERM_COLOR="always" export RUSTFLAGS="--deny warnings" cargo fmt --check cargo clippy -- -D warnings cargo hack test unit-tests-netbsd: needs: conventional runs-on: ubuntu-22.04 timeout-minutes: 20 continue-on-error: ${{ matrix.rust == 'nightly' }} strategy: matrix: rust: [msrv, stable, beta, nightly] steps: - uses: actions/checkout@v6 with: submodules: recursive - name: Compile uses: vmactions/netbsd-vm@v1 with: release: '10.1' usesh: true prepare: | PATH="/root/.cargo/bin:/usr/pkg/sbin:/usr/pkg/bin:$PATH" PKG_PATH="https://ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/amd64/10.1/All/" export PATH PKG_PATH /usr/sbin/pkg_add -uu curl git curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain ${{ matrix.rust == 'msrv' && env.msrv || matrix.rust }} --profile minimal -y . ~/.cargo/env cargo install cargo-hack git config --global --add safe.directory /home/runner/work/eza/eza run: | set -e . ~/.cargo/env export CARGO_TERM_COLOR="always" export RUSTFLAGS="--deny warnings" cargo fmt --check cargo clippy -- -D warnings cargo hack test unit-tests-openbsd: needs: conventional runs-on: ubuntu-22.04 timeout-minutes: 20 steps: - uses: actions/checkout@v6 with: submodules: recursive - name: Compile uses: vmactions/openbsd-vm@v1 with: release: '7.8' usesh: true prepare: | pkg_add rust rust-rustfmt rust-clippy git cargo install cargo-hack git config --global --add safe.directory /home/runner/work/eza/eza run: | set -e export CARGO_TERM_COLOR="always" # Do not treat warnings as failure, since OpenBSD lags behind "rustup" versions. # https://github.com/rust-lang/rustup/issues/2377 # https://github.com/rust-lang/rustup/issues/2168 # export RUSTFLAGS="--deny warnings" cargo fmt --check # cargo clippy -- -D warnings cargo hack test flake-check: needs: conventional name: Check Nix Flake runs-on: ubuntu-latest strategy: matrix: checks: [build, formatting, lint, pre-commit-check, test, trycmd] target: [x86_64-linux] steps: - uses: actions/checkout@v6 - name: Install Nix uses: DeterminateSystems/nix-installer-action@v17 - name: Nix Flake Check run: nix build .#checks.${{ matrix.target }}.${{ matrix.checks }} -L env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} flake-build: needs: conventional name: Build Nix package # if cross compilation is desired add 'aarch64-linux', 'x86_64-darwin' and 'aarch64-darwin' and fix the flake to support cross compilation. strategy: matrix: target: [x86_64-linux] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Install Nix uses: DeterminateSystems/nix-installer-action@v17 - name: Nix Build run: nix build .#packages.${{ matrix.target }}.default -L env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/update-dependencies.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: 2025 Christina Sørensen # # SPDX-License-Identifier: EUPL-1.2 set -euo pipefail commit_changes() { local file_to_check="$1" local commit_subject="$2" local commit_body="$3" # Check if the file has changes staged or unstaged if ! git diff --quiet --exit-code "$file_to_check"; then echo "$file_to_check has been updated. Committing changes." git add "$file_to_check" printf "%s\n\n%s" "$commit_subject" "$commit_body" | git commit -F - else echo "No changes to $file_to_check. Skipping commit." fi } BRANCH_NAME="deps_update_$(date --iso-8601)" if git rev-parse --verify "$BRANCH_NAME" >/dev/null 2>&1; then echo "Branch '$BRANCH_NAME' already exists. Checking out." git switch "$BRANCH_NAME" else git switch -c "$BRANCH_NAME" fi # 1. Update Cargo dependencies echo "Checking for Cargo dependency updates..." # Redirect stderr to stdout to capture cargo's output. CARGO_OUTPUT=$(cargo update --recursive 2>&1) UPDATED_CRATES=$(echo "$CARGO_OUTPUT" | grep 'Updating' || true) commit_changes "Cargo.lock" "build(deps): cargo bump $(date --iso-8601)" "$UPDATED_CRATES" # 2. Update Nix Flake dependencies echo "Checking for Nix Flake dependency updates..." # Use grep -A 2 to capture the 2 lines *after* the match. FLAKE_OUTPUT=$(nix flake update 2>&1) UPDATED_FLAKES=$(echo "$FLAKE_OUTPUT" | grep -A 2 'Updated input' || true) commit_changes "flake.lock" "build(deps): flake bump $(date --iso-8601)" "$UPDATED_FLAKES" echo "Dependency update process complete." git status ================================================ FILE: .github/workflows/update-dependencies.yaml ================================================ # SPDX-FileCopyrightText: 2025 Christina Sørensen # # SPDX-License-Identifier: EUPL-1.2 name: "Automated Dependency Bump" on: workflow_dispatch: schedule: - cron: '0 0 * * 4' jobs: update-and-create-pr: runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true steps: - name: "Checkout repository" uses: actions/checkout@v6 with: fetch-depth: 0 - name: "Install Nix" uses: cachix/install-nix-action@v22 with: nix_path: nixpkgs=channel:nixos-unstable - name: "Set up Git credentials" run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - name: "Run update script" id: run_script run: | chmod +x .github/workflows/update-dependencies.sh .github/workflows/update-dependencies.sh BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) echo "branch=${BRANCH_NAME}" >> $GITHUB_OUTPUT - name: "Create Pull Request" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh pr create \ --title "build(deps): Automatic dependency updates for $(date --iso-8601)" \ --body "This PR was automatically generated by a GitHub Action to update crate and flake dependencies. Please review the changes and merge." \ --base main \ --head ${{ steps.run_script.outputs.branch }} ================================================ FILE: .github/workflows/winget.yml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 # # SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors # SPDX-FileCopyrightText: 2014 Benjamin Sago # SPDX-License-Identifier: MIT name: Publish to Winget on: release: types: [released] jobs: publish: runs-on: ubuntu-latest steps: - uses: vedantmgoyal2009/winget-releaser@v2 with: identifier: eza-community.eza installers-regex: '-pc-windows-gnu\.zip$' token: ${{ secrets.WINGET_TOKEN }} ================================================ FILE: .gitignore ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 # Nix Flake stuff result # Generated by nix-pre-commit-hooks /.pre-commit-config.yaml # Direnv .direnv # Rust stuff target # Vagrant stuff .vagrant *.log # Compiled artifacts # (see devtools/*-package-for-*.sh) /eza-linux-x86_64 /eza-linux-x86_64-*.zip /eza-macos-x86_64 /eza-macos-x86_64-*.zip /eza_*.deb /MD5SUMS /SHA1SUMS # Snap stuff parts prime stage *.snap # VHS testing stuff out.gif tests/tmp ## Dynamically generated tests/test_dir # Miscenallous .idea ================================================ FILE: .pre-commit-config-non-nix.yaml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 repos: - repo: local hooks: - id: rust-linting name: Rust linting description: Run rustfmt on included files entry: cargo fmt -- types: [file, rust] language: system - id: rust-clippy name: Rust clippy description: Run clippy on included files pass_filenames: false entry: cargo clippy --workspace --all-targets --all-features types: [file, rust] language: system ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [0.23.4] - 2025-10-03 ### Bug Fixes - Update nixpkgs/cargo dependencies ## [0.23.3] - 2025-09-14 ### Documentation - Corrected "user_executable_file" option ### Features - Update flake inputs ### Miscellaneous Tasks - Update snapcraft manifest for core24 - Eza v0.23.3 changelogs, version bump ### Build - Update cargo deps ## [0.23.2] - 2025-09-06 ### Miscellaneous Tasks - Eza v0.23.2 changelogs, version bump ### Styling - Update nix style ### Build - Update cargo - Update flake ## [0.23.1] - 2025-08-31 ### Bug Fixes - Restore unsafe blocks for libc major/minor device id - `cargo deb` metadata to `LICENSE.txt` ### Documentation - Improve man page description of flags with default values - "default behavior" means eza's default behavior, not an option's default value ### Features - Added completion ### Miscellaneous Tasks - Eza v0.23.1 changelogs, version bump ### Build - Cargo bump 2025-07-19 - Flake bump 2025-07-19 ## [0.23.0] - 2025-07-18 ### Bug Fixes - [**breaking**] Make --grid work when not in TTY - [**breaking**] Stdin behavior ### Documentation - Add `--smart-group` option to README ### Features - Add icons for changelog and todo files - Use CHANGES icon for CHANGELOG as well ### Miscellaneous Tasks - Remove unused dependency - Eza v0.23.0 changelogs, version bump ## [0.22.1] - 2025-07-12 ### Bug Fixes - Replace default_input_path check with "." check ### Documentation - "cheks" should be "checks" ### Features - [**breaking**] Define -d/--treat-dirs-as-files behavior, tests - Refresh icon set with new glyphs and additions ### Miscellaneous Tasks - Eza v0.22.0 changelogs, version bump - Eza v0.22.1 changelogs, version bump ### Build - Bump phf from 0.11.3 to 0.12.1 - Cargo bump 2025-07-03 - Flake bump 2025-07-03 - Bump windows-sys from 0.59.0 to 0.60.2 ### Ci - Automate dependency updates ## [0.21.6] - 2025-06-26 ### Documentation - Add missing --absolute option to man page ### Features - Add prettier icon for `.prettierrc.{json,json5,toml,yaml,yml}` ### Miscellaneous Tasks - Upgrade FreeBSD to 14.3-RELEASE in unit tests workflow - Eza v0.21.6 changelogs, version bump ### Build - Bump libc from 0.2.172 to 0.2.174 - Cargo bump 2025-06-26 - Flake bump 2025-06-26 ## [0.21.5] - 2025-06-20 ### Bug Fixes - Use OpenBSD 7.7 that has MSRV, instead of 7.6 - Excessive open file descriptors - Typo - Impl desirable behaviour for unreadables - Unreadables format style - Clippy warnings - Missing word in comment ### Miscellaneous Tasks - Eza v0.21.5 changelogs, version bump ### Styling - Fix various clippy warnings for rust 1.86 - Remove unnecessary semicolon ## [0.21.4] - 2025-05-30 ### Bug Fixes - Escape spaces in file path to make them work correctly - List inside working dir with `--list-dirs` and no path passed - Ignore incorrect Unicode path instead of crashing on Windows ### Miscellaneous Tasks - Add Visual Studio icon for *.suo - Add swift icon for *.xcplayground - Add dropbox icon - Eza v0.21.4 changelogs, version bump ### Refactor - Clippy lints ### Styling - Update formatting ### Build - Flake bump 2025-05-29 - Cargo bump 2025-05-29 - 1.81 -> 1.82 ## [0.21.3] - 2025-05-02 ### Features - Add support for .ipynb file icons ### Miscellaneous Tasks - Eza v0.21.3 changelogs, version bump ### Styling - Unbreak formatting ### Build - Bump DeterminateSystems/nix-installer-action from 16 to 17 - Cargo bump 2025-05-01 - Flake bump 2025-05-01 ## [0.21.2] - 2025-04-25 ### Bug Fixes - Make clippy work again ### Documentation - Update Fedora install instructions for Fedora 42 - Specify perf improvements ### Features - Remove dependency on once_cell ### Miscellaneous Tasks - Add icon for '.stowrc' files - Eza v0.21.2 changelogs, version bump ### Performance - Use a hashmap when possible for file extension matching ### Styling - Clean up glob matching code ### Build - Bump uutils_term_grid from 0.6.0 to 0.7.0 ## [0.21.1] - 2025-04-19 ### Bug Fixes - Don’t truncate branch name - Hi extension icon wasnt working as it was in the wrong aray ### Documentation - Update README.md - Add crates.io link for README.md badge ### Miscellaneous Tasks - Add MS DOS icon for *.com - Add ruby icon for config.ru, Gemfile, Gemfile.lock, procfile, rake, rakefile and change ruby icon - Add python icon for *.pxd and *.pyx - Add markdown icon for *.mdx - Add fsharp icon for *.f# and *.fsscript - Add clojure icon for *.cljc and *.edn - Eza v0.21.1 changelogs, version bump ### Build - Flake bump 2025-04-19 - Cargo bump 2025-04-19 ## [0.21.0] - 2025-03-31 ### Bug Fixes - Flake bump 2025-03-20 - Remove unnescesarry unsafe blocks for libc major/minor device id - Unwrap -> expect on libc deviceid calls - Formatting issue - Fix unused PermissionsPlus fields ### Miscellaneous Tasks - Eza v0.21.0 changelogs, version bump ### Build - Cargo deps 2025-03-20 - [**breaking**] Change MSRV 1.78.0 -> 1.81.0 - Bump flake deps 2025-03-30 - Bump cargo deps 2025-03-30 ## [0.20.24] - 2025-03-13 ### Bug Fixes - Make temp files visible on white background ### Documentation - More precise temp files color description ### Features - Add `.exercism` folder icon - Add `.ocamlinit` icon - Add `.opam` folder icon ### Miscellaneous Tasks - Add gcloud icon for .gcloudignore - Add vim icon for .gvimrc, _vimrc and _gvimrc - Add fennel icon for ~/.fennelrc and ~/.config/fennel/fennelrc - Eza v0.20.24 changelogs, version bump ### Build - Bump once_cell from 1.20.3 to 1.21.0 - Bump terminal_size from 0.4.1 to 0.4.2 - Bump serde from 1.0.218 to 1.0.219 - Bump chrono from 0.4.39 to 0.4.40 ## [0.20.23] - 2025-02-27 ### Bug Fixes - Add Pixi installation instructions ### Miscellaneous Tasks - Eza v0.20.23 changelogs, version bump ### Build - Bump libc from 0.2.169 to 0.2.170 - Bump serde from 1.0.217 to 1.0.218 - Bump log from 0.4.25 to 0.4.26 - Bump trycmd from 0.15.8 to 0.15.9 ### Ci - Remove magic nix cache ## [0.20.22] - 2025-02-20 ### Features - Add prettier icon for *.prettierignore - Add icon for *.hrl - Add photoshop icon for *.psb - Add eslint icon for .eslintignore - Add renovate icon for renovate.json - Add elixir icon for *.eex, *.leex and mix.lock ### Miscellaneous Tasks - Eza v0.20.22 changelogs, version bump ### Build - Bump once_cell from 1.20.2 to 1.20.3 ## [0.20.21] - 2025-02-13 ### Bug Fixes - Start publishing libgit arm builds ### Miscellaneous Tasks - Eza v0.20.21 changelogs, version bump ### Build - Add libgit alternative for arm builds ## [0.20.20] - 2025-02-07 ### Bug Fixes - Make `flake.lock` icon the nix logo ### Miscellaneous Tasks - Eza v0.20.20 changelogs, version bump ## [0.20.19] - 2025-01-30 ### Bug Fixes - Update MSRV to 1.78 to solve rust malfunction - Rustc false dead code positives - Rustc false positives in tests - Regression in theme config location, simplify path - Wrong file name for Brewfile ### Documentation - Add note regarding ci msrv update ### Features - Add `.norg` icon ### Miscellaneous Tasks - Eza v0.20.19 changelogs, version bump ### Build - Bump dirs from 5.0.1 to 6.0.0 - Bump NexusPHP/no-merge-commits from 2.1.0 to 2.2.1 - Bump flake - Bump cargo ### Ci - Fix and unify msrv and add matrix to free/netbsd ## [0.20.18] - 2025-01-23 ### Bug Fixes - Support additional yaml file extension, clippy ### Miscellaneous Tasks - Eza v0.20.18 changelogs, version bump ### Build - Cargo bump 2025-01-23 - Flake bump 2025-01-23 ### Ci - Use rust 1.74 instead of latest via rustup ## [0.20.17] - 2025-01-16 ### Features - Add editorconfig icon for .editorconfig ### Miscellaneous Tasks - Eza v0.20.17 changelogs, version bump ### Build - Cargo bump 2025-01-16 - Flake bump 2025-01-16 ## [0.20.16] - 2025-01-09 ### Features - Add brew icon for brewfile and brewfile.lock.json ### Miscellaneous Tasks - Eza v0.20.16 changelogs, version bump ### Build - Update flake inputs 2025-01-08 - Update cargo inputs 2025-01-08 - Bump git2 from 0.19.0 to 0.20.0 ## [0.20.15] - 2025-01-02 ### Features - Add icons from nerd fonts 3.3.0 release & more - Add new icons, extensive list ### Miscellaneous Tasks - Eza v0.20.15 changelogs, version bump ### Build - We switch to our own fork of natord ### Ci - Bump FreeBSD / NetBSD versions. ## [0.20.14] - 2024-12-26 ### Bug Fixes - ...those pesky workflow targets - Remove separate bsd tests - Remove audit workflow ### Features - Audit checks in main CI ### Miscellaneous Tasks - Eza v0.20.14 changelogs, version bump ### Refactor - Move eza, trycmd packages - Move BSD unit tests to main flow ### Styling - Format workflows - Ci checks formatted ### Build - Cargo 2024-12-25 ### Ci - Simplify - Let's just always run em' - Only run big checks on PRs to main - Faster flake checks... maybe? - Flakes on latest ubuntu - Only do no-merge-commits on PR ## [0.20.13] - 2024-12-18 ### Bug Fixes - Pre-commit-hooks.nix trying to be too clever - Remove stray description (originally from `--decay-mode`) ### Miscellaneous Tasks - Eza v0.20.13 changelogs, version bump ### Build - Update crate deps Mon Dec 16 - Update flake deps Mon Dec 16 ### Ci - Update to PRESENT DAY, PRESENT TIME ## [0.20.12] - 2024-12-11 ### Bug Fixes - Add unicode-3.0 license - Use safe terminal_size_of - Use terminal_size_of with borrowed raw handle ### Features - Move MSRV to 1.74 and deep bump cargo deps - Add Gleam lang icon ### Miscellaneous Tasks - Eza v0.20.12 changelogs, version bump ### Testing - Regen for 1.74 ### Build - Bump terminal_size from 0.3.0 to 0.4.1 ### Ci - Openbsd 7.4 -> 7.6 ## [0.20.11] - 2024-12-05 ### Bug Fixes - Bump libc from 0.2.165 to 0.2.167 ### Miscellaneous Tasks - Eza v0.20.11 changelogs, version bump ## [0.20.10] - 2024-11-28 ### Bug Fixes - People dislike the phrasing "maintained" on hackernews ### Miscellaneous Tasks - Eza v0.20.10 changelogs, version bump ### Build - Bump libc from 0.2.164 to 0.2.165 ## [0.20.9] - 2024-11-21 ### Bug Fixes - Remove newline after doc comment of `regen` recipe ### Miscellaneous Tasks - Eza v0.20.9 changelogs, version bump ### Refactor - List all recipes by default - Group related recipes ### Build - Bump libc from 0.2.162 to 0.2.164 - Bump DeterminateSystems/nix-installer-action from 15 to 16 ## [0.20.8] - 2024-11-14 ### Bug Fixes - Cross-compiling by updating to libz-sys to 1.1.20 ### Miscellaneous Tasks - Eza v0.20.8 changelogs, version bump ### Build - Bump palette from 0.7.5 to 0.7.6 - Bump libc from 0.2.161 to 0.2.162 - Bump serde from 1.0.214 to 1.0.215 ## [0.20.7] - 2024-11-07 ### Bug Fixes - Palette v0.7.6 -> v0.7.5 ### Miscellaneous Tasks - Update package.exclude list in Cargo.toml - Eza v0.20.7 changelogs, version bump ### Build - Bump DeterminateSystems/nix-installer-action from 14 to 15 - Bump serde_norway from 0.9.38 to 0.9.39 - Bump trycmd from 0.15.7 to 0.15.8 ## [0.20.6] - 2024-10-31 ### Bug Fixes - Changelog spelling ### Documentation - Fix typo `--get-repos-no-status` to `--git-repos-no-status` ### Miscellaneous Tasks - Eza v0.20.6 changelogs, version bump ### Build - Bump serde from 1.0.210 to 1.0.214 ## [0.20.5] - 2024-10-25 ### Bug Fixes - Ensure nested tree parts align under item name - Remove depricated `chrono` `from_timestamp_opt` ### Miscellaneous Tasks - Update generated test files - Eza v0.20.5 changelogs, version bump ### Build - Bump libc from 0.2.159 to 0.2.161 - Chrono v0.4.34 -> v0.4.38 ## [0.20.4] - 2024-10-18 ### Bug Fixes - Filetype, coloring for executables and folder ### Miscellaneous Tasks - Eza v0.20.4 changelogs, version bump ## [0.20.3] - 2024-10-17 ### Bug Fixes - Git cliff docs issue ### Miscellaneous Tasks - Eza v0.20.3 changelogs, version bump ### Performance - Reuse filetype from DirEntry ## [0.20.2] - 2024-10-09 ### Bug Fixes - Colors in old ms command prompt - Bring help text in line with available flags - Do not print parent filename with --absolute=on ### Miscellaneous Tasks - Add fox installation option - Eza v0.20.2 changelogs, version bump ### Build - Bump once_cell from 1.20.1 to 1.20.2 ## [0.20.1] - 2024-10-03 ### Bug Fixes - Release recipe - Support passing multiple options for generate-trycmd-test.sh - Move options into flags - Rustfmt errors ### Documentation - Fix cross-references - Update file type colors - Document that exit 13 == permission denied ### Features - Update just, add more formats - Recursively walk symlinks pointing at dirs - Add --follow-symlinks option - Add autocomplete for --follow-symlinks - Show directories last ### Miscellaneous Tasks - Eza v0.20.1 changelogs, version bump ### Testing - Add cases for -T and --follow-symlinks - Regenerate tests broken by line number changes ### Build - Fix manual version - Bump once_cell from 1.19.0 to 1.20.1 ### Ci - Remove flakehub, flakestry publish ## [0.20.0] - 2024-09-26 ### Bug Fixes - Flake trycmd bug - Pre-commit-hook taplo bug ### Documentation - Add link to eza-themes repository in readme - Cargo install dir inaccurate - Add x-cmd method to install eza - Adding a testing infos file to guide everyone through tests ### Features - Add `opml` file extension - Add a regen rule - [**breaking**] Relicensed to EUPL-1.2 ### Miscellaneous Tasks - Eza v0.20.0 changelogs, version bump ### Refactor - Move some files to `.config` - Release scripts use `.config` - Relicense to EUPL-1.2 ### Styling - Switch to nixfmt rfc style, format tree - Remove blank line ### Testing - Regenerate integration tests - Regenerate tests ### Build - Darwin devShell resuse eza deps - Ensure flake inputs aren't duplicated' - Remove semnix deps - Bump flake lock 2024-09-26 - Removed unused flake follows - Add cargo to devShell - Add clippy to devShell - Use toolchain in devShell - Bump libc from 0.2.158 to 0.2.159 - Bump unicode-width from 0.1.13 to 0.2.0 ### Ci - Full nix3 command output in logs - Allow EUPL-1.2 - Unblock windows ## [0.19.4] - 2024-09-18 ### Bug Fixes - Remove non_alpha from percent encoding to fix hyprlinks ### Features - Pass from serde_yaml to serde_norway ### Miscellaneous Tasks - Eza v0.19.4 changelogs, version bump ## [0.19.3] - 2024-09-12 ### Bug Fixes - Convert empty space to %20 when render hyperlinks - Split commit workflows and run no-merge-commits only on PRs - Correct naming of commit related workflows ### Documentation - Better version bump commit summary ### Features - Add no-merge-commits job to commits workflow ### Miscellaneous Tasks - Rename justfile - Eza v0.19.3 changelogs, version bump ### Refactor - Rename conventional-commits workflow to commits ### Build - Bump DeterminateSystems/nix-installer-action from 13 to 14 - Bump DeterminateSystems/flake-checker-action from 8 to 9 - Bump actions/checkout from 3 to 4 - Bump libc from 0.2.155 to 0.2.158 - Bump nu-ansi-term from 0.50.0 to 0.50.1 ## [0.19.2] - 2024-09-05 ### Bug Fixes - Remove unnecessary map and make clippy happy - Adjust grid details for CI tests - Imports and merge conflicts - Rustfmt issues - Clippy issues - Revise UiStyles::plain to have no style at all - Pr reviews fixes for theme file - Selectively filter files when recursing #1101 - Fix typo in FromOverride impl - Add serde(default) to StyleOverride.foreground/background fields ### Documentation - Add Flox to INSTALL.md - Add ic for icon color to colors man page - Add further documentation about theme file ### Features - Add c++ module interfaces as source file types - Add icon field to UiStyles - Add ic key for UiStyles icon in set_exa - Add None as icon value in UiStyles.default_theme - Add icon function to FileNameColours trait - Implement FileNameColours.icon for Theme - Adjust FileName.paint to consider possible icon color - Begin implementation of config file - Allow writing default theme.yml file for eventual config file implementation - Theme file configuration base - Add IconOverrides struct and UiStyles.icon_overrides - Add icon_override function to FileNameColours trait - Implement FileNameColours.icon_override for Theme - Handle icon overrides in FileName.paint - Add example config for icon_overrides - Rename UiStyles.icon_override to icons and add Style field - Add shorthand aliases to StyleOverride variables - Add custom deserialize_color and use in StyleOverride - Outsource color_from_str function to make it testable ### Miscellaneous Tasks - Release eza v0.19.2 ### Refactor - Simplify icon style setting in FileName.paint - Make every setting optional with override layer - Simplify sample theme.yml - Formatting for rustfmt macro ### Styling - Fix clippy issue in FileName.paint - Apply rustfmt - Simplify from_str_radix calls to please clippy ### Testing - Add unit tests for color_from_str function ### Build - Bump windows-sys from 0.52.0 to 0.59.0 ### Ci - Allow MPL-2.0 ## [0.19.1] - 2024-08-28 ### Bug Fixes - FreeBSD build. - Typo ### Miscellaneous Tasks - Release eza v0.19.1 ### Build - Bump uzers from 0.12.0 to 0.12.1 ## [0.19.0] - 2024-08-08 ### Bug Fixes - [**breaking**] Implement `EZA_GRID_ROWS` grid details view minimum rows threshold ### Miscellaneous Tasks - Release eza v0.19.0 ## [0.18.24] - 2024-08-03 ### Bug Fixes - 1.80 breakage from time crate ### Miscellaneous Tasks - Release eza v0.18.24 ### Build - Bump time dependency ## [0.18.23] - 2024-07-25 ### Bug Fixes - Disable broken freebsd tests ### Documentation - Clear up confusion around ls ### Miscellaneous Tasks - Release eza v0.18.23 ### Build - Bump log from 0.4.21 to 0.4.22 - Bump DeterminateSystems/nix-installer-action from 12 to 13 - Bump plist from 1.6.1 to 1.7.0 ## [0.18.22] - 2024-07-18 ### Bug Fixes - Use NaiveDateTime::from_timestamp_opt instead of panicky From impl ### Features - Add non-nix pre-commit rustfmt and clippy hooks ### Miscellaneous Tasks - Release eza v0.18.22 ### Ci - Bump FreeBSD version. ## [0.18.21] - 2024-07-01 ### Bug Fixes - Fix missing line breaks in _eza ### Miscellaneous Tasks - Release eza v0.18.21 ## [0.18.20] - 2024-06-27 ### Features - Add --no-|show-symlinks flags for filtering output ### Miscellaneous Tasks - Release eza v0.18.20 ## [0.18.19] - 2024-06-20 ### Bug Fixes - Ship release binaries ### Miscellaneous Tasks - Release eza v0.18.19 ### Build - Bump git2 from 0.18.3 to 0.19.0 ## [0.18.18] - 2024-06-13 ### Features - Extend deny check in audit workflow to all - Add deny.toml and workflow file to audit workflow paths - Run on all features by default - Ask for shell and terminal in bug report template ### Miscellaneous Tasks - Release eza v0.18.18 ### Build - Bump unicode-width from 0.1.12 to 0.1.13 - Bump DeterminateSystems/flake-checker-action from 7 to 8 - Bump DeterminateSystems/nix-installer-action from 11 to 12 ## [0.18.17] - 2024-06-05 ### Features - Add icon for Nushell extension ### Miscellaneous Tasks - Release eza v0.18.17 ### Build - Bump trycmd from 0.15.1 to 0.15.2 - Bump libc from 0.2.154 to 0.2.155 ## [0.18.16] - 2024-05-16 ### Bug Fixes - Change windows-only imports to be windows-only ### Documentation - Replace decay with color-scale - Update INSTALL.md - Fix typo in `INSTALL.md` - Use 3 columns for packaging status badge ### Miscellaneous Tasks - Release eza v0.18.16 ### Build - Bump DeterminateSystems/flake-checker-action from 5 to 7 - Bump DeterminateSystems/nix-installer-action from 10 to 11 ## [0.18.15] - 2024-05-09 ### Bug Fixes - Correct command for latest tag in deb-package.sh ### Documentation - Add how to find man pages in terminal and online. Partly fixes #967 - Correct heading levels in markdown - Move heading out of collapsed section - Add some keywords for benefit of ctrl-f ### Features - Return to original commit at the end of deb-package.sh - Add optional tag argument to deb-package.sh ### Miscellaneous Tasks - Release eza v0.18.15 ## [0.18.14] - 2024-05-02 ### Features - Add icon for "cron.minutely" directory ### Miscellaneous Tasks - Release eza v0.18.14 ### Build - Bump uzers from 0.11.3 to 0.12.0 - Bump libc from 0.2.153 to 0.2.154 - Bump unicode-width from 0.1.11 to 0.1.12 - Bump palette from 0.7.5 to 0.7.6 ## [0.18.13] - 2024-04-25 ### Bug Fixes - Allow unused imports for freebsd - Checking for deref flag in file_name ### Features - Add scheme filetype and icons - Generate completion/manpage tarballs on release ### Miscellaneous Tasks - Release eza v0.18.13 ## [0.18.11] - 2024-04-19 ### Bug Fixes - Fix clippy lints - Enable the rule only for NetBSD. - Build aarch64, arm without libgit2 ### Miscellaneous Tasks - Release eza v0.18.11 ### Ci - Bump NetBSD version to 10.0 ## [0.18.10] - 2024-04-11 ### Bug Fixes - Bump trycmd from 0.15.0 to 0.15.1 ### Miscellaneous Tasks - Release eza v0.18.10 ### Build - Bump nu-ansi-term from 0.49.0 to 0.50.0 ## [0.18.9] - 2024-03-27 ### Features - Switch out ansiterm crate for nu_ansi_term ### Miscellaneous Tasks - Release eza v0.18.9 ### Build - Bump DeterminateSystems/nix-installer-action from 9 to 10 - Bump plist from 1.6.0 to 1.6.1 - Bump rayon from 1.9.0 to 1.10.0 - Bump git2 from 0.18.2 to 0.18.3 ## [0.18.8] - 2024-03-21 ### Bug Fixes - Avoid deprecation warnings - Rustfmt issues ### Features - Add fennel lang icon and associations ### Miscellaneous Tasks - Release eza v0.18.8 ## [0.18.7] - 2024-03-14 ### Bug Fixes - Bugfix to resolve absolute paths that are not symlinks ### Features - Add filetype and icon for .hh extension ### Miscellaneous Tasks - Release eza v0.18.7 ## [0.18.6] - 2024-03-06 ### Bug Fixes - NetBSD did not have fflagstostr and as such did not build properly - Fix total-size option - Add fortran to source filetypes - Fix absolute_path() for broken symlinks - Update line numbers in panic messages in tests ### Features - Add filetype and icon for age - Adding icons for graphql extensions - Add nim icons - Use fsharp icon for fsproj files (similar to cs/csproj) - Add new icons, diverse selection - Adding more haskell related icons - Adding more icons for docker specific files - Adding more dockerfile icons - Add --absolute flag - Add shell completions for --absolute flag ### Miscellaneous Tasks - Cleaning dirs - Release eza v0.18.6 ### Refactor - Port grid and grid-details to new uutils-term-grid ### Testing - Add integration tests and powertests for --absolute flag - Add directory symlink to tests/itest/ ### Build - Bump log from 0.4.20 to 0.4.21 - Bump rayon from 1.8.1 to 1.9.0 ### Ci - Add NetBSD to CI. - Fix warnings. - Add FreeBSD to CI. - Add OpenBSD to CI. ## [0.18.5] - 2024-02-29 ### Bug Fixes - Bump palette from 0.7.4 to 0.7.5 ### Miscellaneous Tasks - Release eza v0.18.5 ## [0.18.4] - 2024-02-22 ### Bug Fixes - Classification width should be taken into account with -F=auto ### Miscellaneous Tasks - Release eza v0.18.4 ### Build - Bump libc from 0.2.152 to 0.2.153 - Bump chrono from 0.4.33 to 0.4.34 - Bump trycmd from 0.14.20 to 0.15.0 ## [0.18.3] - 2024-02-15 ### Bug Fixes - Duplicates in shell completions ### Documentation - Add target arch to deb PPA installation for strict apt environments ### Miscellaneous Tasks - Release eza v0.18.3 ### Performance - Do not pre-compute MountInfo to reduce readlink calls ### Refactor - Use #[default] attribute instead of custom impl for enums ## [0.18.2] - 2024-02-08 ### Bug Fixes - Update libgit2 to 1.7.2 ### Miscellaneous Tasks - Release eza v0.18.2 ## [0.18.1] - 2024-02-08 ### Bug Fixes - Change shasum for main commit ### Documentation - Add manual installation section ### Miscellaneous Tasks - Release eza v0.18.1 ### Refactor - Replace scoped_threadpool with rayon ### Build - Add empty rustfmt to ensure project specific settings - Bump libc from 0.2.151 to 0.2.152 - Bump nick-fields/retry from 2 to 3 - Bump palette from 0.7.3 to 0.7.4 - Bump webiny/action-conventional-commits from 1.2.0 to 1.3.0 ## [0.18.0] - 2024-02-01 ### Features - [**breaking**] Add --classify=always,auto,never ### Miscellaneous Tasks - Remove rustfmt config file that has a nightly only option in favor of rustfmt skip directive which is already in place - Fix small typo in pull request template - Release eza v0.18.0 ### Refactor - Change cast to coertion, remove rustfmt skip and clippy lint ignore directives ### Testing - Regenerate classification related tests ### Build - Change flake inputs ## [0.17.3] - 2024-01-25 ### Bug Fixes - Remove version testing ### Miscellaneous Tasks - Avoid `unwrap()` by changing filter-then-map to `filter_map` - Release eza v0.17.3 ### Build - Bump shlex from 1.2.0 to 1.3.0 - Bump chrono from 0.4.31 to 0.4.33 - Bump trycmd from 0.14.19 to 0.14.20 ## [0.17.2] - 2024-01-20 ### Bug Fixes - Crash using --git-repos on unreadable dir - Crash using --git-repos on unreadable dir ### Miscellaneous Tasks - Release eza v0.17.2 ### Build - Add cargo-bump for releasing ## [0.17.1] - 2024-01-11 ### Bug Fixes - Offset widths in grid mode with utf8 filenames - Format the code - Unformat the code where needed - Format the code correctly this time - Redo everything from scratch - Stack overflow when '-laaR' are used - Stack overflow when '-laaR' is used ### Features - Add Fortran icons ### Miscellaneous Tasks - Adding blake3 to checksums - Release eza v0.17.1 ### Testing - Regenerate version tests... and others - Updated tests to fit new features ### Build - Add b3sum to devshell deps ## [0.17.0] - 2023-12-13 ### Bug Fixes - Add color scale mode to the bash completions - Add color scale mode to the fish completions - Quote symbolic links correctly when their destinations contain spaces ### Documentation - Modify documentation about custom time style ### Features - Add BSD file flags - Add Windows file attributes - [**breaking**] Support different custom time style for non-recent/recent files ### Miscellaneous Tasks - Release eza v0.17.0 ### Testing - Regen powertests - Regenerate - Add tests for non-recent/recent custom time style - Update powertest expected help message output ### Build - Update `flake.lock` - Bump DeterminateSystems/nix-installer-action from 8 to 9 - Bump once_cell from 1.18.0 to 1.19.0 - Bump libc from 0.2.150 to 0.2.151 ### Ci - Remove labeler ## [0.16.3] - 2023-12-07 ### Bug Fixes - Add bare git_repos fn if feature git is disabled - Fixing color of size unit - Color-scale broked size for colors ### Miscellaneous Tasks - Release eza v0.16.3 ### Testing - Fix powertests post-release ### Build - Bump percent-encoding from 2.3.0 to 2.3.1 - Bump actions/labeler from 4 to 5 ## [0.16.2] - 2023-11-30 ### Bug Fixes - Calculate width correctly when using grid icons & classify - Fix the windows build ### Miscellaneous Tasks - Release eza v0.16.2 ### Testing - Fix version tests ### Build - Bump webiny/action-conventional-commits from 1.1.0 to 1.2.0 - Bump DeterminateSystems/nix-installer-action from 7 to 8 - Bump windows-sys from 0.48.0 to 0.52.0 ## [0.16.1] - 2023-11-23 ### Bug Fixes - Don't panic with todo!() on inaccessible dir - Don't panic if the btime of a file is Err - Lifetime annotations and manpage/shell completion nits - Reflow help ### Features - Handle formatting and display of binary extended attributes. - Add netbsd and freebsd support for extended attributes ### Miscellaneous Tasks - Update flake inputs - Release eza v0.16.1 ### Testing - Vars mocking - Display and meta options - Filtering and sorting - Long view options - Regenerate `--help` tests ### Build - Sign release tags ## [0.16.0] - 2023-11-16 ### Bug Fixes - Fix cross compilation - Snap requires a base - Move `--smart-group` to long view options - Colo[u]r-scale didn't have a base value - Fix snapcraft.yaml formatting ### Documentation - Add comments for bzip variants - Added the fact that total-size is unix only ### Features - Add some file extensions - Abort on panic (saving 0.1 M) - Add powertest ### Miscellaneous Tasks - Ignore blame from #644 - Stabilize powertest version - Release eza v0.16.0 ### Testing - Implements tests using the generated directory - Powertests using generated testdirs - Add hashed versions of powertests ## [0.15.3] - 2023-11-09 ### Bug Fixes - Changed quote in --almost-all completion - [**breaking**] Remove Repo column when using --git-repos when no git repo - Reformat `help.rs` - Allow unused macro rule arms ### Documentation - Improve CONTRIBUTING.md, README.md - Improve README.md - Introduce INSTALL.md ### Features - Create EZA_ICONS_AUTO environment variable - Create EZA_ICONS_AUTO environment variable - Demo gif and gif generation recipe - Add ocaml icon filetypes - Add PRQL - Add `--color-scale` ### Miscellaneous Tasks - Add to CODEOWNERS file to make sure I get ping'd on files being touched - Add myself to codeowners to watch modifications on parsing - Improve the PR template - Release eza v0.15.3 ### Refactor - Remove commented out test code - Finalize `decay` -> `color_scale` ### Build - Refactor flake - Bump libc from 0.2.149 to 0.2.150 - Bump DeterminateSystems/nix-installer-action from 4 to 7 - Bump rustix from 0.38.13 to 0.38.21 ### Ci - Refactor pre-commit-hooks - Refactor publish workflow ## [0.15.2] - 2023-11-02 ### Bug Fixes - Correct width when --no-quotes is used - Clippy lint and add option to grid-details - --smart-group only works for current user ### Features - Add Typst to the recognized files ### Miscellaneous Tasks - Release eza v0.15.2 ### Refactor - Replace `lazy_static` with `once_cell` - Replace plain values with TextColours ### Testing - Added more content to the dir generator - Changed size of one of the files ## [0.15.1] - 2023-10-26 ### Bug Fixes - Only store top-level recursive dir size - Changed windows methods - Underscored unused windows variables - Added device for filesystem to hashmap - Don’t display target’s size if we’re not dereferencing - Display offset for filenames with spaces - Fix clippy warnings - Fix doc-tests on RecursiveSize - Fix dead_code warnings on Windows ### Documentation - Fix doc-tests formatting and address other documentation review requests ### Features - Add a new filetype for source code files - Add a new icons for source code files and other files - Support for displaying blocksize on directories ### Miscellaneous Tasks - Release eza v0.15.1 ### Refactor - Move total-size calculations to File - Add RecursiveSize type to simplify total-size calculation ## [0.15.0] - 2023-10-19 ### Bug Fixes - Reenable debug symbols in debug builds - Fmt, windows, and nix fixes - Reverted autofmt changes - Updated match indents - Changed flag name - Clippy lint - Merge conflict with main ### Documentation - Correct color option spellings - Added flag to readme - Added flag to man ### Features - Add option --smart-group - Add completions, man for --smart-group - Added recursive directory parser - Added flag to completions - Add icons=always,auto,never. dont display icons in a tty|piped - Fix auto value for colors and icons + documentation - [**breaking**] Remove --no-icons in favor of --icons=always,auto,never. default is auto ### Miscellaneous Tasks - Upgrade to uutils_term_grid from unmaintained term_grid - Release eza v0.15.0 ### Build - Bump DeterminateSystems/nix-installer-action from 5 to 6 ### Ci - Remove stalebot, is super annoying - Adjust test case to icons=auto (no icons should show due to tty) ## [0.14.2] - 2023-10-12 ### Bug Fixes - Comment out redundant static musl build - Refactor sed command to build manpages - Update additional completions for help, almost-all, dereference - Fix zsh completions ### Documentation - Add missing options to man page and CLI --help info ### Features - Add missing nu shell completions - Adding the EZA_OVERRIDE_GIT env var ### Miscellaneous Tasks - Release eza v0.14.2 ### Refactor - Use musl target for amd64 deb package - Directly use one "big" awk command ### Styling - Remove trailing spaces and trailing line ### Build - Bump libc from 0.2.148 to 0.2.149 - Bump DeterminateSystems/nix-installer-action from 4 to 5 ## [0.14.1] - 2023-10-08 ### Bug Fixes - Replace left-over exa in fish completion - Diabling static linked binaries due to segfault - Make os error 13 fail loud - Root group not painted as expected when eza used by root - Adjust change width calculations for hyperlink and classify ### Documentation - Correct CONTRIBUTING.md on commit message type - Fix typos - Add zsh with homebrew part to completions section - Installation on fedora updated ### Features - Add basic nushell completion file - Add codeowner for nu completions - Readded musl static bin as it works ### Miscellaneous Tasks - Release eza v0.14.1 ### Refactor - Align completions - Do not match for numbers and remove single-use fn - Consistent argument order ### Testing - Classify-hyperlink test case for width 50 - Move classify tests to local ### Build - Bump trycmd from 0.14.17 to 0.14.19 - Make checksums easier to copy-paste - Improve release automation - Fix version bump - Fix double echo - Automate gh release - Add `codegen-units = 1` and remove `opt-level = 3` - Add back `opt-level = 3` ### Ci - Treat warnings as errors ## [0.14.0] - 2023-10-02 ### Bug Fixes - Ignore refs for blame - Avoid unstable inner attributes - Merge conflict with main - Merge conflict with main - Fix manpage generation of default package - Changed dll icon - Changed readme and Added README icon - New R lang icon - README is sorted and formatted - Fix large_enum_variant warning with explanation - Query stdout terminal size to see if the output gose to a tty. - Use windows-specific API for terminal size query on windows - Add `windows-sys` dependency for targeting windows - Use `std::io::IsTerminal` to eliminate compatibility issue - Terminal size query should only check `stdout` - Prefix unused binding name with underscore ### Documentation - Add completions + manpage for --no-quotes flag - Leave nix install instructions open-ended - Adding termux section - Leave nix install instructions open-ended - Added the new colors option to the man - Documenting custom time-style - Time-format supporting custom formats - Updated man to add new colors - Description of `--color` in README, manpage, and completions - Change `color` to `colo[u]r` in the option description. ### Features - Add rustfmt.toml file to prevent flags.rs fmt on save - Add quotations around filenames with spaces. exa pr#1165 - Replace hardcoded version by version variable - Add header to colors-explanation page - Revise man rule to use for loop and insert version - Adding the possibility to change git-repos colors - [**breaking**] Separated root from other users - New Rust icon - Added bdf,psf icons - Added lib icon - Added Contacts,Favorites icons - Added home icon - Added fdmdownload icon - Added statically linked binaries ### Miscellaneous Tasks - Release 0.14.0 ### Refactor - Ignore options/flags.rs - Renamed and reintended some code - Reformatted a line ### Styling - Format some parts correctly ### Build - Bump unicode-width from 0.1.10 to 0.1.11 - Bump git2 from 0.18.0 to 0.18.1 - Temporarily disable aarch64-unknown-linux-gnu - Name static binaries ## [0.13.1] - 2023-09-25 ### Bug Fixes - Typo `this` -> `that` - Don’t show color when color is disabled - Respect spec on Windows and make it for with Konsole - Major and minor device on MacOS - Linux uses u32 for major/minor device numbers - Error for missed semicolon - More than 3 bools in a struct - Enable rustfmt by removing .rustfmt.toml which disables it - Replace rustfmt::skip on expressions because experimental - Remove unnecessary rustfmt::skip's in windows code - Add src/options/flags.rs to rustfmt.excludes - Left-over merge conflict in src/output/table ### Documentation - Update README.md - Update --mounts option to include MacOS - Documenting --only-files ### Features - Add EXA_COLOR bindings for un-themed items - Add EZA_ environment variables with fallback to EXA_ - Listing files only using '--only-files' flag - Add rustfmt check to unit-tests workflow ### Miscellaneous Tasks - Add completion for --only-fies (zsh,fish) - Release 0.13.1 ### Refactor - Fix rustfmt issues and place skips where needed - Reorder unit-tests to fmt, clippy and tests ### Styling - Formatted using treefmt - Fix clippy warning after rustfmt - Fix treefmt issues in options module - Reapply rustfmt after rebase from main ### Testing - Add unit tests for new style abbreviations - Regen git_repos_no_status - Test for listing files only ### Build - Bump actions/checkout from 2 to 4 - Bump chrono from 0.4.30 to 0.4.31 - Bump timeago from 0.4.1 to 0.4.2 - Bump libc from 0.2.147 to 0.2.148 - Bump terminal_size from 0.2.6 to 0.3.0 ### Ci - Added formatters to treefmt - Make various improvements - Only apply labels when opening a PR ## [0.13.0] - 2023-09-18 ### Bug Fixes - Crate can't contain broken symlink - Remove executable flag from fish completion file - Use proc_mounts only on linux - Hotfix harmful documentation - Fix hyperlinks on Windows - Needless_borrow - Nix flake check also builds the package - [**breaking**] Change number_huge and unit_huge to match the man page short codes ### Documentation - Added instructions to install completions of eza to the readme - Added cafkafk suggestions - Fix codeblocks in zsh completions - Update README.md - Add Winget install info - Link directly to space - Document new file type two letter codes in man page - Document filetypes theme and rename trait - Update deb instructions to use keyring - Fix chmod in deb installation instructions - Add potential gpg install to deb installation instructions - Add install instructions for Void Linux - Document dimmed and italic style codes - Document character style pairs in the code and match with man page - Documentation of 'sn' and 'sb' conflicted with later docs ### Features - Add completion files in deb packaging script - Adds filtering for Windows hidden files - Make file types themeable - Lazy loading of a files extended attributes and absolute path ### Miscellaneous Tasks - Augment gitter size in README - Release 0.13.0 ### Performance - Add criterion for benchmarking ### Refactor - Refactor just in crossfile - DRY up justfile - Ignore missing MSVC docker image - Removed unused imports, mark mods as allow unused - Format code - Move ALL_MOUNTS to fs::mounts - Migrate ALL_MOUNTS from lazy_static to OnceLock - Rename FileType::Immediate to more obvious FileType::Build ### Testing - Autogenerate testing dir - Stabalised unit-tests.yml - Autogenerate test dirs - Generate device files - Add unit tests that test both exa and ls style codes together - Address variable names ### Build - Set optlevel to 3 - Add musl binary for linux - Fix checksums - Add TODOs to targets ### Ci - Add Winget Releaser workflow - Add nix Flake check to flake.yml - Removed nix build in favor of nix flake check - Include bash completion script in treefmt and fixed shellcheck formatting in completion script - Fix spelling attemps -> attempts ## [0.12.0] - 2023-09-14 ### Bug Fixes - RUSTSEC-2020-0071 - Expand `--all` help - Generalize gitignore to ignore all eza deb packages - Change trycmd config to use test/itest folder for testing - Revert to old apt install command suggestion and add hint - Canonicalize errors when the destination of a symbolic link is bad - Handle other canonicalize errors in hyperlinks and git - Fix windows build when canonicalize returns an error - Is_some_and is an unstable Rust feature until 1.70 - Remove stray backslashes - Exit 13 on os error 13 - Rewrite comment - Improve trace strings - Tracing typo - Revert "Support for Windows Hidden Files" - Shellcheck warnings - Revert "Support for Windows Hidden Files" - Shellcheck warnings ### Documentation - Expand `--all` documentation - Add gentoo - Fix gentoo install - Add MacPorts install info - Add pthorpe92 gist - Add docs for --git-repos & --git-repos-no-status - Fix gpg armor flag for deb release in readme - Add scoop install info - Add Mac support for the --mount option in the man page - Add SAFETY comments to unsafe code blocks - Remove license from developemnt section - Update rust badge - Add better explanation of git repos + no status - Remove color specifications. change unknown git repo status to `~` - Fix missing color specification from man page - Add missing man page for debian release ### Features - Add audit workflow - Add trycmd as dev-dependency - Add minimal trycmd binary - Add a few trycmd tests as example - Add apt installation workflow - Support --mount option on Mac - Support --mount option on Mac - Adds filtering on Windows hidden files - Document and change output for --git-repos - Add PERMISSION_DENIED exit code - Adds filtering on Windows hidden files - Adds filtering on Windows hidden files - Added shellcheck to treefmt - Adds filtering on Windows hidden files ### Miscellaneous Tasks - Bump uzers to v0.11.3 - Bump chrono from 0.4.27 to 0.4.30 - Removal of xtests - Removal of vagrant - Remove deprecated devtools - [**breaking**] MSRV 1.70 - Run spellcheck - Release 0.12.0 ### Refactor - Over-engineer deb-package.sh - Hide xtests folder - Split trycmd into tests for all, unix and windows - Limit unit-tests run on workflow change to unit-tests itself - Add tracing to various code parts - Make std::process::exit global - Moved generateTest.sh to devtools/ - Renamed the file ### Revert - "Support for Windows Hidden Files" ### Styling - Remove TODO message on the absolute_path property - Fix shellcheck issues in deb-package.sh - Fix shellcheck issues in deb-package.sh - Fix shellcheck issues in deb-package.sh ### Testing - Remove vhs from flake - Remove vhs-runner files - Dump trycmd from nix sandbox - Fix name of trydump - Add trycmd - Add nix feature - Add example long tests for sandbox - Set itests files to unix epoch - Set itest files to unix epoch - Refactor setting unix epoch - Auto discard old definitions - Fix test reference - Add long_all_nix.toml - Add long_blocksize_nix.toml - Add long_extended_nix.toml - Add long_git_nix.toml - Add long_git_repos_nix.toml - Add long_git_repos_no_status_nix.toml - Add long_grid_nix.toml - Add long_header_nix.toml - Add long_icons_nix.toml - Add long_octal_nix.toml - Add long_time_style_relative_nix.toml - Freeze nix tests - Fix trydump when no files to delete - Adding more content to test - Modified unix and all tests - Regenerate nix tests - Convert windows tests with new itest dir - Fixed windows tests being wrong - Added a test generator - Add more unix_tests - Fixed unix tests to remove any distro specific - Removed git test breaking on nix - Remove non-deterministic test ### Build - Add compression, checksum gen for bin - Add deny.toml - Update flake.lock, cargo.lock - Remove org warnings - Remove itest - Update flake.lock - Add itest, idump - Make trycmd part of checks ### Ci - Don't use nix feature on ci - Fix windows build - 1.65 -> 1.70 - Enforce conventional commits - Enforce conventional commits ### Doc - Remove xtests section from readme - Add deprecation warning to xtests/readme - Add deprecation warning to just xtest commands - Add deprecation warning to vagrantfile - Add guidelines for commit messages ## [0.11.1] - 2023-09-11 ### Bug Fixes - Add vendored-libgit2 feature to git2 dependency - Filename escaping (last character lost sometimes, no hyperlink) - Build for Windows with chrono ### Documentation - Explain vendored-libgit2 - Add homebrew, misc fixes - Fix code of conduct link - Update archlinux - Remove broken dependabot link - Add informaton about lazy_static - Add star history - Add bright color options in man pages - Add bright color support in readme changelog ### Features - Add highlighting of mounted directories (Linux only) - Mark `.git` as ignored, which hides it when using `--git-ignore` - Expose git2 feature vendored-libgit2 - Add build commands to deb-package.sh - Add bright colour options, change punctuation default - Support the MSRV of Rust (1.65.0) - Use chrono crate to handle datetime-related features ### Miscellaneous Tasks - Bump actions/checkout from 3 to 4 - Release 0.11.1 ### Testing - Stabilize testing without sandbox - Disable gif rendering ### Build - Add release binaries - Fix binary gen - Add armhf binary ### Deps - Change ansi_term to ansiterm from rustadopt ## [0.11.0] - 2023-09-04 ### Bug Fixes - Add windows implementation of is_empty_dir - Re-align `--git-ignore` in help message - Avoid direnv error if nix isn't installed ### Documentation - Empty dir functions - Document is_empty_dir functions - Add function documentation for get_file_type and icon_for_file. ### Features - Optimize checking for empty directories when a directory has subdirectories - Use perfect hash tables for file types and icons - Add backlog of icons from various exa pull requests and others - Add backlog of icons from various exa issues ### Miscellaneous Tasks - Bump git2 from 0.17.2 to 0.18.0 - Bump uzers from 0.11.1 to 0.11.2 - Bump DeterminateSystems/flake-checker-action from 4 to 5 - Bump DeterminateSystems/nix-installer-action from 3 to 4 - Bump glob from 0.3.0 to 0.3.1 - Bump actions/stale from 5 to 8 - Bump terminal_size from 0.1.16 to 0.2.6 - Bump timeago from 0.3.1 to 0.4.1 - Release 0.11.0 ### Refactor - Use phf macros instead of codegen to create icon and filetype tables - Add constants for most of the commonly used icons - Add constants for the rest of icons used multiple times - Rename class FileExtension to FileTypeClassifier to better reflect the purpose - Move get_file_type to FileType enum ### Styling - Is_empty_dir() was put between the unix size() and windows size() ### Build - Use rust stable - Add unstable package - Disable clippy check 'unreadable_literal' in generated files ## [0.10.9] - 2023-08-28 ### Bug Fixes - Respect git-repos flags ### Documentation - Add badge for eza gitter/matrix room - Fix matrix link - Add ignored flags to readme - Add ignored flags to manual - Add ignored flags to help - Add ignored flags to xtest ### Features - `--no-git` option ### Miscellaneous Tasks - Add funding.yml - Release 0.10.9 ### Tree-wide - Fix Windows build ### Build - Add convco to dev ### Ci - Create flakehub-publish-tagged.yml - Add workflow_dispatch to flakehub-pub - Edit workflow_dispath - Refactor workflow_dispath - Refactor workflow_dispath - Remove broken dispatch - Add flakehub-backfill - Add codeowners - Add gierens as .deb codeowner - Add windows to CI ## [0.10.8] - 2023-08-22 ### Bug Fixes - TextCell building of detailed grid view for hyperlink and icon options - Block's Colours trait as for file sizes - --blocksize completion, new description - Option.views unit tests use --blocksize - Add missing colon before -w/--width - Replace exa by eza in help string - Change exa to eza in invalid option error - Add missing name section to eza_colors-explanation manpage - Replace exa by eza in .gitignore ### Documentation - Update issue templates - Cafkafk -> eza-community ### Features - Add git-ignored color/style option - Add `just` and `pandoc` to devShell bc they are necessary for man - Add `.envrc` so direnv automatically opens the nix dev environment - Match folder icon to reflect contents - Match folder icon to reflect contents - --blocksize completion, new description - Add script deb-package.sh ### Miscellaneous Tasks - Bump git2 from 0.16.1 to 0.17.2 - Bump unicode-width from 0.1.8 to 0.1.10 - Bump libc from 0.2.93 to 0.2.147 - Bump num_cpus from 1.13.0 to 1.16.0 - Release 0.10.8 ### Refactor - Fs::fields::Blocks - File::blocks() name, revise calculation - Rendering Blocksize like file sizes - Rename Blocks column to Blocksize - Use -S/--blocksize and, var BLOCKSIZE - Unit tests for output.render.blocks - Flip if (as suggested/demanded by clippy) - Migrate to uzers lib ### Build - Add charm to nix develop - Add tests/tmp to gitignore - Add initial tape - Add test runner sketch - Add test runner to justfile - Add out.gif to .gitignore - Add run_tests NAME arg - Add reference main.txt - Add gen_test - Fix typo - Handle arbitrary NAMES - Remove commented out code - Fix code formatting - Add vhs-runner main function - Gen_test support automatic gen - Automatic tape detection - Add print_msg with ansi color - Slight documentation/refactor - Use ansi output on all output - Disable vhs publish ad - Add better tracing - Remove defective sed - Add color variables - Add eza-long test - Add itest testing dir - Add parallel runner ### Ci - Help text in xtests - Nix flake check - Add labeler for flake - Add flake description ### Deps - Change users depedency to uzers ### Doc - Add git-ignore style/color information to manpage - --blocksize, new description - --blocksize, new description - --blocksize, new description - Add gpg public key for the deb repository - Add section about debian and ubuntu installation ### Git - Add deb package to .gitignore ## [0.10.7] - 2023-08-13 ### Bug Fixes - Respect GIT_CEILING_DIRECTORIES - MacOS flake support - Broken zsh completion syntax ### Features - Add gitlab-ci.yml - Improve icon for Earthfile - Better.ps1, add .psd1, .psm1 icons - Replace .bat icon by windows cli icon - Use TeX icons and add .bib, .bst icon - Use Ocaml logo, add .mli, .mll, .mly - Add many more icons - Add -w/--width to help string - Add -w/--width to README - Add -w/--width to flags - Add -w/--width to manpage - Fish -w/--width - Zsh -w/--width ### Miscellaneous Tasks - Add PR template - Bump log from 0.4.14 to 0.4.20 - Release 0.10.7 ### Refactor - GIT_DIR handling - Turn unused var into value - Fix borrowed trait implements required - Simplify format strings - Consistent style - Clippy::explicit_auto_deref - Clippy::explicit_auto_deref - Clippy::redundant_else - Clippy::manual_map - Clippy::semicolon_if_nothing_returned - Clippy::extra_unused_lifetimes - Allow clippy::wildcard_in_or_patterns - Clippy::uninlined_format_args - Allow Colours::new call with self - Clippy::explicit_iter_loop - Clippy::uninlined_format_args - Clippy::needless_late_init - Clippy::useless_conversion - Clippy::implicit_clone - Clippy::uninlined_format_args - Clippy::into-iter-on-ref - Clippy::semicolon_if_nothing_returned - Clippy::into_iter_on_ref - Clippy::needless_lifetimes - Clippy::uninlined_format_args - Trivial clippy lints - Clippy::semicolon_if_nothing_returned - Clippy::semicolon_if_nothing_returned - Clippy::manual_let_else - Clippy::semicolon_if_nothing_returned - Clippy::semicolon_if_nothing_returned - Clippy::uninlined_format_args - Clippy::manual_let_else - Clippy::manual_let_else - Clippy::manual_let_else - Clippy::manual_let_else - Clippy::manual_let_else - Fix trivial cast - Clippy::needless-borrow - TerminalWidth::deduce to -w/--width ### Ci - Create pull_request_template.md - Add clippy check - Add dependabot updater ### Doc - Create SECURITY.md - Create CONTRIBUTING.md ## [0.10.6] - 2023-08-07 ### Bug Fixes - Rename eza-colors-explanation - Exa -> eza in manpage ### Documentation - Adding --git-repos to help. ### Features - Use GIT_DIR env var to find the repo - Add color explanations ### Miscellaneous Tasks - Release 0.10.6 ### Doc - Add aur, nixpkgs installation ### Git - Use GIT_DIR env var to find the repo - Use open_from_env before discover ## [0.10.5] - 2023-08-03 ### Bug Fixes - Output wraps in terminal - Respect icon spacing ### Miscellaneous Tasks - Release 0.10.5 ## [0.10.4] - 2023-08-02 ### Bug Fixes - Syntax error ### Features - Added ".out" files for latex - Add changelog generation ### Miscellaneous Tasks - Release 0.10.4 ## [0.10.3] - 2023-07-31 ### Bug Fixes - More JPG extensions - Add compression icon to .tXX files #930 - Dereferencing linksfile size. - Dereferencing links users. - Dereferencing links groups. - Dereferencing links permissions. - Dereferencing links timestamps. - Add Svelte icon - Fish completion for -i/--inode option - Typo - Use eprintln instead - Use stderr on no timezone info - Bump openssl-src from 111.15.0+1.1.1k to 111.26.0+1.1.1u - Bump openssl-src from 111.15.0+1.1.1k to 111.26.0+1.1.1u - Changed bin name via cargo.toml - Change man pages to reffer to new binary name - Change completions to new binary name - Change completion file names - Change name to eza - Bump git2 from 0.13.20 to 0.16.1 - Fixed grid bug - Fixed grid bug - Bump rust to 1.71.0 - Take -a and -A equally serious - Changed default folder icon - Add clippy as part of the toolchain - Change license icon - Change gpg icons to keys - Add icon for ocaml (.ml extension) - Better license icon - Replace obsolete icons - Add Emacs icon for .el and org-mode for .org - Added icons for .rmeta - Add icon support for .mjs, .cjs, .mts, .cts files - Add webpack.config.cjs to immediate files list - .ipynb icon comment - Removed result - Update --version info - Sort is_immediate - Add flake, autoconf, cargo lock - Added trailing commas - Update snapscraft.yaml - Remove accidentally commited test files ### Feat - Add JPF to image filetype ### Features - Add support Typescript and ReasonML projects - New Icons and CLI argument to suppress icons - Add sty file - Add julia file extension icon - Add symlink dereferencing flag - Add -X/--dereference completions - Add -X/--dereference completions - Symlinks report their target's valid size - Update Cargo.toml to optimise binaries for size - Add status for git repos - Add selinux contexts support - Add -o shorcut to --octal-permissions - Hyperlink flag - Update Cargo.toml to optimise binaries for size - Add git-status-.* completions - Zsh add git-status-.* completions - Add git-status-.* completions - Add Zig module icons - Add icon for Vagrantfile - Add git icon to .gitignore_global file - Left align relative time - Add support for --time-style=relative - Add vim icon - Add justfile - Add pxm - Add compressed types - Add compressed icons ### Fixup - Split prefix tests by property ### Improve - Vim icon ### Makefile - Be compatible with BSD and OS X ### Miscellaneous Tasks - Update zoneinfo_compiled, datetime to 0.5 - Update users to 0.10 - PR feedback - Bump to v0.10.2 - Bump to v0.10.3 - Update cargo lock ### Refactor - Use shorthand fields - Removed commented code - Sorted file types, color table ### StatResult - :Path -> Dir ### Styling - Add icon for reStructuredText (src) files ### Testing - Change to /usr/bin/env bash ### ToStr - :to_str -> ToString::to_string ### Add - Mp2 audio format icon ### Build - Use binary name only ### Ci - Remove unused .github files - Remove unused .github files - Create unit-tests.yml - Create unit-tests.yml - Add trivial nix flake - Add treefmt, rust-toolchain, nixfmt - Add .#test, .#clippy, .#check - Add nix flake - Change branch - Bump rust to 1.71.0 - Automatically mark issues/PRs stale - Run tests when building with nix - Moving actions to dtolnay's version - Update Cargo.toml - Create labeler.yml - Add snap to labeler.yml - Add filetype.rs autolabel ### Details - `filter` is only used when recursing ### Doc - Add -X/--dereference flag - Change name in README.md - Add `nix run` to readme - Fix flow issue - Fix typos - Add mandatory snowflake emoji - Document nix flake development - Document nix flakew - Update README.md - Update README.md - Update README.md - Update README.md - Update README.md - Readme change screenshot to eza - Add CoC badge to readme - Add CODE_OF_CONDUCT.md - Add crates.io badge, license badge - Fix links - Update README.md - Update README.md ### Documentation - Add hint how to install exa on Android / Termux ### Git-feature - Display if a file is updated but unmerged (conflicted) ### Icons - Add Gentoo for .ebuild ### Io - :Result -> IOResult ### Src/main.rs - Remove clippy::unnested_or_patterns ### Vagrant - Update apt before installing ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## 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. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at: matrix: @cafkafk:nixos.dev All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## 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 eza If you'd like to contribute to eza, there are several things you should make sure to familiarize yourself with first. - Code of conduct [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md) - Requirement of conformance to [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - Requirement of conformance to [Semantic Versioning](https://semver.org/) - The [Security Policy](SECURITY.md) - [Free and Open Source (FOSS) software](https://www.gnu.org/philosophy/free-sw.en.html) ## Hacking on eza It is strongly recommended that you install Nix for hacking on eza. We leverage nix as a way to easily test and distribute eza to many users, and it allows us to provide multiple tools easily for developers. Instead of having to install each dependency manually and setting up a development environment, Nix allows you to use the same environment as the devs use. Therefore, it is useful that you have a version of Nix installed with the "experimental" feature flakes enabled. Further, to make hacking on eza as easy as possible for yourself, you'd do yourself a favor to install [direnv](https://direnv.net/). When you enter the eza repository, if you have `direnv` installed, you'll be prompted to allow it with `direnv allow`. Doing this will save you from having to manually enter the development environment each time you open the folder. If you don't have direnv installed however, you can run `nix develop` in a pinch, to enter the direnv. The development environment includes basic checks of conformance to conventional commits, cargo clippy lints, and much more. It also contains a pre-commit-hook making it a lot easier not to make potential mistakes that will unnecessarily delay getting your PRs accepted. Most importantly, it ensures your commits are conforming to conventional commits. Some useful commands include: - `nix flake check`: checks everything is correct. - `nix build`: build eza. - `nix build .#test`: runs eza's cargo tests - `nix build .#clippy`: runs clippy on eza - `nix fmt`: automatically formats your code as required by flake checks and pre-commit-hooks.nix - `just itest`: runs integration tests **For non-nix users,** There are traditional `pre-commit` hooks, which you can install with your system package manager or `brew|pip install pre-commit`, and run `pre-commit install -c .pre-commit-config-non-nix.yaml` in the root of the repository. Then these hooks will run automatically when you commit. The [just](https://github.com/casey/just) command runner can be used to run some helpful development commands, in a manner similar to `make`. Run `just --list` to get an overview of what’s available. To compile the manual pages, you will need [pandoc](https://pandoc.org/), which the nix flake should have installed for you. The `just man` command will compile the Markdown into manual pages, which it will place in the `target/man` directory. eza depends on [libgit2](https://github.com/rust-lang/git2-rs) for certain features. If you’re unable to compile libgit2, you can opt out of Git support by running `cargo build --no-default-features`. Again, the nix flake should have taken care of this for you, if not, please file an issue. If you intend to compile for musl, you will need to use the flag `vendored-openssl` if you want to get the Git feature working. The full command is `cargo build --release --target=x86_64-unknown-linux-musl --features vendored-openssl,git`. If you want more information on the tests please read: [testing on eza](https://github.com/eza-community/eza/blob/main/TESTING.md) ## Creating a PR First, use the pull request template. Please make sure that the thing you worked on... actually works. Make sure to also add how you ensured this in the PR description. Further, it's expected that you do your best to check for regressions. If your PR introduces a flag, you MUST: - Add completions for bash, zsh, fish, nushell - Add documentation to the man page - Add your option to the help flag - Add your option to the README.md Before submitting, you SHOULD have run `nix flake check` and ensured that all issues are addressed. For formatting issues, `nix fmt` will format the code for you. Most clippy issues can be resolved with `cargo clippy --fix` (although it might be educational to fix them yourself). If you have reuse issues, you can run the following command to annotate your code: Here are the absolute basics: - your commit summary MUST follow conventional commits. - your commits SHOULD be separated into small, logical chunks. - reviewers may ask you to rebase your commits into more sensible chunks. - your PR will need to pass CI and local `cargo test`. - you may be asked to refactor parts of your code by reviewers. Remember that no one here is an employee, and treat everyone with respect, as the code of conduct specifies. Also remember to be patient if it takes a while to get a response on your PR. Usually it doesn't, but there's only so many hours in a day, and if possible, there would be no delay. The delay alone is evidence of it's own necessity. ## Commit Messages A common commit message contains at least a summary and reference with closing action to the corresponding issue if any, and may also include a description and signature. ### Summary For you commit messages, please use the first line for a brief summary what the commit changes. Try to stay within the 72 char limit and prepend what type of change. See the following list for some guidance: - feat: adds a new feature to eza - feat(zsh): adds something to zsh completion - refactor: revises parts of the code - docs(readme): revise the README - docs(man): revision of the man pages - fix: bugfix in the code base - fix(ci): bugfix in the continuous integration - ... Note that this list is not complete and there may be cases where a commit could be characterized by different types, so just try to make your best guess. This spares the maintainers a lot of work when merging your PR. ### Description If you commit warrants it due to complexity or external information required to follow it, you should add a more detailed description of the changes, reasoning and also link external documentation if necessary. This description should go two lines below the summary and except for links stay in the 80 char limit. ### Issue Reference If the commit resolves an issue add: `Resolves: #abc` where `abc` is the issue number. In case of a bugfix you can also use `Fixes: #abc`. ### Signature You may add a signature at the end two lines below the description or issue reference. ### Example Here is an example of a commit message for a breaking change that follows these rules: ``` fix(hyperlinks)!: TextCell building of detailed grid view, hyperlink, icon options The hyperlink option adds an escape sequence which in the normal TextCell creation also becomes part of the length calculation. This patch applies the same logic the normal grid already did, by using the filenames bare width when a hyperlink is embedded. It also respects the ShowIcons option just like the normal grid view. BREAKING CHANGE: The style codes for huge file and units where documented to be `nt` and `ut` but the code was using `nh` and `uh`. The code has been updated to match the documented style codes. EXA_COLORS using style codes `nh` and `uh` will need to be updated to use `nt` and `ut`. Resolves: #129 Ref: #473, #319 Co-authored-by: 9glenda Signed-off-by: Christina Sørensen ``` ### Additional Examples - feat: add column selection - fix(output): fix width issue with columns - test(fs): add tests for filesystem metadata - feat!: breaking change / feat(config)!: implement config file - chore(deps): update dependencies ### Commit types - build: Changes that affect the build system or external dependencies (example libgit2) - ci: Changes to CI configuration files and scripts (example scopes: Nix, Vagrant, Docker) - chore: Changes which do not change source code or tests (example: changes to the build process, auxiliary tools, libraries) - docs: Documentation, README, completions, manpage only - feat: A new feature - fix: A bug fix - perf: A code change that improves or addresses a performance issue - refactor: A code change that neither fixes a bug nor adds a feature - revert: Revert something - style: Changes that do not affect the meaning of the code (example: clippy) - test: Adding missing tests or correcting existing tests ================================================ FILE: Cargo.toml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 [package] name = "eza" description = "A modern replacement for ls" authors = ["Christina Sørensen "] categories = ["command-line-utilities"] edition = "2024" rust-version = "1.90" exclude = [ "/docs/", "/devtools/", "/snap/", "/tests/", "/.config/", "/.github/", "/deb.asc", "/deny.toml", "/flake.*", "/justfile", "/powertest.yaml", "/rust-toolchain.toml", "/.envrc", "/.gitignore", "/.git-blame-ignore-revs", "/.pre-commit-config-non-nix.yaml", ] readme = "README.md" homepage = "https://github.com/eza-community/eza" license = "EUPL-1.2" repository = "https://github.com/eza-community/eza" version = "0.23.4" [package.metadata.deb] license-file = ["LICENSE.txt", "4"] depends = "$auto" extended-description = """ eza is a modern, maintained replacement for ls """ section = "utils" priority = "optional" assets = [ [ "target/release/eza", "/usr/bin/eza", "0755", ], [ "target/release/../man/eza.1", "/usr/share/man/man1/eza.1", "0644", ], [ "target/release/../man/eza_colors.5", "/usr/share/man/man5/eza_colors.5", "0644", ], [ "target/release/../man/eza_colors-explanation.5", "/usr/share/man/man5/eza_colors-explanation.5", "0644", ], [ "completions/bash/eza", "/usr/share/bash-completion/completions/eza", "0644", ], [ "completions/zsh/_eza", "/usr/share/zsh/site-functions/_eza", "0644", ], [ "completions/fish/eza.fish", "/usr/share/fish/vendor_completions.d/eza.fish", "0644", ], ] [[bin]] name = "eza" [dependencies] rayon = "1.10.0" chrono = { version = "0.4.40", default-features = false, features = ["clock"] } nu-ansi-term = { version = "0.50.1", features = [ "serde", "derive_serde_style", ] } glob = "0.3" libc = "0.2" locale = "0.2" log = "0.4" natord-plus-plus = "2.0" path-clean = "1.0.1" unit-prefix = "0.5.2" # palette 0.7.6 is broken: https://github.com/eza-community/eza/pull/1207 palette = { version = "=0.7.5", default-features = false, features = ["std"] } percent-encoding = "2.3.1" phf = { version = "0.13.1", features = ["macros"] } plist = { version = "1.7.0", default-features = false } uutils_term_grid = "0.7.0" terminal_size = "0.4.2" timeago = { version = "0.6.0", default-features = false } unicode-width = "0.2" ansi-width = "0.1.0" serde = { version = "1.0.219", features = ["derive"] } dirs = "6.0.0" serde_norway = "0.9" backtrace = "0.3" clap = { version = "4.5.38", features = ["cargo", "derive"] } [dependencies.git2] version = "0.20" optional = true default-features = false [target.'cfg(target_os = "linux")'.dependencies] proc-mounts = "0.3" [target.'cfg(unix)'.dependencies] uzers = "0.12.1" [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { version = "0.61.2", features = [ "Win32_System_Console", "Win32_Foundation", ] } [build-dependencies] chrono = { version = "0.4.40", default-features = false, features = ["clock"] } [dev-dependencies] criterion = { version = "0.8.2", features = ["html_reports"] } trycmd = "1.0" [features] default = ["git"] git = ["git2"] vendored-openssl = ["git2/vendored-openssl"] vendored-libgit2 = ["git2/vendored-libgit2"] # Should only be used inside of flake.nix nix = [] # Should only be used inside of flake.nix locally (not on CI) nix-local = [] # Should only be used inside of flake.nix # Shouldn't ever be used in CI (slow!) powertest = [] nix-generated = [] # use LTO for smaller binaries (that take longer to build) [profile.release] lto = true strip = true opt-level = 3 codegen-units = 1 panic = 'abort' [[bench]] name = "my_benchmark" harness = false ================================================ FILE: INSTALL.md ================================================ # Installation eza is available for Windows, macOS and Linux. ### Cargo (crates.io) ![Crates.io](https://img.shields.io/crates/v/eza?link=https%3A%2F%2Fcrates.io%2Fcrates%2Feza) If you already have a Rust environment set up, you can use the `cargo install` command: cargo install eza Cargo will build the `eza` binary and place it in your `CARGO_INSTALL_ROOT`. For more details on installation location see [the cargo book](https://doc.rust-lang.org/cargo/commands/cargo-install.html#description). ### Cargo (git) If you already have a Rust environment set up, you can use the `cargo install` command in your local clone of the repo: git clone https://github.com/eza-community/eza.git cd eza cargo install --path . Cargo will build the `eza` binary and place it in `$HOME/.cargo`. ### Arch Linux [![Arch Linux package](https://repology.org/badge/version-for-repo/arch/eza.svg)](https://repology.org/project/eza/versions) Eza is available in the [\[extra\]](https://archlinux.org/packages/extra/x86_64/eza/) repository of Arch Linux. ```bash pacman -S eza ``` ### Debian and Ubuntu Eza is available from [deb.gierens.de](http://deb.gierens.de). The GPG public key is in this repo under [deb.asc](/deb.asc). First make sure you have the `gpg` command, and otherwise install it via: ```bash sudo apt update sudo apt install -y gpg ``` Then install eza via: ```bash sudo mkdir -p /etc/apt/keyrings wget -qO- https://raw.githubusercontent.com/eza-community/eza/main/deb.asc | sudo gpg --dearmor -o /etc/apt/keyrings/gierens.gpg echo "deb [signed-by=/etc/apt/keyrings/gierens.gpg] http://deb.gierens.de stable main" | sudo tee /etc/apt/sources.list.d/gierens.list sudo chmod 644 /etc/apt/keyrings/gierens.gpg /etc/apt/sources.list.d/gierens.list sudo apt update sudo apt install -y eza ``` _Note_: In strict apt environments, you may need to add the target: `echo "deb [arch=amd64 signed-by=...` ### Nix (Linux, MacOS) [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/eza.svg)](https://repology.org/project/eza/versions) > **Note** > Installing packages imperatively isn't idiomatic Nix, as this can lead to [many issues](https://stop-using-nix-env.privatevoid.net/). Eza is available from [Nixpkgs](https://github.com/NixOS/nixpkgs) and from the flake in this repository. For `nix profile` users: ```shell nix profile install nixpkgs#eza ``` For `nix-env` users: ```shell nix-env -i eza ``` **Declarative Nix Installations** - Simple NixOS installation: [rfaulhaber/dotfiles](https://github.com/rfaulhaber/dotfiles/blob/a8d084d178efd0592b7ac02d34a450fb58913aca/nix/modules/programs/eza/default.nix#L15) - Using the flake via NixOS: [hallettj/home.nix](https://github.com/hallettj/home.nix/blob/a8388483e5d78e110be73c5af0e7f0e3ca8f8aa3/flake.nix#L19) - Using home-manager on NixOS: [Misterio77/nix-config](https://github.com/Misterio77/nix-config/blob/6867d66a2fe7899c608b9c8e5a8f9aee279d188b/home/misterio/features/cli/fish.nix#L6) ### Gentoo [![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/eza.svg)](https://repology.org/project/eza/versions) On Gentoo, eza is available as a package [`sys-apps/eza`](https://packages.gentoo.org/packages/sys-apps/eza): ```bash emerge --ask sys-apps/eza ``` ### openSUSE Eza is available at [openSUSE:Factory/eza](https://build.opensuse.org/package/show/openSUSE:Factory/eza): ```bash zypper ar https://download.opensuse.org/tumbleweed/repo/oss/ factory-oss zypper in eza ``` The preceding repository also contains the Bash, Fish, and Zsh completions. ### Fedora [![Fedora package](https://repology.org/badge/version-for-repo/fedora_39/rust:eza.svg)](https://repology.org/project/eza/versions) > ⚠️ **Note:** As of **Fedora 42**, `eza` is **no longer available** in the official Fedora repositories due to the absence of an active maintainer. > > If you're using Fedora 42 or newer, consider one of these options: > > - **Use a pre-built binary** from the [Releases](https://github.com/eza-community/eza/releases) page > - **Build from source** by following the [Cargo (git)](#cargo-git) instructions above > > 💬 Interested in helping? [Become a Fedora package maintainer](https://docs.fedoraproject.org/en-US/package-maintainers/) or reach out via [Matrix](https://matrix.to/#/#eza-community:gitter.im). For Fedora versions **prior to 42**, `eza` is available in the official repository: ```bash sudo dnf install eza ``` ### Void Linux [![Void Linux package](https://repology.org/badge/version-for-repo/void_x86_64/eza.svg)](https://repology.org/project/eza/versions) Eza is available as the [eza](https://github.com/void-linux/void-packages/tree/master/srcpkgs/eza) package in the official Void Linux repository. ```bash sudo xbps-install eza ``` ### Termux Eza is available as the [eza](https://github.com/termux/termux-packages/tree/master/packages/eza) package in the official Termux repository. ```bash pkg install eza ``` ### Manual (Linux) Example is for x86_64 GNU, replaces the file names if downloading for a different arch. ```shell wget -c https://github.com/eza-community/eza/releases/latest/download/eza_x86_64-unknown-linux-gnu.tar.gz -O - | tar xz sudo chmod +x eza sudo chown root:root eza sudo mv eza /usr/local/bin/eza ``` If `exa` was installed before, replace it with `eza`: ```shell sudo rm -f /usr/local/bin/exa sudo ln -s /usr/local/bin/eza /usr/local/bin/exa ``` ### Pixi (Linux, MacOS, and Windows) [![conda-forge package](https://img.shields.io/conda/vn/conda-forge/eza)](https://prefix.dev/channels/conda-forge/packages/eza) Eza is available in the conda-forge repository and can be installed using [Pixi](https://pixi.sh/latest/): ```shell pixi global install eza ``` ### Brew (MacOS) [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/eza.svg)](https://repology.org/project/eza/versions) Eza is available from [Homebrew](https://formulae.brew.sh/formula/eza#default). To install eza, run: ```shell brew install eza ``` ### MacPorts (macOS) [![MacPorts port](https://repology.org/badge/version-for-repo/macports/eza.svg)](https://repology.org/project/eza/versions) On macOS, eza is also available via [MacPorts](https://ports.macports.org/port/eza/). To install eza, run: ```shell sudo port install eza ``` ### Winget (Windows) [![Windows package](https://repology.org/badge/version-for-repo/winget/eza.svg)](https://repology.org/project/eza/versions) Eza is available on Winget. To install eza, run: ```shell winget install eza-community.eza ``` ### Scoop (Windows) [![Windows package](https://repology.org/badge/version-for-repo/scoop/eza.svg)](https://repology.org/project/eza/versions) Eza is available from [Scoop](https://scoop.sh/#/apps?q=eza&id=a52070d25f94bbcc884f80bef53eb47ed1268198). To install eza, run: ```shell scoop install eza ``` ### Flox (Linux, macOS, Windows WSL) Eza is available from [Flox](https://flox.dev). To install eza, run: ```shell flox install eza ``` ### X-CMD (Linux, macOS, Windows WSL, Windows GitBash) Eza is available from [x-cmd](https://www.x-cmd.com). To install eza, run: ```shell x env use eza # or x eza ``` ### fox (Linux, macOS) Eza is available from [fox](https://www.getfox.sh/). To install eza, run: ```shell fox install eza ``` ### Completions #### For zsh: > **Note** > Change `~/.zshrc` to your preferred zsh config file. ##### Clone the repository: ```sh git clone https://github.com/eza-community/eza.git ``` ##### Add the completion path to your zsh configuration: Replace `` with the actual path where you cloned the `eza` repository. ```sh echo 'export FPATH="/completions/zsh:$FPATH"' >> ~/.zshrc ``` ##### Reload your zsh configuration: ```sh source ~/.zshrc ``` #### For zsh with homebrew: In case zsh completions don't work out of the box with homebrew, add the following to your `~/.zshrc`: ```bash if type brew &>/dev/null; then FPATH="$(brew --prefix)/share/zsh/site-functions:${FPATH}" autoload -Uz compinit compinit fi ``` For reference: - https://docs.brew.sh/Shell-Completion#configuring-completions-in-zsh - https://github.com/Homebrew/brew/issues/8984 ================================================ FILE: LICENSE.txt ================================================ EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016 This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such use is covered by a right of the copyright holder of the Work). The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following notice immediately following the copyright notice for the Work: Licensed under the EUPL or has expressed by any other means his willingness to license under the EUPL. 1.Definitions In this Licence, the following terms have the following meaning: — ‘The Licence’:this Licence. — ‘The Original Work’:the work or software distributed or communicated by the Licensor under this Licence, available as Source Code and also as Executable Code as the case may be. — ‘Derivative Works’:the works or software that could be created by the Licensee, based upon the Original Work or modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in the country mentioned in Article 15. — ‘The Work’:the Original Work or its Derivative Works. — ‘The Source Code’:the human-readable form of the Work which is the most convenient for people to study and modify. — ‘The Executable Code’:any code which has generally been compiled and which is meant to be interpreted by a computer as a program. — ‘The Licensor’:the natural or legal person that distributes or communicates the Work under the Licence. — ‘Contributor(s)’:any natural or legal person who modifies the Work under the Licence, or otherwise contributes to the creation of a Derivative Work. — ‘The Licensee’ or ‘You’:any natural or legal person who makes any usage of the Work under the terms of the Licence. — ‘Distribution’ or ‘Communication’:any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential functionalities at the disposal of any other natural or legal person. 2.Scope of the rights granted by the Licence The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for the duration of copyright vested in the Original Work: — use the Work in any circumstance and for all usage, — reproduce the Work, — modify the Work, and make Derivative Works based upon the Work, — communicate to the public, including the right to make available or display the Work or copies thereof to the public and perform publicly, as the case may be, the Work, — distribute the Work or copies thereof, — lend and rent the Work or copies thereof, — sublicense rights in the Work or copies thereof. Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the applicable law permits so. In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed by law in order to make effective the licence of the economic rights here above listed. The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the extent necessary to make use of the rights granted on the Work under this Licence. 3.Communication of the Source Code The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to distribute or communicate the Work. 4.Limitations on copyright Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations thereto. 5.Obligations of the Licensee The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those obligations are the following: Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work to carry prominent notices stating that the Work has been modified and the date of modification. Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless the Original Work is expressly distributed only under this version of the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the Work or Derivative Work that alter or restrict the terms of the Licence. Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available for as long as the Licensee continues to distribute or communicate the Work. Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the copyright notice. 6.Chain of Authorship The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions to the Work, under the terms of this Licence. 7.Disclaimer of Warranty The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work and may therefore contain defects or ‘bugs’ inherent to this type of development. For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this Licence. This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work. 8.Disclaimer of Liability Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, the Licensor will be liable under statutory product liability laws as far such laws apply to the Work. 9.Additional agreements While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by the fact You have accepted any warranty or additional liability. 10.Acceptance of the Licence The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms and conditions. Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution or Communication by You of the Work or copies thereof. 11.Information to the public In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, by offering to download the Work from a remote location) the distribution channel or media (for example, a website) must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence and the way it may be accessible, concluded, stored and reproduced by the Licensee. 12.Termination of the Licence The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms of the Licence. Such a termination will not terminate the licences of any person who has received the Work from the Licensee under the Licence, provided such persons remain in full compliance with the Licence. 13.Miscellaneous Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the Work. If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid and enforceable. The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. New versions of the Licence will be published with a unique version number. All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take advantage of the linguistic version of their choice. 14.Jurisdiction Without prejudice to specific agreement between parties, — any litigation resulting from the interpretation of this License, arising between the European Union institutions, bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, — any litigation arising between other parties and resulting from the interpretation of this License, will be subject to the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. 15.Applicable Law Without prejudice to specific agreement between parties, — this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, resides or has his registered office, — this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside a European Union Member State. Appendix ‘Compatible Licences’ according to Article 5 EUPL are: — GNU General Public License (GPL) v. 2, v. 3 — GNU Affero General Public License (AGPL) v. 3 — Open Software License (OSL) v. 2.1, v. 3.0 — Eclipse Public License (EPL) v. 1.0 — CeCILL v. 2.0, v. 2.1 — Mozilla Public Licence (MPL) v. 2 — GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 — Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software — European Union Public Licence (EUPL) v. 1.1, v. 1.2 — Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+). The European Commission may update this Appendix to later versions of the above licences without producing a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the covered Source Code from exclusive appropriation. All other changes or additions to this Appendix require the production of a new EUPL version. ================================================ FILE: LICENSES/CC-BY-4.0.txt ================================================ Creative Commons Attribution 4.0 International Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. Creative Commons Attribution 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 – Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 – Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: A. reproduce and Share the Licensed Material, in whole or in part; and B. produce, reproduce, and Share Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 5. Downstream recipients. A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 – License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: A. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. Section 4 – Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 – Disclaimer of Warranties and Limitation of Liability. a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 – Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 – Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 – Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. ================================================ FILE: LICENSES/EUPL-1.2.txt ================================================ EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016 This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such use is covered by a right of the copyright holder of the Work). The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following notice immediately following the copyright notice for the Work: Licensed under the EUPL or has expressed by any other means his willingness to license under the EUPL. 1.Definitions In this Licence, the following terms have the following meaning: — ‘The Licence’:this Licence. — ‘The Original Work’:the work or software distributed or communicated by the Licensor under this Licence, available as Source Code and also as Executable Code as the case may be. — ‘Derivative Works’:the works or software that could be created by the Licensee, based upon the Original Work or modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in the country mentioned in Article 15. — ‘The Work’:the Original Work or its Derivative Works. — ‘The Source Code’:the human-readable form of the Work which is the most convenient for people to study and modify. — ‘The Executable Code’:any code which has generally been compiled and which is meant to be interpreted by a computer as a program. — ‘The Licensor’:the natural or legal person that distributes or communicates the Work under the Licence. — ‘Contributor(s)’:any natural or legal person who modifies the Work under the Licence, or otherwise contributes to the creation of a Derivative Work. — ‘The Licensee’ or ‘You’:any natural or legal person who makes any usage of the Work under the terms of the Licence. — ‘Distribution’ or ‘Communication’:any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential functionalities at the disposal of any other natural or legal person. 2.Scope of the rights granted by the Licence The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for the duration of copyright vested in the Original Work: — use the Work in any circumstance and for all usage, — reproduce the Work, — modify the Work, and make Derivative Works based upon the Work, — communicate to the public, including the right to make available or display the Work or copies thereof to the public and perform publicly, as the case may be, the Work, — distribute the Work or copies thereof, — lend and rent the Work or copies thereof, — sublicense rights in the Work or copies thereof. Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the applicable law permits so. In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed by law in order to make effective the licence of the economic rights here above listed. The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the extent necessary to make use of the rights granted on the Work under this Licence. 3.Communication of the Source Code The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to distribute or communicate the Work. 4.Limitations on copyright Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations thereto. 5.Obligations of the Licensee The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those obligations are the following: Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work to carry prominent notices stating that the Work has been modified and the date of modification. Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless the Original Work is expressly distributed only under this version of the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the Work or Derivative Work that alter or restrict the terms of the Licence. Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available for as long as the Licensee continues to distribute or communicate the Work. Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the copyright notice. 6.Chain of Authorship The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions to the Work, under the terms of this Licence. 7.Disclaimer of Warranty The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work and may therefore contain defects or ‘bugs’ inherent to this type of development. For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this Licence. This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work. 8.Disclaimer of Liability Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, the Licensor will be liable under statutory product liability laws as far such laws apply to the Work. 9.Additional agreements While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by the fact You have accepted any warranty or additional liability. 10.Acceptance of the Licence The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms and conditions. Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution or Communication by You of the Work or copies thereof. 11.Information to the public In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, by offering to download the Work from a remote location) the distribution channel or media (for example, a website) must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence and the way it may be accessible, concluded, stored and reproduced by the Licensee. 12.Termination of the Licence The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms of the Licence. Such a termination will not terminate the licences of any person who has received the Work from the Licensee under the Licence, provided such persons remain in full compliance with the Licence. 13.Miscellaneous Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the Work. If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid and enforceable. The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. New versions of the Licence will be published with a unique version number. All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take advantage of the linguistic version of their choice. 14.Jurisdiction Without prejudice to specific agreement between parties, — any litigation resulting from the interpretation of this License, arising between the European Union institutions, bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, — any litigation arising between other parties and resulting from the interpretation of this License, will be subject to the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. 15.Applicable Law Without prejudice to specific agreement between parties, — this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, resides or has his registered office, — this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside a European Union Member State. Appendix ‘Compatible Licences’ according to Article 5 EUPL are: — GNU General Public License (GPL) v. 2, v. 3 — GNU Affero General Public License (AGPL) v. 3 — Open Software License (OSL) v. 2.1, v. 3.0 — Eclipse Public License (EPL) v. 1.0 — CeCILL v. 2.0, v. 2.1 — Mozilla Public Licence (MPL) v. 2 — GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 — Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software — European Union Public Licence (EUPL) v. 1.1, v. 1.2 — Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+). The European Commission may update this Appendix to later versions of the above licences without producing a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the covered Source Code from exclusive appropriation. All other changes or additions to this Appendix require the production of a new EUPL version. ================================================ FILE: LICENSES/MIT.txt ================================================ MIT License Copyright (c) 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 ================================================
Special thanks to:

Warp sponsorship ### [Warp, the AI terminal for developers](https://www.warp.dev/eza) [Available for MacOS, Linux, & Windows](https://www.warp.dev/eza)
# eza A modern replacement for ls. Gitter [![Built with Nix](https://img.shields.io/badge/Built_With-Nix-5277C3.svg?logo=nixos&labelColor=73C3D5)](https://nixos.org) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) [![Unit tests](https://github.com/eza-community/eza/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/eza-community/eza/actions/workflows/unit-tests.yml) [![Crates.io](https://img.shields.io/crates/v/eza?link=https%3A%2F%2Fcrates.io%2Fcrates%2Feza)](https://crates.io/crates/eza) ![Crates.io](https://img.shields.io/crates/l/eza?link=https%3A%2F%2Fgithub.com%2Feza-community%2Feza%2Fblob%2Fmain%2FLICENCE)
![eza demo gif](docs/images/screenshots.png) --- **eza** is a modern alternative for the venerable file-listing command-line program `ls` that ships with Unix and Linux operating systems, giving it more features and better defaults. It uses colours to distinguish file types and metadata. It knows about symlinks, extended attributes, and Git. And it’s **small**, **fast**, and just **one single binary**. By deliberately making some decisions differently, eza attempts to be a more featureful, more user-friendly version of `ls`. --- **eza** features not in exa (non-exhaustive): - Fixes [“The Grid Bug”](https://github.com/eza-community/eza/issues/66#issuecomment-1656758327) introduced in exa 2021. - Hyperlink support. - Mount point details. - Selinux context output. - Git repo status output. - Human readable relative dates. - Several security fixes. - Support for `bright` terminal colours. - Many smaller bug fixes/changes! - Configuration `theme.yml` file for customization of colors and icons. ...and like, so much more that it became exhausting to update this all the time. Like seriously, we have a lot of good stuff. ---

Try it!

### Nix ❄️ If you already have Nix setup with flake support, you can try out eza with the `nix run` command: nix run github:eza-community/eza Nix will build eza and run it. If you want to pass arguments this way, use e.g. `nix run github:eza-community/eza -- -ol`. # Installation eza is available for Windows, macOS and Linux. Platform and distribution specific installation instructions can be found in [INSTALL.md](INSTALL.md). [![Packaging status](https://repology.org/badge/vertical-allrepos/eza.svg?columns=3)](https://repology.org/project/eza/versions) ---

Command-line options

eza’s options are almost, but not quite, entirely unlike `ls`’s. Quick overview: ## Display options
Click to expand - **-1**, **--oneline**: display one entry per line - **-G**, **--grid**: display entries as a grid (default) - **-l**, **--long**: display extended details and attributes - **-R**, **--recurse**: recurse into directories - **-T**, **--tree**: recurse into directories as a tree - **-x**, **--across**: sort the grid across, rather than downwards - **-F**, **--classify=(when)**: display type indicator by file names (always, auto, never) - **--colo[u]r=(when)**: when to use terminal colours (always, auto, never) - **--colo[u]r-scale=(field)**: highlight levels of `field` distinctly(all, age, size) - **--color-scale-mode=(mode)**: use gradient or fixed colors in --color-scale. valid options are `fixed` or `gradient` - **--icons=(when)**: when to display icons (always, auto, never) - **--hyperlink**: display entries as hyperlinks - **--absolute=(mode)**: display entries with their absolute path (on, follow, off) - **-w**, **--width=(columns)**: set screen width in columns
## Filtering options
Click to expand - **-a**, **--all**: show hidden and 'dot' files - **-d**, **--treat-dirs-as-files**: list directories like regular files - **-L**, **--level=(depth)**: limit the depth of recursion - **-r**, **--reverse**: reverse the sort order - **-s**, **--sort=(field)**: which field to sort by - **--group-directories-first**: list directories before other files - **--group-directories-last**: list directories after other files - **-D**, **--only-dirs**: list only directories - **-f**, **--only-files**: list only files - **--no-symlinks**: don't show symbolic links - **--show-symlinks**: explicitly show links (with `--only-dirs`, `--only-files`, to show symlinks that match the filter) - **--git-ignore**: ignore files mentioned in `.gitignore` - **-I**, **--ignore-glob=(globs)**: glob patterns (pipe-separated) of files to ignore Pass the `--all` option twice to also show the `.` and `..` directories.
## Long view options
Click to expand These options are available when running with `--long` (`-l`): - **-b**, **--binary**: list file sizes with binary prefixes - **-B**, **--bytes**: list file sizes in bytes, without any prefixes - **-g**, **--group**: list each file’s group - **--smart-group**: only show group if it has a different name from owner - **-h**, **--header**: add a header row to each column - **-H**, **--links**: list each file’s number of hard links - **-i**, **--inode**: list each file’s inode number - **-m**, **--modified**: use the modified timestamp field - **-M**, **--mounts**: Show mount details (Linux and MacOS only). - **-S**, **--blocksize**: show size of allocated file system blocks - **-t**, **--time=(field)**: which timestamp field to use - **-u**, **--accessed**: use the accessed timestamp field - **-U**, **--created**: use the created timestamp field - **-X**, **--dereference**: dereference symlinks for file information - **-Z**, **--context**: list each file’s security context - **-@**, **--extended**: list each file’s extended attributes and sizes - **--changed**: use the changed timestamp field - **--git**: list each file’s Git status, if tracked or ignored - **--git-repos**: list each directory’s Git status, if tracked - **--git-repos-no-status**: list whether a directory is a Git repository, but not its status (faster) - **--no-git**: suppress Git status (always overrides `--git`, `--git-repos`, `--git-repos-no-status`) - **--time-style**: how to format timestamps. valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, ‘`full-iso`’, ‘`relative`’, or a custom style ‘`+`’ (E.g., ‘`+%Y-%m-%d %H:%M`’ => ‘`2023-09-30 13:00`’. For more specifications on the format string, see the _`eza(1)` manual page_ and [chrono documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).). - **--total-size**: show recursive directory size - **--no-permissions**: suppress the permissions field - **-o**, **--octal-permissions**: list each file's permission in octal format - **--no-filesize**: suppress the filesize field - **--no-user**: suppress the user field - **--no-time**: suppress the time field - **--stdin**: read file names from stdin Some of the options accept parameters: - Valid **--colo\[u\]r** options are **always**, **automatic** (or **auto** for short), and **never**. - Valid sort fields are **accessed**, **changed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter sort uppercase before lowercase. The modified field has the aliases **date**, **time**, and **newest**, while its reverse has the aliases **age** and **oldest**. - Valid time fields are **modified**, **changed**, **accessed**, and **created**. - Valid time styles are **default**, **iso**, **long-iso**, **full-iso**, and **relative**. See the `man` pages for further documentation of usage. They are available - online [in the repo](https://github.com/eza-community/eza/tree/main/man) - in your terminal via `man eza`, as of version [`[0.18.13] - 2024-04-25`](https://github.com/eza-community/eza/blob/main/CHANGELOG.md#01813---2024-04-25)
## Custom Themes
Click to expand **Eza** has recently added support for a `theme.yml` file, where you can specify all of the existing theme-ing options available for the `LS_COLORS` and `EXA_COLORS` environment variables, as well as the option to specify different icons for different file types and extensions. Any existing environment variables set will continue to work and will take precedence for backwards compatibility. #### **New** Pre-made themes Check out the themes available in the official [eza-themes](https://github.com/eza-community/eza-themes) repository, or contribute your own. An example theme file is available in `docs/theme.yml`, and needs to either be placed in a directory specified by the environment variable `EZA_CONFIG_DIR`, or will looked for by default in `$XDG_CONFIG_HOME/eza`. Full details are available on the [man page](https://github.com/eza-community/eza/tree/main/man/eza_colors-explanation.5.md) and an example theme file is included [here](https://github.com/eza-community/eza/tree/main/docs/theme.yml)
# Hacking on eza If you wanna contribute to eza, firstly, you're expected to follow our [code of conduct](https://github.com/eza-community/eza/blob/main/CODE_OF_CONDUCT.md). After having understood the code of conduct, you can have a look at our [CONTRIBUTING.md](https://github.com/eza-community/eza/blob/main/CONTRIBUTING.md) for more info about actual hacking. [![Star History Chart](https://api.star-history.com/svg?repos=eza-community/eza&type=Date)](https://star-history.com/#eza-community/eza&Date) ================================================ FILE: REUSE.toml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # # SPDX-License-Identifier: EUPL-1.2 version = 1 SPDX-PackageName = "eza" SPDX-PackageSupplier = "Christina Sørensen " [[annotations]] path = "flake.lock" precedence = "aggregate" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" [[annotations]] path = "Cargo.lock" precedence = "aggregate" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" [[annotations]] path = ".envrc" precedence = "aggregate" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" [[annotations]] path = "tests/**" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" [[annotations]] path = "completions/**" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" [[annotations]] path = "man/**" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" [[annotations]] path = "deb.asc" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" [[annotations]] path = "devtools/**" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" [[annotations]] path = "docs/**" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" [[annotations]] path = ".github/**.md" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" # pre-commit-hooks.nix cause these to appear in commit check:( [[annotations]] path = ".cache/pre-commit/README" precedence = "aggregate" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" [[annotations]] path = ".cache/pre-commit/db.db" precedence = "aggregate" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" [[annotations]] path = ".gitconfig" precedence = "aggregate" SPDX-FileCopyrightText = "2024 Christina Sørensen" SPDX-License-Identifier = "EUPL-1.2" ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions This section shows which versions of eza are currently being supported with security updates. | Version | Supported | | ------- | ------------------ | | latest | :white_check_mark: | | < 0.10.6 | :x: | ## Reporting a Vulnerability Please email all vulnerabilities to christina@cafkafk.com, with PGP encryption and signature, and ideally send along plaintext public key or instructions on where to find public key (keyserver etc.). ================================================ FILE: TESTING.md ================================================ # Testing eza ## Running tests In order to run the tests in eza you need: - [just](https://github.com/casey/just) - [nix](https://nixos.org) then either run: - `just itest` - `nix build -L trycmd-local` ## Modifying tests In order to test your changes on eza, you will need to do one or multiple things in different cases. You will need the additional tool - [powertest](https://github.com/eza-community/powertest) You will also need to modify the `devtools/dir-generator.sh` file if you want to add some test cases ### You added/modified an option Please run `just regen` to regenerate powertesting. Then look into `tests/gen` or `tests/cmd` for any tests not passing ### You changed the output of eza Please run `nix build -L trydump` or `just idump` And lookout for any test no longer passing ================================================ FILE: benches/my_benchmark.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT use std::hint::black_box; use criterion::{Criterion, criterion_group, criterion_main}; pub fn criterion_benchmark(c: &mut Criterion) { c.bench_function("logger", |b| { b.iter(|| { eza::logger::configure(black_box(std::env::var_os(eza::options::vars::EZA_DEBUG))); }); }); } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); ================================================ FILE: build.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT /// The version string isn’t the simplest: we want to show the version, /// current Git hash, and compilation date when building *debug* versions, but /// just the version for *release* versions so the builds are reproducible. /// /// This script generates the string from the environment variables that Cargo /// adds () and runs `git` to /// get the SHA1 hash. It then writes the string into a file, which exa then /// includes at build-time. /// /// - /// - use std::env; use std::fs::File; use std::io::{self, Write}; use std::path::PathBuf; use chrono::prelude::*; /// The build script entry point. fn main() -> io::Result<()> { #![allow(clippy::write_with_newline)] let tagline = "eza - A modern, maintained replacement for ls"; let url = "https://github.com/eza-community/eza"; let ver = if is_debug_build() { format!( "{}\nv{} \\1;31m(pre-release debug build!)\\0m\n\\1;4;34m{}\\0m", tagline, version_string(), url ) } else if is_development_version() { format!( "{}\nv{} [{}] built on {} \\1;31m(pre-release!)\\0m\n\\1;4;34m{}\\0m", tagline, version_string(), git_hash(), build_date(), url ) } else { format!("{}\nv{}\n\\1;4;34m{}\\0m", tagline, version_string(), url) }; // We need to create these files in the Cargo output directory. let out = PathBuf::from(env::var("OUT_DIR").unwrap()); let path = &out.join("version_string.txt"); // Bland version text let mut f = File::create(path).unwrap_or_else(|_| panic!("{}", path.to_string_lossy().to_string())); writeln!(f, "{}", strip_codes(&ver))?; Ok(()) } /// Removes escape codes from a string. fn strip_codes(input: &str) -> String { input .replace("\\0m", "") .replace("\\1;31m", "") .replace("\\1;4;34m", "") } /// Retrieve the project’s current Git hash, as a string. fn git_hash() -> String { use std::process::Command; String::from_utf8_lossy( &Command::new("git") .args(["rev-parse", "--short", "HEAD"]) .output() .unwrap() .stdout, ) .trim() .to_string() } /// Whether we should show pre-release info in the version string. /// /// Both weekly releases and actual releases are --release releases, /// but actual releases will have a proper version number. fn is_development_version() -> bool { cargo_version().ends_with("-pre") || env::var("PROFILE").unwrap() == "debug" } /// Whether we are building in debug mode. fn is_debug_build() -> bool { env::var("PROFILE").unwrap() == "debug" } /// Retrieves the [package] version in Cargo.toml as a string. fn cargo_version() -> String { env::var("CARGO_PKG_VERSION").unwrap() } /// Returns the version and build parameters string. fn version_string() -> String { let mut ver = cargo_version(); let feats = nonstandard_features_string(); if !feats.is_empty() { ver.push_str(&format!(" [{}]", &feats)); } ver } /// Finds whether a feature is enabled by examining the Cargo variable. fn feature_enabled(name: &str) -> bool { env::var(format!("CARGO_FEATURE_{name}")) .map(|e| !e.is_empty()) .unwrap_or(false) } /// A comma-separated list of non-standard feature choices. fn nonstandard_features_string() -> String { let mut s = Vec::new(); if feature_enabled("GIT") { s.push("+git"); } else { s.push("-git"); } s.join(", ") } /// Formats the current date as an ISO 8601 string. fn build_date() -> String { let now = Local::now(); now.date_naive().format("%Y-%m-%d").to_string() } ================================================ FILE: completions/bash/eza ================================================ # shellcheck shell=bash _eza() { cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} case "$prev" in --help|-v|--version|--smart-group) return ;; --colour) mapfile -t COMPREPLY < <(compgen -W 'always automatic auto never' -- "$cur") return ;; --icons) mapfile -t COMPREPLY < <(compgen -W 'always automatic auto never' -- "$cur") return ;; -L|--level) mapfile -t COMPREPLY < <(compgen -W '{0..9}' -- "$cur") return ;; -s|--sort) mapfile -t COMPREPLY < <(compgen -W 'name filename Name Filename size filesize extension Extension date time modified changed accessed created type inode oldest newest age none --' -- "$cur") return ;; -t|--time) mapfile -t COMPREPLY < <(compgen -W 'modified changed accessed created --' -- "$cur") return ;; --time-style) mapfile -t COMPREPLY < <(compgen -W 'default iso long-iso full-iso relative +FORMAT --' -- "$cur") return ;; --color-scale) mapfile -t COMPREPLY < <(compgen -W 'all age size --' -- "$cur") return ;; --color-scale-mode) mapfile -t COMPREPLY < <(compgen -W 'fixed gradient --' -- "$cur") return ;; --absolute) mapfile -t COMPREPLY < <(compgen -W 'on follow off --' -- "$cur") return ;; esac case "$cur" in # _parse_help doesn’t pick up short options when they are on the same line than long options --*) # colo[u]r isn’t parsed correctly so we filter these options out and add them by hand parse_help=$(eza --help | grep -oE ' (--[[:alnum:]@-]+)' | tr -d ' ' | grep -v '\--colo') completions=$(echo '--color --colour --color-scale --colour-scale --color-scale-mode --colour-scale-mode' "$parse_help") mapfile -t COMPREPLY < <(compgen -W "$completions" -- "$cur") ;; -*) completions=$(eza --help | grep -oE ' (-[[:alnum:]@])' | tr -d ' ') mapfile -t COMPREPLY < <(compgen -W "$completions" -- "$cur") ;; *) _filedir ;; esac } && complete -o filenames -o bashdefault -F _eza eza ================================================ FILE: completions/fish/eza.fish ================================================ # Meta-stuff complete -c eza -s v -l version -d "Show version of eza" complete -c eza -l help -d "Show list of command-line options" # Display options complete -c eza -s 1 -l oneline -d "Display one entry per line" complete -c eza -s l -l long -d "Display extended file metadata as a table" complete -c eza -s G -l grid -d "Display entries in a grid" complete -c eza -s x -l across -d "Sort the grid across, rather than downwards" complete -c eza -s R -l recurse -d "Recurse into directories" complete -c eza -s T -l tree -d "Recurse into directories as a tree" complete -c eza -s X -l dereference -d "Dereference symbolic links when displaying file information" complete -c eza -s F -l classify -d "Display type indicator by file names" complete -c eza -l color \ -l colour -d "When to use terminal colours" -x -a " always\t'Always use colour' auto\t'Use colour if standard output is a terminal' automatic\t'Use colour if standard output is a terminal' never\t'Never use colour' " complete -c eza -l color-scale \ -l colour-scale -d "Highlight levels 'field' distinctly" -x -a " all\t'' age\t'' size\t'' " complete -c eza -l color-scale-mode \ -l colour-scale-mode \ -d "Use gradient or fixed colors in --color-scale" -x -a " fixed\t'Highlight based on fixed colors' gradient\t'Highlight based \'field\' in relation to other files' " complete -c eza -l icons -d "When to display icons" -x -a " always\t'Always display icons' auto\t'Display icons if standard output is a terminal' automatic\t'Display icons if standard output is a terminal' never\t'Never display icons' " complete -c eza -l no-quotes -d "Don't quote file names with spaces" complete -c eza -l hyperlink -d "Display entries as hyperlinks" complete -c eza -l follow-symlinks -d "Drill down into symbolic links that point to directories" complete -c eza -l absolute -d "Display entries with their absolute path" -x -a " on\t'Show absolute path for listed entries' follow\t'Show absolute path with followed symlinks' off\t'Do not show the absolute path' " complete -c eza -l smart-group -d "Only show group if it has a different name from owner" # Filtering and sorting options complete -c eza -l group-directories-first -d "Sort directories before other files" complete -c eza -l group-directories-last -d "Sort directories after other files" complete -c eza -l git-ignore -d "Ignore files mentioned in '.gitignore'" complete -c eza -s a -l all -d "Show hidden and 'dot' files. Use this twice to also show the '.' and '..' directories" complete -c eza -s A -l almost-all -d "Equivalent to --all; included for compatibility with `ls -A`" complete -c eza -s d -l treat-dirs-as-files -d "List directories like regular files" complete -c eza -s L -l level -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9" complete -c eza -s w -l width -d "Limits column output of grid, 0 implies auto-width" complete -c eza -s r -l reverse -d "Reverse the sort order" complete -c eza -s s -l sort -d "Which field to sort by" -x -a " accessed\t'Sort by file accessed time' age\t'Sort by file modified time (newest first)' changed\t'Sort by changed time' created\t'Sort by file modified time' date\t'Sort by file modified time' ext\t'Sort by file extension' Ext\t'Sort by file extension (uppercase first)' extension\t'Sort by file extension' Extension\t'Sort by file extension (uppercase first)' filename\t'Sort by filename' Filename\t'Sort by filename (uppercase first)' inode\t'Sort by file inode' modified\t'Sort by file modified time' name\t'Sort by filename' Name\t'Sort by filename (uppercase first)' newest\t'Sort by file modified time (newest first)' none\t'Do not sort files at all' oldest\t'Sort by file modified time' size\t'Sort by file size' time\t'Sort by file modified time' type\t'Sort by file type' " complete -c eza -s I -l ignore-glob -d "Ignore files that match these glob patterns" -r complete -c eza -s D -l only-dirs -d "List only directories" complete -c eza -s f -l only-files -d "List only files" complete -c eza -l show-symlinks -d "Explicitly show symbolic links (For use with --only-dirs | --only-files)" complete -c eza -l no-symlinks -d "Do not show symbolic links" # Long view options complete -c eza -s b -l binary -d "List file sizes with binary prefixes" complete -c eza -s B -l bytes -d "List file sizes in bytes, without any prefixes" complete -c eza -s g -l group -d "List each file's group" complete -c eza -s h -l header -d "Add a header row to each column" complete -c eza -s H -l links -d "List each file's number of hard links" complete -c eza -s i -l inode -d "List each file's inode number" complete -c eza -s S -l blocksize -d "List each file's size of allocated file system blocks" complete -c eza -s t -l time -d "Which timestamp field to list" -x -a " modified\t'Display modified time' changed\t'Display changed time' accessed\t'Display accessed time' created\t'Display created time' " complete -c eza -s m -l modified -d "Use the modified timestamp field" complete -c eza -s n -l numeric -d "List numeric user and group IDs." complete -c eza -l changed -d "Use the changed timestamp field" complete -c eza -s u -l accessed -d "Use the accessed timestamp field" complete -c eza -s U -l created -d "Use the created timestamp field" complete -c eza -l time-style -d "How to format timestamps" -x -a " default\t'Use the default time style' iso\t'Display brief ISO timestamps' long-iso\t'Display longer ISO timestamps, up to the minute' full-iso\t'Display full ISO timestamps, up to the nanosecond' relative\t'Display relative timestamps' +FORMAT\t'Use custom time style' " complete -c eza -l total-size -d "Show recursive directory size (unix only)" complete -c eza -l no-permissions -d "Suppress the permissions field" complete -c eza -s o -l octal-permissions -d "List each file's permission in octal format" complete -c eza -l no-filesize -d "Suppress the filesize field" complete -c eza -l no-user -d "Suppress the user field" complete -c eza -l no-time -d "Suppress the time field" complete -c eza -s M -l mounts -d "Show mount details" complete -c eza -l stdin -d "When piping to eza. Read file names from stdin" # Optional extras complete -c eza -l git -d "List each file's Git status, if tracked" complete -c eza -l no-git -d "Suppress Git status" complete -c eza -l git-repos -d "List each git-repos status and branch name" complete -c eza -l git-repos-no-status -d "List each git-repos branch name (much faster)" complete -c eza -s '@' -l extended -d "List each file's extended attributes and sizes" complete -c eza -s Z -l context -d "List each file's security context" ================================================ FILE: completions/nush/eza.nu ================================================ export extern "eza" [ --version(-v) # Show version of eza --help # Show list of command-line options --oneline(-1) # Display one entry per line --long(-l) # Display extended file metadata as a table --grid(-G) # Display entries in a grid --across(-x) # Sort the grid across, rather than downwards --recurse(-R) # Recurse into directories --tree(-T) # Recurse into directories as a tree --dereference(-X) # Dereference symbolic links when displaying file information --classify(-F) # Display type indicator by file names --color # When to use terminal colours --colour # When to use terminal colours --color-scale # Highlight levels of file sizes distinctly --colour-scale # Highlight levels of file sizes distinctly --color-scale-mode # Use gradient or fixed colors in --color-scale --colour-scale-mode # Use gradient or fixed colors in --colour-scale --icons # When to display icons --no-quotes # Don't quote file names with spaces --hyperlink # Display entries as hyperlinks --absolute # Display entries with their absolute path --follow-symlinks # Drill down into symbolic links that point to directories --group-directories-first # Sort directories before other files --group-directories-last # Sort directories after other files --git-ignore # Ignore files mentioned in '.gitignore' --all(-a) # Show hidden and 'dot' files. Use this twice to also show the '.' and '..' directories --almost-all(-A) # Equivalent to --all; included for compatibility with `ls -A` --treat-dirs-as-files(-d) # List directories like regular files --level(-L): string # Limit the depth of recursion --width(-w) # Limits column output of grid, 0 implies auto-width --reverse(-r) # Reverse the sort order --sort(-s) # Which field to sort by --only-dirs(-D) # List only directories --only-files(-f) # List only files --show-symlinks # Explicitly show symbolic links (for use with --only-dirs | --only-files) --no-symlinks # Do not show symbolic links --binary(-b) # List file sizes with binary prefixes --bytes(-B) # List file sizes in bytes, without any prefixes --group(-g) # List each file's group --header(-h) # Add a header row to each column --links(-H) # List each file's number of hard links --inode(-i) # List each file's inode number --blocksize(-S) # List each file's size of allocated file system blocks --time(-t) -d # Which timestamp field to list --modified(-m) # Use the modified timestamp field --numeric(-n) # List numeric user and group IDs. --changed # Use the changed timestamp field --accessed(-u) # Use the accessed timestamp field --created(-U) # Use the created timestamp field --time-style # How to format timestamps --total-size # Show recursive directory size (unix only) --no-permissions # Suppress the permissions field --octal-permissions(-o) # List each file's permission in octal format --no-filesize # Suppress the filesize field --no-user # Suppress the user field --no-time # Suppress the time field --mounts(-M) # Show mount details --git # List each file's Git status, if tracked --no-git # Suppress Git status --git-repos # List each git-repos status and branch name --git-repos-no-status # List each git-repos branch name (much faster) --extended(-@) # List each file's extended attributes and sizes --context(-Z) # List each file's security context --smart-group # Only show group if it has a different name from owner --stdin # When piping to eza. Read file paths from stdin ] ================================================ FILE: completions/pwsh/_eza.ps1 ================================================ using namespace System.Management.Automation using namespace System.Management.Automation.Language Register-ArgumentCompleter -Native -CommandName 'eza' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) $ArrayWhen = @('always', 'auto', 'never') $ArraySort = @('name', 'extension', 'size', 'type', 'created', 'modified', 'accessed', 'changed', 'inode', 'none') $ArrayColorScaleMode = @('fixed', 'gradient') $ArrayColorScale = @('all', 'age', 'size') $ArrayAbsolute = @('on', 'follow', 'off') $ArrayTime = @('modified', 'accessed', 'created') $ArrayTimeStyle = @('default', 'iso', 'long-iso', 'full-iso', 'relative', '+%Y-%m-%d %H:%M', '+%Y.%m.%d %H:$M:$s') $commandElements = $commandAst.CommandElements $command = @( 'eza' for ($i = 1; $i -lt $commandElements.Count; $i++) { $element = $commandElements[$i] if ($element -isnot [StringConstantExpressionAst] -or $element.StringConstantType -ne [StringConstantType]::BareWord -or $element.Value -eq $wordToComplete) { break } $element.Value }) -join ';' $completions = @(switch -Wildcard ($command) { '*;--help' { break } '*;--version' { break } '*;--absolute' { $ArrayAbsolute | ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)} break } '*;--sort' { $ArraySort | ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)} break } '*;--color-scale' { $ArrayColorScale | ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)} break } '*;--color-scale-mode' { $ArrayColorScaleMode | ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)} break } '*--long;*--time-style' { $ArrayTimeStyle | ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)} break } '*--long;*--time' { $ArrayTime | ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)} break } '*;--classify' { $ArrayWhen | ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)} break } '*;--color' { $ArrayWhen | ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)} break } '*;--icons' { $ArrayWhen | ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)} break } '*;--all' { [CompletionResult]::new('--show-symlinks' ,'listfilessyl' , [CompletionResultType]::ParameterName, 'explicitly show symbolic links (for use with --only-dirs | --only-files)') [CompletionResult]::new('--no-symlinks' ,'listfilessyl' , [CompletionResultType]::ParameterName, 'do not show symbolic links') break } '*long*' { # [CompletionResult]::new('-b' ,'binary' , [CompletionResultType]::ParameterName, 'list file sizes with binary prefixes') [CompletionResult]::new('--binary' ,'binary' , [CompletionResultType]::ParameterName, 'list file sizes with binary prefixes') # [CompletionResult]::new('-B' ,'bytes' , [CompletionResultType]::ParameterName, 'list file sizes in bytes, without any prefixes') [CompletionResult]::new('--bytes' ,'bytes' , [CompletionResultType]::ParameterName, 'list file sizes in bytes, without any prefixes') # [CompletionResult]::new('-g' ,'group' , [CompletionResultType]::ParameterName, 'list each file''s group') [CompletionResult]::new('--smart-group' ,'smart-group' , [CompletionResultType]::ParameterName, 'only show group if it has a different name from owner') [CompletionResult]::new('--group' ,'group' , [CompletionResultType]::ParameterName, 'list each file''s group') # [CompletionResult]::new('-h' ,'header' , [CompletionResultType]::ParameterName, 'add a header row to each column') [CompletionResult]::new('--header' ,'header' , [CompletionResultType]::ParameterName, 'add a header row to each column') # [CompletionResult]::new('-H' ,'links' , [CompletionResultType]::ParameterName, 'list each file''s number of hard links') [CompletionResult]::new('--links' ,'links' , [CompletionResultType]::ParameterName, 'list each file''s number of hard links') # [CompletionResult]::new('-i' ,'inode' , [CompletionResultType]::ParameterName, 'list each file''s inode number') [CompletionResult]::new('--inode' ,'inode' , [CompletionResultType]::ParameterName, 'list each file''s inode number') # [CompletionResult]::new('-M' ,'mounts' , [CompletionResultType]::ParameterName, 'show mount details (Linux and Mac only)') # [CompletionResult]::new('--mounts' ,'mounts' , [CompletionResultType]::ParameterName, 'show mount details (Linux and Mac only)') # [CompletionResult]::new('-n' ,'numeric' , [CompletionResultType]::ParameterName, 'list numeric user and group IDs') [CompletionResult]::new('--numeric' ,'numeric' , [CompletionResultType]::ParameterName, 'list numeric user and group IDs') # [CompletionResult]::new('-O' ,'flags' , [CompletionResultType]::ParameterName, 'list file flags (Mac, BSD, and Windows only)') [CompletionResult]::new('--flags' ,'flags' , [CompletionResultType]::ParameterName, 'list file flags (Mac, BSD, and Windows only)') # [CompletionResult]::new('-S' ,'blocksize' , [CompletionResultType]::ParameterName, 'show size of allocated file system blocks') [CompletionResult]::new('--blocksize' ,'blocksize' , [CompletionResultType]::ParameterName, 'show size of allocated file system blocks') # [CompletionResult]::new('-t' ,'time' , [CompletionResultType]::ParameterName, 'which timestamp field to list (modified, accessed, created)') [CompletionResult]::new('--time' ,'time' , [CompletionResultType]::ParameterName, 'which timestamp field to list (modified, accessed, created)') # [CompletionResult]::new('-m' ,'modified' , [CompletionResultType]::ParameterName, 'use the modified timestamp field') [CompletionResult]::new('--modified' ,'modified' , [CompletionResultType]::ParameterName, 'use the modified timestamp field') # [CompletionResult]::new('-u' ,'accessed' , [CompletionResultType]::ParameterName, 'use the accessed timestamp field') [CompletionResult]::new('--accessed' ,'accessed' , [CompletionResultType]::ParameterName, 'use the accessed timestamp field') # [CompletionResult]::new('-U' ,'created' , [CompletionResultType]::ParameterName, 'use the created timestamp field') [CompletionResult]::new('--changed' ,'changed' , [CompletionResultType]::ParameterName, 'use the changed timestamp field') [CompletionResult]::new('--created' ,'created' , [CompletionResultType]::ParameterName, 'use the created timestamp field') [CompletionResult]::new('--time-style' ,'time-style' , [CompletionResultType]::ParameterName, 'how to format timestamps (default, iso, long-iso,full-iso, relative, or a custom style ''+'' like ''+%Y-%m-%d %H:%M'')') # [CompletionResult]::new('--total-size' ,'total-size' , [CompletionResultType]::ParameterName, 'show the size of a directory as the size of all files and directories inside (unix only)') # [CompletionResult]::new('-o' ,'octal-permissions' , [CompletionResultType]::ParameterName, 'list each file''s permission in octal format') [CompletionResult]::new('--no-permissions' ,'no-permissions' , [CompletionResultType]::ParameterName, 'suppress the permissions field') [CompletionResult]::new('--octal-permissions' ,'octal-permissions' , [CompletionResultType]::ParameterName, 'list each file''s permission in octal format') [CompletionResult]::new('--no-filesize' ,'no-filesize' , [CompletionResultType]::ParameterName, 'suppress the filesize field') [CompletionResult]::new('--no-user' ,'no-user' , [CompletionResultType]::ParameterName, 'suppress the user field') [CompletionResult]::new('--no-time' ,'no-time' , [CompletionResultType]::ParameterName, 'suppress the time field') [CompletionResult]::new('--stdin' ,'stdin' , [CompletionResultType]::ParameterName, 'read file names from stdin, one per line or other separator specified in environment') [CompletionResult]::new('--git' ,'git' , [CompletionResultType]::ParameterName, 'list each file''s Git status, if tracked or ignored') [CompletionResult]::new('--no-git' ,'no-git' , [CompletionResultType]::ParameterName, 'suppress Git status (always overrides -git, --git-repos, --git-repos-no-status)') [CompletionResult]::new('--git-repos' ,'git-repos' , [CompletionResultType]::ParameterName, 'list root of git-tree status') [CompletionResult]::new('--git-repos-no-status' ,'git-repos-no-status' , [CompletionResultType]::ParameterName, 'list each git-repos branch name (much faster)') break } default { # [CompletionResult]::new('-?' ,'help' , [CompletionResultType]::ParameterName, 'show list of command-line options') [CompletionResult]::new('--help' ,'help' , [CompletionResultType]::ParameterName, 'show list of command-line options') # [CompletionResult]::new('-v' ,'version' , [CompletionResultType]::ParameterName, 'show version of eza') [CompletionResult]::new('--version' ,'version' , [CompletionResultType]::ParameterName, 'show version of eza') # [CompletionResult]::new('-1' ,'oneline' , [CompletionResultType]::ParameterName, 'display one entry per line') [CompletionResult]::new('--oneline' ,'oneline' , [CompletionResultType]::ParameterName, 'display one entry per line') # [CompletionResult]::new('-l' ,'long' , [CompletionResultType]::ParameterName, 'display extended file metadata as a table') [CompletionResult]::new('--long' ,'long' , [CompletionResultType]::ParameterName, 'display extended file metadata as a table') # [CompletionResult]::new('-G' ,'grid' , [CompletionResultType]::ParameterName, 'display entries as a grid (default)') [CompletionResult]::new('--grid' ,'grid' , [CompletionResultType]::ParameterName, 'display entries as a grid (default)') # [CompletionResult]::new('-x' ,'across' , [CompletionResultType]::ParameterName, 'sort the grid across, rather than downwards') [CompletionResult]::new('--across' ,'across' , [CompletionResultType]::ParameterName, 'sort the grid across, rather than downwards') # [CompletionResult]::new('-R' ,'recurse' , [CompletionResultType]::ParameterName, 'recurse into directories') [CompletionResult]::new('--recurse' ,'recurse' , [CompletionResultType]::ParameterName, 'recurse into directories') # [CompletionResult]::new('-T' ,'tree' , [CompletionResultType]::ParameterName, 'recurse into directories as a tree') [CompletionResult]::new('--tree' ,'tree' , [CompletionResultType]::ParameterName, 'recurse into directories as a tree') # [CompletionResult]::new('-X' ,'dereference' , [CompletionResultType]::ParameterName, 'dereference symbolic links when displaying information') [CompletionResult]::new('--dereference' ,'dereference' , [CompletionResultType]::ParameterName, 'dereference symbolic links when displaying information') # [CompletionResult]::new('-F' ,'classify' , [CompletionResultType]::ParameterName, 'display type indicator by file names (always, auto, never)') [CompletionResult]::new('--classify' ,'classify' , [CompletionResultType]::ParameterName, 'display type indicator by file names (always, auto, never)') [CompletionResult]::new('--color' ,'color' , [CompletionResultType]::ParameterName, 'when to use terminal colours (always, auto, never)') # [CompletionResult]::new('--colour' ,'color' , [CompletionResultType]::ParameterName, 'when to use terminal colours (always, auto, never)') [CompletionResult]::new('--color-scale' ,'colorscale' , [CompletionResultType]::ParameterName, 'highlight levels of ''field'' distinctly(all, age, size)') # [CompletionResult]::new('--colour-scale' ,'colorscale' , [CompletionResultType]::ParameterName, 'highlight levels of ''field'' distinctly(all, age, size)') [CompletionResult]::new('--color-scale-mode' ,'colorscalemode' , [CompletionResultType]::ParameterName, 'use gradient or fixed colors in --color-scale (fixed, gradient)') # [CompletionResult]::new('--colour-scale-mode' ,'colorscalemode' , [CompletionResultType]::ParameterName, 'use gradient or fixed colors in --color-scale (fixed, gradient)') [CompletionResult]::new('--icons' ,'icons' , [CompletionResultType]::ParameterName, 'when to display icons (always, auto, never)') [CompletionResult]::new('--no-quotes' ,'noquotes' , [CompletionResultType]::ParameterName, 'don''t quote file names with spaces') [CompletionResult]::new('--hyperlink' ,'hyperlink' , [CompletionResultType]::ParameterName, 'display entries as hyperlinks') [CompletionResult]::new('--absolute' ,'absolute' , [CompletionResultType]::ParameterName, 'display entries with their absolute path (on, follow, off)') [CompletionResult]::new('--follow-symlinks' ,'followsymlinks' , [CompletionResultType]::ParameterName, 'drill down into symbolic links that point to directories') # [CompletionResult]::new('-w' ,'widths' , [CompletionResultType]::ParameterName, 'set screen width in columns') [CompletionResult]::new('--width' ,'widths' , [CompletionResultType]::ParameterName, 'set screen width in columns') # [CompletionResult]::new('-a' ,'filter' , [CompletionResultType]::ParameterName, 'show hidden and ''dot'' files. Use this twice to also show the ''.'' and ''..'' directories') [CompletionResult]::new('--all' ,'filter' , [CompletionResultType]::ParameterName, 'show hidden and ''dot'' files. Use this twice to also show the ''.'' and ''..'' directories') # [CompletionResult]::new('-A' ,'filter' , [CompletionResultType]::ParameterName, 'equivalent to --all; included for compatibility with `ls -A`') # [CompletionResult]::new('--almost-all' ,'filter' , [CompletionResultType]::ParameterName, 'equivalent to --all; included for compatibility with `ls -A`') # [CompletionResult]::new('-d' ,'filter' , [CompletionResultType]::ParameterName, 'list directories as files; don''t list their contents') [CompletionResult]::new('--treat-dirs-as-files' ,'filter' , [CompletionResultType]::ParameterName, 'list directories as files; don''t list their contents') # [CompletionResult]::new('-D' ,'filter' , [CompletionResultType]::ParameterName, 'list only directories') [CompletionResult]::new('--only-dirs' ,'filter' , [CompletionResultType]::ParameterName, 'list only directories') # [CompletionResult]::new('-f' ,'filter' , [CompletionResultType]::ParameterName, 'list only files') [CompletionResult]::new('--only-files' ,'filter' , [CompletionResultType]::ParameterName, 'list only files') # [CompletionResult]::new('-L' ,'level' , [CompletionResultType]::ParameterName, 'limit the depth of recursion') [CompletionResult]::new('--level' ,'level' , [CompletionResultType]::ParameterName, 'limit the depth of recursion') # [CompletionResult]::new('-r' ,'reverse' , [CompletionResultType]::ParameterName, 'reverse the sort order') [CompletionResult]::new('--reverse' ,'reverse' , [CompletionResultType]::ParameterName, 'reverse the sort order') # [CompletionResult]::new('-s' ,'sort' , [CompletionResultType]::ParameterName, 'which field to sort by SORT_FIELD') [CompletionResult]::new('--sort' ,'sort' , [CompletionResultType]::ParameterName, 'which field to sort by SORT_FIELD') [CompletionResult]::new('--group-directories-first' ,'gdf' , [CompletionResultType]::ParameterName, 'list directories before other files') [CompletionResult]::new('--group-directories-last' ,'gdl' , [CompletionResultType]::ParameterName, 'list directories after other files') # [CompletionResult]::new('-I' ,'ignore-glob' , [CompletionResultType]::ParameterName, 'glob patterns (pipe-separated) of files to ignore GLOBS') [CompletionResult]::new('--ignore-glob' ,'ignore-glob' , [CompletionResultType]::ParameterName, 'glob patterns (pipe-separated) of files to ignore GLOBS') [CompletionResult]::new('--git-ignore' ,'git-ignore' , [CompletionResultType]::ParameterName, 'ignore files mentioned in ''.gitignore''') break } }) $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | Sort-Object -Property completionText } ================================================ FILE: completions/zsh/_eza ================================================ #compdef eza # Save this file as _eza in /usr/local/share/zsh/site-functions or in any # other folder in $fpath. E.g. save it in a folder called ~/.zfunc and add a # line containing `fpath=(~/.zfunc $fpath)` somewhere before `compinit` in your # ~/.zshrc. __eza() { # Give completions using the `_arguments` utility function with # `-s` for option stacking like `eza -ab` for `eza -a -b` and # `-S` for delimiting options with `--` like in `eza -- -a`. _arguments -s -S \ "(- *)"{-v,--version}"[Show version of eza]" \ "(- *)"--help"[Show list of command-line options]" \ {-1,--oneline}"[Display one entry per line]" \ {-l,--long}"[Display extended file metadata as a table]" \ {-G,--grid}"[Display entries as a grid]" \ {-x,--across}"[Sort the grid across, rather than downwards]" \ {-R,--recurse}"[Recurse into directories]" \ {-T,--tree}"[Recurse into directories as a tree]" \ {-X,--dereference}"[Dereference symbolic links when displaying file information]" \ {-F,--classify}"[Display type indicator by file names]:(when):(always auto automatic never)" \ --colo{,u}r="[When to use terminal colours]:(when):(always auto automatic never)" \ --colo{,u}r-scale"[highlight levels of 'field' distinctly]:(fields):(all age size)" \ --colo{,u}r-scale-mode"[Use gradient or fixed colors in --color-scale]:(mode):(fixed gradient)" \ --icons="[When to display icons]:(when):(always auto automatic never)" \ --no-quotes"[Don't quote filenames with spaces]" \ --hyperlink"[Display entries as hyperlinks]" \ --absolute"[Display entries with their absolute path]:(mode):(on follow off)" \ --follow-symlinks"[Drill down into symbolic links that point to directories]" \ --group-directories-first"[Sort directories before other files]" \ --group-directories-last"[Sort directories after other files]" \ --git-ignore"[Ignore files mentioned in '.gitignore']" \ {-a,--all}"[Show hidden and 'dot' files. Use this twice to also show the '.' and '..' directories]" \ {-A,--almost-all}"[Equivalent to --all; included for compatibility with \'ls -A\']" \ {-d,--treat-dirs-as-files}"[List directories like regular files]" \ {-D,--only-dirs}"[List only directories]" \ --no-symlinks"[Do not show symbolic links]" \ --show-symlinks"[Explictly show symbolic links: for use with '--only-dirs'| '--only-files']" \ {-f,--only-files}"[List only files]" \ {-L,--level}"+[Limit the depth of recursion]" \ {-w,--width}"+[Limits column output of grid, 0 implies auto-width]" \ {-r,--reverse}"[Reverse the sort order]" \ {-s,--sort}="[Which field to sort by]:(sort field):(accessed age changed created date extension Extension filename Filename inode modified oldest name Name newest none size time type)" \ {-I,--ignore-glob}"[Ignore files that match these glob patterns]" \ {-b,--binary}"[List file sizes with binary prefixes]" \ {-B,--bytes}"[List file sizes in bytes, without any prefixes]" \ --changed"[Use the changed timestamp field]" \ {-g,--group}"[List each file's group]" \ {-h,--header}"[Add a header row to each column]" \ {-H,--links}"[List each file's number of hard links]" \ {-i,--inode}"[List each file's inode number]" \ {-m,--modified}"[Use the modified timestamp field]" \ {-n,--numeric}"[List numeric user and group IDs.]" \ {-S,--blocksize}"[List each file's size of allocated file system blocks.]" \ {-t,--time}="[Which time field to show]:(time field):(accessed changed created modified)" \ --time-style="[How to format timestamps]:(time style):(default iso long-iso full-iso relative +FORMAT)" \ --total-size"[Show recursive directory size (unix only)]" \ --no-permissions"[Suppress the permissions field]" \ {-o,--octal-permissions}"[List each file's permission in octal format]" \ --no-filesize"[Suppress the filesize field]" \ --no-user"[Suppress the user field]" \ --no-time"[Suppress the time field]" \ {-u,--accessed}"[Use the accessed timestamp field]" \ {-U,--created}"[Use the created timestamp field]" \ --git"[List each file's Git status, if tracked]" \ --no-git"[Suppress Git status]" \ --git-repos"[List each git-repos status and branch name]" \ --git-repos-no-status"[List each git-repos branch name (much faster)]" \ {-@,--extended}"[List each file's extended attributes and sizes]" \ {-Z,--context}"[List each file's security context]" \ {-M,--mounts}"[Show mount details (long mode only)]" \ '*:filename:_files' \ --smart-group"[Only show group if it has a different name from owner]" \ --stdin"[When piping to eza. Read file names from stdin]" } __eza ================================================ FILE: deb.asc ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBGTejj8BEAC3Qdi6up8rkFvekeuZiGpuC5OTic+Nd/x6zacrtDKJwdVa6fw3 tVydFW1ELcw33ifWDztvgENqvgMuhcB/lnoDnaAhK8nzT0l+r0gQ7JptPH+8XsZx PuIFRxgUkS6M45jrZApu/c3/PX3akiBuBnibd+sik72pVSi9pYm00I/yY/+u9Vvo u4vy/PG/y4Kp1+ewbVyOnaTQoiJXgqceUYqNfhShcN7dssa/Td6G0xPhS1XeQZ81 QWwPNEzGWomGJ/igZPcm31qtIwcHXn3m8UZbCxUHHzseZ0hOOYqVSrSIj+U1RIma rmcbdAAi1wQei0P142/Gkq1fLdscrZPc0b0L8JbZnymAvg2WWE6SQ3/7Ux0Y9hc3 wsXuiwU38Qg0EaDMSXyUKAbK+4/tuP9mbx4PyKuDUmjJnPTCvbJxe2kC8rdZrOgn 4qbwHY7FCEBpG28VhV07dwueZcFC/VCqGrbmrJC7rl6avT5xpOIRslUTJGmifx3Z 0hmL6rzTQz00FVNSXyxLnz1XYUTLm7QyzywTv8HTTfy5o21N2eOHqbMkBA9MJiic lnVSH1vCn/5rd+fwRQ/7yLKAdeUcRyxkdVWTUYDnVtQF199MAuXWv4nudjvS3046 Y9WpJGiZEi9PqeIxmYYHOZ/jYVF1RpMOyVnZhzRhn4Yy+uzJtUeW45nGyQARAQAB tDZTYW5kcm8tQWxlc3NpbyBHaWVyZW5zIChhcHRseSBrZXkpIDxzYW5kcm9AZ2ll cmVucy5kZT6JAk4EEwEKADgWIQQVSLyKS00miPmw2vfsKeIJDOP9QwUCZN6OPwIb LwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRDsKeIJDOP9Q26qEACkbm29cF5f 7J95TAMOzNfjwfgEEPTZt9YUQxmYYlO3LGOShx/hogoWGhpw6qXQP0/lRlXcmNWD 3J5rN+YmQSIYRP0VvOVJ/XYpgsoS90JwmukbJte4Wu1Goomh1dPv1z/ag4jToSzu faSbwoUa46BmdRBOrOH/w/Ro7U6jLhH9saIivJfexksYeMprXahgoWsFGurl7mt0 OaN3C6YSMkoJjpufCTfHKagBRT4ZYRw1JpTF+oap0VZgKtg33pnxVvSKdZJEY9BG su47/eZM0bOCMll1upitF8TQ5DFING6+SRrmT8xAyUOCQBIid/9hBxX5mTYTjKW2 ZVteOkCowJwMwx134BffTMWMAMRw27Vxtuuugn6a9yd9pbK95aug62VpbvO8xWM+ RocqeuQvh0Ii8kpH+sLjdpceMf1c01cYu97DjtdQ54FGtI4r1kOyh/BO7mI0omoJ hFunhQKKM8q1xUyIs3RvYALuM5VzEWCHwXBYdiNWYDVBC/sNje67A8SXXXjJLhcr 9cCpJ5AUmkrLoQvgdewMpuIlmtPRbdv8hkmOUDPkD58AcDirvggXp63IiWlFYQ9C EoDLfUYQ6aJmw1fRI8/QsP3Q50aN6dkZRsDCcpxoNX9YzuU6+o1ha+ZNRpmfJvZg ULP33eq15gJlsGrxo2HZ+f6w4pFFy4juDQ== =MX5x -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: deny.toml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 [graph] # This template contains all of the possible sections and their default values # Note that all fields that take a lint level have these possible values: # * deny - An error will be produced and the check will fail # * warn - A warning will be produced, but the check will not fail # * allow - No warning or error will be produced, though in some cases a note # will be # The values provided in this template are the default values that will be used # when any section or field is not specified in your own configuration # Root options # If 1 or more target triples (and optionally, target_features) are specified, # only the specified targets will be checked when running `cargo deny check`. # This means, if a particular package is only ever used as a target specific # dependency, such as, for example, the `nix` crate only being used via the # `target_family = "unix"` configuration, that only having windows targets in # this list would mean the nix crate, as well as any of its exclusive # dependencies not shared by any other crates, would be ignored, as the target # list here is effectively saying which targets you are building for. targets = [ # The triple can be any string, but only the target triples built in to # rustc (as of 1.40) can be checked against actual config expressions #{ triple = "x86_64-unknown-linux-musl" }, # You can also specify which target_features you promise are enabled for a # particular target. target_features are currently not validated against # the actual valid features supported by the target architecture. #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, ] # When creating the dependency graph used as the source of truth when checks are # executed, this field can be used to prune crates from the graph, removing them # from the view of cargo-deny. This is an extremely heavy hammer, as if a crate # is pruned from the graph, all of its dependencies will also be pruned unless # they are connected to another crate in the graph that hasn't been pruned, # so it should be used with care. The identifiers are [Package ID Specifications] # (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) #exclude = [] # If true, metadata will be collected with `--all-features`. Note that this can't # be toggled off if true, if you want to conditionally enable `--all-features` it # is recommended to pass `--all-features` on the cmd line instead all-features = true # If true, metadata will be collected with `--no-default-features`. The same # caveat with `all-features` applies no-default-features = false [output] # If set, these feature will be enabled when collecting metadata. If `--features` # is specified on the cmd line they will take precedence over this option. #features = [] # When outputting inclusion graphs in diagnostics that include features, this # option can be used to specify the depth at which feature edges will be added. # This option is included since the graphs can be quite large and the addition # of features from the crate(s) to all of the graph roots can be far too verbose. # This option can be overridden via `--feature-depth` on the cmd line feature-depth = 1 # This section is considered when running `cargo deny check advisories` # More documentation for the advisories section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html [advisories] version = 2 # The path where the advisory database is cloned/fetched into db-path = "~/.cargo/advisory-db" # The url(s) of the advisory databases to use db-urls = ["https://github.com/rustsec/advisory-db"] # The lint level for crates that have been yanked from their source registry yanked = "warn" # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ #"RUSTSEC-0000-0000", ] # Threshold for security vulnerabilities, any vulnerability with a CVSS score # lower than the range specified will be ignored. Note that ignored advisories # will still output a note when they are encountered. # * None - CVSS Score 0.0 # * Low - CVSS Score 0.1 - 3.9 # * Medium - CVSS Score 4.0 - 6.9 # * High - CVSS Score 7.0 - 8.9 # * Critical - CVSS Score 9.0 - 10.0 #severity-threshold = # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. # See Git Authentication for more information about setting up git authentication. #git-fetch-with-cli = true # This section is considered when running `cargo deny check licenses` # More documentation for the licenses section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html [licenses] version = 2 # List of explicitly allowed licenses # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. allow = [ "EUPL-1.2", "MIT", "Unicode-DFS-2016", "Apache-2.0", "MPL-2.0", "Unicode-3.0", #"Apache-2.0 WITH LLVM-exception", ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the # canonical license text of a valid SPDX license file. # [possible values: any between 0.0 and 1.0]. confidence-threshold = 0.8 # Allow 1 or more licenses on a per-crate basis, so that particular licenses # aren't accepted for every possible crate as with the normal allow list exceptions = [ # Each entry is the crate and version constraint, and its specific allow # list #{ allow = ["Zlib"], name = "adler32", version = "*" }, ] # Some crates don't have (easily) machine readable licensing information, # adding a clarification entry for it allows you to manually specify the # licensing information #[[licenses.clarify]] # The name of the crate the clarification applies to #name = "ring" # The optional version constraint for the crate #version = "*" # The SPDX expression for the license requirements of the crate #expression = "MIT AND ISC AND OpenSSL" # One or more files in the crate's source used as the "source of truth" for # the license expression. If the contents match, the clarification will be used # when running the license check, otherwise the clarification will be ignored # and the crate will be checked normally, which may produce warnings or errors # depending on the rest of your configuration #license-files = [ # Each entry is a crate relative path, and the (opaque) hash of its contents #{ path = "LICENSE", hash = 0xbd0eed23 } #] [licenses.private] # If true, ignores workspace crates that aren't published, or are only # published to private registries. # To see how to mark a crate as unpublished (to the official registry), # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. ignore = false # One or more private registries that you might publish crates to, if a crate # is only published to private registries, and ignore is true, the crate will # not have its license(s) checked registries = [ #"https://sekretz.com/registry ] # This section is considered when running `cargo deny check bans`. # More documentation about the 'bans' section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html [bans] # Lint level for when multiple versions of the same crate are detected multiple-versions = "warn" # Lint level for when a crate version requirement is `*` wildcards = "allow" # The graph highlighting used when creating dotgraphs for crates # with multiple versions # * lowest-version - The path to the lowest versioned duplicate is highlighted # * simplest-path - The path to the version with the fewest edges is highlighted # * all - Both lowest-version and simplest-path are used highlight = "all" # The default lint level for `default` features for crates that are members of # the workspace that is being checked. This can be overridden by allowing/denying # `default` on a crate-by-crate basis if desired. workspace-default-features = "allow" # The default lint level for `default` features for external crates that are not # members of the workspace. This can be overridden by allowing/denying `default` # on a crate-by-crate basis if desired. external-default-features = "allow" # List of crates that are allowed. Use with care! allow = [ #{ name = "ansi_term", version = "=0.11.0" }, ] # List of crates to deny deny = [ # Each entry the name of a crate and a version range. If version is # not specified, all versions will be matched. #{ name = "ansi_term", version = "=0.11.0" }, # # Wrapper crates can optionally be specified to allow the crate when it # is a direct dependency of the otherwise banned crate #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, ] # List of features to allow/deny # Each entry the name of a crate and a version range. If version is # not specified, all versions will be matched. #[[bans.features]] #name = "reqwest" # Features to not allow #deny = ["json"] # Features to allow #allow = [ # "rustls", # "__rustls", # "__tls", # "hyper-rustls", # "rustls", # "rustls-pemfile", # "rustls-tls-webpki-roots", # "tokio-rustls", # "webpki-roots", #] # If true, the allowed features must exactly match the enabled feature set. If # this is set there is no point setting `deny` #exact = true # Certain crates/versions that will be skipped when doing duplicate detection. skip = [ #{ name = "ansi_term", version = "=0.11.0" }, ] # Similarly to `skip` allows you to skip certain crates during duplicate # detection. Unlike skip, it also includes the entire tree of transitive # dependencies starting at the specified crate, up to a certain depth, which is # by default infinite. skip-tree = [ #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, ] # This section is considered when running `cargo deny check sources`. # More documentation about the 'sources' section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html [sources] # Lint level for what to happen when a crate from a crate registry that is not # in the allow list is encountered unknown-registry = "warn" # Lint level for what to happen when a crate from a git repository that is not # in the allow list is encountered unknown-git = "warn" # List of URLs for allowed crate registries. Defaults to the crates.io index # if not specified. If it is specified but empty, no registries are allowed. allow-registry = ["https://github.com/rust-lang/crates.io-index"] # List of URLs for allowed Git repositories allow-git = [] [sources.allow-org] # 1 or more github.com organizations to allow git sources for # github = [""] # # 1 or more gitlab.com organizations to allow git sources for # gitlab = [""] # # 1 or more bitbucket.org organizations to allow git sources for # bitbucket = [""] ================================================ FILE: devtools/deb-package.sh ================================================ #!/bin/bash -e REPO_URL="https://github.com/eza-community/eza" NAME="eza" DESTDIR=/usr/bin DOCDIR=/usr/share/man/ COMMIT=$(git rev-parse --abbrev-ref HEAD) TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)") if [ -n "$1" ]; then TAG=$1 fi VERSION=${TAG:1} echo "checkout tag ${TAG}" git checkout --quiet "${TAG}" echo "build man pages" just man declare -A TARGETS TARGETS["amd64"]="x86_64-unknown-linux-musl" TARGETS["arm64"]="aarch64-unknown-linux-gnu" TARGETS["armhf"]="arm-unknown-linux-gnueabihf" echo "download release notes" RELEASE_NOTES=$(curl -s "${REPO_URL}/releases/tag/${TAG}") for ARCH in "${!TARGETS[@]}"; do echo "building ${ARCH} package:" DEB_TMP_DIR="${NAME}_${VERSION}_${ARCH}" DEB_PACKAGE="${NAME}_${VERSION}_${ARCH}.deb" TARGET=${TARGETS[$ARCH]} echo " -> downloading ${TARGET} archive" wget -q -O "${ARCH}.tar.gz" "${REPO_URL}/releases/download/${TAG}/${NAME}_${TARGET}.tar.gz" echo " -> verifying ${TARGET} archive" CHECKSUM=$(md5sum "${ARCH}.tar.gz" | cut -d ' ' -f 1) echo " checksum: ${CHECKSUM}" grep -q "${CHECKSUM}" <<< "${RELEASE_NOTES}" \ || (echo "checksum mismatch" && exit 1) echo " checksum ok" echo " -> creating directory structure" mkdir -p "${DEB_TMP_DIR}" mkdir -p "${DEB_TMP_DIR}${DESTDIR}" mkdir -p "${DEB_TMP_DIR}${DOCDIR}" mkdir -p "${DEB_TMP_DIR}${DOCDIR}/man1" mkdir -p "${DEB_TMP_DIR}${DOCDIR}/man5" mkdir -p "${DEB_TMP_DIR}/DEBIAN" mkdir -p "${DEB_TMP_DIR}/usr/share/doc/${NAME}" mkdir -p "${DEB_TMP_DIR}/usr/share/bash-completion/completions/" mkdir -p "${DEB_TMP_DIR}/usr/share/fish/vendor_completions.d/" mkdir -p "${DEB_TMP_DIR}/usr/share/zsh/vendor-completions/" chmod 755 -R "${DEB_TMP_DIR}" echo " -> extract executable" tar -xzf "${ARCH}.tar.gz" cp ${NAME} "${DEB_TMP_DIR}${DESTDIR}" chmod 755 "${DEB_TMP_DIR}${DESTDIR}/${NAME}" echo " -> compress man pages" gzip -cn9 target/man/eza.1 > "${DEB_TMP_DIR}${DOCDIR}man1/eza.1.gz" gzip -cn9 target/man/eza_colors.5 > "${DEB_TMP_DIR}${DOCDIR}man5/eza_colors.5.gz" gzip -cn9 target/man/eza_colors-explanation.5 > "${DEB_TMP_DIR}${DOCDIR}man5/eza_colors-explanation.5.gz" chmod 644 "${DEB_TMP_DIR}${DOCDIR}"/**/*.gz echo " -> copy completions" cp completions/bash/eza "${DEB_TMP_DIR}/usr/share/bash-completion/completions/" cp completions/fish/eza.fish "${DEB_TMP_DIR}/usr/share/fish/vendor_completions.d/" cp completions/zsh/_eza "${DEB_TMP_DIR}/usr/share/zsh/vendor-completions/" echo " -> create control file" touch "${DEB_TMP_DIR}/DEBIAN/control" cat > "${DEB_TMP_DIR}/DEBIAN/control" < Description: Modern replacement for ls eza is a modern replacement for ls. It uses colours for information by default, helping you distinguish between many types of files, such as whether you are the owner, or in the owning group. . It also has extra features not present in the original ls, such as viewing the Git status for a directory, or recursing into directories with a tree view. EOM chmod 644 "${DEB_TMP_DIR}/DEBIAN/control" echo " -> copy changelog" cp CHANGELOG.md "${DEB_TMP_DIR}/usr/share/doc/${NAME}/changelog" gzip -cn9 "${DEB_TMP_DIR}/usr/share/doc/${NAME}/changelog" > "${DEB_TMP_DIR}/usr/share/doc/${NAME}/changelog.gz" rm "${DEB_TMP_DIR}/usr/share/doc/${NAME}/changelog" chmod 644 "${DEB_TMP_DIR}/usr/share/doc/${NAME}/changelog.gz" echo " -> create copyright file" touch "${DEB_TMP_DIR}/usr/share/doc/${NAME}/copyright" cat > "${DEB_TMP_DIR}/usr/share/doc/${NAME}/copyright" << EOM Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: ${NAME} Upstream-Contact: Christina Sørensen Source: https://github.com/eza-community/eza/releases Files: * License: MIT Copyright: 2023 Christina Sørensen Files: debian/* License: MIT Copyright: 2023 Christina Sørensen License: MIT 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. EOM chmod 644 "${DEB_TMP_DIR}/usr/share/doc/${NAME}/copyright" echo " -> build ${ARCH} package" dpkg-deb --build --root-owner-group "${DEB_TMP_DIR}" > /dev/null echo " -> cleanup" rm -rf "${DEB_TMP_DIR}" "${ARCH}.tar.gz" "${NAME}" # gierens: this does not work on my arch at the moment and # i'm verifying on the repo host anyway thus the || true echo " -> lint ${ARCH} package" lintian "${DEB_PACKAGE}" || true done echo "return to original commit" git checkout --quiet "${COMMIT}" ================================================ FILE: devtools/dir-generator.sh ================================================ #!/usr/bin/env bash if [ -z "$1" ]; then echo "Usage: $0 "; exit 1; fi rm "$1" -rf; mkdir -p "$1"; cd "$1" || exit; sudo groupadd -f eza_test # BEGIN grid mkdir -p grid cd grid || exit mkdir $(seq -w 001 1000); seq 0001 1000 | split -l 1 -a 3 -d - file_ # Set time to unix epoch touch --date=@0 ./*; cd .. || exit # END grid # BEGIN git mkdir -p git cd git || exit mkdir $(seq -w 001 10); for f in ./* do cd "$f" || exit git init seq 01 10 | split -l 1 -a 3 -d - file_ cd .. || exit done cd .. # END git # BEGIN test_root sudo mkdir root sudo chmod 777 root sudo mkdir root/empty # END test_root # BEGIN mknod mkdir -p specials sudo mknod specials/block-device b 3 60 sudo mknod specials/char-device c 14 40 sudo mknod specials/named-pipe p # END test_root # BEGIN test_symlinks mkdir -p symlinks touch symlinks/file --date=@0 ln -s file symlinks/symlink ln -s symlink symlinks/symlink2 mkdir -p symlinks/dir ln -s dir symlinks/symlink3 ln -s pipitek symlinks/symlink4 touch "symlinks/ lorem ipsum" --date=@0 ln -s "lorem ipsum" "symlinks/ lorem ipsum" # END test_symlinks # BEGIN test_perms mkdir -p perms touch perms/file --date=@0 touch perms/file2 --date=@0 chmod 777 perms/file chmod 001 perms/file2 # END test_perms # BEGIN test_group mkdir -p group touch group/file --date=@0 sudo chgrp eza_test group/file # END test_group # BEGIN test_size mkdir -p size touch size/1M --date=@0 dd if=/dev/zero of=size/1M bs=1 count=0 seek=1M touch size/1K --date=@0 dd if=/dev/zero of=size/1K bs=1 count=0 seek=1K touch size/1B --date=@0 dd if=/dev/zero of=size/1B bs=1 count=0 seek=1 touch size/1337 --date=@0 dd if=/dev/zero of=size/1337 bs=1 count=0 seek=1337 # END test_size # BEGIN test_time mkdir -p time touch time/epoch --date=@0 touch time/1s --date=@1 touch time/1m --date=@60 touch time/1h --date=@3600 touch time/1d --date=@86400 touch time/1y --date=@31536000 # END test_time # BEGIN test_icons mkdir -p icons touch icons/file --date=@0 touch icons/go.go --date=@0 touch icons/rust.rs --date=@0 touch icons/c.c --date=@0 touch icons/c++.cpp --date=@0 touch icons/python.py --date=@0 touch icons/java.java --date=@0 touch icons/javascript.js --date=@0 touch icons/html.html --date=@0 touch icons/css.css --date=@0 touch icons/php.php --date=@0 touch icons/ruby.rb --date=@0 touch icons/shell.sh --date=@0 touch icons/unknown.unknown --date=@0 touch icons/man.1 --date=@0 touch icons/marked.md --date=@0 # END test_icons # BEGIN set date touch --date=@0 ./*; # END set date ================================================ FILE: devtools/generate-timestamp-test-dir.sh ================================================ #!/usr/bin/env bash if [ -z "$1" ]; then echo "Usage: $0 "; exit 1; fi rm "$1" -rf; mkdir -p "$1"; cd "$1" || exit; # generate files of various age # TODO: some are commented out due to undeterministic behavior, see: # https://github.com/eza-community/eza/issues/574 touch --date="13 month ago" ./13_month #touch --date="11 month ago" ./11_month #touch --date="7 month ago" ./07_month #touch --date="5 month ago" ./05_month touch --date="now" ./now #touch --date="next hour" ./next_hour #touch --date="next month" ./next_month #touch --date="next year" ./next_year ================================================ FILE: devtools/generate-trycmd-test.sh ================================================ #!/usr/bin/env bash # Generate test data for the program if [ $# -le 1 ]; then echo "Usage: $0 " exit 1 fi test_name=$1 shift # Clean up previous test data if [ -f tests/cmd/"$test_name".toml ]; then rm tests/cmd/"$test_name".toml fi if [ -f tests/cmd/"$test_name".stdout ]; then rm tests/cmd/"$test_name".stdout fi if [ -f tests/cmd/"$test_name".stderr ]; then rm tests/cmd/"$test_name".stderr fi # Generate test data touch tests/cmd/"$test_name".toml echo 'bin.name = "eza"' >> tests/cmd/"$test_name".toml echo 'args = "'"$*"'"' >> tests/cmd/"$test_name".toml # Generate expected output if [ -f target/debug/eza ]; then target/debug/eza "$@" > tests/cmd/"$test_name".stdout 2> tests/cmd/"$test_name".stderr returncode=$? if [ $returncode -ne 0 ]; then echo -e 'status.code = '$returncode'' >> tests/cmd/"$test_name".toml exit 0 fi else echo "Please build the program first" exit 1 fi ================================================ FILE: docs/tapes/demo.tape ================================================ Output docs/images/demo.gif Set Width 1536 Set Height 512 Set Shell fish Set FontFamily "FantasqueSansM Nerd Font Mono" Set FontSize 13 Type "# eza is a modern, maintained replacement for ls." Enter Type "# " Enter Type "# It has too many features to cover in a gif, but we'll" Enter Type "# try to show off some cool stuff :3" Enter Type "# " Enter Sleep 4s Enter Type "# Let's start with the basics." Enter Type "eza src/" Sleep 2s Enter Sleep 4s Type "# ...but add some icons." Enter Type "eza -l --icons src/" Sleep 2s Enter Sleep 4s Type "# ...and how about a grid view." Enter Type "eza -l --icons --grid" Sleep 2s Enter Sleep 4s Type "# ...with headers." Enter Type "eza -l --icons --grid --header" Sleep 2s Enter Sleep 4s Type "# ...and perhaps some octal permissions." Enter Type "eza -l --icons --grid --header --octal-permissions" Sleep 2s Enter Sleep 4s Type "# ...or what about blocksize." Enter Type "eza -l --icons --grid --header --blocksize" Sleep 2s Enter Sleep 4s Type "# ...or hyperlinks." Enter Type "eza -l --icons --grid --header --hyperlink" Sleep 2s Enter Sleep 4s Type "# Admittedly, those aren't really very visual in a" Enter Type "# gif like this >_<" Enter Type "# ...what about a custom time style?" Enter Type 'eza -l --icons --grid --header --time-style "+%Y/%m/%d - week %W"' Sleep 2s Enter Sleep 4s Type "# ...or a bit of git status." Enter Type 'eza -l --icons --grid --header --git' Sleep 2s Enter Sleep 4s Type "# ...or a lot of git." Enter Type 'eza -l --icons --grid --header --git --git-repos' Sleep 2s Enter Sleep 4s Type "# Of course, this was only a glimpse off all " Enter Type "# the features that eza has to offer." Enter Type "# " Enter Type "# For more info, checkout the README.md or check" Enter Type "# out the github repo or the website at https://eza.rocks" Enter Type "# " Enter Type "# Thanks for watching <3" Enter Sleep 4s # Type 'eza -l --icons --grid --blocksize -l --hyperlink --grid --git-repos -x --time-style "+%Y, %Y, %d%d%d - %a %u" --git --header --icons -Z -@ -o --group --smart-group --no-quotes' # Sleep 500ms # Enter # Sleep 2s ================================================ FILE: docs/theme.yml ================================================ filekinds: normal: foreground: Blue directory: foreground: Blue symlink: foreground: Cyan executable: foreground: Green perms: user_read: foreground: Yellow is_bold: true user_write: foreground: Red is_bold: true user_execute_file: foreground: Green is_bold: true user_execute_other: foreground: Green is_bold: true group_read: foreground: Yellow group_write: foreground: Red group_execute: foreground: Green other_read: foreground: Yellow other_write: foreground: Red other_execute: foreground: Green filenames: # Just change the icon glyph Cargo.toml: {icon: {glyph: 🦀}} Cargo.lock: {icon: {glyph: 🦀}} extensions: # Change the filename color and icon # NOTE: not all unicode glyphs support color changes rs: {filename: {foreground: Red}, icon: {glyph: 🦀}} # Change the icon glyph and color nix: {icon: {glyph: ❄, style: {foreground: White}}} ================================================ FILE: flake.nix ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 # # SPDX-FileCopyrightText: 2014-2024 Christina Sørensen, eza contributors # SPDX-License-Identifier: MIT { description = "eza: a modern, maintained replacement for ls"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; systems.url = "github:nix-systems/default"; flake-utils = { url = "github:numtide/flake-utils"; inputs = { systems.follows = "systems"; }; }; naersk = { url = "github:nix-community/naersk"; inputs.nixpkgs.follows = "nixpkgs"; }; rust-overlay = { url = "github:oxalica/rust-overlay"; inputs = { nixpkgs.follows = "nixpkgs"; }; }; treefmt-nix = { url = "github:numtide/treefmt-nix"; inputs = { nixpkgs.follows = "nixpkgs"; }; }; powertest = { url = "github:eza-community/powertest"; inputs = { nixpkgs.follows = "nixpkgs"; flake-utils.follows = "flake-utils"; naersk.follows = "naersk"; treefmt-nix.follows = "treefmt-nix"; rust-overlay.follows = "rust-overlay"; }; }; pre-commit-hooks = { url = "github:cachix/pre-commit-hooks.nix"; inputs.nixpkgs.follows = "nixpkgs"; }; advisory-db = { url = "github:rustsec/advisory-db"; flake = false; }; }; outputs = { self, flake-utils, naersk, nixpkgs, treefmt-nix, rust-overlay, powertest, pre-commit-hooks, ... }: flake-utils.lib.eachDefaultSystem ( system: let overlays = [ (import rust-overlay) ]; pkgs = (import nixpkgs) { inherit system overlays; }; toolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; naersk' = pkgs.callPackage naersk { cargo = toolchain; rustc = toolchain; clippy = toolchain; }; treefmtEval = treefmt-nix.lib.evalModule pkgs .config/treefmt.nix; darwinBuildInputs = pkgs.lib.optionals pkgs.stdenv.isDarwin [ pkgs.libiconv pkgs.darwin.apple_sdk.frameworks.Security ]; buildInputs = [ pkgs.zlib ] ++ darwinBuildInputs; in rec { # For `nix fmt` formatter = treefmtEval.config.build.wrapper; packages = { default = import ./nix/eza.nix { inherit pkgs naersk' buildInputs; }; check = naersk'.buildPackage { inherit buildInputs; src = ./.; mode = "check"; }; test = naersk'.buildPackage { inherit buildInputs; src = ./.; mode = "test"; }; clippy = naersk'.buildPackage { inherit buildInputs; src = ./.; mode = "clippy"; }; } // (import ./nix/trycmd.nix { inherit pkgs naersk' buildInputs; }); devShells.default = pkgs.mkShell { inherit (self.checks.${system}.pre-commit-check) shellHook; nativeBuildInputs = with pkgs; [ # cargo # clippy rustup toolchain just pandoc convco zip reuse # For releases b3sum cargo-bump # For generating demo vhs powertest.packages.${pkgs.system}.default cargo-hack cargo-udeps cargo-outdated ] ++ darwinBuildInputs; }; checks = { pre-commit-check = let toFilter = [ "yamlfmt" "nixfmt" "taplo" "shellcheck" # this doesn't respect our excludes:w ]; filterFn = n: _v: (!builtins.elem n toFilter); treefmtFormatters = pkgs.lib.mapAttrs (_n: v: { inherit (v) enable; }) ( pkgs.lib.filterAttrs filterFn (import .config/treefmt.nix).programs ); in pre-commit-hooks.lib.${system}.run { src = ./.; hooks = treefmtFormatters // { nixfmt-rfc-style.enable = true; convco.enable = true; reuse = { enable = true; name = "reuse"; entry = with pkgs; "${reuse}/bin/reuse lint"; pass_filenames = false; }; }; }; formatting = treefmtEval.config.build.check self; build = packages.check; inherit (packages) default test trycmd ; lint = packages.clippy; }; } ); } ================================================ FILE: justfile ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 _default: @just -l all: build test all-release: build-release test-release [group('house-keeping')] genDemo: fish_prompt="> " fish_history="eza_history" vhs < docs/tapes/demo.tape nsxiv -a docs/images/demo.gif #----------# # building # #----------# # compile the exa binary [group('building')] @build: cargo build # compile the exa binary (in release mode) [group('building')] @build-release: cargo build --release --verbose # produce an HTML chart of compilation timings [group('building')] @build-time: cargo +nightly clean cargo +nightly build -Z timings # check that the exa binary can compile [group('building')] @check: cargo check #---------------# # running tests # #---------------# # run unit tests [group('testing')] @test: cargo test --workspace -- --quiet # run unit tests (in release mode) [group('testing')] @test-release: cargo test --workspace --release --verbose #-----------------------# # code quality and misc # #-----------------------# # lint the code [group('house-keeping')] @clippy: touch src/main.rs cargo clippy # update dependency versions, and checks for outdated ones [group('house-keeping')] @update-deps: cargo update command -v cargo-outdated >/dev/null || (echo "cargo-outdated not installed" && exit 1) cargo outdated # list unused dependencies [group('house-keeping')] @unused-deps: command -v cargo-udeps >/dev/null || (echo "cargo-udeps not installed" && exit 1) cargo +nightly udeps # check that every combination of feature flags is successful [group('house-keeping')] @check-features: command -v cargo-hack >/dev/null || (echo "cargo-hack not installed" && exit 1) cargo hack check --feature-powerset # print versions of the necessary build tools [group('house-keeping')] @versions: rustc --version cargo --version #---------------# # documentation # #---------------# # build the man pages [group('documentation')] @man: mkdir -p "${CARGO_TARGET_DIR:-target}/man" version=$(awk 'BEGIN { FS = "\"" } ; /^version/ { print $2 ; exit }' Cargo.toml); \ for page in eza.1 eza_colors.5 eza_colors-explanation.5; do \ sed "s/\$version/v${version}/g" "man/${page}.md" | pandoc --standalone -f markdown -t man > "${CARGO_TARGET_DIR:-target}/man/${page}"; \ done; # build and preview the main man page (eza.1) [group('documentation')] @man-1-preview: man man "${CARGO_TARGET_DIR:-target}/man/eza.1" # build and preview the colour configuration man page (eza_colors.5) [group('documentation')] @man-5-preview: man man "${CARGO_TARGET_DIR:-target}/man/eza_colors.5" # build and preview the colour configuration man page (eza_colors.5) [group('documentation')] @man-5-explanations-preview: man man "${CARGO_TARGET_DIR:-target}/man/eza_colors-explanation.5" #---------------# # release # #---------------# # To override the version, pass it as an argument before the recipe: # e.g. just version=1.2.3 release # If version is not set, it will be determined by convco. version := "" new_version := if version == "" { "$(convco version --bump)" } else { version } # If you're not cafkafk and she isn't dead, don't run this! # # usage: release major, release minor, release patch [group('release')] release: cargo bump "{{new_version}}" git cliff -c .config/cliff.toml -t "{{new_version}}" > CHANGELOG.md cargo check nix build -L ./#clippy git checkout -b "cafk-release-v{{new_version}}" git commit -asm "chore: eza v{{new_version}} changelogs, version bump" git push @echo "waiting 10 seconds for github to catch up..." sleep 10 gh pr create --draft --title "chore: release v{{new_version}}" --body "This PR was auto-generated by our lovely just file" --reviewer cafkafk @echo "Now go review that and come back and run gh-release" [group('release')] @gh-release: git tag -d "v{{new_version}}" || echo "tag not found, creating"; git tag --sign -a "v{{new_version}}" -m "auto generated by the justfile for eza v$(convco version)" just cross just mangen just completions mkdir -p ./target/"release-notes-$(convco version)" git cliff -c .config/cliff.toml -t "v$(convco version)" --current > ./target/"release-notes-$(convco version)/RELEASE.md" just checksum >> ./target/"release-notes-$(convco version)/RELEASE.md" git push origin "v{{new_version}}" gh release create "v$(convco version)" --target "$(git rev-parse HEAD)" --title "eza v$(convco version)" -d -F ./target/"release-notes-$(convco version)/RELEASE.md" ./target/"bin-$(convco version)"/* ./target/"man-$(convco version).tar.gz" ./target/"completions-$(convco version).tar.gz" #----------------# # binaries # #----------------# # TODO: make static/no_static DRY # TODO: add name prefix/suffix arguments [group('binaries')] tar BINARY TARGET: tar czvf ./target/"bin-$(convco version)"/{{BINARY}}_{{TARGET}}.tar.gz -C ./target/{{TARGET}}/release/ ./{{BINARY}} [group('binaries')] zip BINARY TARGET: zip -j ./target/"bin-$(convco version)"/{{BINARY}}_{{TARGET}}.zip ./target/{{TARGET}}/release/{{BINARY}} [group('binaries')] tar_static BINARY TARGET: tar czvf ./target/"bin-$(convco version)"/{{BINARY}}_{{TARGET}}_static.tar.gz -C ./target/{{TARGET}}/release/ ./{{BINARY}} [group('binaries')] zip_static BINARY TARGET: zip -j ./target/"bin-$(convco version)"/{{BINARY}}_{{TARGET}}_static.zip ./target/{{TARGET}}/release/{{BINARY}} [group('binaries')] tar_no_libgit BINARY TARGET: tar czvf ./target/"bin-$(convco version)"/{{BINARY}}_{{TARGET}}_no_libgit.tar.gz -C ./target/{{TARGET}}/release/ ./{{BINARY}} [group('binaries')] zip_no_libgit BINARY TARGET: zip -j ./target/"bin-$(convco version)"/{{BINARY}}_{{TARGET}}_no_libgit.zip ./target/{{TARGET}}/release/{{BINARY}} [group('binaries')] tar_static_no_libgit BINARY TARGET: tar czvf ./target/"bin-$(convco version)"/{{BINARY}}_{{TARGET}}_static_no_libgit.tar.gz -C ./target/{{TARGET}}/release/ ./{{BINARY}} [group('binaries')] zip_static_no_libgit BINARY TARGET: zip -j ./target/"bin-$(convco version)"/{{BINARY}}_{{TARGET}}_static_no_libgit.zip ./target/{{TARGET}}/release/{{BINARY}} [group('binaries')] binary BINARY TARGET: rustup target add {{TARGET}} cross build --release --target {{TARGET}} just tar {{BINARY}} {{TARGET}} just zip {{BINARY}} {{TARGET}} [group('binaries')] binary_static BINARY TARGET: rustup target add {{TARGET}} RUSTFLAGS='-C target-feature=+crt-static' cross build --release --target {{TARGET}} just tar_static {{BINARY}} {{TARGET}} just zip_static {{BINARY}} {{TARGET}} [group('binaries')] binary_no_libgit BINARY TARGET: rustup target add {{TARGET}} cross build --no-default-features --release --target {{TARGET}} just tar_no_libgit {{BINARY}} {{TARGET}} just zip_no_libgit {{BINARY}} {{TARGET}} [group('binaries')] binary_static_no_libgit BINARY TARGET: rustup target add {{TARGET}} RUSTFLAGS='-C target-feature=+crt-static' cross build --no-default-features --release --target {{TARGET}} just tar_static_no_libgit {{BINARY}} {{TARGET}} just zip_static_no_libgit {{BINARY}} {{TARGET}} [group('binaries')] checksum: @echo "# Checksums" @echo "## sha256sum" @echo '```' @sha256sum ./target/"bin-$(convco version)"/* @echo '```' @echo "## md5sum" @echo '```' @md5sum ./target/"bin-$(convco version)"/* @echo '```' @echo "## blake3sum" @echo '```' @b3sum ./target/"bin-$(convco version)"/* @echo '```' alias c := cross # Generate release binaries for EZA # # usage: cross [group('binaries')] @cross: # Setup Output Directory mkdir -p ./target/"bin-$(convco version)" # Install Toolchains/Targets rustup toolchain install stable ## Linux ### x86 just binary eza x86_64-unknown-linux-gnu # just binary_static eza x86_64-unknown-linux-gnu just binary eza x86_64-unknown-linux-musl # just binary_static eza x86_64-unknown-linux-musl ### aarch just binary eza aarch64-unknown-linux-gnu just binary_no_libgit eza aarch64-unknown-linux-gnu # BUG: just binary_static eza aarch64-unknown-linux-gnu ### arm just binary eza arm-unknown-linux-gnueabihf just binary_no_libgit eza arm-unknown-linux-gnueabihf # just binary_static eza arm-unknown-linux-gnueabihf ## MacOS # TODO: just binary eza x86_64-apple-darwin ## Windows ### x86 just binary eza.exe x86_64-pc-windows-gnu # just binary_static eza.exe x86_64-pc-windows-gnu # TODO: just binary eza.exe x86_64-pc-windows-gnullvm # TODO: just binary eza.exe x86_64-pc-windows-msvc # Generate Checksums # TODO: moved to gh-release just checksum [group('documentation')] @mangen: # Setup Output Directory mkdir -p ./target/"man-$(convco version)" pandoc --standalone -f markdown -t man man/eza.1.md > ./target/"man-$(convco version)"/eza.1 pandoc --standalone -f markdown -t man man/eza_colors.5.md > ./target/"man-$(convco version)"/eza_colors.5 pandoc --standalone -f markdown -t man man/eza_colors-explanation.5.md > ./target/"man-$(convco version)"/eza_colors-explanation.5 tar czvf ./target/"man-$(convco version)".tar.gz ./target/"man-$(convco version)" [group('documentation')] [group('binaries')] @completions: # Setup Output Directory mkdir -p ./target/"completions-$(convco version)" cp completions/*/* ./target/"completions-$(convco version)"/ tar czvf ./target/"completions-$(convco version)".tar.gz ./target/"completions-$(convco version)" #---------------------# # Integration testing # #---------------------# alias gen := gen_test_dir test_dir := "tests/test_dir" [group('testing')] gen_test_dir: bash devtools/dir-generator.sh {{ test_dir }} # Runs integration tests in nix sandbox # # Required nix, likely won't work on windows. [group('testing')] @itest: nix build -L ./#trycmd-local # Runs integration tests in nix sandbox, and dumps outputs. # # WARNING: this can cause loss of work [group('testing')] @idump: rm ./tests/gen/*_nix.stderr -f || echo rm ./tests/gen/*_nix.stdout -f || echo rm ./tests/gen/*_unix.stderr -f || echo rm ./tests/gen/*_unix.stdout -f || echo rm ./tests/ptests/ptest_*.stderr -f || echo rm ./tests/ptests/ptest_*.stdout -f || echo nix build -L ./#trydump find result/dump -type f \( -name "*.stdout" -o -name "*.stderr" \) -exec sh -c 'base=$(basename {}); if [ -e "tests/gen/${base%.*}.toml" ]; then cp {} tests/gen/; elif [ -e "tests/cmd/${base%.*}.toml" ]; then cp {} tests/cmd/; elif [ -e "tests/ptests/${base%.*}.toml" ]; then cp {} tests/ptests/; fi' \; [group('testing')] @itest-gen: nix build -L ./#trycmd # Fully re-generates the integration tests using powertest [group('testing')] @regen: which powertest >&- 2>&- || (echo -e "Powertest not installed. Please Clone the repo and run:\n\tcargo install --path . --locked" && exit 1) echo "WARNING: this will delete all tests in tests/ptest" sleep 5 echo "Deleting tests/ptests" rm -rf tests/ptests echo "Generating tests/ptests" powertest nix build -L ./#trydump find result/dump -type f \( -name "*.stdout" -o -name "*.stderr" \) -exec sh -c 'base=$(basename {}); if [ -e "tests/ptests/${base%.*}.toml" ]; then cp {} tests/ptests/; fi' \; ================================================ FILE: man/eza.1.md ================================================ % eza(1) $version NAME ==== eza — a modern replacement for ls SYNOPSIS ======== `eza [options] [files...]` **eza** is a modern replacement for `ls`. It uses colours for information by default, helping you distinguish between many types of files, such as whether you are the owner, or in the owning group. It also has extra features not present in the original `ls`, such as viewing the Git status for a directory, or recursing into directories with a tree view. EXAMPLES ======== `eza` : Lists the contents of the current directory in a grid. `eza --oneline --reverse --sort=size` : Displays a list of files with the largest at the top. `eza --long --header --inode --git` : Displays a table of files with a header, showing each file’s metadata, inode, and Git status. `eza --long --tree --level=3` : Displays a tree of files, three levels deep, as well as each file’s metadata. META OPTIONS =============== `--help` : Show list of command-line options. `-v`, `--version` : Show version of eza. DISPLAY OPTIONS =============== `-1`, `--oneline` : Display one entry per line. `--absolute=WHEN` : Display entries with their absolute path. Valid settings are '`on`', '`follow`', and '`off`'. When used without a value, defaults to '`on`'. '`on`': Show absolute paths for all entries. '`follow`': Show absolute paths and resolve symbolic links to their targets. '`off`': Show relative paths (default behavior). `-F`, `--classify=WHEN` : Display file kind indicators next to file names. Valid settings are ‘`always`’, ‘`automatic`’ (or ‘`auto`’ for short), and ‘`never`’. When used without a value, defaults to ‘`automatic`’. `automatic` or `auto` will display file kind indicators only when the standard output is connected to a real terminal. If `eza` is ran while in a `tty`, or the output of `eza` is either redirected to a file or piped into another program, file kind indicators will not be used. Setting this option to ‘`always`’ causes `eza` to always display file kind indicators, while ‘`never`’ disables the use of file kind indicators. `-G`, `--grid` : Display entries as a grid (default). `-l`, `--long` : Display extended file metadata as a table. `-R`, `--recurse` : Recurse into directories. `-T`, `--tree` : Recurse into directories as a tree. `--follow-symlinks` : Drill down into symbolic links that point to directories. `-X`, `--dereference` : Dereference symbolic links when displaying information. `-x`, `--across` : Sort the grid across, rather than downwards. `--color=WHEN`, `--colour=WHEN` : When to use terminal colours (using ANSI escape code to colorize the output). Valid settings are ‘`always`’, ‘`automatic`’ (or ‘`auto`’ for short), and ‘`never`’. When used without a value, defaults to ‘`automatic`’. The default behavior (‘`automatic`’ or ‘`auto`’) is to colorize the output only when the standard output is connected to a real terminal. If the output of `eza` is redirected to a file or piped into another program, terminal colors will not be used. Setting this option to ‘`always`’ causes `eza` to always output terminal color, while ‘`never`’ disables the use of terminal color. Manually setting this option overrides `NO_COLOR` environment. `--color-scale`, `--colour-scale` : highlight levels of `field` distinctly. Use comma(,) separated list of all, age, size `--color-scale-mode=MODE`, `--colour-scale-mode=MODE` : Use gradient or fixed colors in `--color-scale`. Valid options are `fixed` or `gradient`. When used without a value, defaults to `gradient`. `--icons=WHEN` : Display icons next to file names. Valid settings are ‘`always`’, ‘`automatic`’ (‘`auto`’ for short), and ‘`never`’. When used without a value, defaults to ‘`automatic`’. `automatic` or `auto` will display icons only when the standard output is connected to a real terminal. If `eza` is ran while in a `tty`, or the output of `eza` is either redirected to a file or piped into another program, icons will not be used. Setting this option to ‘`always`’ causes `eza` to always display icons, while ‘`never`’ disables the use of icons. `--no-quotes` : Don't quote file names with spaces. `--hyperlink` : Display entries as hyperlinks `-w`, `--width=COLS` : Set screen width in columns. FILTERING AND SORTING OPTIONS ============================= `-a`, `--all` : Show hidden and “dot” files. Use this twice to also show the ‘`.`’ and ‘`..`’ directories. `-A`, `--almost-all` : Equivalent to --all; included for compatibility with `ls -A`. `-d`, `--treat-dirs-as-files` : This flag, inherited from `ls`, changes how `eza` handles directory arguments. : Instead of recursing into directories and listing their contents (the default behavior), it treats directories as regular files and lists information about the directory entry itself. : This is useful when you want to see metadata about the directory (e.g., permissions, size, modification time) rather than its contents. : For simply listing only directories and not files, consider using the `--only-dirs` (`-D`) option as an alternative. `-L`, `--level=DEPTH` : Limit the depth of recursion. `-r`, `--reverse` : Reverse the sort order. `-s`, `--sort=SORT_FIELD` : Which field to sort by. Valid sort fields are ‘`name`’, ‘`Name`’, ‘`extension`’, ‘`Extension`’, ‘`size`’, ‘`modified`’, ‘`changed`’, ‘`accessed`’, ‘`created`’, ‘`inode`’, ‘`type`’, and ‘`none`’. The `modified` sort field has the aliases ‘`date`’, ‘`time`’, and ‘`newest`’, and its reverse order has the aliases ‘`age`’ and ‘`oldest`’. Sort fields starting with a capital letter will sort uppercase before lowercase: ‘A’ then ‘B’ then ‘a’ then ‘b’. Fields starting with a lowercase letter will mix them: ‘A’ then ‘a’ then ‘B’ then ‘b’. `-I`, `--ignore-glob=GLOBS` : Glob patterns, pipe-separated, of files to ignore. `--git-ignore` [if eza was built with git support] : Do not list files that are ignored by Git. `--group-directories-first` : List directories before other files. `--group-directories-last` : List directories after other files. `-D`, `--only-dirs` : List only directories, not files. `-f`, `--only-files` : List only files, not directories. `--show-symlinks` : Explicitly show symbolic links (when used with `--only-files` | `--only-dirs`) `--no-symlinks` : Do not show symbolic links LONG VIEW OPTIONS ================= These options are available when running with `--long` (`-l`): `-b`, `--binary` : List file sizes with binary prefixes. `-B`, `--bytes` : List file sizes in bytes, without any prefixes. `--changed` : Use the changed timestamp field. `-g`, `--group` : List each file’s group. `--smart-group` : Only show group if it has a different name from owner `-h`, `--header` : Add a header row to each column. `-H`, `--links` : List each file’s number of hard links. `-i`, `--inode` : List each file’s inode number. `-m`, `--modified` : Use the modified timestamp field. `-M`, `--mounts` : Show mount details (Linux and Mac only) `-n`, `--numeric` : List numeric user and group IDs. `-O`, `--flags` : List file flags on Mac and BSD systems and file attributes on Windows systems. By default, Windows attributes are displayed in a long form. To display in attributes as single character set the environment variable `EZA_WINDOWS_ATTRIBUTES=short`. On BSD systems see chflags(1) for a list of file flags and their meanings. `-S`, `--blocksize` : List each file’s size of allocated file system blocks. `-t`, `--time=WORD` : Which timestamp field to list. : Valid timestamp fields are ‘`modified`’, ‘`changed`’, ‘`accessed`’, and ‘`created`’. `--time-style=STYLE` : How to format timestamps. : Valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, ‘`full-iso`’, ‘`relative`’, or a custom style ‘`+`’ (e.g., ‘`+%Y-%m-%d %H:%M`’ => ‘`2023-09-30 13:00`’). `` should be a chrono format string. For details on the chrono format syntax, please read: https://docs.rs/chrono/latest/chrono/format/strftime/index.html . Alternatively, `` can be a two line string, the first line will be used for non-recent files and the second for recent files. E.g., if `` is "`%Y-%m-%d %H--%m-%d %H:%M`", non-recent files => "`2022-12-30 13`", recent files => "`--09-30 13:34`". `--total-size` : Show recursive directory size (unix only). `-u`, `--accessed` : Use the accessed timestamp field. `-U`, `--created` : Use the created timestamp field. `--no-permissions` : Suppress the permissions field. `-o`, `--octal-permissions` : List each file's permissions in octal format. `--no-filesize` : Suppress the file size field. `--no-user` : Suppress the user field. `--no-time` : Suppress the time field. `--stdin` : When you wish to pipe directories to eza/read from stdin. Separate one per line or define custom separation char in `EZA_STDIN_SEPARATOR` env variable. `-@`, `--extended` : List each file’s extended attributes and sizes. `-Z`, `--context` : List each file's security context. `--git` [if eza was built with git support] : List each file’s Git status, if tracked. This adds a two-character column indicating the staged and unstaged statuses respectively. The status character can be ‘`-`’ for not modified, ‘`M`’ for a modified file, ‘`N`’ for a new file, ‘`D`’ for deleted, ‘`R`’ for renamed, ‘`T`’ for type-change, ‘`I`’ for ignored, and ‘`U`’ for conflicted. Directories will be shown to have the status of their contents, which is how ‘deleted’ is possible if a directory contains a file that has a certain status, it will be shown to have that status. `--git-repos` [if eza was built with git support] : List each directory’s Git status, if tracked. Symbols shown are `|`= clean, `+`= dirty, and `~`= for unknown. `--git-repos-no-status` [if eza was built with git support] : List if a directory is a Git repository, but not its status. All Git repository directories will be shown as (themed) `-` without status indicated. `--no-git` : Don't show Git status (always overrides `--git`, `--git-repos`, `--git-repos-no-status`) ENVIRONMENT VARIABLES ===================== If an environment variable prefixed with `EZA_` is not set, for backward compatibility, it will default to its counterpart starting with `EXA_`. eza responds to the following environment variables: ## `COLUMNS` Overrides the width of the terminal, in characters, however, `-w` takes precedence. For example, ‘`COLUMNS=80 eza`’ will show a grid view with a maximum width of 80 characters. This option won’t do anything when eza’s output doesn’t wrap, such as when using the `--long` view. ## `EZA_STRICT` Enables _strict mode_, which will make eza error when two command-line options are incompatible. Usually, options can override each other going right-to-left on the command line, so that eza can be given aliases: creating an alias ‘`eza=eza --sort=ext`’ then running ‘`eza --sort=size`’ with that alias will run ‘`eza --sort=ext --sort=size`’, and the sorting specified by the user will override the sorting specified by the alias. In strict mode, the two options will not co-operate, and eza will error. This option is intended for use with automated scripts and other situations where you want to be certain you’re typing in the right command. ## `EZA_GRID_ROWS` Limits the grid-details view (‘`eza --grid --long`’) so it’s only activated when at least the given number of rows of output would be generated. With widescreen displays, it’s possible for the grid to look very wide and sparse, on just one or two lines with none of the columns lining up. By specifying a minimum number of rows, you can only use the view if it’s going to be worth using. ## `EZA_ICON_SPACING` Specifies the number of spaces to print between an icon (see the ‘`--icons`’ option) and its file name. Different terminals display icons differently, as they usually take up more than one character width on screen, so there’s no “standard” number of spaces that eza can use to separate an icon from text. One space may place the icon too close to the text, and two spaces may place it too far away. So the choice is left up to the user to configure depending on their terminal emulator. ## `NO_COLOR` Disables colours in the output (regardless of its value). Can be overridden by `--color` option. See `https://no-color.org/` for details. ## `LS_COLORS`, `EZA_COLORS` Specifies the colour scheme used to highlight files based on their name and kind, as well as highlighting metadata and parts of the UI. For more information on the format of these environment variables, see the [eza_colors.5.md](eza_colors.5.md) manual page. ## `EZA_OVERRIDE_GIT` Overrides any `--git` or `--git-repos` argument ## `EZA_MIN_LUMINANCE` Specifies the minimum luminance to use when color-scale is active. It's value can be between -100 to 100. ## `EZA_ICONS_AUTO` If set, automates the same behavior as using `--icons` or `--icons=auto`. Useful for if you always want to have icons enabled. Any explicit use of the `--icons=WHEN` flag overrides this behavior. ## `EZA_STDIN_SEPARATOR` Specifies the separator to use when file names are piped from stdin. Defaults to newline. ## `EZA_CONFIG_DIR` Specifies the directory where eza will look for its configuration and theme files. Defaults to `$XDG_CONFIG_HOME/eza` or `$HOME/.config/eza` if `XDG_CONFIG_HOME` is not set. EXIT STATUSES ============= 0 : If everything goes OK. 1 : If there was an I/O error during operation. 3 : If there was a problem with the command-line arguments. 13 : If permission is denied to access a path. AUTHOR ====== eza is maintained by Christina Sørensen and many other contributors. **Source code:** `https://github.com/eza-community/eza` \ **Contributors:** `https://github.com/eza-community/eza/graphs/contributors` Our infinite thanks to Benjamin ‘ogham’ Sago and all the other contributors of exa, from which eza was forked. SEE ALSO ======== - [**eza_colors**(5)](eza_colors.5.md) - [**eza_colors-explanation**(5)](eza_colors-explanation.5.md) ================================================ FILE: man/eza_colors-explanation.5.md ================================================ % eza_colors-explanation(5) $version # Name eza_colors-explanation — more details on customizing eza colors # Eza Color Explanation eza provides its own built\-in set of file extension mappings that cover a large range of common file extensions, including documents, archives, media, and temporary files. Any mappings in the environment variables will override this default set: running eza with `LS_COLORS="*.zip=32"` will turn zip files green but leave the colours of other compressed files alone. You can also disable this built\-in set entirely by including a `reset` entry at the beginning of `EZA_COLORS`. So setting `EZA_COLORS="reset:*.txt=31"` will highlight only text files; setting `EZA_COLORS="reset"` will highlight nothing. ## Examples - Disable the "current user" highlighting: `EZA_COLORS="uu=0:gu=0"` - Turn the date column green: `EZA_COLORS="da=32"` - Highlight Vagrantfiles: `EZA_COLORS="Vagrantfile=1;4;33"` - Override the existing zip colour: `EZA_COLORS="*.zip=38;5;125"` - Markdown files a shade of green, log files a shade of grey: `EZA_COLORS="*.md=38;5;121:*.log=38;5;248"` ## BUILT\-IN EXTENSIONS - eza now supports bright colours! As supported by most modern 256\-colour terminals, you can now choose from `bright` colour codes when selecting your custom colours in your `#EZA_COLORS` environment variable. - Build (Makefile, Cargo.toml, package.json) are yellow and underlined. - Images (png, jpeg, gif) are purple. - Videos (mp4, ogv, m2ts) are a slightly purpler purple. - Music (mp3, m4a, ogg) is a faint blue. - Lossless music (flac, alac, wav) is a less faint blue. - Cryptographic files (asc, enc, p12) are bright green. - Documents (pdf, doc, dvi) are a fainter green. - Compressed files (zip, tgz, Z) are red. - Temporary files (tmp, swp, ~) are dimmed default foreground color. - Compiled files (class, o, pyc) are yellow. A file is also counted as compiled if it uses a common extension and is in the same directory as one of its source files: styles.css will count as compiled when next to styles.less or styles.sass, and scripts.js when next to scripts.ts or scripts.coffee. - Source files (cpp, js, java) are bright yellow. ## Theme Configuration file Now you can specify these options and more in a `theme.yml` file with convenient syntax for defining your styles. Set `EZA_CONFIG_DIR` to specify which directory you would like eza to look for your `theme.yml` file, otherwise eza will look for `$XDG_CONFIG_HOME/eza/theme.yml`. These are the available options: LIST OF THEME OPTIONS ===================== ```yaml filekinds: normal directory symlink pipe block_device char_device socket special executable mount_point perms: user_read user_write user_execute_file user_execute_other group_read group_write group_execute other_read other_write other_execute special_user_file special_other attribute size: major minor number_byte number_kilo number_mega number_giga number_huge unit_byte unit_kilo unit_mega unit_giga unit_huge users: user_you user_root user_other group_yours group_other group_root links: normal multi_link_file git: new modified deleted renamed ignored conflicted git_repo: branch_main branch_other git_clean git_dirty security_context: none: selinux: colon user role typ range file_type: image video music crypto document compressed temp compiled build source punctuation: date: inode: blocks: header: octal: flags: control_char: broken_symlink: broken_path_overlay: ``` Each of those fields/sub fields can have the following styling properties defined beneath it ```yaml foreground: Blue background: null is_bold: false is_dimmed: false is_italic: false is_underline: false is_blink: false is_reverse: false is_hidden: false is_strikethrough: true prefix_with_reset: false ``` Example: ```yaml file_type: image: foreground: Blue is_italic: true date: foreground: White security_context: selinux: role: is_hidden: true ``` Icons can now be customized as well in the `filenames` and `extensions` fields ```yaml filenames: # Just change the icon glyph Cargo.toml: {icon: {glyph: 🦀}} Cargo.lock: {icon: {glyph: 🦀}} extensions: rs: { filename: {foreground: Red}, icon: {glyph: 🦀}} ``` **NOTES:** Not all glyphs support changing colors. If your theme is not working properly, double check the syntax in the config file, as a syntax issue can cause multiple properties to not be applied. You must name the file `theme.yml`, no matter the directory you specify. ## See also - [**eza**(1)](eza.1.md) - [**eza_colors**(5)](eza_colors.5.md) ================================================ FILE: man/eza_colors.5.md ================================================ % eza_colors(5) $version NAME ==== eza_colors — customising the file and UI colours of eza SYNOPSIS ======== The `EZA_COLORS` environment variable can be used to customise the colours that `eza` uses to highlight file names, file metadata, and parts of the UI. You can use the `dircolors` program to generate a script that sets the variable from an input file, or if you don’t mind editing long strings of text, you can just type it out directly. These variables have the following structure: - A list of key-value pairs separated by ‘`=`’, such as ‘`*.txt=32`’. - Multiple ANSI formatting codes are separated by ‘`;`’, such as ‘`*.txt=32;1;4`’. - Finally, multiple pairs are separated by ‘`:`’, such as ‘`*.txt=32:*.mp3=1;35`’. The key half of the pair can either be a two-letter code or a file glob, and anything that’s not a valid code will be treated as a glob, including keys that happen to be two letters long. For backwards compatibility `EXA_COLORS` environment variables is checked if `EZA_COLORS` is unset. EXAMPLES ======== `EZA_COLORS="uu=0:gu=0"` : Disable the “current user” highlighting `EZA_COLORS="da=32"` : Turn the date column green `EZA_COLORS="Vagrantfile=1;4;33"` : Highlight Vagrantfiles `EZA_COLORS="*.zip=38;5;125"` : Override the existing zip colour `EZA_COLORS="*.md=38;5;121:*.log=38;5;248"` : Markdown files a shade of green, log files a shade of grey LIST OF CODES ============= `LS_COLORS` can use these ten codes: `di` : directories `ex` : executable files `fi` : regular files `pi` : named pipes `so` : sockets `bd` : block devices `cd` : character devices `ln` : symlinks `or` : symlinks with no target `EZA_COLORS` can use many more: `oc` : the permissions displayed as octal `ur` : the user-read permission bit `uw` : the user-write permission bit `ux` : the user-execute permission bit for regular files `ue` : the user-execute for other file kinds `gr` : the group-read permission bit `gw` : the group-write permission bit `gx` : the group-execute permission bit `tr` : the others-read permission bit `tw` : the others-write permission bit `tx` : the others-execute permission bit `su` : setuid, setgid, and sticky permission bits for files `sf` : setuid, setgid, and sticky for other file kinds `xa` : the extended attribute indicator `sn` : the numbers of a file’s size (sets `nb`, `nk`, `nm`, `ng` and `nt`) `nb` : the numbers of a file’s size if it is lower than 1 KB/Kib `nk` : the numbers of a file’s size if it is between 1 KB/KiB and 1 MB/MiB `nm` : the numbers of a file’s size if it is between 1 MB/MiB and 1 GB/GiB `ng` : the numbers of a file’s size if it is between 1 GB/GiB and 1 TB/TiB `nt` : the numbers of a file’s size if it is 1 TB/TiB or higher `sb` : the units of a file’s size (sets `ub`, `uk`, `um`, `ug` and `ut`) `ub` : the units of a file’s size if it is lower than 1 KB/Kib `uk` : the units of a file’s size if it is between 1 KB/KiB and 1 MB/MiB `um` : the units of a file’s size if it is between 1 MB/MiB and 1 GB/GiB `ug` : the units of a file’s size if it is between 1 GB/GiB and 1 TB/TiB `ut` : the units of a file’s size if it is 1 TB/TiB or higher `df` : a device’s major ID `ds` : a device’s minor ID `uu` : a user that’s you `uR` : a user that's root `un` : a user that’s someone else `gu` : a group that you belong to `gR` : a group related to root `gn` : a group you aren’t a member of `lc` : a number of hard links `lm` : a number of hard links for a regular file with at least two `ga` : a new flag in Git `gm` : a modified flag in Git `gd` : a deleted flag in Git `gv` : a renamed flag in Git `gt` : a modified metadata flag in Git `gi` : an ignored flag in Git `gc` : a conflicted flag in Git `Gm` : main branch of repo `Go` : other branch of repo `Gc` : clean branch of repo `Gd` : dirty branch of repo `xx` : “punctuation”, including many background UI elements `da` : a file’s date `in` : a file’s inode number `bl` : a file’s number of blocks `hd` : the header row of a table `lp` : the path of a symlink `cc` : an escaped character in a filename `bO` : the overlay style for broken symlink paths `sp` : special (not file, dir, mount, exec, pipe, socket, block device, char device, or link) `mp` : a mount point `im` : a regular file that is an image `vi` : a regular file that is a video `mu` : a regular file that is lossy music `lo` : a regular file that is lossless music `cr` : a regular file that is related to cryptography (ex: key or certificate) `do` : a regular file that is a document (ex: office suite document or PDF) `co` : a regular file that is compressed `tm` : a regular file that is temporary (ex: a text editor's backup file) `cm` : a regular file that is a compilation artifact (ex: Java class file) `bu` : a regular file that is used to build a project (ex: Makefile) `sc` : a regular file that is source code `ic` : the icon (this is optional, if not set the icon color matches the file name's) `Sn` : No security context on a file `Su` : SELinux user `Sr` : SELinux role `St` : SELinux type `Sl` : SELinux level `ff` : BSD file flags Values in `EXA_COLORS` override those given in `LS_COLORS`, so you don’t need to re-write an existing `LS_COLORS` variable with proprietary extensions. LIST OF STYLES ============== Unlike some versions of `ls`, the given ANSI values must be valid colour codes: eza won’t just print out whichever characters are given. The codes accepted by eza are: `1` : for bold `2` : for dimmed `3` : for italic `4` : for underline `31` : for red text `32` : for green text `33` : for yellow text `34` : for blue text `35` : for purple text `36` : for cyan text `37` : for white text `90` : for dark gray text `91` : for bright red text `92` : for bright green text `93` : for bright yellow text `94` : for bright blue text `95` : for bright purple text `96` : for bright cyan text `97` : for bright text `38;5;nnn` : for a colour from 0 to 255 (replace the `nnn` part) Many terminals will treat bolded text as a different colour, or at least provide the option to. eza provides its own built-in set of file extension mappings that cover a large range of common file extensions, including documents, archives, media, and temporary files. Any mappings in the environment variables will override this default set: running eza with `LS_COLORS="*.zip=32"` will turn zip files green but leave the colours of other compressed files alone. You can also disable this built-in set entirely by including a `reset` entry at the beginning of `EZA_COLORS`. So setting `EZA_COLORS="reset:*.txt=31"` will highlight only text files; setting `EZA_COLORS="reset"` will highlight nothing. AUTHOR ====== eza is maintained by Christina Sørensen and many other contributors. **Source code:** `https://github.com/eza-community/eza` \ **Contributors:** `https://github.com/eza-community/eza/graphs/contributors` Our infinite thanks to Benjamin ‘ogham’ Sago and all the other contributors of exa, from which eza was forked. SEE ALSO ======== - [**eza**(1)](eza.1.md) - [**eza_colors-explanation**(5)](eza_colors-explanation.5.md) ================================================ FILE: nix/eza.nix ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 { pkgs, naersk', buildInputs, ... }: naersk'.buildPackage rec { pname = "eza"; version = "git"; src = ../.; doCheck = true; inherit buildInputs; nativeBuildInputs = with pkgs; [ cmake pkg-config installShellFiles pandoc ]; buildNoDefaultFeatures = true; buildFeatures = "git"; postInstall = '' for page in eza.1 eza_colors.5 eza_colors-explanation.5; do sed "s/\$version/${version}/g" "man/$page.md" | pandoc --standalone -f markdown -t man >"man/$page" done installManPage man/eza.1 man/eza_colors.5 man/eza_colors-explanation.5 installShellCompletion \ --bash completions/bash/eza \ --fish completions/fish/eza.fish \ --zsh completions/zsh/_eza ''; meta = with pkgs.lib; { description = "A modern, maintained replacement for ls"; longDescription = '' eza is a modern replacement for ls. It uses colours for information by default, helping you distinguish between many types of files, such as whether you are the owner, or in the owning group. It also has extra features not present in the original ls, such as viewing the Git status for a directory, or recursing into directories with a tree view. eza is written in Rust, so it’s small, fast, and portable. ''; homepage = "https://github.com/eza-community/eza"; license = licenses.mit; mainProgram = "eza"; maintainers = with maintainers; [ cafkafk ]; }; } ================================================ FILE: nix/trycmd.nix ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 { pkgs, naersk', buildInputs, ... }: { trycmd = naersk'.buildPackage { src = ../.; mode = "test"; doCheck = true; # No reason to wait for release build release = false; # buildPhase files differ between dep and main phase singleStep = true; # generate testing files buildPhase = '' bash devtools/dir-generator.sh tests/test_dir && echo "Dir generated" bash devtools/generate-timestamp-test-dir.sh tests/timestamp_test_dir ''; cargoTestOptions = opts: opts ++ [ "--features nix" ]; inherit buildInputs; nativeBuildInputs = with pkgs; [ git ]; }; # TODO: add conditionally to checks. # Run `nix build .#trycmd` to run integration tests trycmd-local = naersk'.buildPackage { src = ../.; mode = "test"; doCheck = true; # No reason to wait for release build release = false; # buildPhase files differ between dep and main phase singleStep = true; # set itests files creation date to unix epoch buildPhase = '' bash devtools/dir-generator.sh tests/test_dir bash devtools/generate-timestamp-test-dir.sh tests/timestamp_test_dir touch --date=@0 tests/itest/* touch --date=@0 tests/ptests/*; fd -e stdout -e stderr -H -t file -X sed -i 's/[CWD]\//\/build\/source\//g' ''; cargoTestOptions = opts: opts ++ [ "--features nix" "--features nix-local" "--features powertest" ]; inherit buildInputs; nativeBuildInputs = with pkgs; [ git ]; }; # Run `nix build .#trydump` to dump testing files trydump = naersk'.buildPackage { src = ../.; mode = "test"; doCheck = true; # No reason to wait for release build release = false; # buildPhase files differ between dep and main phase singleStep = true; # set itests files creation date to unix epoch buildPhase = '' bash devtools/dir-generator.sh tests/test_dir bash devtools/generate-timestamp-test-dir.sh tests/timestamp_test_dir touch --date=@0 tests/itest/*; rm tests/cmd/*.stdout || echo; rm tests/cmd/*.stderr || echo; touch --date=@0 tests/ptests/*; rm tests/ptests/*.stdout || echo; rm tests/ptests/*.stderr || echo; ''; cargoTestOptions = opts: opts ++ [ "--features nix" "--features nix-local" "--features powertest" #"-F trycmd/debug" ]; TRYCMD = "dump"; postInstall = '' fd -e stdout -e stderr -H -t file -X sed -i 's/\/build\/source\//[CWD]\//g' cp dump $out -r ''; inherit buildInputs; nativeBuildInputs = with pkgs; [ fd gnused git ]; }; } ================================================ FILE: powertest.yaml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 dump_dir: tests/ptests depth: 1 binary: eza gen_binary: target/debug/eza args: tests/test_dir commands: ? - null # Meta options - --help : ? - -v - --version : ? - -1 # Display Options - --oneline : ? - -l - --long : ? - -G - --grid : ? - -x - --across : ? - -R - --recurse : ? - -T - --tree : ? - -X - --dereference : ? - -F - --classify : ? - -F - --classify : values: - auto - always - never ? - null - --color : values: - auto - always - never ? - null - --colour : values: - auto - always - never ? - null - --icons : ? - null - --icons : values: - auto - always - never ? - null - --no-quotes : ? - null - --hyperlink : ? - null - --absolute : values: - on - follow - off ? - -w - --width : values: - 15 - 30 - 200 ? - null - --smart-group : ? - -a # Filtering and Sorting Options - --all : ? - -A - --almost-all : ? - -d - --treat-dirs-as-files : ? - null - --treat-dirs-as-files : ? - -L # Hidden alias - --level : prefix: -T values: - 0 - 1 - 2 - 3 - 4 - 5 ? - -r - --reverse : ? - -s - --sort : short: -s long: time-style prefix: -l values: #- accessed - age #- changed #- created - date - Ext - ext - Extension - extension - Filename - filename - inode - modified - Name - name - newest #- none seems non-deterministic - oldest - size - time - type ? - null - --group-directories-first : ? - null - --group-directories-last : ? - -D - --only-dirs : ? - -f - --only-files : ? - -f - --only-files : ? - -I # TODO: add more globs - --ignore-glob : prefix: -l values: - "*.toml" ? - null - --git-ignore : ? - -b # Long View Options - --binary : ? - -B - --bytes : ? - -g - --group : ? - -h - --header : ? - -H - --links : ? - -i - --inode : ? - -m - --modified : ? - -M - --mounts : ? - -n - --numeric : ? - -S - --blocksize : ? - -t - --time : prefix: -l values: - modified #- accessed BROKEN #- changed #- created ? - -u - --accessed : ? - -U - --created : ? - null - --changed : ? - null - --time-style : long: time-style values: - default - iso - long-iso - full-iso - relative ? - null - --total-size : ? - null - --no-permissions : ? - -o - --octal-permissions : ? - null - --no-filesize : ? - null - --no-user : ? - null - --git : ? - null - --no-git : ? - null - --git-repos : ? - -@ - --extended : ? - -Z - --context : ================================================ FILE: rust-toolchain.toml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 [toolchain] # NOTE: don't forget to also update Cargo.toml and .github/workflows/unit-tests.yml # At the time of writing, 1.90 is the latest version supported by OpenBSD channel = "1.90" components = [ "rustfmt", "rustc", "rust-src", "rust-analyzer", "cargo", "clippy", ] profile = "minimal" ================================================ FILE: snap/snapcraft.yaml ================================================ # SPDX-FileCopyrightText: 2024 Christina Sørensen # SPDX-License-Identifier: EUPL-1.2 name: eza base: core24 version: 'latest' summary: Replacement for 'ls' written in Rust description: | It uses colours for information by default, helping you distinguish between many types of files, such as whether you are the owner, or in the owning group. It also has extra features not present in the original ls, such as viewing the Git status for a directory, or recursing into directories with a tree view. eza is written in Rust, and it's small, fast, and portable. issues: https://github.com/eza-community/eza/issues source-code: https://github.com/eza-community/eza contact: christina@cafkafk.com website: https://eza.rocks/ license: MIT grade: stable confinement: classic apps: eza: command: bin/eza parts: eza: plugin: rust source: . build-attributes: - enable-patchelf stage-packages: - cmake - libz-dev platforms: amd64: build-on: [amd64, arm64] arm64: build-on: [amd64, arm64] armhf: build-on: [amd64, arm64] ================================================ FILE: src/fs/dir.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT use crate::fs::feature::git::GitCache; use crate::fs::fields::GitStatus; use std::fs; use std::fs::DirEntry; use std::io; use std::path::{Path, PathBuf}; use std::slice::Iter as SliceIter; use log::info; use crate::fs::File; /// A **Dir** provides a cached list of the file paths in a directory that’s /// being listed. /// /// This object gets passed to the Files themselves, in order for them to /// check the existence of surrounding files, then highlight themselves /// accordingly. (See `File#get_source_files`) pub struct Dir { /// A vector of the files that have been read from this directory. contents: Vec, /// The path that was read. pub path: PathBuf, } impl Dir { /// Create a new, empty `Dir` object representing the directory at the given path. /// /// This function does not attempt to read the contents of the directory; it merely /// initializes an instance of `Dir` with an empty `DirEntry` list and the specified path. /// To populate the `Dir` object with actual directory contents, use the `read` function. pub fn new(path: PathBuf) -> Self { Self { contents: vec![], path, } } /// Reads the contents of the directory into `DirEntry`. /// /// It is recommended to use this method in conjunction with `new` in recursive /// calls, rather than `read_dir`, to avoid holding multiple open file descriptors /// simultaneously, which can lead to "too many open files" errors. pub fn read(&mut self) -> io::Result<&Self> { info!("Reading directory {:?}", &self.path); self.contents = fs::read_dir(&self.path)?.collect::, _>>()?; info!("Read directory success {:?}", &self.path); Ok(self) } /// Create a new Dir object filled with all the files in the directory /// pointed to by the given path. Fails if the directory can’t be read, or /// isn’t actually a directory, or if there’s an IO error that occurs at /// any point. /// /// The `read_dir` iterator doesn’t actually yield the `.` and `..` /// entries, so if the user wants to see them, we’ll have to add them /// ourselves after the files have been read. pub fn read_dir(path: PathBuf) -> io::Result { info!("Reading directory {:?}", &path); let contents = fs::read_dir(&path)?.collect::, _>>()?; info!("Read directory success {:?}", &path); Ok(Self { contents, path }) } /// Produce an iterator of IO results of trying to read all the files in /// this directory. #[must_use] pub fn files<'dir, 'ig>( &'dir self, dots: DotFilter, git: Option<&'ig GitCache>, git_ignoring: bool, deref_links: bool, total_size: bool, ) -> Files<'dir, 'ig> { Files { inner: self.contents.iter(), dir: self, dotfiles: dots.shows_dotfiles(), dots: dots.dots(), git, git_ignoring, deref_links, total_size, } } /// Whether this directory contains a file with the given path. #[must_use] pub fn contains(&self, path: &Path) -> bool { self.contents.iter().any(|p| p.path().as_path() == path) } /// Append a path onto the path specified by this directory. #[must_use] pub fn join(&self, child: &Path) -> PathBuf { self.path.join(child) } } /// Iterator over reading the contents of a directory as `File` objects. #[allow(clippy::struct_excessive_bools)] pub struct Files<'dir, 'ig> { /// The internal iterator over the paths that have been read already. inner: SliceIter<'dir, DirEntry>, /// The directory that begat those paths. dir: &'dir Dir, /// Whether to include dotfiles in the list. dotfiles: bool, /// Whether the `.` or `..` directories should be produced first, before /// any files have been listed. dots: DotsNext, git: Option<&'ig GitCache>, git_ignoring: bool, /// Whether symbolic links should be dereferenced when querying information. deref_links: bool, /// Whether to calculate the directory size recursively total_size: bool, } impl<'dir> Files<'dir, '_> { fn parent(&self) -> PathBuf { // We can’t use `Path#parent` here because all it does is remove the // last path component, which is no good for us if the path is // relative. For example, while the parent of `/testcases/files` is // `/testcases`, the parent of `.` is an empty path. Adding `..` on // the end is the only way to get to the *actual* parent directory. self.dir.path.join("..") } /// Go through the directory until we encounter a file we can list (which /// varies depending on the dotfile visibility flag) fn next_visible_file(&mut self) -> Option> { loop { if let Some(entry) = self.inner.next() { let path = entry.path(); let filename = File::filename(&path); if !self.dotfiles && filename.starts_with('.') { continue; } // Also hide _prefix files on Windows because it's used by old applications // as an alternative to dot-prefix files. #[cfg(windows)] if !self.dotfiles && filename.starts_with('_') { continue; } if self.git_ignoring { let git_status = self.git.map(|g| g.get(&path, false)).unwrap_or_default(); if git_status.unstaged == GitStatus::Ignored { continue; } } let file = File::from_args( path, self.dir, filename, self.deref_links, self.total_size, entry.file_type().ok(), ); // Windows has its own concept of hidden files, when dotfiles are // hidden Windows hidden files should also be filtered out #[cfg(windows)] if !self.dotfiles && file.attributes().map_or(false, |a| a.hidden) { continue; } return Some(file); } return None; } } } /// The dot directories that need to be listed before actual files, if any. /// If these aren’t being printed, then `FilesNext` is used to skip them. enum DotsNext { /// List the `.` directory next. Dot, /// List the `..` directory next. DotDot, /// Forget about the dot directories and just list files. Files, } impl<'dir> Iterator for Files<'dir, '_> { type Item = File<'dir>; fn next(&mut self) -> Option { match self.dots { DotsNext::Dot => { self.dots = DotsNext::DotDot; Some(File::new_aa_current(self.dir, self.total_size)) } DotsNext::DotDot => { self.dots = DotsNext::Files; Some(File::new_aa_parent( self.parent(), self.dir, self.total_size, )) } DotsNext::Files => self.next_visible_file(), } } } /// Usually files in Unix use a leading dot to be hidden or visible, but two /// entries in particular are “extra-hidden”: `.` and `..`, which only become /// visible after an extra `-a` option. #[derive(PartialEq, Eq, Debug, Default, Copy, Clone)] pub enum DotFilter { /// Shows files, dotfiles, and `.` and `..`. DotfilesAndDots, /// Show files and dotfiles, but hide `.` and `..`. Dotfiles, /// Just show files, hiding anything beginning with a dot. #[default] JustFiles, } impl DotFilter { /// Whether this filter should show dotfiles in a listing. fn shows_dotfiles(self) -> bool { match self { Self::JustFiles => false, Self::Dotfiles => true, Self::DotfilesAndDots => true, } } /// Whether this filter should add dot directories to a listing. fn dots(self) -> DotsNext { match self { Self::JustFiles => DotsNext::Files, Self::Dotfiles => DotsNext::Files, Self::DotfilesAndDots => DotsNext::Dot, } } } ================================================ FILE: src/fs/dir_action.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT //! What to do when encountering a directory? /// The action to take when trying to list a file that turns out to be a /// directory. /// /// By default, exa will display the information about files passed in as /// command-line arguments, with one file per entry. However, if a directory /// is passed in, exa assumes that the user wants to see its contents, rather /// than the directory itself. /// /// This can get annoying sometimes: if a user does `exa ~/Downloads/img-*` /// to see the details of every file starting with `img-`, any directories /// that happen to start with the same will be listed after the files at /// the end in a separate block. By listing directories as files, their /// directory status will be ignored, and both will be listed side-by-side. /// /// These two modes have recursive analogues in the “recurse” and “tree” /// modes. Here, instead of just listing the directories, exa will descend /// into them and print out their contents. The recurse mode does this by /// having extra output blocks at the end, while the tree mode will show /// directories inline, with their contents immediately underneath. #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum DirAction { /// This directory should be listed along with the regular files, instead /// of having its contents queried. AsFile, /// This directory should not be listed, and should instead be opened and /// *its* files listed separately. This is the default behaviour. List, /// This directory should be listed along with the regular files, and then /// its contents should be listed afterward. The recursive contents of /// *those* contents are dictated by the options argument. Recurse(RecurseOptions), } impl DirAction { /// Gets the recurse options, if this dir action has any. #[must_use] pub fn recurse_options(self) -> Option { match self { Self::Recurse(o) => Some(o), _ => None, } } /// Whether to treat directories as regular files or not. #[must_use] pub fn treat_dirs_as_files(self) -> bool { match self { Self::AsFile => true, Self::Recurse(o) => o.tree, Self::List => false, } } } /// The options that determine how to recurse into a directory. #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub struct RecurseOptions { /// Whether recursion should be done as a tree or as multiple individual /// views of files. pub tree: bool, /// The maximum number of times that recursion should descend to, if one /// is specified. pub max_depth: Option, } impl RecurseOptions { /// Returns whether a directory of the given depth would be too deep. #[must_use] pub fn is_too_deep(self, depth: usize) -> bool { match self.max_depth { None => false, Some(d) => d <= depth, } } } ================================================ FILE: src/fs/feature/git.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT //! Getting the Git status of files and directories. use std::env; use std::ffi::OsStr; #[cfg(target_family = "unix")] use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::sync::Mutex; use git2::StatusEntry; use log::{debug, error, info, warn}; use crate::fs::fields as f; /// A **Git cache** is assembled based on the user’s input arguments. /// /// This uses vectors to avoid the overhead of hashing: it’s not worth it when the /// expected number of Git repositories per exa invocation is 0 or 1... pub struct GitCache { /// A list of discovered Git repositories and their paths. repos: Vec, /// Paths that we’ve confirmed do not have Git repositories underneath them. misses: Vec, } impl GitCache { #[must_use] pub fn has_anything_for(&self, index: &Path) -> bool { self.repos.iter().any(|e| e.has_path(index)) } #[must_use] pub fn get(&self, index: &Path, prefix_lookup: bool) -> f::Git { self.repos .iter() .find(|repo| repo.has_path(index)) .map(|repo| repo.search(index, prefix_lookup)) .unwrap_or_default() } } use std::iter::FromIterator; impl FromIterator for GitCache { fn from_iter(iter: I) -> Self where I: IntoIterator, { let iter = iter.into_iter(); let mut git = Self { repos: Vec::with_capacity(iter.size_hint().0), misses: Vec::new(), }; if let Ok(path) = env::var("GIT_DIR") { // These flags are consistent with how `git` uses GIT_DIR: let flags = git2::RepositoryOpenFlags::NO_SEARCH | git2::RepositoryOpenFlags::NO_DOTGIT; match GitRepo::discover(path.into(), flags) { Ok(repo) => { debug!("Opened GIT_DIR repo"); git.repos.push(repo); } Err(miss) => { git.misses.push(miss); } } } for path in iter { if git.misses.contains(&path) { debug!("Skipping {path:?} because it already came back Gitless"); } else if git.repos.iter().any(|e| e.has_path(&path)) { debug!("Skipping {path:?} because we already queried it"); } else { let flags = git2::RepositoryOpenFlags::FROM_ENV; match GitRepo::discover(path, flags) { Ok(r) => { if let Some(r2) = git.repos.iter_mut().find(|e| e.has_workdir(&r.workdir)) { debug!( "Adding to existing repo (workdir matches with {:?})", r2.workdir ); r2.extra_paths.push(r.original_path); continue; } debug!("Discovered new Git repo"); git.repos.push(r); } Err(miss) => { git.misses.push(miss); } } } } git } } /// A **Git repository** is one we’ve discovered somewhere on the filesystem. pub struct GitRepo { /// The queryable contents of the repository: either a `git2` repo, or the /// cached results from when we queried it last time. contents: Mutex, /// The working directory of this repository. /// This is used to check whether two repositories are the same. workdir: PathBuf, /// The path that was originally checked to discover this repository. /// This is as important as the `extra_paths` (it gets checked first), but /// is separate to avoid having to deal with a non-empty Vec. original_path: PathBuf, /// Any other paths that were checked only to result in this same /// repository. extra_paths: Vec, } /// A repository’s queried state. enum GitContents { /// All the interesting Git stuff goes through this. Before { repo: git2::Repository }, /// Temporary value used in `repo_to_statuses` so we can move the /// repository out of the `Before` variant. Processing, /// The data we’ve extracted from the repository, but only after we’ve /// actually done so. After { statuses: Git }, } impl GitRepo { /// Searches through this repository for a path (to a file or directory, /// depending on the prefix-lookup flag) and returns its Git status. /// /// Actually querying the `git2` repository for the mapping of paths to /// Git statuses is only done once, and gets cached so we don’t need to /// re-query the entire repository the times after that. /// /// The temporary `Processing` enum variant is used after the `git2` /// repository is moved out, but before the results have been moved in! /// See fn search(&self, index: &Path, prefix_lookup: bool) -> f::Git { use std::mem::replace; let mut contents = self.contents.lock().unwrap(); if let GitContents::After { ref statuses } = *contents { debug!("Git repo {:?} has been found in cache", &self.workdir); return statuses.status(index, prefix_lookup); } debug!("Querying Git repo {:?} for the first time", &self.workdir); let repo = replace(&mut *contents, GitContents::Processing).inner_repo(); let statuses = repo_to_statuses(&repo, &self.workdir); let result = statuses.status(index, prefix_lookup); let _processing = replace(&mut *contents, GitContents::After { statuses }); result } /// Whether this repository has the given working directory. fn has_workdir(&self, path: &Path) -> bool { self.workdir == path } /// Whether this repository cares about the given path at all. fn has_path(&self, path: &Path) -> bool { path.starts_with(&self.original_path) || self.extra_paths.iter().any(|e| path.starts_with(e)) } /// Open a Git repository. Depending on the flags, the path is either /// the repository's "gitdir" (or a "gitlink" to the gitdir), or the /// path is the start of a rootwards search for the repository. fn discover(path: PathBuf, flags: git2::RepositoryOpenFlags) -> Result { info!("Opening Git repository for {path:?} ({flags:?})"); let unused: [&OsStr; 0] = []; let repo = match git2::Repository::open_ext(&path, flags, unused) { Ok(r) => r, Err(e) => { error!("Error opening Git repository for {path:?}: {e:?}"); return Err(path); } }; if let Some(workdir) = repo.workdir() { let workdir = workdir.to_path_buf(); let contents = Mutex::new(GitContents::Before { repo }); Ok(Self { contents, workdir, original_path: path, extra_paths: Vec::new(), }) } else { warn!("Repository has no workdir?"); Err(path) } } } impl GitContents { /// Assumes that the repository hasn’t been queried, and extracts it /// (consuming the value) if it has. This is needed because the entire /// enum variant gets replaced when a repo is queried (see above). fn inner_repo(self) -> git2::Repository { if let Self::Before { repo } = self { repo } else { unreachable!("Tried to extract a non-Repository") } } } /// Iterates through a repository’s statuses, consuming it and returning the /// mapping of files to their Git status. /// We will have already used the working directory at this point, so it gets /// passed in rather than deriving it from the `Repository` again. fn repo_to_statuses(repo: &git2::Repository, workdir: &Path) -> Git { let mut statuses = Vec::new(); info!("Getting Git statuses for repo with workdir {workdir:?}"); match repo.statuses(None) { Ok(es) => { for e in es.iter() { if let Some(p) = get_path_from_status_entry(&e) { let elem = (workdir.join(p), e.status()); statuses.push(elem); } } // We manually add the `.git` at the root of the repo as ignored, since it is in practice. // Also we want to avoid `eza --tree --all --git-ignore` to display files inside `.git`. statuses.push((workdir.join(".git"), git2::Status::IGNORED)); } Err(e) => { error!("Error looking up Git statuses: {e:?}"); } } Git { statuses } } #[allow(clippy::unnecessary_wraps)] fn get_path_from_status_entry(e: &StatusEntry<'_>) -> Option { #[cfg(target_family = "unix")] return Some(PathBuf::from(OsStr::from_bytes(e.path_bytes()))); #[cfg(not(target_family = "unix"))] return if let Some(p) = e.path() { Some(PathBuf::from(p)) } else { info!("Git status ignored for non ASCII path {:?}", e.path_bytes()); None }; } // The `repo.statuses` call above takes a long time. exa debug output: // // 20.311276 INFO:exa::fs::feature::git: Getting Git statuses for repo with workdir "/vagrant/" // 20.799610 DEBUG:exa::output::table: Getting Git status for file "./Cargo.toml" // // Even inserting another logging line immediately afterwards doesn’t make it // look any faster. /// Container of Git statuses for all the files in this folder’s Git repository. struct Git { statuses: Vec<(PathBuf, git2::Status)>, } impl Git { /// Get either the file or directory status for the given path. /// “Prefix lookup” means that it should report an aggregate status of all /// paths starting with the given prefix (in other words, a directory). fn status(&self, index: &Path, prefix_lookup: bool) -> f::Git { if prefix_lookup { self.dir_status(index) } else { self.file_status(index) } } /// Get the user-facing status of a file. /// We check the statuses directly applying to a file, and for the ignored /// status we check if any of its parents directories is ignored by git. fn file_status(&self, file: &Path) -> f::Git { let path = reorient(file); let s = self .statuses .iter() .filter(|p| { if p.1 == git2::Status::IGNORED { path.starts_with(&p.0) } else { p.0 == path } }) .fold(git2::Status::empty(), |a, b| a | b.1); let staged = index_status(s); let unstaged = working_tree_status(s); f::Git { staged, unstaged } } /// Get the combined, user-facing status of a directory. /// Statuses are aggregating (for example, a directory is considered /// modified if any file under it has the status modified), except for /// ignored status which applies to files under (for example, a directory /// is considered ignored if one of its parent directories is ignored). fn dir_status(&self, dir: &Path) -> f::Git { let path = reorient(dir); let s = self .statuses .iter() .filter(|p| { if p.1 == git2::Status::IGNORED { path.starts_with(&p.0) } else { p.0.starts_with(&path) } }) .fold(git2::Status::empty(), |a, b| a | b.1); let staged = index_status(s); let unstaged = working_tree_status(s); f::Git { staged, unstaged } } } /// Converts a path to an absolute path based on the current directory. /// Paths need to be absolute for them to be compared properly, otherwise /// you’d ask a repo about “./README.md” but it only knows about /// “/vagrant/README.md”, prefixed by the workdir. #[cfg(unix)] fn reorient(path: &Path) -> PathBuf { use std::env::current_dir; // TODO: I’m not 100% on this func tbh let path = match current_dir() { Err(_) => Path::new(".").join(path), Ok(dir) => dir.join(path), }; path.canonicalize().unwrap_or(path) } #[cfg(windows)] fn reorient(path: &Path) -> PathBuf { let unc_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); // On Windows UNC path is returned. We need to strip the prefix for it to work. let normal_path = unc_path .as_os_str() .to_str() .unwrap() .trim_start_matches("\\\\?\\"); PathBuf::from(normal_path) } /// The character to display if the file has been modified, but not staged. fn working_tree_status(status: git2::Status) -> f::GitStatus { #[rustfmt::skip] return match status { s if s.contains(git2::Status::WT_NEW) => f::GitStatus::New, s if s.contains(git2::Status::WT_MODIFIED) => f::GitStatus::Modified, s if s.contains(git2::Status::WT_DELETED) => f::GitStatus::Deleted, s if s.contains(git2::Status::WT_RENAMED) => f::GitStatus::Renamed, s if s.contains(git2::Status::WT_TYPECHANGE) => f::GitStatus::TypeChange, s if s.contains(git2::Status::IGNORED) => f::GitStatus::Ignored, s if s.contains(git2::Status::CONFLICTED) => f::GitStatus::Conflicted, _ => f::GitStatus::NotModified, }; } /// The character to display if the file has been modified and the change /// has been staged. fn index_status(status: git2::Status) -> f::GitStatus { #[rustfmt::skip] return match status { s if s.contains(git2::Status::INDEX_NEW) => f::GitStatus::New, s if s.contains(git2::Status::INDEX_MODIFIED) => f::GitStatus::Modified, s if s.contains(git2::Status::INDEX_DELETED) => f::GitStatus::Deleted, s if s.contains(git2::Status::INDEX_RENAMED) => f::GitStatus::Renamed, s if s.contains(git2::Status::INDEX_TYPECHANGE) => f::GitStatus::TypeChange, _ => f::GitStatus::NotModified, }; } fn current_branch(repo: &git2::Repository) -> Option { let head = match repo.head() { Ok(head) => Some(head), Err(ref e) if e.code() == git2::ErrorCode::UnbornBranch || e.code() == git2::ErrorCode::NotFound => { return None; } Err(e) => { error!("Error looking up Git branch: {e:?}"); return None; } }; head.and_then(|h| h.shorthand().map(std::string::ToString::to_string)) } impl f::SubdirGitRepo { #[must_use] pub fn from_path(dir: &Path, status: bool) -> Self { let path = &reorient(dir); if let Ok(repo) = git2::Repository::open(path) { let branch = current_branch(&repo); if !status { return Self { status: None, branch, }; } match repo.statuses(None) { Ok(es) => { if es.iter().any(|s| s.status() != git2::Status::IGNORED) { return Self { status: Some(f::SubdirGitRepoStatus::GitDirty), branch, }; } return Self { status: Some(f::SubdirGitRepoStatus::GitClean), branch, }; } Err(e) => { error!("Error looking up Git statuses: {e:?}"); } } } f::SubdirGitRepo { status: if status { Some(f::SubdirGitRepoStatus::NoRepo) } else { None }, branch: None, } } } ================================================ FILE: src/fs/feature/mod.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT pub mod xattr; #[cfg(feature = "git")] pub mod git; #[cfg(not(feature = "git"))] pub mod git { use std::iter::FromIterator; use std::path::{Path, PathBuf}; use crate::fs::fields as f; pub struct GitCache; impl FromIterator for GitCache { fn from_iter(_iter: I) -> Self where I: IntoIterator, { Self } } impl GitCache { pub fn has_anything_for(&self, _index: &Path) -> bool { false } pub fn get(&self, _index: &Path, _prefix_lookup: bool) -> f::Git { unreachable!(); } } impl f::SubdirGitRepo { pub fn from_path(_dir: &Path, _status: bool) -> Self { panic!("Tried to get subdir Git status, but Git support is disabled") } } } ================================================ FILE: src/fs/feature/xattr.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT //! Extended attribute support for `NetBSD`, `Darwin`, and `Linux` systems. #![allow(trivial_casts)] // for ARM use std::fmt::{Display, Formatter}; use std::io; use std::path::Path; use std::str; pub const ENABLED: bool = cfg!(any( target_os = "macos", target_os = "linux", target_os = "netbsd", target_os = "freebsd" )); #[derive(Debug)] pub struct Attribute { pub name: String, pub value: Option>, } pub trait FileAttributes { fn attributes(&self) -> io::Result>; fn symlink_attributes(&self) -> io::Result>; } #[cfg(any( target_os = "macos", target_os = "linux", target_os = "netbsd", target_os = "freebsd" ))] impl FileAttributes for Path { fn attributes(&self) -> io::Result> { extended_attrs::attributes(self, true) } fn symlink_attributes(&self) -> io::Result> { extended_attrs::attributes(self, false) } } #[cfg(not(any( target_os = "macos", target_os = "linux", target_os = "netbsd", target_os = "freebsd" )))] impl FileAttributes for Path { fn attributes(&self) -> io::Result> { Ok(Vec::new()) } fn symlink_attributes(&self) -> io::Result> { Ok(Vec::new()) } } #[cfg(any( target_os = "macos", target_os = "linux", target_os = "netbsd", target_os = "freebsd" ))] mod extended_attrs { use super::Attribute; use libc::{ERANGE, c_char, c_void, size_t, ssize_t}; use std::ffi::{CStr, CString, OsStr, OsString}; use std::io; use std::os::unix::ffi::OsStrExt; use std::path::Path; use std::ptr::null_mut; #[cfg(target_os = "macos")] mod os { use libc::{ XATTR_NOFOLLOW, XATTR_SHOWCOMPRESSION, c_char, c_int, c_void, getxattr, listxattr, size_t, ssize_t, }; // Options to use for MacOS versions of getxattr and listxattr fn get_options(follow_symlinks: bool) -> c_int { if follow_symlinks { XATTR_SHOWCOMPRESSION } else { XATTR_NOFOLLOW | XATTR_SHOWCOMPRESSION } } // Wrapper around listxattr that handles symbolic links pub(super) fn list_xattr( follow_symlinks: bool, path: *const c_char, namebuf: *mut c_char, size: size_t, ) -> ssize_t { // SAFETY: Calling C function unsafe { listxattr(path, namebuf, size, get_options(follow_symlinks)) } } // Wrapper around getxattr that handles symbolic links pub(super) fn get_xattr( follow_symlinks: bool, path: *const c_char, name: *const c_char, value: *mut c_void, size: size_t, ) -> ssize_t { // SAFETY: Calling C function unsafe { getxattr(path, name, value, size, 0, get_options(follow_symlinks)) } } } #[cfg(target_os = "linux")] mod os { use libc::{c_char, c_void, size_t, ssize_t}; use libc::{getxattr, lgetxattr, listxattr, llistxattr}; // Wrapper around listxattr and llistattr for handling symbolic links pub(super) fn list_xattr( follow_symlinks: bool, path: *const c_char, namebuf: *mut c_char, size: size_t, ) -> ssize_t { if follow_symlinks { // SAFETY: Calling C function unsafe { listxattr(path, namebuf, size) } } else { // SAFETY: Calling C function unsafe { llistxattr(path, namebuf, size) } } } // Wrapper around getxattr and lgetxattr for handling symbolic links pub(super) fn get_xattr( follow_symlinks: bool, path: *const c_char, name: *const c_char, value: *mut c_void, size: size_t, ) -> ssize_t { if follow_symlinks { // SAFETY: Calling C function unsafe { getxattr(path, name, value, size) } } else { // SAFETY: Calling C function unsafe { lgetxattr(path, name, value, size) } } } } #[cfg(any(target_os = "netbsd", target_os = "freebsd"))] mod os { use libc::{ EXTATTR_NAMESPACE_SYSTEM, EXTATTR_NAMESPACE_USER, c_char, c_int, c_void, extattr_get_file, extattr_get_link, extattr_list_file, extattr_list_link, size_t, ssize_t, }; // Wrapper around listxattr that handles symbolic links fn list_xattr( follow_symlinks: bool, path: *const c_char, namespace: c_int, value: *mut c_void, size: size_t, ) -> ssize_t { if follow_symlinks { // SAFETY: Calling C function unsafe { extattr_list_file(path, namespace, value, size) } } else { // SAFETY: Calling C function unsafe { extattr_list_link(path, namespace, value, size) } } } fn get_xattr( follow_symlinks: bool, path: *const c_char, namespace: c_int, name: *const c_char, value: *mut c_void, size: size_t, ) -> ssize_t { if follow_symlinks { // SAFETY: Calling C function unsafe { extattr_get_file(path, namespace, name, value, size) } } else { // SAFETY: Calling C function unsafe { extattr_get_link(path, namespace, name, value, size) } } } pub(super) fn list_system_xattr( follow_symlinks: bool, path: *const c_char, namebuf: *mut c_char, size: size_t, ) -> ssize_t { list_xattr( follow_symlinks, path, EXTATTR_NAMESPACE_SYSTEM, namebuf.cast(), size, ) } pub(super) fn list_user_xattr( follow_symlinks: bool, path: *const c_char, namebuf: *mut c_char, size: size_t, ) -> ssize_t { list_xattr( follow_symlinks, path, EXTATTR_NAMESPACE_USER, namebuf.cast(), size, ) } pub(super) fn get_system_xattr( follow_symlinks: bool, path: *const c_char, name: *const c_char, value: *mut c_void, size: size_t, ) -> ssize_t { get_xattr( follow_symlinks, path, EXTATTR_NAMESPACE_SYSTEM, name, value.cast(), size, ) } pub(super) fn get_user_xattr( follow_symlinks: bool, path: *const c_char, name: *const c_char, value: *mut c_void, size: size_t, ) -> ssize_t { get_xattr( follow_symlinks, path, EXTATTR_NAMESPACE_USER, name, value.cast(), size, ) } } // Split attribute name list. Each attribute name is null terminated in the // list. #[cfg(any(target_os = "macos", target_os = "linux"))] fn split_attribute_list(buffer: &[u8]) -> Vec { buffer[..buffer.len() - 1] // Skip trailing null .split(|&c| c == 0) .filter(|&s| !s.is_empty()) .map(OsStr::from_bytes) .map(std::borrow::ToOwned::to_owned) .collect() } // Split attribute name list. Each attribute is a one byte name length // followed by the name. #[cfg(any(target_os = "netbsd", target_os = "freebsd"))] fn split_attribute_list(buffer: &[u8]) -> Vec { let mut result = Vec::new(); let mut index = 0; let length = buffer.len(); while index < length { let item_length = buffer[index] as usize; let start = index + 1; let end = start + item_length; if end <= length { result.push(OsStr::from_bytes(&buffer[start..end]).to_owned()); } index = end; } result } // Calling getxattr and listxattr is a two part process. The first call // a null ptr for buffer and a zero buffer size is passed and the function // returns the needed buffer size. The second call the buffer ptr and the // buffer size is passed and the buffer is filled. Care must be taken if // the buffer size changes between the first and second call. fn get_loop ssize_t>(f: F) -> io::Result>> { let mut buffer: Vec = Vec::new(); loop { let buffer_size = match f(null_mut(), 0) { -1 => return Err(io::Error::last_os_error()), 0 => return Ok(None), size => size as size_t, }; buffer.resize(buffer_size, 0); return match f(buffer.as_mut_ptr(), buffer_size) { -1 => { let last_os_error = io::Error::last_os_error(); if last_os_error.raw_os_error() == Some(ERANGE) { // Passed buffer was to small so retry again. continue; } Err(last_os_error) } 0 => Ok(None), len => { // Just in case the size shrunk buffer.truncate(len as usize); Ok(Some(buffer)) } }; } } // Get a list of all attribute names on `path` fn list_attributes( path: &CStr, follow_symlinks: bool, lister: fn( follow_symlinks: bool, path: *const c_char, namebuf: *mut c_char, size: size_t, ) -> ssize_t, ) -> io::Result> { Ok( get_loop(|buf, size| lister(follow_symlinks, path.as_ptr(), buf.cast(), size))? .map_or_else(Vec::new, |buffer| split_attribute_list(&buffer)), ) } // Get the attribute value `name` on `path` #[cfg(any(target_os = "macos", target_os = "linux"))] fn get_attribute( path: &CStr, name: &CStr, follow_symlinks: bool, getter: fn( follow_symlinks: bool, path: *const c_char, name: *const c_char, value: *mut c_void, size: size_t, ) -> ssize_t, ) -> io::Result>> { use libc::ENODATA; get_loop(|buf, size| { getter( follow_symlinks, path.as_ptr(), name.as_ptr(), buf.cast(), size, ) }) .or_else(|err| { if err.raw_os_error() == Some(ENODATA) { // This handles the case when the named attribute is not on the // path. This is for mainly handling the special case for the // security.selinux attribute mentioned below. This can // also happen when an attribute is deleted between listing // the attributes and getting its value. Ok(None) } else { Err(err) } }) } #[cfg(any(target_os = "netbsd", target_os = "freebsd"))] fn get_attribute( path: &CStr, name: &CStr, follow_symlinks: bool, getter: fn( follow_symlinks: bool, path: *const c_char, name: *const c_char, value: *mut c_void, size: size_t, ) -> ssize_t, ) -> io::Result>> { get_loop(|buf, size| { getter( follow_symlinks, path.as_ptr(), name.as_ptr(), buf.cast(), size, ) }) } // Specially handle security.linux for filesystem that do not list attributes. #[cfg(target_os = "linux")] fn get_selinux_attribute(path: &CStr, follow_symlinks: bool) -> io::Result> { const SELINUX_XATTR_NAME: &str = "security.selinux"; let name = CString::new(SELINUX_XATTR_NAME).unwrap(); get_attribute(path, &name, follow_symlinks, os::get_xattr).map(|value| { if value.is_some() { vec![Attribute { name: String::from(SELINUX_XATTR_NAME), value, }] } else { Vec::new() } }) } // Get a vector of all attribute names and values on `path` #[cfg(any(target_os = "macos", target_os = "linux"))] pub fn attributes(path: &Path, follow_symlinks: bool) -> io::Result> { let path = CString::new(path.as_os_str().as_bytes()).map_err(io::Error::other)?; let attr_names = list_attributes(&path, follow_symlinks, os::list_xattr)?; #[cfg(target_os = "linux")] if attr_names.is_empty() { // Some filesystems, like sysfs, return nothing on listxattr, even though the security // attribute is set. return get_selinux_attribute(&path, follow_symlinks); } let mut attrs = Vec::with_capacity(attr_names.len()); for attr_name in attr_names { if let Some(name) = attr_name.to_str() { let attr_name = CString::new(name).map_err(io::Error::other)?; let value = get_attribute(&path, &attr_name, follow_symlinks, os::get_xattr)?; attrs.push(Attribute { name: name.to_string(), value, }); } } Ok(attrs) } #[cfg(any(target_os = "netbsd", target_os = "freebsd"))] fn get_namespace_attributes( path: &CStr, follow_symlinks: bool, attr_names: Vec, namespace: &str, getter: fn( follow_symlinks: bool, path: *const c_char, name: *const c_char, value: *mut c_void, size: size_t, ) -> ssize_t, attrs: &mut Vec, ) -> io::Result<()> { for attr_name in attr_names { if let Some(name) = attr_name.to_str() { let attr_name = CString::new(name).map_err(io::Error::other)?; let value = get_attribute(path, &attr_name, follow_symlinks, getter)?; attrs.push(Attribute { name: format!("{namespace}::{name}"), value, }); } } Ok(()) } #[cfg(any(target_os = "netbsd", target_os = "freebsd"))] pub fn attributes(path: &Path, follow_symlinks: bool) -> io::Result> { use libc::EPERM; let path = CString::new(path.as_os_str().as_bytes()).map_err(io::Error::other)?; let attr_names_system = list_attributes(&path, follow_symlinks, os::list_system_xattr) .or_else(|err| { // Reading of attributes in the system namespace is only supported for root if err.raw_os_error() == Some(EPERM) { Ok(Vec::new()) } else { Err(err) } })?; let attr_names_user = list_attributes(&path, follow_symlinks, os::list_user_xattr)?; let mut attrs = Vec::with_capacity(attr_names_system.len() + attr_names_user.len()); get_namespace_attributes( &path, follow_symlinks, attr_names_system, "system", os::get_system_xattr, &mut attrs, )?; get_namespace_attributes( &path, follow_symlinks, attr_names_user, "user", os::get_user_xattr, &mut attrs, )?; Ok(attrs) } } const ATTRIBUTE_VALUE_MAX_HEX_LENGTH: usize = 16; // Display for an attribute. Attribute values that have a custom display are // enclosed in curley brackets. impl Display for Attribute { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("{}: ", self.name))?; if let Some(value) = custom_attr_display(self) { f.write_fmt(format_args!("<{value}>")) } else { match &self.value { None => f.write_str(""), Some(value) => { if let Some(val) = custom_value_display(value) { f.write_fmt(format_args!("<{val}>")) } else if let Ok(v) = str::from_utf8(value) { f.write_fmt(format_args!("{:?}", v.trim_end_matches(char::from(0)))) } else if value.len() <= ATTRIBUTE_VALUE_MAX_HEX_LENGTH { f.write_fmt(format_args!("{value:02x?}")) } else { f.write_fmt(format_args!("", value.len())) } } } } } } struct AttributeDisplay { pub attribute: &'static str, pub display: fn(&Attribute) -> Option, } // Check for a custom display by attribute name and call the display function fn custom_attr_display(attribute: &Attribute) -> Option { let name = attribute.name.as_str(); // Strip off MacOS Metadata Persistence Flags // See https://eclecticlight.co/2020/11/02/controlling-metadata-tricks-with-persistence/ #[cfg(target_os = "macos")] let name = name.rsplit_once('#').map_or(name, |n| n.0); ATTRIBUTE_DISPLAYS .iter() .find(|c| c.attribute == name) .and_then(|c| (c.display)(attribute)) } #[cfg(target_os = "macos")] const ATTRIBUTE_DISPLAYS: &[AttributeDisplay] = &[ AttributeDisplay { attribute: "com.apple.lastuseddate", display: display_lastuseddate, }, AttributeDisplay { attribute: "com.apple.macl", display: display_macl, }, ]; #[cfg(not(target_os = "macos"))] const ATTRIBUTE_DISPLAYS: &[AttributeDisplay] = &[]; // com.apple.lastuseddate is two 64-bit values representing the seconds and nano seconds // from January 1, 1970 #[cfg(target_os = "macos")] fn display_lastuseddate(attribute: &Attribute) -> Option { use chrono::{Local, SecondsFormat, TimeZone}; attribute .value .as_ref() .filter(|value| value.len() == 16) .and_then(|value| { let sec = i64::from_le_bytes(value[0..8].try_into().unwrap()); let n_sec = i64::from_le_bytes(value[8..].try_into().unwrap()); Local .timestamp_opt(sec, n_sec as u32) .map(|dt| dt.to_rfc3339_opts(SecondsFormat::Nanos, true)) .single() }) } // com.apple.macl is a two byte flag followed by a uuid for the application #[cfg(target_os = "macos")] fn format_macl(value: &[u8]) -> String { const HEX: [u8; 16] = [ b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c', b'd', b'e', b'f', ]; const GROUPS: [(usize, usize, u8); 6] = [ (0, 4, b';'), (5, 13, b'-'), (14, 18, b'-'), (19, 23, b'-'), (24, 28, b'-'), (29, 41, 0), ]; let mut dst = [0; 41]; let mut i = 0; for (start, end, sep) in GROUPS { for j in (start..end).step_by(2) { let x = value[i]; i += 1; dst[j] = HEX[(x >> 4) as usize]; dst[j + 1] = HEX[(x & 0x0f) as usize]; } if sep != 0 { dst[end] = sep; } } // SAFETY: Vector generated above with only ASCII characters. unsafe { String::from_utf8_unchecked(dst.to_vec()) } } // See https://book.hacktricks.xyz/macos-hardening/macos-security-and-privilege-escalation/macos-security-protections/macos-tcc #[cfg(target_os = "macos")] fn display_macl(attribute: &Attribute) -> Option { attribute .value .as_ref() .filter(|v| v.len() % 18 == 0) .map(|v| { let macls = v .as_slice() .chunks(18) .filter(|c| c[0] != 0 || c[1] != 0) .map(format_macl) .collect::>() .join(", "); format!("[{macls}]") }) } // plist::XmlWriter takes the writer instead of borrowing it. This is a // wrapper around a borrowed vector that just forwards the Write trait // calls to the borrowed vector. struct BorrowedWriter<'a> { pub buffer: &'a mut Vec, } impl io::Write for BorrowedWriter<'_> { fn write(&mut self, buf: &[u8]) -> io::Result { self.buffer.write(buf) } fn flush(&mut self) -> io::Result<()> { self.buffer.flush() } fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { self.buffer.write_all(buf) } } fn custom_value_display(value: &[u8]) -> Option { if value.starts_with(b"bplist") { plist_value_display(value) } else { None } } // Convert a binary plist to a XML plist. fn plist_value_display(value: &[u8]) -> Option { let reader = io::Cursor::new(value); plist::Value::from_reader(reader).ok().and_then(|v| { let mut buffer = Vec::new(); v.to_writer_xml_with_options( BorrowedWriter { buffer: &mut buffer, }, &plist::XmlWriteOptions::default() .indent(b' ', 0) .root_element(false), ) .ok() .and_then(|()| str::from_utf8(&buffer).ok()) .map(|s| format!("{}", s.replace('\n', ""))) }) } ================================================ FILE: src/fs/fields.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT //! Wrapper types for the values returned from `File`s. //! //! The methods of `File` that return information about the entry on the //! filesystem -- size, modification date, block count, or Git status -- used //! to just return these as formatted strings, but this became inflexible once //! customisable output styles landed. //! //! Instead, they will return a wrapper type from this module, which tags the //! type with what field it is while containing the actual raw value. //! //! The `output::details` module, among others, uses these types to render and //! display the information as formatted strings. #![allow(non_camel_case_types)] #![allow(clippy::struct_excessive_bools)] /// The type of a file’s group ID. #[cfg(unix)] pub type gid_t = u32; /// The type of a file’s inode. #[cfg(unix)] pub type ino_t = u64; /// The type of a file’s number of links. #[cfg(unix)] pub type nlink_t = u64; /// The type of a file’s user ID. #[cfg(unix)] pub type uid_t = u32; /// The type of user file flags pub type flag_t = u32; /// The file’s base type, which gets displayed in the very first column of the /// details output. /// /// This type is set entirely by the filesystem, rather than relying on a /// file’s contents. So “link” is a type, but “image” is just a type of /// regular file. (See the `filetype` module for those checks.) /// /// Its ordering is used when sorting by type. #[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] pub enum Type { Directory, File, Link, Pipe, Socket, CharDevice, BlockDevice, Special, } impl Type { #[must_use] pub fn is_regular_file(self) -> bool { matches!(self, Self::File) } } /// The file’s Unix permission bitfield, with one entry per bit. #[derive(Copy, Clone)] #[rustfmt::skip] #[cfg(unix)] pub struct Permissions { pub user_read: bool, pub user_write: bool, pub user_execute: bool, pub group_read: bool, pub group_write: bool, pub group_execute: bool, pub other_read: bool, pub other_write: bool, pub other_execute: bool, pub sticky: bool, pub setgid: bool, pub setuid: bool, } /// The file's `FileAttributes` field, available only on Windows. #[derive(Copy, Clone)] #[rustfmt::skip] #[cfg(windows)] pub struct Attributes { pub archive: bool, pub directory: bool, pub readonly: bool, pub hidden: bool, pub system: bool, pub reparse_point: bool, } /// The three pieces of information that are displayed as a single column in /// the details view. These values are fused together to make the output a /// little more compressed. #[derive(Copy, Clone)] pub struct PermissionsPlus { #[allow(unused)] pub file_type: Type, #[cfg(unix)] pub permissions: Permissions, #[cfg(windows)] pub attributes: Attributes, #[allow(unused)] pub xattrs: bool, } /// The permissions encoded as octal values #[derive(Copy, Clone)] #[cfg(unix)] pub struct OctalPermissions { pub permissions: Permissions, } /// A file’s number of hard links on the filesystem. /// /// Under Unix, a file can exist on the filesystem only once but appear in /// multiple directories. However, it’s rare (but occasionally useful!) for a /// regular file to have a link count greater than 1, so we highlight the /// block count specifically for this case. #[cfg(unix)] #[derive(Copy, Clone)] pub struct Links { /// The actual link count. pub count: nlink_t, /// Whether this file is a regular file with more than one hard link. pub multiple: bool, } /// A file’s inode. Every directory entry on a Unix filesystem has an inode, /// including directories and links, so this is applicable to everything exa /// can deal with. #[cfg(unix)] #[derive(Copy, Clone)] pub struct Inode(pub ino_t); /// A file's size of allocated file system blocks. #[derive(Copy, Clone)] #[cfg(unix)] pub enum Blocksize { /// This file has the given number of blocks. Some(u64), /// This file isn’t of a type that can take up blocks. None, } /// The ID of the user that owns a file. This will only ever be a number; /// looking up the username is done in the `display` module. #[cfg(unix)] #[derive(Copy, Clone)] pub struct User(pub uid_t); /// The ID of the group that a file belongs to. #[cfg(unix)] #[derive(Copy, Clone)] pub struct Group(pub gid_t); /// A file’s size, in bytes. This is usually formatted by the `unit_prefix` /// crate into something human-readable. #[derive(Copy, Clone)] pub enum Size { /// This file has a defined size. Some(u64), /// This file has no size, or has a size but we aren’t interested in it. /// /// Under Unix, directory entries that aren’t regular files will still /// have a file size. For example, a directory will just contain a list of /// its files as its “contents” and will be specially flagged as being a /// directory, rather than a file. However, seeing the “file size” of this /// data is rarely useful — I can’t think of a time when I’ve seen it and /// learnt something. So we discard it and just output “-” instead. /// /// See this answer for more: None, /// This file is a block or character device, so instead of a size, print /// out the file’s major and minor device IDs. /// /// This is what ls does as well. Without it, the devices will just have /// file sizes of zero. DeviceIDs(DeviceIDs), } /// The major and minor device IDs that gets displayed for device files. /// /// You can see what these device numbers mean: /// - /// - #[derive(Copy, Clone)] pub struct DeviceIDs { pub major: u32, pub minor: u32, } /// A file’s status in a Git repository. Whether a file is in a repository or /// not is handled by the Git module, rather than having a “null” variant in /// this enum. #[derive(PartialEq, Eq, Copy, Clone)] pub enum GitStatus { /// This file hasn’t changed since the last commit. NotModified, /// This file didn’t exist for the last commit, and is not specified in /// the ignored files list. New, /// A file that’s been modified since the last commit. Modified, /// A deleted file. This can’t ever be shown, but it’s here anyway! Deleted, /// A file that Git has tracked a rename for. Renamed, /// A file that’s had its type (such as the file permissions) changed. TypeChange, /// A file that’s ignored (that matches a line in .gitignore) Ignored, /// A file that’s updated but unmerged. Conflicted, } /// A file’s complete Git status. It’s possible to make changes to a file, add /// it to the staging area, then make *more* changes, so we need to list each /// file’s status for both of these. #[derive(Copy, Clone)] pub struct Git { pub staged: GitStatus, pub unstaged: GitStatus, } impl Default for Git { /// Create a Git status for a file with nothing done to it. fn default() -> Self { Self { staged: GitStatus::NotModified, unstaged: GitStatus::NotModified, } } } pub enum SecurityContextType<'a> { SELinux(&'a str), None, } pub struct SecurityContext<'a> { pub context: SecurityContextType<'a>, } #[allow(dead_code)] #[derive(PartialEq, Copy, Clone)] pub enum SubdirGitRepoStatus { NoRepo, GitClean, GitDirty, } #[derive(Clone)] pub struct SubdirGitRepo { pub status: Option, pub branch: Option, } impl Default for SubdirGitRepo { fn default() -> Self { Self { status: Some(SubdirGitRepoStatus::NoRepo), branch: None, } } } /// The user file flags on the file. This will only ever be a number; /// looking up the flags is done in the `display` module. pub struct Flags(pub flag_t); ================================================ FILE: src/fs/file.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT //! Files, and methods and fields to access their metadata. #[cfg(unix)] use std::collections::HashMap; use std::fs::FileType; use std::io; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; #[cfg(windows)] use std::os::windows::fs::MetadataExt; use std::path::{Path, PathBuf}; #[cfg(unix)] use std::str; #[cfg(unix)] use std::sync::Mutex; use std::sync::OnceLock; use std::time::SystemTime; use chrono::prelude::*; use log::{debug, error, trace}; #[cfg(unix)] use std::sync::LazyLock; use crate::fs::dir::Dir; use crate::fs::feature::xattr; use crate::fs::feature::xattr::{Attribute, FileAttributes}; use crate::fs::fields as f; use crate::fs::fields::SecurityContextType; use crate::fs::recursive_size::RecursiveSize; use super::mounts::MountedFs; use super::mounts::all_mounts; // Maps (device_id, inode) => (size_in_bytes, size_in_blocks) // Mutex::new is const but HashMap::new is not const requiring us to use lazy // initialization. #[allow(clippy::type_complexity)] #[cfg(unix)] static DIRECTORY_SIZE_CACHE: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); /// A **File** is a wrapper around one of Rust’s `PathBuf` values, along with /// associated data about the file. /// /// Each file is definitely going to have its filename displayed at least /// once, have its file extension extracted at least once, and have its metadata /// information queried at least once, so it makes sense to do all this at the /// start and hold on to all the information. pub struct File<'dir> { /// The filename portion of this file’s path, including the extension. /// /// This is used to compare against certain filenames (such as checking if /// it’s “Makefile” or something) and to highlight only the filename in /// colour when displaying the path. pub name: String, /// The file’s name’s extension, if present, extracted from the name. /// /// This is queried many times over, so it’s worth caching it. pub ext: Option, /// The path that begat this file. /// /// Even though the file’s name is extracted, the path needs to be kept /// around, as certain operations involve looking up the file’s absolute /// location (such as searching for compiled files) or using its original /// path (following a symlink). pub path: PathBuf, /// The cached filetype for this file pub filetype: OnceLock>, /// A cached `metadata` (`stat`) call for this file. /// /// This too is queried multiple times, and is *not* cached by the OS, as /// it could easily change between invocations — but exa is so short-lived /// it’s better to just cache it. pub metadata: OnceLock>, /// A reference to the directory that contains this file, if any. /// /// Filenames that get passed in on the command-line directly will have no /// parent directory reference — although they technically have one on the /// filesystem, we’ll never need to look at it, so it’ll be `None`. /// However, *directories* that get passed in will produce files that /// contain a reference to it, which is used in certain operations (such /// as looking up compiled files). pub parent_dir: Option<&'dir Dir>, /// Whether this is one of the two `--all all` directories, `.` and `..`. /// /// Unlike all other entries, these are not returned as part of the /// directory’s children, and are in fact added specifically by exa; this /// means that they should be skipped when recursing. pub is_all_all: bool, /// Whether to dereference symbolic links when querying for information. /// /// For instance, when querying the size of a symbolic link, if /// dereferencing is enabled, the size of the target will be displayed /// instead. pub deref_links: bool, /// The recursive directory size when `total_size` is used. recursive_size: RecursiveSize, /// The extended attributes of this file. extended_attributes: OnceLock>, /// The absolute value of this path, used to look up mount points. absolute_path: OnceLock>, } impl<'dir> File<'dir> { pub fn from_args( path: PathBuf, parent_dir: PD, filename: FN, deref_links: bool, total_size: bool, filetype: Option, ) -> File<'dir> where PD: Into>, FN: Into>, { let parent_dir = parent_dir.into(); let name = filename.into().unwrap_or_else(|| File::filename(&path)); let ext = File::ext(&path); let is_all_all = false; let recursive_size = if total_size { RecursiveSize::Unknown } else { RecursiveSize::None }; debug!("deref_links {deref_links}"); let filetype = match filetype { Some(f) => OnceLock::from(Some(f)), None => OnceLock::new(), }; debug!("deref_links {deref_links}"); let mut file = File { name, ext, path, parent_dir, is_all_all, deref_links, recursive_size, filetype, metadata: OnceLock::new(), extended_attributes: OnceLock::new(), absolute_path: OnceLock::new(), }; if total_size { file.recursive_size = file.recursive_directory_size(); } file } fn new_aa( path: PathBuf, parent_dir: &'dir Dir, name: &'static str, total_size: bool, ) -> File<'dir> { let ext = File::ext(&path); let is_all_all = true; let parent_dir = Some(parent_dir); let recursive_size = if total_size { RecursiveSize::Unknown } else { RecursiveSize::None }; let mut file = File { name: name.into(), ext, path, parent_dir, is_all_all, deref_links: false, recursive_size, metadata: OnceLock::new(), absolute_path: OnceLock::new(), extended_attributes: OnceLock::new(), filetype: OnceLock::new(), }; if total_size { file.recursive_size = file.recursive_directory_size(); } file } #[must_use] pub fn new_aa_current(parent_dir: &'dir Dir, total_size: bool) -> File<'dir> { File::new_aa(parent_dir.path.clone(), parent_dir, ".", total_size) } #[must_use] pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir, total_size: bool) -> File<'dir> { File::new_aa(path, parent_dir, "..", total_size) } /// A file’s name is derived from its string. This needs to handle directories /// such as `/` or `..`, which have no `file_name` component. So instead, just /// use the last component as the name. #[must_use] pub fn filename(path: &Path) -> String { if let Some(back) = path.components().next_back() { back.as_os_str().to_string_lossy().to_string() } else { // use the path as fallback error!("Path {path:?} has no last component"); path.display().to_string() } } /// Extract an extension from a file path, if one is present, in lowercase. /// /// The extension is the series of characters after the last dot. This /// deliberately counts dotfiles, so the “.git” folder has the extension “git”. /// /// ASCII lowercasing is used because these extensions are only compared /// against a pre-compiled list of extensions which are known to only exist /// within ASCII, so it’s alright. fn ext(path: &Path) -> Option { let name = path.file_name().map(|f| f.to_string_lossy().to_string())?; name.rfind('.').map(|p| name[p + 1..].to_ascii_lowercase()) } /// Read the extended attributes of a file path. fn gather_extended_attributes(&self) -> Vec { if xattr::ENABLED { let attributes = if self.deref_links { self.path.attributes() } else { self.path.symlink_attributes() }; match attributes { Ok(xattrs) => xattrs, Err(e) => { error!( "Error looking up extended attributes for {}: {}", self.path.display(), e ); Vec::new() } } } else { Vec::new() } } fn filetype(&self) -> Option<&std::fs::FileType> { self.filetype .get_or_init(|| self.metadata().as_ref().ok().map(|md| md.file_type())) .as_ref() } pub fn metadata(&self) -> Result<&std::fs::Metadata, &io::Error> { self.metadata .get_or_init(|| { debug!("Statting file {:?}", &self.path); std::fs::symlink_metadata(&self.path) }) .as_ref() } /// Get the extended attributes of a file path on demand. pub fn extended_attributes(&self) -> &Vec { self.extended_attributes .get_or_init(|| self.gather_extended_attributes()) } /// Whether this file is a directory on the filesystem. pub fn is_directory(&self) -> bool { self.filetype().is_some_and(std::fs::FileType::is_dir) } /// Whether this file is a directory, or a symlink pointing to a directory. pub fn points_to_directory(&self) -> bool { if self.is_directory() { return true; } if self.is_link() { let target = self.link_target(); if let FileTarget::Ok(target) = target { return target.points_to_directory(); } } false } /// Initializes a new `Dir` object using the `PathBuf` of /// the current file. It does not perform any validation to check if the /// file is actually a directory. To verify that, use `is_directory()`. pub fn to_dir(&self) -> Dir { trace!("read_dir: initializating dir from path"); Dir::new(self.path.clone()) } /// If this file is a directory on the filesystem, then clone its /// `PathBuf` for use in one of our own `Dir` values, and read a list of /// its contents. /// /// Returns an IO error upon failure, but this shouldn’t be used to check /// if a `File` is a directory or not! For that, just use `is_directory()`. pub fn read_dir(&self) -> io::Result { trace!("read_dir: reading dir"); Dir::read_dir(self.path.clone()) } /// Whether this file is a regular file on the filesystem — that is, not a /// directory, a link, or anything else treated specially. pub fn is_file(&self) -> bool { self.filetype().is_some_and(std::fs::FileType::is_file) } /// Whether this file is both a regular file *and* executable for the /// current user. An executable file has a different purpose from an /// executable directory, so they should be highlighted differently. #[cfg(unix)] pub fn is_executable_file(&self) -> bool { let bit = modes::USER_EXECUTE; if !self.is_file() { return false; } let Ok(md) = self.metadata() else { return false; }; (md.permissions().mode() & bit) == bit } /// Whether this file is a symlink on the filesystem. pub fn is_link(&self) -> bool { self.filetype().is_some_and(FileType::is_symlink) } /// Whether this file is a named pipe on the filesystem. #[cfg(unix)] pub fn is_pipe(&self) -> bool { self.filetype().is_some_and(FileTypeExt::is_fifo) } /// Whether this file is a char device on the filesystem. #[cfg(unix)] pub fn is_char_device(&self) -> bool { self.filetype().is_some_and(FileTypeExt::is_char_device) } /// Whether this file is a block device on the filesystem. #[cfg(unix)] pub fn is_block_device(&self) -> bool { self.filetype().is_some_and(FileTypeExt::is_block_device) } /// Whether this file is a socket on the filesystem. #[cfg(unix)] pub fn is_socket(&self) -> bool { self.filetype().is_some_and(FileTypeExt::is_socket) } /// Determine the full path resolving all symbolic links on demand. pub fn absolute_path(&self) -> Option<&PathBuf> { self.absolute_path .get_or_init(|| { if self.is_link() && self.link_target().is_broken() { // workaround for broken symlinks to get absolute path for parent and then // append name of file; std::fs::canonicalize requires all path components // (including the last one) to exist self.path .parent() .and_then(|parent| std::fs::canonicalize(parent).ok()) .map(|p| p.join(self.name.clone())) } else { std::fs::canonicalize(&self.path).ok() } }) .as_ref() } /// Whether this file is a mount point pub fn is_mount_point(&self) -> bool { cfg!(any(target_os = "linux", target_os = "macos")) && self.is_directory() && self .absolute_path() .is_some_and(|p| all_mounts().contains_key(p)) } /// The filesystem device and type for a mount point pub fn mount_point_info(&self) -> Option<&MountedFs> { if cfg!(any(target_os = "linux", target_os = "macos")) { return self.absolute_path().and_then(|p| all_mounts().get(p)); } None } /// Re-prefixes the path pointed to by this file, if it’s a symlink, to /// make it an absolute path that can be accessed from whichever /// directory exa is being run from. fn reorient_target_path(&self, path: &Path) -> PathBuf { if path.is_absolute() { path.to_path_buf() } else if let Some(dir) = self.parent_dir { dir.join(path) } else if let Some(parent) = self.path.parent() { parent.join(path) } else { self.path.join(path) } } /// Again assuming this file is a symlink, follows that link and returns /// the result of following it. /// /// For a working symlink that the user is allowed to follow, /// this will be the `File` object at the other end, which can then have /// its name, colour, and other details read. /// /// For a broken symlink, returns where the file *would* be, if it /// existed. If this file cannot be read at all, returns the error that /// we got when we tried to read it. pub fn link_target(&self) -> FileTarget<'dir> { // We need to be careful to treat the path actually pointed to by // this file — which could be absolute or relative — to the path // we actually look up and turn into a `File` — which needs to be // absolute to be accessible from any directory. debug!("Reading link {:?}", &self.path); let path = match std::fs::read_link(&self.path) { Ok(p) => p, Err(e) => return FileTarget::Err(e), }; let absolute_path = self.reorient_target_path(&path); // Use plain `metadata` instead of `symlink_metadata` - we *want* to // follow links. match std::fs::metadata(&absolute_path) { Ok(metadata) => { let ext = File::ext(&path); let name = File::filename(&path); let extended_attributes = OnceLock::new(); let absolute_path_cell = OnceLock::from(Some(absolute_path)); let file = File { parent_dir: None, path, ext, filetype: OnceLock::from(Some(metadata.file_type())), metadata: OnceLock::from(Ok(metadata)), name, is_all_all: false, deref_links: self.deref_links, extended_attributes, absolute_path: absolute_path_cell, recursive_size: RecursiveSize::None, }; FileTarget::Ok(Box::new(file)) } Err(e) => { error!("Error following link {:?}: {:#?}", &path, e); FileTarget::Broken(path) } } } /// Assuming this file is a symlink, follows that link and any further /// links recursively, returning the result from following the trail. /// /// For a working symlink that the user is allowed to follow, /// this will be the `File` object at the other end, which can then have /// its name, colour, and other details read. /// /// For a broken symlink, returns where the file *would* be, if it /// existed. If this file cannot be read at all, returns the error that /// we got when we tried to read it. pub fn link_target_recurse(&self) -> FileTarget<'dir> { let target = self.link_target(); if let FileTarget::Ok(f) = target { if f.is_link() { return f.link_target_recurse(); } return FileTarget::Ok(f); } target } /// This file’s number of hard links. /// /// It also reports whether this is both a regular file, and a file with /// multiple links. This is important, because a file with multiple links /// is uncommon, while you come across directories and other types /// with multiple links much more often. Thus, it should get highlighted /// more attentively. #[cfg(unix)] pub fn links(&self) -> f::Links { let count = self.metadata().map_or(0, MetadataExt::nlink); f::Links { count, multiple: self.is_file() && count > 1, } } /// This file’s inode. #[cfg(unix)] pub fn inode(&self) -> f::Inode { f::Inode(self.metadata().map_or(0, MetadataExt::ino)) } /// This actual size the file takes up on disk, in bytes. #[cfg(unix)] pub fn blocksize(&self) -> f::Blocksize { if self.deref_links && self.is_link() { match self.link_target() { FileTarget::Ok(f) => f.blocksize(), _ => f::Blocksize::None, } } else if self.is_directory() { self.recursive_size.map_or(f::Blocksize::None, |_, blocks| { f::Blocksize::Some(blocks * 512) }) } else if self.is_file() { // Note that metadata.blocks returns the number of blocks // for 512 byte blocks according to the POSIX standard // even though the physical block size may be different. f::Blocksize::Some(self.metadata().map_or(0, |md| md.blocks() * 512)) } else { // directory or symlinks f::Blocksize::None } } /// The ID of the user that own this file. If dereferencing links, the links /// may be broken, in which case `None` will be returned. #[cfg(unix)] pub fn user(&self) -> Option { if self.is_link() && self.deref_links { return match self.link_target_recurse() { FileTarget::Ok(f) => f.user(), _ => None, }; } Some(f::User(self.metadata().map_or(0, MetadataExt::uid))) } /// The ID of the group that owns this file. #[cfg(unix)] pub fn group(&self) -> Option { if self.is_link() && self.deref_links { return match self.link_target_recurse() { FileTarget::Ok(f) => f.group(), _ => None, }; } Some(f::Group(self.metadata().map_or(0, MetadataExt::gid))) } /// This file’s size, if it’s a regular file. /// /// For directories, the recursive size or no size is given depending on /// flags. Although they do have a size on some filesystems, I’ve never /// looked at one of those numbers and gained any information from it. /// /// Block and character devices return their device IDs, because they /// usually just have a file size of zero. /// /// Links will return the size of their target (recursively through other /// links) if dereferencing is enabled, otherwise None. #[cfg(unix)] pub fn size(&self) -> f::Size { if self.deref_links && self.is_link() { match self.link_target() { FileTarget::Ok(f) => f.size(), _ => f::Size::None, } } else if self.is_directory() { self.recursive_size .map_or(f::Size::None, |bytes, _| f::Size::Some(bytes)) } else if self.is_char_device() || self.is_block_device() { let device_id = self.metadata().map_or(0, MetadataExt::rdev); // MacOS and Linux have different arguments and return types for the // functions major and minor. On Linux the try_into().unwrap() and // the "as u32" cast are not needed. We turn off the warning to // allow it to compile cleanly on Linux. // // On illumos and Solaris, major and minor are extern "C" fns and // therefore unsafe; on other platforms the functions are defined as // macros and copied as const fns in the libc crate. #[allow(trivial_numeric_casts, unused_unsafe)] #[allow(clippy::unnecessary_cast, clippy::useless_conversion)] { let device_id = device_id .try_into() .expect("Malformed device major ID when getting filesize"); f::Size::DeviceIDs(f::DeviceIDs { major: unsafe { libc::major(device_id) as u32 }, minor: unsafe { libc::minor(device_id) as u32 }, }) } } else if self.is_file() { f::Size::Some(self.metadata().map_or(0, std::fs::Metadata::len)) } else { // symlink f::Size::None } } /// Returns the size of the file or indicates no size if it's a directory. /// /// For Windows platforms, the size of directories is not computed and will /// return `Size::None`. #[cfg(windows)] pub fn size(&self) -> f::Size { if self.is_directory() { f::Size::None } else { f::Size::Some(self.metadata().map_or(0, std::fs::Metadata::len)) } } /// Calculate the total directory size recursively. If not a directory `None` /// will be returned. The directory size is cached for recursive directory /// listing. #[cfg(unix)] fn recursive_directory_size(&self) -> RecursiveSize { if self.is_directory() { let key = ( self.metadata().map_or(0, MetadataExt::dev), self.metadata().map_or(0, MetadataExt::ino), ); if let Some(size) = DIRECTORY_SIZE_CACHE.lock().unwrap().get(&key) { return RecursiveSize::Some(size.0, size.1); } Dir::read_dir(self.path.clone()).map_or(RecursiveSize::Unknown, |dir| { let mut size = 0; let mut blocks = 0; for file in dir.files(super::DotFilter::Dotfiles, None, false, false, true) { match file.recursive_directory_size() { RecursiveSize::Some(bytes, blks) => { size += bytes; blocks += blks; } RecursiveSize::Unknown => {} RecursiveSize::None => { size += file.metadata().map_or(0, MetadataExt::size); blocks += file.metadata().map_or(0, MetadataExt::blocks); } } } DIRECTORY_SIZE_CACHE .lock() .unwrap() .insert(key, (size, blocks)); RecursiveSize::Some(size, blocks) }) } else { RecursiveSize::None } } /// Windows version always returns None. The metadata for /// `volume_serial_number` and `file_index` are marked unstable so we can /// not cache the sizes. Without caching we could end up walking the /// directory structure several times. #[cfg(windows)] fn recursive_directory_size(&self) -> RecursiveSize { RecursiveSize::None } /// Returns the same value as `self.metadata.len()` or the recursive size /// of a directory when `total_size` is used. #[inline] pub fn length(&self) -> u64 { self.recursive_size .unwrap_bytes_or(self.metadata().map_or(0, std::fs::Metadata::len)) } /// Is the file is using recursive size calculation #[inline] pub fn is_recursive_size(&self) -> bool { !self.recursive_size.is_none() } /// Determines if the directory is empty or not. /// /// For Unix platforms, this function first checks the link count to quickly /// determine non-empty directories. On most UNIX filesystems the link count /// is two plus the number of subdirectories. If the link count is less than /// or equal to 2, it then checks the directory contents to determine if /// it's truly empty. The naive approach used here checks the contents /// directly, as certain filesystems make it difficult to infer emptiness /// based on directory size alone. #[cfg(unix)] pub fn is_empty_dir(&self) -> bool { if self.is_directory() { if self.metadata().map_or(0, MetadataExt::nlink) > 2 { // Directories will have a link count of two if they do not have any subdirectories. // The '.' entry is a link to itself and the '..' is a link to the parent directory. // A subdirectory will have a link to its parent directory increasing the link count // above two. This will avoid the expensive read_dir call below when a directory // has subdirectories. false } else { self.is_empty_directory() } } else { false } } /// Determines if the directory is empty or not. /// /// For Windows platforms, this function checks the directory contents directly /// to determine if it's empty. Since certain filesystems on Windows make it /// challenging to infer emptiness based on directory size, this approach is used. #[cfg(windows)] pub fn is_empty_dir(&self) -> bool { if self.is_directory() { self.is_empty_directory() } else { false } } /// Checks the contents of the directory to determine if it's empty. /// /// This function avoids counting '.' and '..' when determining if the directory is /// empty. If any other entries are found, it returns `false`. /// /// The naive approach, as one would think that this info may have been cached. /// but as mentioned in the size function comment above, different filesystems /// make it difficult to get any info about a dir by it's size, so this may be it. fn is_empty_directory(&self) -> bool { trace!("is_empty_directory: reading dir"); match Dir::read_dir(self.path.clone()) { // . & .. are skipped, if the returned iterator has .next(), it's not empty Ok(has_files) => has_files .files(super::DotFilter::Dotfiles, None, false, false, false) .next() .is_none(), Err(_) => false, } } /// Converts a `SystemTime` to a `NaiveDateTime` without panicking. /// /// Fixes #655 and #667 in `Self::modified_time`, `Self::accessed_time` and /// `Self::created_time`. fn systemtime_to_naivedatetime(st: SystemTime) -> Option { let duration = st.duration_since(SystemTime::UNIX_EPOCH).ok()?; DateTime::from_timestamp( duration.as_secs().try_into().ok()?, (duration.as_nanos() % 1_000_000_000).try_into().ok()?, ) .map(|dt| dt.naive_local()) } /// This file’s last modified timestamp, if available on this platform. pub fn modified_time(&self) -> Option { if self.is_link() && self.deref_links { return match self.link_target_recurse() { FileTarget::Ok(f) => f.modified_time(), _ => None, }; } self.metadata() .ok() .and_then(|md| md.modified().ok()) .and_then(Self::systemtime_to_naivedatetime) } /// This file’s last changed timestamp, if available on this platform. #[cfg(unix)] pub fn changed_time(&self) -> Option { if self.is_link() && self.deref_links { return match self.link_target_recurse() { FileTarget::Ok(f) => f.changed_time(), _ => None, }; } let md = self.metadata(); DateTime::from_timestamp( md.map_or(0, MetadataExt::ctime), md.map_or(0, |md| md.ctime_nsec() as u32), ) .map(|dt| dt.naive_local()) } #[cfg(windows)] pub fn changed_time(&self) -> Option { self.modified_time() } /// This file’s last accessed timestamp, if available on this platform. pub fn accessed_time(&self) -> Option { if self.is_link() && self.deref_links { return match self.link_target_recurse() { FileTarget::Ok(f) => f.accessed_time(), _ => None, }; } self.metadata() .ok() .and_then(|md| md.accessed().ok()) .and_then(Self::systemtime_to_naivedatetime) } /// This file’s created timestamp, if available on this platform. pub fn created_time(&self) -> Option { if self.is_link() && self.deref_links { return match self.link_target_recurse() { FileTarget::Ok(f) => f.created_time(), _ => None, }; } let btime = self.metadata().ok()?.created().ok()?; Self::systemtime_to_naivedatetime(btime) } /// This file’s ‘type’. /// /// This is used a the leftmost character of the permissions column. /// The file type can usually be guessed from the colour of the file, but /// ls puts this character there. #[cfg(unix)] pub fn type_char(&self) -> f::Type { if self.is_file() { f::Type::File } else if self.is_directory() { f::Type::Directory } else if self.is_pipe() { f::Type::Pipe } else if self.is_link() { f::Type::Link } else if self.is_char_device() { f::Type::CharDevice } else if self.is_block_device() { f::Type::BlockDevice } else if self.is_socket() { f::Type::Socket } else { f::Type::Special } } #[cfg(windows)] pub fn type_char(&self) -> f::Type { if self.is_file() { f::Type::File } else if self.is_directory() { f::Type::Directory } else { f::Type::Special } } /// This file’s permissions, with flags for each bit. #[cfg(unix)] pub fn permissions(&self) -> Option { if self.is_link() && self.deref_links { // If the chain of links is broken, we instead fall through and // return the permissions of the original link, as would have been // done if we were not dereferencing. return match self.link_target_recurse() { FileTarget::Ok(f) => f.permissions(), _ => None, }; } let bits = self.metadata().map_or(0, MetadataExt::mode); let has_bit = |bit| bits & bit == bit; Some(f::Permissions { user_read: has_bit(modes::USER_READ), user_write: has_bit(modes::USER_WRITE), user_execute: has_bit(modes::USER_EXECUTE), group_read: has_bit(modes::GROUP_READ), group_write: has_bit(modes::GROUP_WRITE), group_execute: has_bit(modes::GROUP_EXECUTE), other_read: has_bit(modes::OTHER_READ), other_write: has_bit(modes::OTHER_WRITE), other_execute: has_bit(modes::OTHER_EXECUTE), sticky: has_bit(modes::STICKY), setgid: has_bit(modes::SETGID), setuid: has_bit(modes::SETUID), }) } #[cfg(windows)] pub fn attributes(&self) -> Option { let bits = self.metadata().ok()?.file_attributes(); let has_bit = |bit| bits & bit == bit; // https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants Some(f::Attributes { directory: has_bit(0x10), archive: has_bit(0x20), readonly: has_bit(0x1), hidden: has_bit(0x2), system: has_bit(0x4), reparse_point: has_bit(0x400), }) } /// This file’s security context field. #[cfg(unix)] pub fn security_context(&self) -> f::SecurityContext<'_> { let context = match self .extended_attributes() .iter() .find(|a| a.name == "security.selinux") { Some(attr) => match &attr.value { None => SecurityContextType::None, Some(value) => match str::from_utf8(value) { Ok(v) => SecurityContextType::SELinux(v.trim_end_matches(char::from(0))), Err(_) => SecurityContextType::None, }, }, None => SecurityContextType::None, }; f::SecurityContext { context } } #[cfg(windows)] pub fn security_context(&self) -> f::SecurityContext<'_> { f::SecurityContext { context: SecurityContextType::None, } } /// User file flags. #[cfg(any( target_os = "macos", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly" ))] pub fn flags(&self) -> f::Flags { #[cfg(target_os = "dragonfly")] use std::os::dragonfly::fs::MetadataExt; #[cfg(target_os = "freebsd")] use std::os::freebsd::fs::MetadataExt; #[cfg(target_os = "macos")] use std::os::macos::fs::MetadataExt; #[cfg(target_os = "netbsd")] use std::os::netbsd::fs::MetadataExt; #[cfg(target_os = "openbsd")] use std::os::openbsd::fs::MetadataExt; f::Flags( self.metadata() .map(MetadataExt::st_flags) .unwrap_or_default(), ) } #[cfg(windows)] pub fn flags(&self) -> f::Flags { f::Flags(self.metadata().map_or(0, |md| md.file_attributes())) } #[cfg(not(any( target_os = "macos", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly", target_os = "windows" )))] pub fn flags(&self) -> f::Flags { f::Flags(0) } } impl<'a> AsRef> for File<'a> { fn as_ref(&self) -> &File<'a> { self } } /// The result of following a symlink. pub enum FileTarget<'dir> { /// The symlink pointed at a file that exists. Ok(Box>), /// The symlink pointed at a file that does not exist. Holds the path /// where the file would be, if it existed. Broken(PathBuf), /// There was an IO error when following the link. This can happen if the /// file isn’t a link to begin with, but also if, say, we don’t have /// permission to follow it. Err(io::Error), // Err is its own variant, instead of having the whole thing be inside an // `io::Result`, because being unable to follow a symlink is not a serious // error — we just display the error message and move on. } impl FileTarget<'_> { /// Whether this link doesn’t lead to a file, for whatever reason. This /// gets used to determine how to highlight the link in grid views. #[must_use] pub fn is_broken(&self) -> bool { matches!(self, Self::Broken(_) | Self::Err(_)) } } /// More readable aliases for the permission bits exposed by libc. #[allow(trivial_numeric_casts)] #[cfg(unix)] mod modes { // The `libc::mode_t` type’s actual type varies, but the value returned // from `metadata.permissions().mode()` is always `u32`. pub type Mode = u32; pub const USER_READ: Mode = libc::S_IRUSR as Mode; pub const USER_WRITE: Mode = libc::S_IWUSR as Mode; pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode; pub const GROUP_READ: Mode = libc::S_IRGRP as Mode; pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode; pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode; pub const OTHER_READ: Mode = libc::S_IROTH as Mode; pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode; pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode; pub const STICKY: Mode = libc::S_ISVTX as Mode; pub const SETGID: Mode = libc::S_ISGID as Mode; pub const SETUID: Mode = libc::S_ISUID as Mode; } #[cfg(test)] mod ext_test { use super::File; use std::path::Path; #[test] fn extension() { assert_eq!(Some("dat".to_string()), File::ext(Path::new("fester.dat"))); } #[test] fn dotfile() { assert_eq!(Some("vimrc".to_string()), File::ext(Path::new(".vimrc"))); } #[test] fn no_extension() { assert_eq!(None, File::ext(Path::new("jarlsberg"))); } } #[cfg(test)] mod filename_test { use super::File; use std::path::Path; #[test] fn file() { assert_eq!("fester.dat", File::filename(Path::new("fester.dat"))); } #[test] fn no_path() { assert_eq!("foo.wha", File::filename(Path::new("/var/cache/foo.wha"))); } #[test] fn here() { assert_eq!(".", File::filename(Path::new("."))); } #[test] fn there() { assert_eq!("..", File::filename(Path::new(".."))); } #[test] fn everywhere() { assert_eq!("..", File::filename(Path::new("./.."))); } #[test] #[cfg(unix)] fn topmost() { assert_eq!("/", File::filename(Path::new("/"))); } } ================================================ FILE: src/fs/filter.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT //! Filtering and sorting the list of files before displaying them. use std::cmp::Ordering; use std::iter::FromIterator; #[cfg(unix)] use std::os::unix::fs::MetadataExt; use crate::fs::DotFilter; use crate::fs::File; /// Flags used to manage the **file filter** process #[derive(PartialEq, Eq, Debug, Clone)] pub enum FileFilterFlags { /// Whether to reverse the sorting order. This would sort the largest /// files first, or files starting with Z, or the most-recently-changed /// ones, depending on the sort field. Reverse, /// Whether to only show directories. OnlyDirs, /// Whether to only show files. OnlyFiles, /// Whether to ignore symlinks NoSymlinks, /// Whether to explicitly show symlinks ShowSymlinks, /// Whether directories should be listed first, and other types of file /// second. Some users prefer it like this. ListDirsFirst, /// Whether directories should be listed as the last items, after other /// types of file. Some users prefer it like this. ListDirsLast, } /// The **file filter** processes a list of files before displaying them to /// the user, by removing files they don’t want to see, and putting the list /// in the desired order. /// /// Usually a user does not want to see *every* file in the list. The most /// common case is to remove files starting with `.`, which are designated /// as ‘hidden’ files. /// /// The special files `.` and `..` files are not actually filtered out, but /// need to be inserted into the list, in a special case. /// /// The filter also governs sorting the list. After being filtered, pairs of /// files are compared and sorted based on the result, with the sort field /// performing the comparison. #[derive(PartialEq, Eq, Debug, Clone)] pub struct FileFilter { /// The metadata field to sort by. pub sort_field: SortField, // Flags that the file filtering process follow pub flags: Vec, /// Which invisible “dot” files to include when listing a directory. /// /// Files starting with a single “.” are used to determine “system” or /// “configuration” files that should not be displayed in a regular /// directory listing, and the directory entries “.” and “..” are /// considered extra-special. /// /// This came about more or less by a complete historical accident, /// when the original `ls` tried to hide `.` and `..`: /// /// [Linux History: How Dot Files Became Hidden Files](https://linux-audit.com/linux-history-how-dot-files-became-hidden-files/) pub dot_filter: DotFilter, /// Glob patterns to ignore. Any file name that matches *any* of these /// patterns won’t be displayed in the list. pub ignore_patterns: IgnorePatterns, /// Whether to ignore Git-ignored patterns. pub git_ignore: GitIgnore, /// Whether to ignore symlinks pub no_symlinks: bool, /// Whether to explicitly show symlinks pub show_symlinks: bool, } impl FileFilter { /// Remove every file in the given vector that does *not* pass the /// filter predicate for files found inside a directory. #[rustfmt::skip] pub fn filter_child_files(&self, is_recurse: bool, files: &mut Vec>) { use FileFilterFlags::{NoSymlinks, OnlyDirs, OnlyFiles, ShowSymlinks}; files.retain(|f| !self.ignore_patterns.is_ignored(&f.name)); files.retain(|f| { match ( self.flags.contains(&OnlyDirs), self.flags.contains(&OnlyFiles), self.flags.contains(&NoSymlinks), self.flags.contains(&ShowSymlinks), ) { (true, false, false, false) => f.is_directory(), (true, false, true, false) => f.is_directory(), (true, false, false, true) => f.is_directory() || f.points_to_directory(), (false, true, false, false) => if is_recurse { true } else {f.is_file() }, (false, true, false, true) => if is_recurse { true } else { f.is_file() || f.is_link() && !f.points_to_directory() }, (false, false, true, false) => !f.is_link(), _ => true, } }); } /// Remove every file in the given vector that does *not* pass the /// filter predicate for file names specified on the command-line. /// /// The rules are different for these types of files than the other /// type because the ignore rules can be used with globbing. For /// example, running `exa -I='*. tmp' .vimrc` shouldn’t filter out the /// dotfile, because it’s been directly specified. But running /// `exa -I='*.ogg' music/*` should filter out the ogg files obtained /// from the glob, even though the globbing is done by the shell! pub fn filter_argument_files(&self, files: &mut Vec>) { files.retain(|f| !self.ignore_patterns.is_ignored(&f.name)); } /// Sort the files in the given vector based on the sort field option. pub fn sort_files<'a, F>(&self, files: &mut [F]) where F: AsRef>, { files.sort_by(|a, b| self.sort_field.compare_files(a.as_ref(), b.as_ref())); if self.flags.contains(&FileFilterFlags::Reverse) { files.reverse(); } if self.flags.contains(&FileFilterFlags::ListDirsFirst) { // This relies on the fact that `sort_by` is *stable*: it will keep // adjacent elements next to each other. files.sort_by(|a, b| { b.as_ref() .points_to_directory() .cmp(&a.as_ref().points_to_directory()) }); } else if self.flags.contains(&FileFilterFlags::ListDirsLast) { files.sort_by(|a, b| { a.as_ref() .points_to_directory() .cmp(&b.as_ref().points_to_directory()) }); } } } /// User-supplied field to sort by. #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum SortField { /// Don’t apply any sorting. This is usually used as an optimisation in /// scripts, where the order doesn’t matter. Unsorted, /// The file name. This is the default sorting. Name(SortCase), /// The file’s extension, with extensionless files being listed first. Extension(SortCase), /// The file’s size, in bytes. Size, /// The file’s inode, which usually corresponds to the order in which /// files were created on the filesystem, more or less. #[cfg(unix)] FileInode, /// The time the file was modified (the “mtime”). /// /// As this is stored as a Unix timestamp, rather than a local time /// instance, the time zone does not matter and will only be used to /// display the timestamps, not compare them. ModifiedDate, /// The time the file was accessed (the “atime”). /// /// Oddly enough, this field rarely holds the *actual* accessed time. /// Recording a read time means writing to the file each time it’s read /// slows the whole operation down, so many systems will only update the /// timestamp in certain circumstances. This has become common enough that /// it’s now expected behaviour! /// AccessedDate, /// The time the file was changed (the “ctime”). /// /// This field is used to mark the time when a file’s metadata /// changed — its permissions, owners, or link count. /// /// In original Unix, this was, however, meant as creation time. /// ChangedDate, /// The time the file was created (the “btime” or “birthtime”). CreatedDate, /// The type of the file: directories, links, pipes, regular, files, etc. /// /// Files are ordered according to the `PartialOrd` implementation of /// `fs::fields::Type`, so changing that will change this. FileType, /// The “age” of the file, which is the time it was modified sorted /// backwards. The reverse of the `ModifiedDate` ordering! /// /// It turns out that listing the most-recently-modified files first is a /// common-enough use case that it deserves its own variant. This would be /// implemented by just using the modified date and setting the reverse /// flag, but this would make reversing *that* output not work, which is /// bad, even though that’s kind of nonsensical. So it’s its own variant /// that can be reversed like usual. ModifiedAge, /// The file's name, however if the name of the file begins with `.` /// ignore the leading `.` and then sort as Name NameMixHidden(SortCase), } /// Whether a field should be sorted case-sensitively or case-insensitively. /// This determines which of the `natord` functions to use. /// /// I kept on forgetting which one was sensitive and which one was /// insensitive. Would a case-sensitive sort put capital letters first because /// it takes the case of the letters into account, or intermingle them with /// lowercase letters because it takes the difference between the two cases /// into account? I gave up and just named these two variants after the /// effects they have. #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum SortCase { /// Sort files case-sensitively with uppercase first, with ‘A’ coming /// before ‘a’. ABCabc, /// Sort files case-insensitively, with ‘A’ being equal to ‘a’. AaBbCc, } impl SortField { /// Compares two files to determine the order they should be listed in, /// depending on the search field. /// /// The `natord` crate is used here to provide a more *natural* sorting /// order than just sorting character-by-character. This splits filenames /// into groups between letters and numbers, and then sorts those blocks /// together, so `file10` will sort after `file9`, instead of before it /// because of the `1`. pub fn compare_files(self, a: &File<'_>, b: &File<'_>) -> Ordering { use self::SortCase::{ABCabc, AaBbCc}; #[rustfmt::skip] return match self { Self::Unsorted => Ordering::Equal, Self::Name(ABCabc) => natord::compare(&a.name, &b.name), Self::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name), Self::Size => a.length().cmp(&b.length()), #[cfg(unix)] Self::FileInode => { a.metadata().map_or(0, MetadataExt::ino) .cmp(&b.metadata().map_or(0, MetadataExt::ino)) } Self::ModifiedDate => a.modified_time().cmp(&b.modified_time()), Self::AccessedDate => a.accessed_time().cmp(&b.accessed_time()), Self::ChangedDate => a.changed_time().cmp(&b.changed_time()), Self::CreatedDate => a.created_time().cmp(&b.created_time()), Self::ModifiedAge => b.modified_time().cmp(&a.modified_time()), // flip b and a Self::FileType => match a.type_char().cmp(&b.type_char()) { // todo: this recomputes Ordering::Equal => natord::compare(&a.name, &b.name), order => order, }, Self::Extension(ABCabc) => match a.ext.cmp(&b.ext) { Ordering::Equal => natord::compare(&a.name, &b.name), order => order, }, Self::Extension(AaBbCc) => match a.ext.cmp(&b.ext) { Ordering::Equal => natord::compare_ignore_case(&a.name, &b.name), order => order, }, Self::NameMixHidden(ABCabc) => natord::compare( Self::strip_dot(&a.name), Self::strip_dot(&b.name) ), Self::NameMixHidden(AaBbCc) => natord::compare_ignore_case( Self::strip_dot(&a.name), Self::strip_dot(&b.name) ), }; } fn strip_dot(n: &str) -> &str { match n.strip_prefix('.') { Some(s) => s, None => n, } } } /// The **ignore patterns** are a list of globs that are tested against /// each filename, and if any of them match, that file isn’t displayed. /// This lets a user hide, say, text files by ignoring `*.txt`. #[derive(PartialEq, Eq, Default, Debug, Clone)] pub struct IgnorePatterns { patterns: Vec, } impl FromIterator for IgnorePatterns { fn from_iter(iter: I) -> Self where I: IntoIterator, { let patterns = iter.into_iter().collect(); Self { patterns } } } impl IgnorePatterns { /// Create a new list from the input glob strings, turning the inputs that /// are valid glob patterns into an `IgnorePatterns`. The inputs that /// don’t parse correctly are returned separately. pub fn parse_from_iter<'a, I: IntoIterator>( iter: I, ) -> (Self, Vec) { let iter = iter.into_iter(); // Almost all glob patterns are valid, so it’s worth pre-allocating // the vector with enough space for all of them. let mut patterns = match iter.size_hint() { (_, Some(count)) => Vec::with_capacity(count), _ => Vec::new(), }; // Similarly, assume there won’t be any errors. let mut errors = Vec::new(); for input in iter { match glob::Pattern::new(input) { Ok(pat) => patterns.push(pat), Err(e) => errors.push(e), } } (Self { patterns }, errors) } /// Create a new empty set of patterns that matches nothing. #[must_use] pub fn empty() -> Self { Self { patterns: Vec::new(), } } /// Test whether the given file should be hidden from the results. fn is_ignored(&self, file: &str) -> bool { self.patterns.iter().any(|p| p.matches(file)) } } /// Whether to ignore or display files that Git would ignore. #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum GitIgnore { /// Ignore files that Git would ignore. CheckAndIgnore, /// Display files, even if Git would ignore them. Off, } #[cfg(test)] mod test_ignores { use super::*; #[test] fn empty_matches_nothing() { let pats = IgnorePatterns::empty(); assert!(!pats.is_ignored("nothing")); assert!(!pats.is_ignored("test.mp3")); } #[test] fn ignores_a_glob() { let (pats, fails) = IgnorePatterns::parse_from_iter(vec!["*.mp3"]); assert!(fails.is_empty()); assert!(!pats.is_ignored("nothing")); assert!(pats.is_ignored("test.mp3")); } #[test] fn ignores_an_exact_filename() { let (pats, fails) = IgnorePatterns::parse_from_iter(vec!["nothing"]); assert!(fails.is_empty()); assert!(pats.is_ignored("nothing")); assert!(!pats.is_ignored("test.mp3")); } #[test] fn ignores_both() { let (pats, fails) = IgnorePatterns::parse_from_iter(vec!["nothing", "*.mp3"]); assert!(fails.is_empty()); assert!(pats.is_ignored("nothing")); assert!(pats.is_ignored("test.mp3")); } } ================================================ FILE: src/fs/mod.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT mod dir; pub use self::dir::{Dir, DotFilter}; mod file; pub use self::file::{File, FileTarget}; pub mod dir_action; pub mod feature; pub mod fields; pub mod filter; pub mod mounts; pub mod recursive_size; ================================================ FILE: src/fs/mounts/linux.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT use crate::fs::mounts::{Error, MountedFs}; use proc_mounts::MountList; /// Get a list of all mounted filesystems pub fn mounts() -> Result, Error> { Ok(MountList::new() .map_err(Error::IOError)? .0 .iter() .map(|mount| MountedFs { dest: mount.dest.clone(), fstype: mount.fstype.clone(), source: mount.source.to_string_lossy().into(), }) .collect()) } ================================================ FILE: src/fs/mounts/macos.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT use crate::fs::mounts::{Error, MountedFs}; use libc::{__error, MNT_NOWAIT, getfsstat, statfs}; use std::ffi::{CStr, OsStr}; use std::os::raw::{c_char, c_int}; use std::os::unix::ffi::OsStrExt; use std::path::PathBuf; use std::{mem, ptr}; /// Get a list of all mounted filesystem pub fn mounts() -> Result, Error> { // SAFETY: // Calling external "C" function getfsstat. Passing a null pointer and zero // bufsize will return the number of mounts. let mut count: i32 = unsafe { getfsstat(ptr::null_mut(), 0, MNT_NOWAIT) }; let mut mntbuf = Vec::::new(); if count > 0 { // SAFETY: Zero out buffer memory as we allocate. mntbuf.resize_with(count as usize, || unsafe { mem::zeroed() }); let bufsize = mntbuf.len() * mem::size_of::(); // SAFETY: // Calling external "C" function getfsstate with actual buffer now. The // function takes a buffer size to not overflow. If the mount table // changes size between calls we are protected by bufsize count = unsafe { getfsstat(mntbuf.as_mut_ptr(), bufsize as c_int, MNT_NOWAIT) }; // Resize if the mount table has shrunk since last call if count >= 0 { mntbuf.truncate(count as usize); } } if count < 0 { // SAFETY: Calling external "C" errno function to get the error number return Err(Error::GetFSStatError(unsafe { *__error() })); } let mut mounts = Vec::with_capacity(count as usize); for mnt in &mntbuf { let mount_point = OsStr::from_bytes( // SAFETY: Converting null terminated "C" string unsafe { CStr::from_ptr(mnt.f_mntonname.as_ptr().cast::()) }.to_bytes(), ); let dest = PathBuf::from(mount_point); // SAFETY: Converting null terminated "C" string let fstype = unsafe { CStr::from_ptr(mnt.f_fstypename.as_ptr().cast::()) } .to_string_lossy() .into(); // SAFETY: Converting null terminated "C" string let source = unsafe { CStr::from_ptr(mnt.f_mntfromname.as_ptr().cast::()) } .to_string_lossy() .into(); mounts.push(MountedFs { dest, fstype, source, }); } Ok(mounts) } ================================================ FILE: src/fs/mounts/mod.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT use std::collections::HashMap; use std::path::PathBuf; use std::sync::OnceLock; #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "linux")] use linux::mounts; #[cfg(target_os = "macos")] use macos::mounts; /// Details of a mounted filesystem. #[derive(Clone)] pub struct MountedFs { pub dest: PathBuf, pub fstype: String, pub source: String, } #[derive(Debug)] #[cfg(any(target_os = "macos", target_os = "linux"))] pub enum Error { #[cfg(target_os = "macos")] GetFSStatError(i32), #[cfg(target_os = "linux")] IOError(std::io::Error), } #[cfg(any(target_os = "macos", target_os = "linux"))] impl std::error::Error for Error {} #[cfg(any(target_os = "macos", target_os = "linux"))] impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Allow unreachable_patterns for windows build match self { #[cfg(target_os = "macos")] Error::GetFSStatError(err) => write!(f, "getfsstat failed: {err}"), #[cfg(target_os = "linux")] Error::IOError(err) => write!(f, "failed to read /proc/mounts: {err}"), } } } // A lazily initialised static map of all mounted file systems. // // The map contains a mapping from the mounted directory path to the // corresponding mount information. If there's an error retrieving the mount // list or if we're not running on Linux or Mac, the map will be empty. // // Initialise this at application start so we don't have to look the details // up for every directory. Ideally this would only be done if the --mounts // option is specified which will be significantly easier once the move // to `clap` is complete. pub(super) fn all_mounts() -> &'static HashMap { static ALL_MOUNTS: OnceLock> = OnceLock::new(); ALL_MOUNTS.get_or_init(|| { // Allow unused_mut for windows build #[allow(unused_mut)] let mut mount_map: HashMap = HashMap::new(); #[cfg(any(target_os = "linux", target_os = "macos"))] if let Ok(mounts) = mounts() { for mount in mounts { mount_map.insert(mount.dest.clone(), mount); } } mount_map }) } ================================================ FILE: src/fs/recursive_size.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT /// Used to represent a the size of a recursive directory traversal. `None` /// should be used when the file does not represent a directory or the recursive /// size should not be calculated. #[derive(Copy, Clone, Debug)] pub enum RecursiveSize { /// Size should not be computed None, /// Size should be computed but has not been computed yet Unknown, /// Size has been computed. First field is size in bytes and second field /// is size in blocks #[cfg_attr(target_family = "windows", allow(dead_code))] Some(u64, u64), } impl RecursiveSize { /// Returns `true` if `None` /// /// # Examples /// /// ``` /// use eza::fs::recursive_size::RecursiveSize; /// /// let x = RecursiveSize::None; /// assert_eq!(x.is_none(), true); /// /// let x = RecursiveSize::Unknown; /// assert_eq!(x.is_none(), false); /// /// let x = RecursiveSize::Some(0, 0); /// assert_eq!(x.is_none(), false); /// ``` #[inline] #[must_use] pub const fn is_none(&self) -> bool { matches!(*self, Self::None) } /// Returns the contained [`Some`] value or a provided default. /// /// # Examples /// /// ``` /// use eza::fs::recursive_size::RecursiveSize; /// /// assert_eq!(RecursiveSize::None.unwrap_bytes_or(1), 1); /// assert_eq!(RecursiveSize::Unknown.unwrap_bytes_or(1), 1); /// assert_eq!(RecursiveSize::Some(2, 3).unwrap_bytes_or(1), 2); /// ``` #[inline] #[must_use] pub const fn unwrap_bytes_or(self, default: u64) -> u64 { match self { Self::Some(bytes, _blocks) => bytes, _ => default, } } /// Returns the provided default result (if None or Unknown), /// or applies a function to the contained value (if Some). /// /// # Examples /// /// ``` /// use eza::fs::recursive_size::RecursiveSize; /// /// assert_eq!(RecursiveSize::None.map_or(None, |s, _| Some(s * 2)), None); /// assert_eq!(RecursiveSize::Unknown.map_or(None, |s, _| Some(s * 2)), None); /// assert_eq!(RecursiveSize::Some(2, 3).map_or(None, |s, _| Some(s * 2)), Some(4)); /// ``` #[inline] #[cfg_attr(target_family = "windows", allow(dead_code))] pub fn map_or(self, default: U, f: F) -> U where F: FnOnce(u64, u64) -> U, { match self { RecursiveSize::Some(bytes, blocks) => f(bytes, blocks), _ => default, } } } ================================================ FILE: src/info/filetype.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT //! Tests for various types of file (video, image, compressed, etc). //! //! Currently this is dependent on the file’s name and extension, because //! those are the only metadata that we have access to without reading the //! file’s contents. //! //! # Contributors //! Please keep these lists sorted. If you're using vim, :sort i use phf::{Map, phf_map}; use crate::fs::File; #[derive(Debug, Clone)] pub enum FileType { Image, Video, Music, Lossless, // Lossless music, rather than any other kind of data... Crypto, Document, Compressed, Temp, Compiled, Build, // A “build file is something that can be run or activated somehow in order to // kick off the build of a project. It’s usually only present in directories full of // source code. Source, } /// Mapping from full filenames to file type. const FILENAME_TYPES: Map<&'static str, FileType> = phf_map! { /* Immediate file - kick off the build of a project */ "Brewfile" => FileType::Build, "bsconfig.json" => FileType::Build, "BUILD" => FileType::Build, "BUILD.bazel" => FileType::Build, "build.gradle" => FileType::Build, "build.sbt" => FileType::Build, "build.xml" => FileType::Build, "Cargo.toml" => FileType::Build, "CMakeLists.txt" => FileType::Build, "composer.json" => FileType::Build, "configure" => FileType::Build, "Containerfile" => FileType::Build, "Dockerfile" => FileType::Build, "Earthfile" => FileType::Build, "flake.nix" => FileType::Build, "Gemfile" => FileType::Build, "GNUmakefile" => FileType::Build, "Gruntfile.coffee" => FileType::Build, "Gruntfile.js" => FileType::Build, "jsconfig.json" => FileType::Build, "Justfile" => FileType::Build, "justfile" => FileType::Build, "Makefile" => FileType::Build, "makefile" => FileType::Build, "meson.build" => FileType::Build, "mix.exs" => FileType::Build, "package.json" => FileType::Build, "Pipfile" => FileType::Build, "PKGBUILD" => FileType::Build, "Podfile" => FileType::Build, "pom.xml" => FileType::Build, "Procfile" => FileType::Build, "pyproject.toml" => FileType::Build, "Rakefile" => FileType::Build, "RoboFile.php" => FileType::Build, "SConstruct" => FileType::Build, "tsconfig.json" => FileType::Build, "Vagrantfile" => FileType::Build, "webpack.config.cjs" => FileType::Build, "webpack.config.js" => FileType::Build, "WORKSPACE" => FileType::Build, /* Cryptology files */ "id_dsa" => FileType::Crypto, "id_ecdsa" => FileType::Crypto, "id_ecdsa_sk" => FileType::Crypto, "id_ed25519" => FileType::Crypto, "id_ed25519_sk" => FileType::Crypto, "id_rsa" => FileType::Crypto, }; /// Mapping from lowercase file extension to file type. If an image, video, music, or lossless /// extension is added also update the extension icon map. const EXTENSION_TYPES: Map<&'static str, FileType> = phf_map! { /* Immediate file - kick off the build of a project */ "ninja" => FileType::Build, /* Image files */ "arw" => FileType::Image, "avif" => FileType::Image, "bmp" => FileType::Image, "cbr" => FileType::Image, "cbz" => FileType::Image, "cr2" => FileType::Image, "dvi" => FileType::Image, "eps" => FileType::Image, "fodg" => FileType::Image, "gif" => FileType::Image, "heic" => FileType::Image, "heif" => FileType::Image, "ico" => FileType::Image, "j2c" => FileType::Image, "j2k" => FileType::Image, "jfi" => FileType::Image, "jfif" => FileType::Image, "jif" => FileType::Image, "jp2" => FileType::Image, "jpe" => FileType::Image, "jpeg" => FileType::Image, "jpf" => FileType::Image, "jpg" => FileType::Image, "jpx" => FileType::Image, "jxl" => FileType::Image, "kra" => FileType::Image, "krz" => FileType::Image, "nef" => FileType::Image, "odg" => FileType::Image, "orf" => FileType::Image, "pbm" => FileType::Image, "pgm" => FileType::Image, "png" => FileType::Image, "pnm" => FileType::Image, "ppm" => FileType::Image, "ps" => FileType::Image, "psd" => FileType::Image, "pxm" => FileType::Image, "raw" => FileType::Image, "qoi" => FileType::Image, "svg" => FileType::Image, "tif" => FileType::Image, "tiff" => FileType::Image, "webp" => FileType::Image, "xcf" => FileType::Image, "xpm" => FileType::Image, /* Video files */ "avi" => FileType::Video, "flv" => FileType::Video, "h264" => FileType::Video, "heics" => FileType::Video, "m2ts" => FileType::Video, "m2v" => FileType::Video, "m4v" => FileType::Video, "mkv" => FileType::Video, "mov" => FileType::Video, "mp4" => FileType::Video, "mpeg" => FileType::Video, "mpg" => FileType::Video, "ogm" => FileType::Video, "ogv" => FileType::Video, "video" => FileType::Video, "vob" => FileType::Video, "webm" => FileType::Video, "wmv" => FileType::Video, /* Music files */ "aac" => FileType::Music, // Advanced Audio Coding "m4a" => FileType::Music, "mka" => FileType::Music, "mp2" => FileType::Music, "mp3" => FileType::Music, "ogg" => FileType::Music, "opus" => FileType::Music, "wma" => FileType::Music, /* Lossless music, rather than any other kind of data... */ "aif" => FileType::Lossless, "aifc" => FileType::Lossless, "aiff" => FileType::Lossless, "alac" => FileType::Lossless, "ape" => FileType::Lossless, "flac" => FileType::Lossless, "pcm" => FileType::Lossless, "wav" => FileType::Lossless, "wv" => FileType::Lossless, /* Cryptology files */ "age" => FileType::Crypto, // age encrypted file "asc" => FileType::Crypto, // GnuPG ASCII armored file "cer" => FileType::Crypto, "crt" => FileType::Crypto, "csr" => FileType::Crypto, // PKCS#10 Certificate Signing Request "gpg" => FileType::Crypto, // GnuPG encrypted file "kbx" => FileType::Crypto, // GnuPG keybox "md5" => FileType::Crypto, // MD5 checksum "p12" => FileType::Crypto, // PKCS#12 certificate (Netscape) "pem" => FileType::Crypto, // Privacy-Enhanced Mail certificate "pfx" => FileType::Crypto, // PKCS#12 certificate (Microsoft) "pgp" => FileType::Crypto, // PGP security key "pub" => FileType::Crypto, // Public key "sha1" => FileType::Crypto, // SHA-1 hash "sha224" => FileType::Crypto, // SHA-224 hash "sha256" => FileType::Crypto, // SHA-256 hash "sha384" => FileType::Crypto, // SHA-384 hash "sha512" => FileType::Crypto, // SHA-512 hash "sig" => FileType::Crypto, // GnuPG signed file "signature" => FileType::Crypto, // e-Filing Digital Signature File (India) /* Document files */ "djvu" => FileType::Document, "doc" => FileType::Document, "docx" => FileType::Document, "eml" => FileType::Document, "fodp" => FileType::Document, "fods" => FileType::Document, "fodt" => FileType::Document, "fotd" => FileType::Document, "gdoc" => FileType::Document, "key" => FileType::Document, "keynote" => FileType::Document, "numbers" => FileType::Document, "odp" => FileType::Document, "ods" => FileType::Document, "odt" => FileType::Document, "pages" => FileType::Document, "pdf" => FileType::Document, "ppt" => FileType::Document, "pptx" => FileType::Document, "rtf" => FileType::Document, // Rich Text Format "xls" => FileType::Document, "xlsm" => FileType::Document, "xlsx" => FileType::Document, /* Compressed/archive files */ "7z" => FileType::Compressed, // 7-Zip "ar" => FileType::Compressed, "arj" => FileType::Compressed, "br" => FileType::Compressed, // Brotli "bz" => FileType::Compressed, // bzip "bz2" => FileType::Compressed, // bzip2 "bz3" => FileType::Compressed, // bzip3 "cpio" => FileType::Compressed, "deb" => FileType::Compressed, // Debian "dmg" => FileType::Compressed, "gz" => FileType::Compressed, // gzip "iso" => FileType::Compressed, "lz" => FileType::Compressed, "lz4" => FileType::Compressed, "lzh" => FileType::Compressed, "lzma" => FileType::Compressed, "lzo" => FileType::Compressed, "phar" => FileType::Compressed, // PHP PHAR "qcow" => FileType::Compressed, "qcow2" => FileType::Compressed, "rar" => FileType::Compressed, "rpm" => FileType::Compressed, "tar" => FileType::Compressed, "taz" => FileType::Compressed, "tbz" => FileType::Compressed, "tbz2" => FileType::Compressed, "tc" => FileType::Compressed, "tgz" => FileType::Compressed, "tlz" => FileType::Compressed, "txz" => FileType::Compressed, "tz" => FileType::Compressed, "xz" => FileType::Compressed, "vdi" => FileType::Compressed, "vhd" => FileType::Compressed, "vhdx" => FileType::Compressed, "vmdk" => FileType::Compressed, "z" => FileType::Compressed, "zip" => FileType::Compressed, "zst" => FileType::Compressed, // Zstandard /* Temporary files */ "bak" => FileType::Temp, "bk" => FileType::Temp, "bkp" => FileType::Temp, "crdownload" => FileType::Temp, "download" => FileType::Temp, "fcbak" => FileType::Temp, "fcstd1" => FileType::Temp, "fdmdownload"=> FileType::Temp, "part" => FileType::Temp, "swn" => FileType::Temp, "swo" => FileType::Temp, "swp" => FileType::Temp, "tmp" => FileType::Temp, /* Compiler output files */ "a" => FileType::Compiled, // Unix static library "bundle" => FileType::Compiled, // macOS application bundle "class" => FileType::Compiled, // Java class file "cma" => FileType::Compiled, // OCaml bytecode library "cmi" => FileType::Compiled, // OCaml interface "cmo" => FileType::Compiled, // OCaml bytecode object "cmx" => FileType::Compiled, // OCaml bytecode object for inlining "dll" => FileType::Compiled, // Windows dynamic link library "dylib" => FileType::Compiled, // Mach-O dynamic library "elc" => FileType::Compiled, // Emacs compiled lisp "elf" => FileType::Compiled, // Executable and Linkable Format "ko" => FileType::Compiled, // Linux kernel module "lib" => FileType::Compiled, // Windows static library "o" => FileType::Compiled, // Compiled object file "obj" => FileType::Compiled, // Compiled object file "pyc" => FileType::Compiled, // Python compiled code "pyd" => FileType::Compiled, // Python dynamic module "pyo" => FileType::Compiled, // Python optimized code "so" => FileType::Compiled, // Unix shared library "zwc" => FileType::Compiled, // zsh compiled file /* Source code files */ "applescript"=> FileType::Source, // Apple script "as" => FileType::Source, // Action script "asa" => FileType::Source, // asp "awk" => FileType::Source, // awk "c" => FileType::Source, // C/C++ "c++" => FileType::Source, // C/C++ "c++m" => FileType::Source, // C/C++ module "cabal" => FileType::Source, // Cabal "cc" => FileType::Source, // C/C++ "ccm" => FileType::Source, // C/C++ module "clj" => FileType::Source, // Clojure "cp" => FileType::Source, // C/C++ Xcode "cpp" => FileType::Source, // C/C++ "cppm" => FileType::Source, // C/C++ module "cr" => FileType::Source, // Crystal "cs" => FileType::Source, // C# "css" => FileType::Source, // css "csx" => FileType::Source, // C# "cu" => FileType::Source, // CUDA "cxx" => FileType::Source, // C/C++ "cxxm" => FileType::Source, // C/C++ module "cypher" => FileType::Source, // Cypher (query language) "d" => FileType::Source, // D "dart" => FileType::Source, // Dart "di" => FileType::Source, // D "dpr" => FileType::Source, // Delphi Pascal "el" => FileType::Source, // Lisp "elm" => FileType::Source, // Elm "erl" => FileType::Source, // Erlang "ex" => FileType::Source, // Elixir "exs" => FileType::Source, // Elixir "f" => FileType::Source, // Fortran "f90" => FileType::Source, // Fortran "fcmacro" => FileType::Source, // FreeCAD macro "fcscript" => FileType::Source, // FreeCAD script "fnl" => FileType::Source, // Fennel "for" => FileType::Source, // Fortran "fs" => FileType::Source, // F# "fsh" => FileType::Source, // Fragment shader "fsi" => FileType::Source, // F# "fsx" => FileType::Source, // F# "gd" => FileType::Source, // GDScript "go" => FileType::Source, // Go "gradle" => FileType::Source, // Gradle "groovy" => FileType::Source, // Groovy "gvy" => FileType::Source, // Groovy "h" => FileType::Source, // C/C++ header "h++" => FileType::Source, // C/C++ header "hh" => FileType::Source, // C/C++ header "hpp" => FileType::Source, // C/C++ header "hc" => FileType::Source, // HolyC "hs" => FileType::Source, // Haskell "htc" => FileType::Source, // JavaScript "hxx" => FileType::Source, // C/C++ header "inc" => FileType::Source, "inl" => FileType::Source, // C/C++ Microsoft "ino" => FileType::Source, // Arduino "ipynb" => FileType::Source, // Jupyter Notebook "ixx" => FileType::Source, // C/C++ module "java" => FileType::Source, // Java "jl" => FileType::Source, // Julia "js" => FileType::Source, // JavaScript "jsx" => FileType::Source, // React "kt" => FileType::Source, // Kotlin "kts" => FileType::Source, // Kotlin "kusto" => FileType::Source, // Kusto (query language) "less" => FileType::Source, // less "lhs" => FileType::Source, // Haskell "lisp" => FileType::Source, // Lisp "ltx" => FileType::Source, // LaTeX "lua" => FileType::Source, // Lua "m" => FileType::Source, // Matlab "malloy" => FileType::Source, // Malloy (query language) "matlab" => FileType::Source, // Matlab "ml" => FileType::Source, // OCaml "mli" => FileType::Source, // OCaml "mn" => FileType::Source, // Matlab "nb" => FileType::Source, // Mathematica "p" => FileType::Source, // Pascal "pas" => FileType::Source, // Pascal "php" => FileType::Source, // PHP "pl" => FileType::Source, // Perl "pm" => FileType::Source, // Perl "pod" => FileType::Source, // Perl "pp" => FileType::Source, // Puppet "prql" => FileType::Source, // PRQL "ps1" => FileType::Source, // PowerShell "psd1" => FileType::Source, // PowerShell "psm1" => FileType::Source, // PowerShell "purs" => FileType::Source, // PureScript "py" => FileType::Source, // Python "r" => FileType::Source, // R "rb" => FileType::Source, // Ruby "rs" => FileType::Source, // Rust "rq" => FileType::Source, // SPARQL (query language) "sass" => FileType::Source, // Sass "scala" => FileType::Source, // Scala "scm" => FileType::Source, // Scheme "scad" => FileType::Source, // OpenSCAD "scss" => FileType::Source, // Sass "sld" => FileType::Source, // Scheme Library Definition "sql" => FileType::Source, // SQL "ss" => FileType::Source, // Scheme Source "swift" => FileType::Source, // Swift "tcl" => FileType::Source, // TCL "tex" => FileType::Source, // LaTeX "ts" => FileType::Source, // TypeScript "v" => FileType::Source, // V "vb" => FileType::Source, // Visual Basic "vsh" => FileType::Source, // Vertex shader "zig" => FileType::Source, // Zig }; impl FileType { /// Lookup the file type based on the file's name, by the file name /// lowercase extension, or if the file could be compiled from related /// source code. pub(crate) fn get_file_type(file: &File<'_>) -> Option { // Case-insensitive readme is checked first for backwards compatibility. if file.name.to_lowercase().starts_with("readme") { return Some(Self::Build); } if let Some(file_type) = FILENAME_TYPES.get(&file.name) { return Some(file_type.clone()); } if let Some(file_type) = file.ext.as_ref().and_then(|ext| EXTENSION_TYPES.get(ext)) { return Some(file_type.clone()); } if file.name.ends_with('~') || (file.name.starts_with('#') && file.name.ends_with('#')) { return Some(Self::Temp); } if let Some(dir) = file.parent_dir && file .get_source_files() .iter() .any(|path| dir.contains(path)) { return Some(Self::Compiled); } None } } ================================================ FILE: src/info/mod.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT //! The “info” module contains routines that aren’t about probing the //! filesystem nor displaying output to the user, but are internal “business //! logic” routines that are performed on a file’s already-read metadata. //! (This counts the file name as metadata.) pub mod filetype; mod sources; ================================================ FILE: src/info/sources.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT use std::path::PathBuf; use crate::fs::File; impl File<'_> { /// For this file, return a vector of alternate file paths that, if any of /// them exist, mean that *this* file should be coloured as “compiled”. /// /// The point of this is to highlight compiled files such as `foo.js` when /// their source file `foo.coffee` exists in the same directory. /// For example, `foo.js` is perfectly valid without `foo.coffee`, so we /// don’t want to always blindly highlight `*.js` as compiled. /// (See also `FileType`) pub fn get_source_files(&self) -> Vec { if let Some(ext) = &self.ext { match &ext[..] { "css" => vec![self.path.with_extension("sass"), self.path.with_extension("scss"), // SASS, SCSS self.path.with_extension("styl"), self.path.with_extension("less")], // Stylus, Less "mjs" => vec![self.path.with_extension("mts")], // JavaScript ES Modules source "cjs" => vec![self.path.with_extension("cts")], // JavaScript Commonjs Modules source "js" => vec![self.path.with_extension("coffee"), self.path.with_extension("ts")], // CoffeeScript, TypeScript "aux" | // TeX: auxiliary file "bbl" | // BibTeX bibliography file "bcf" | // biblatex control file "blg" | // BibTeX log file "fdb_latexmk" | // TeX latexmk file "fls" | // TeX -recorder file "headfootlength" | // TeX package autofancyhdr file "lof" | // TeX list of figures "log" | // TeX log file "lot" | // TeX list of tables "out" | // hyperref list of bookmarks "toc" | // TeX table of contents "xdv" => vec![self.path.with_extension("tex")], // XeTeX dvi _ => vec![], // No source files if none of the above } } else { vec![] // No source files if there’s no extension, either! } } } ================================================ FILE: src/lib.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT #[allow(unused)] pub mod fs; #[allow(unused)] pub mod info; #[allow(unused)] pub mod logger; #[allow(unused)] pub mod options; #[allow(unused)] pub mod output; #[allow(unused)] pub mod theme; ================================================ FILE: src/logger.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT //! Debug error logging. use std::ffi::OsStr; use nu_ansi_term::{AnsiString as ANSIString, Color as Colour}; /// Sets the internal logger, changing the log level based on the value of an /// environment variable. pub fn configure>(ev: Option) { let Some(ev) = ev else { return }; let env_var = ev.as_ref(); if env_var.is_empty() { return; } if env_var == "trace" { log::set_max_level(log::LevelFilter::Trace); } else { log::set_max_level(log::LevelFilter::Debug); } let result = log::set_logger(GLOBAL_LOGGER); if let Err(e) = result { eprintln!("Failed to initialize logger: {e}"); } } #[derive(Debug)] struct Logger; const GLOBAL_LOGGER: &Logger = &Logger; impl log::Log for Logger { fn enabled(&self, _: &log::Metadata<'_>) -> bool { true // no need to filter after using ‘set_max_level’. } fn log(&self, record: &log::Record<'_>) { let open = Colour::Fixed(243).paint("["); let level = level(record.level()); let close = Colour::Fixed(243).paint("]"); eprintln!( "{}{} {}{} {}", open, level, record.target(), close, record.args() ); } fn flush(&self) { // no need to flush with ‘eprintln!’. } } fn level(level: log::Level) -> ANSIString<'static> { #[rustfmt::skip] return match level { log::Level::Error => Colour::Red.paint("ERROR"), log::Level::Warn => Colour::Yellow.paint("WARN"), log::Level::Info => Colour::Cyan.paint("INFO"), log::Level::Debug => Colour::Blue.paint("DEBUG"), log::Level::Trace => Colour::Fixed(245).paint("TRACE"), }; } ================================================ FILE: src/main.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT #![warn(future_incompatible)] #![warn(trivial_casts, trivial_numeric_casts)] #![warn(clippy::all)] #![allow(clippy::non_ascii_literal)] use std::env; use std::ffi::{OsStr, OsString}; use std::io::{self, ErrorKind, IsTerminal, Read, Write, stdin}; use std::path::{Component, PathBuf}; use std::process::exit; use nu_ansi_term::{AnsiStrings as ANSIStrings, Style}; use options::parser::get_command; use crate::fs::feature::git::GitCache; use crate::fs::filter::{FileFilterFlags::OnlyFiles, GitIgnore}; use crate::fs::{Dir, File}; use crate::options::stdin::FilesInput; use crate::options::{Options, Vars, vars}; use crate::output::{Mode, View, details, escape, file_name, grid, grid_details, lines}; use crate::theme::Theme; use log::*; mod fs; mod info; mod logger; mod options; mod output; mod theme; fn main() { #[cfg(unix)] unsafe { libc::signal(libc::SIGPIPE, libc::SIG_DFL); } logger::configure(env::var_os(vars::EZA_DEBUG).or_else(|| env::var_os(vars::EXA_DEBUG))); let cli = get_command().get_matches(); let stdout_istty = io::stdout().is_terminal(); let mut input = String::new(); let mut input_paths: Vec<&OsStr> = match cli.get_many("FILE") { Some(x) => x.map(OsString::as_os_str).collect(), None => vec![], }; match Options::deduce(&cli, &LiveVars) { Ok(options) => { if input_paths.is_empty() { match &options.stdin { FilesInput::Args => { input_paths = vec![OsStr::new(".")]; } FilesInput::Stdin(separator) => { stdin() .read_to_string(&mut input) .expect("Failed to read from stdin"); input_paths.extend( input .split(&separator.clone().into_string().unwrap_or("\n".to_string())) .map(OsStr::new) .filter(|s| !s.is_empty()) .collect::>(), ); } } } let git = git_options(&options, &input_paths); let writer = io::stdout(); let git_repos = git_repos(&options, &input_paths); let console_width = options.view.width.actual_terminal_width(); let theme = options.theme.to_theme(stdout_istty); let exa = Exa { options, writer, input_paths, theme, console_width, git, git_repos, }; info!("matching on exa.run"); match exa.run() { Ok(exit_status) => { trace!("exa.run: exit Ok({exit_status})"); exit(exit_status); } Err(e) if e.kind() == ErrorKind::BrokenPipe => { warn!("Broken pipe error: {e}"); exit(exits::SUCCESS); } Err(e) => { eprintln!("{e}"); trace!("exa.run: exit RUNTIME_ERROR"); exit(exits::RUNTIME_ERROR); } } } Err(error) => { eprintln!("eza: {error}"); exit(exits::OPTIONS_ERROR); } } } /// The main program wrapper. pub struct Exa<'args> { /// List of command-line options, having been successfully parsed. pub options: Options, /// The output handle that we write to. pub writer: io::Stdout, /// List of the free command-line arguments that should correspond to file /// names (anything that isn’t an option). pub input_paths: Vec<&'args OsStr>, /// The theme that has been configured from the command-line options and /// environment variables. If colours are disabled, this is a theme with /// every style set to the default. pub theme: Theme, /// The detected width of the console. This is used to determine which /// view to use. pub console_width: Option, /// A global Git cache, if the option was passed in. /// This has to last the lifetime of the program, because the user might /// want to list several directories in the same repository. pub git: Option, pub git_repos: bool, } /// The “real” environment variables type. /// Instead of just calling `var_os` from within the options module, /// the method of looking up environment variables has to be passed in. struct LiveVars; impl Vars for LiveVars { fn get(&self, name: &'static str) -> Option { env::var_os(name) } } /// Create a Git cache populated with the arguments that are going to be /// listed before they’re actually listed, if the options demand it. fn git_options(options: &Options, args: &[&OsStr]) -> Option { if options.should_scan_for_git() { Some(args.iter().map(PathBuf::from).collect()) } else { None } } #[cfg(not(feature = "git"))] fn git_repos(_options: &Options, _args: &[&OsStr]) -> bool { return false; } #[cfg(feature = "git")] fn get_files_in_dir(paths: &mut Vec, path: PathBuf) { let temp_paths = if path.is_dir() { match path.read_dir() { Err(_) => { vec![path] } Ok(d) => d .filter_map(|entry| entry.ok().map(|e| e.path())) .collect::>(), } } else { vec![path] }; paths.extend(temp_paths); } #[cfg(feature = "git")] fn git_repos(options: &Options, args: &[&OsStr]) -> bool { let option_enabled = match options.view.mode { Mode::Details(details::Options { table: Some(ref table), .. }) | Mode::GridDetails(grid_details::Options { details: details::Options { table: Some(ref table), .. }, .. }) => table.columns.subdir_git_repos || table.columns.subdir_git_repos_no_stat, _ => false, }; if option_enabled { let paths: Vec = args.iter().map(PathBuf::from).collect::>(); let mut files: Vec = Vec::new(); for path in paths { get_files_in_dir(&mut files, path); } let repos: Vec = files .iter() .map(git2::Repository::open) .map(|repo| repo.is_ok()) .collect(); repos.contains(&true) } else { false } } impl Exa<'_> { /// # Errors /// /// Will return `Err` if printing to stderr fails. pub fn run(mut self) -> io::Result { debug!("Running with options: {:#?}", self.options); let mut files = Vec::new(); let mut dirs = Vec::new(); let mut exit_status = 0; for file_path in &self.input_paths { let f = File::from_args( PathBuf::from(file_path), None, None, self.options.view.deref_links, self.options.view.total_size, None, ); // We don't know whether this file exists, so we have to try to get // the metadata to verify. if let Err(e) = f.metadata() { exit_status = 2; writeln!(io::stderr(), "{file_path:?}: {e}")?; continue; } if f.points_to_directory() && !self.options.dir_action.treat_dirs_as_files() { trace!("matching on new Dir"); dirs.push(f.to_dir()); } else { files.push(f); } } // We want to print a directory’s name before we list it, *except* in // the case where it’s the only directory, *except* if there are any // files to print as well. (It’s a double negative) let no_files = files.is_empty(); let is_only_dir = dirs.len() == 1 && no_files; self.options.filter.filter_argument_files(&mut files); self.print_files(None, files)?; self.print_dirs(dirs, no_files, is_only_dir, exit_status) } fn print_dirs( &mut self, dir_files: Vec, mut first: bool, is_only_dir: bool, exit_status: i32, ) -> io::Result { let View { file_style: file_name::Options { quote_style, .. }, .. } = self.options.view; let mut denied_dirs = vec![]; for mut dir in dir_files { let dir = match dir.read() { Ok(dir) => dir, Err(e) => { if e.kind() == ErrorKind::PermissionDenied { eprintln!( "Permission denied: {} - code: {}", dir.path.display(), exits::PERMISSION_DENIED ); denied_dirs.push(dir.path); continue; } eprintln!("{}: {}", dir.path.display(), e); continue; } }; // Put a gap between directories, or between the list of files and // the first directory. if first { first = false; } else { writeln!(&mut self.writer)?; } if !is_only_dir { let mut bits = Vec::new(); escape( dir.path.display().to_string(), &mut bits, Style::default(), Style::default(), quote_style, ); writeln!(&mut self.writer, "{}:", ANSIStrings(&bits))?; } let mut children = Vec::new(); let git_ignore = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore; for file in dir.files( self.options.filter.dot_filter, self.git.as_ref(), git_ignore, self.options.view.deref_links, self.options.view.total_size, ) { children.push(file); } let recursing = self.options.dir_action.recurse_options().is_some(); self.options .filter .filter_child_files(recursing, &mut children); self.options.filter.sort_files(&mut children); if let Some(recurse_opts) = self.options.dir_action.recurse_options() { let depth = dir .path .components() .filter(|&c| c != Component::CurDir) .count() + 1; let follow_links = self.options.view.follow_links; if !recurse_opts.tree && !recurse_opts.is_too_deep(depth) { let child_dirs = children .iter() .filter(|f| { (if follow_links { f.points_to_directory() } else { f.is_directory() }) && !f.is_all_all }) .map(fs::File::to_dir) .collect::>(); self.print_files(Some(dir), children)?; match self.print_dirs(child_dirs, false, false, exit_status) { Ok(_) => (), Err(e) => return Err(e), } continue; } } self.print_files(Some(dir), children)?; } if !denied_dirs.is_empty() { eprintln!( "\nSkipped {} directories due to permission denied: ", denied_dirs.len() ); for path in denied_dirs { eprintln!(" {}", path.display()); } } Ok(exit_status) } /// Prints the list of files using whichever view is selected. fn print_files(&mut self, dir: Option<&Dir>, mut files: Vec>) -> io::Result<()> { if files.is_empty() { return Ok(()); } let recursing = self.options.dir_action.recurse_options().is_some(); let only_files = self.options.filter.flags.contains(&OnlyFiles); if recursing && only_files { files = files .into_iter() .filter(|f| !f.is_directory()) .collect::>(); } let theme = &self.theme; let View { ref mode, ref file_style, .. } = self.options.view; match (mode, self.console_width) { (Mode::Grid(opts), Some(console_width)) => { let filter = &self.options.filter; let r = grid::Render { files, theme, file_style, opts, console_width, filter, }; r.render(&mut self.writer) } (Mode::Grid(opts), None) => { let filter = &self.options.filter; let r = grid::Render { files, theme, file_style, opts, console_width: 80, filter, }; r.render(&mut self.writer) } (Mode::Lines, _) => { let filter = &self.options.filter; let r = lines::Render { files, theme, file_style, filter, }; r.render(&mut self.writer) } (Mode::Details(opts), _) => { let filter = &self.options.filter; let recurse = self.options.dir_action.recurse_options(); let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore; let git = self.git.as_ref(); let git_repos = self.git_repos; let r = details::Render { dir, files, theme, file_style, opts, recurse, filter, git_ignoring, git, git_repos, }; r.render(&mut self.writer) } (Mode::GridDetails(opts), Some(console_width)) => { let details = &opts.details; let row_threshold = opts.row_threshold; let filter = &self.options.filter; let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore; let git = self.git.as_ref(); let git_repos = self.git_repos; let r = grid_details::Render { dir, files, theme, file_style, details, filter, row_threshold, git_ignoring, git, console_width, git_repos, }; r.render(&mut self.writer) } (Mode::GridDetails(opts), None) => { let opts = &opts.to_details_options(); let filter = &self.options.filter; let recurse = self.options.dir_action.recurse_options(); let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore; let git = self.git.as_ref(); let git_repos = self.git_repos; let r = details::Render { dir, files, theme, file_style, opts, recurse, filter, git_ignoring, git, git_repos, }; r.render(&mut self.writer) } } } } mod exits { /// Exit code for when exa runs OK. pub const SUCCESS: i32 = 0; /// Exit code for when there was at least one I/O error during execution. pub const RUNTIME_ERROR: i32 = 1; /// Exit code for when the command-line options are invalid. pub const OPTIONS_ERROR: i32 = 3; /// Exit code for missing file permissions pub const PERMISSION_DENIED: i32 = 13; } ================================================ FILE: src/options/config.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT use crate::theme::ThemeFileType as FileType; use crate::theme::{ FileKinds, FileNameStyle, Git, GitRepo, IconStyle, Links, Permissions, SELinuxContext, SecurityContext, Size, UiStyles, Users, }; use nu_ansi_term::{Color, Style}; use serde::{Deserialize, Deserializer, Serialize}; use serde_norway; use std::collections::HashMap; use std::path::PathBuf; #[derive(Debug, Eq, PartialEq)] pub struct ThemeConfig { // This is rather bare for now, will be expanded with config file location: PathBuf, } impl Default for ThemeConfig { fn default() -> Self { ThemeConfig { location: dirs::config_dir() .unwrap_or_default() .join("eza") .join("theme.yml"), } } } trait FromOverride: Sized { fn from(value: T, default: Self) -> Self; } impl FromOverride> for Option where T: FromOverride + Default, { fn from(value: Option, default: Option) -> Option { match (value, default) { (Some(value), Some(default)) => Some(FromOverride::from(value, default)), (Some(value), None) => Some(FromOverride::from(value, T::default())), (None, Some(default)) => Some(default), (None, None) => None, } } } #[rustfmt::skip] fn color_from_str(s: &str) -> Option { use Color::{Black, Blue, Cyan, DarkGray, Default, Fixed, Green, LightBlue, LightCyan, LightGray, LightGreen, LightMagenta, LightPurple, LightRed, LightYellow, Magenta, Purple, Red, Rgb, White, Yellow}; match s { // nothing "" | "none" | "None" => None, // hardcoded colors "default" | "Default" => Some(Default), "black" | "Black" => Some(Black), "darkgray" | "DarkGray" => Some(DarkGray), "red" | "Red" => Some(Red), "lightred" | "LightRed" => Some(LightRed), "green" | "Green" => Some(Green), "lightgreen" | "LightGreen" => Some(LightGreen), "yellow" | "Yellow" => Some(Yellow), "lightyellow" | "LightYellow" => Some(LightYellow), "blue" | "Blue" => Some(Blue), "lightblue" | "LightBlue" => Some(LightBlue), "purple" | "Purple" => Some(Purple), "lightpurple" | "LightPurple" => Some(LightPurple), "magenta" | "Magenta" => Some(Magenta), "lightmagenta" | "LightMagenta" => Some(LightMagenta), "cyan" | "Cyan" => Some(Cyan), "lightcyan" | "LightCyan" => Some(LightCyan), "white" | "White" => Some(White), "lightgray" | "LightGray" => Some(LightGray), // some other string s => match s.chars().collect::>()[..] { // #rrggbb hex color ['#', r1, r2, g1, g2, b1, b2] => { let Ok(r) = u8::from_str_radix(&format!("{r1}{r2}"), 16) else { return None }; let Ok(g) = u8::from_str_radix(&format!("{g1}{g2}"), 16) else { return None }; let Ok(b) = u8::from_str_radix(&format!("{b1}{b2}"), 16) else { return None }; Some(Rgb(r, g, b)) }, // #rgb shorthand hex color ['#', r, g, b] => { let Ok(r) = u8::from_str_radix(&format!("{r}{r}"), 16) else { return None }; let Ok(g) = u8::from_str_radix(&format!("{g}{g}"), 16) else { return None }; let Ok(b) = u8::from_str_radix(&format!("{b}{b}"), 16) else { return None }; Some(Rgb(r, g, b)) }, // 0-255 color code (1-3 digits) [c1] => { let Ok(c) = str::parse::(&format!("{c1}")) else { return None }; Some(Fixed(c)) }, [c1, c2] => { let Ok(c) = str::parse::(&format!("{c1}{c2}")) else { return None }; Some(Fixed(c)) }, [c1, c2, c3] => { let Ok(c) = str::parse::(&format!("{c1}{c2}{c3}")) else { return None }; Some(Fixed(c)) }, // unknown format _ => None, } } } #[rustfmt::skip] fn deserialize_color<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de> { Ok(color_from_str(&String::deserialize(deserializer)?)) } #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Default)] pub struct StyleOverride { /// The style's foreground color, if it has one. #[serde(alias = "fg", deserialize_with = "deserialize_color", default)] pub foreground: Option, /// The style's background color, if it has one. #[serde(alias = "bg", deserialize_with = "deserialize_color", default)] pub background: Option, /// Whether this style is bold. #[serde(alias = "bold")] pub is_bold: Option, /// Whether this style is dimmed. #[serde(alias = "dimmed")] pub is_dimmed: Option, /// Whether this style is italic. #[serde(alias = "italic")] pub is_italic: Option, /// Whether this style is underlined. #[serde(alias = "underline")] pub is_underline: Option, /// Whether this style is blinking. #[serde(alias = "blink")] pub is_blink: Option, /// Whether this style has reverse colors. #[serde(alias = "reverse")] pub is_reverse: Option, /// Whether this style is hidden. #[serde(alias = "hidden")] pub is_hidden: Option, /// Whether this style is struckthrough. #[serde(alias = "strikethrough")] pub is_strikethrough: Option, /// Wether this style is always displayed starting with a reset code to clear any remaining style artifacts #[serde(alias = "prefix_reset")] pub prefix_with_reset: Option, } impl FromOverride for Style { fn from(value: StyleOverride, default: Self) -> Self { let mut style = default; if value.foreground.is_some() { style.foreground = value.foreground; } if value.background.is_some() { style.background = value.background; } if let Some(bold) = value.is_bold { style.is_bold = bold; } if let Some(dimmed) = value.is_dimmed { style.is_dimmed = dimmed; } if let Some(italic) = value.is_italic { style.is_italic = italic; } if let Some(underline) = value.is_underline { style.is_underline = underline; } if let Some(blink) = value.is_blink { style.is_blink = blink; } if let Some(reverse) = value.is_reverse { style.is_reverse = reverse; } if let Some(hidden) = value.is_hidden { style.is_hidden = hidden; } if let Some(strikethrough) = value.is_strikethrough { style.is_strikethrough = strikethrough; } if let Some(reset) = value.prefix_with_reset { style.prefix_with_reset = reset; } style } } #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct IconStyleOverride { pub glyph: Option, pub style: Option, } impl FromOverride for char { fn from(value: char, _default: char) -> char { value } } impl FromOverride for IconStyle { fn from(value: IconStyleOverride, default: Self) -> Self { IconStyle { glyph: FromOverride::from(value.glyph, default.glyph), style: FromOverride::from(value.style, default.style), } } } #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct FileNameStyleOverride { pub icon: Option, pub filename: Option, } impl FromOverride for FileNameStyle { fn from(value: FileNameStyleOverride, default: Self) -> Self { FileNameStyle { icon: FromOverride::from(value.icon, default.icon), filename: FromOverride::from(value.filename, default.filename), } } } impl FromOverride> for HashMap where T: FromOverride, R: Clone + Eq + std::hash::Hash, T: Clone + Eq + Default, { fn from(value: HashMap, default: HashMap) -> HashMap { let mut result = default.clone(); for (r, s) in value { let t = match default.get(&r) { Some(t) => t.clone(), None => T::default(), }; result.insert(r, FromOverride::from(s, t)); } result } } #[rustfmt::skip] #[derive(Clone, Eq, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct FileKindsOverride { pub normal: Option, // fi pub directory: Option, // di pub symlink: Option, // ln pub pipe: Option, // pi pub block_device: Option, // bd pub char_device: Option, // cd pub socket: Option, // so pub special: Option, // sp pub executable: Option, // ex pub mount_point: Option, // mp } impl FromOverride for FileKinds { fn from(value: FileKindsOverride, default: Self) -> Self { FileKinds { normal: FromOverride::from(value.normal, default.normal), directory: FromOverride::from(value.directory, default.directory), symlink: FromOverride::from(value.symlink, default.symlink), pipe: FromOverride::from(value.pipe, default.pipe), block_device: FromOverride::from(value.block_device, default.block_device), char_device: FromOverride::from(value.char_device, default.char_device), socket: FromOverride::from(value.socket, default.socket), special: FromOverride::from(value.special, default.special), executable: FromOverride::from(value.executable, default.executable), mount_point: FromOverride::from(value.mount_point, default.mount_point), } } } #[rustfmt::skip] #[derive(Clone, Copy,Eq, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct PermissionsOverride { pub user_read: Option, // ur pub user_write: Option, // uw pub user_execute_file: Option, // ux pub user_execute_other: Option, // ue pub group_read: Option, // gr pub group_write: Option, // gw pub group_execute: Option, // gx pub other_read: Option, // tr pub other_write: Option, // tw pub other_execute: Option, // tx pub special_user_file: Option, // su pub special_other: Option, // sf pub attribute: Option, // xa } impl FromOverride for Permissions { fn from(value: PermissionsOverride, default: Self) -> Self { Permissions { user_read: FromOverride::from(value.user_read, default.user_read), user_write: FromOverride::from(value.user_write, default.user_write), user_execute_file: FromOverride::from( value.user_execute_file, default.user_execute_file, ), user_execute_other: FromOverride::from( value.user_execute_other, default.user_execute_other, ), group_read: FromOverride::from(value.group_read, default.group_read), group_write: FromOverride::from(value.group_write, default.group_write), group_execute: FromOverride::from(value.group_execute, default.group_execute), other_read: FromOverride::from(value.other_read, default.other_read), other_write: FromOverride::from(value.other_write, default.other_write), other_execute: FromOverride::from(value.other_execute, default.other_execute), special_user_file: FromOverride::from( value.special_user_file, default.special_user_file, ), special_other: FromOverride::from(value.special_other, default.special_other), attribute: FromOverride::from(value.attribute, default.attribute), } } } #[rustfmt::skip] #[derive(Clone, Copy, Eq, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct SizeOverride { pub major: Option, // df pub minor: Option, // ds pub number_byte: Option, // sn nb pub number_kilo: Option, // sn nk pub number_mega: Option, // sn nm pub number_giga: Option, // sn ng pub number_huge: Option, // sn nt pub unit_byte: Option, // sb ub pub unit_kilo: Option, // sb uk pub unit_mega: Option, // sb um pub unit_giga: Option, // sb ug pub unit_huge: Option, // sb ut } impl FromOverride for Size { fn from(value: SizeOverride, default: Self) -> Self { Size { major: FromOverride::from(value.major, default.major), minor: FromOverride::from(value.minor, default.minor), number_byte: FromOverride::from(value.number_byte, default.number_byte), number_kilo: FromOverride::from(value.number_kilo, default.number_kilo), number_mega: FromOverride::from(value.number_mega, default.number_mega), number_giga: FromOverride::from(value.number_giga, default.number_giga), number_huge: FromOverride::from(value.number_huge, default.number_huge), unit_byte: FromOverride::from(value.unit_byte, default.unit_byte), unit_kilo: FromOverride::from(value.unit_kilo, default.unit_kilo), unit_mega: FromOverride::from(value.unit_mega, default.unit_mega), unit_giga: FromOverride::from(value.unit_giga, default.unit_giga), unit_huge: FromOverride::from(value.unit_huge, default.unit_huge), } } } #[rustfmt::skip] #[derive(Clone, Copy, Debug,Eq, Default, PartialEq, Serialize, Deserialize)] pub struct UsersOverride { pub user_you: Option, // uu pub user_root: Option, // uR pub user_other: Option, // un pub group_yours: Option, // gu pub group_other: Option, // gn pub group_root: Option, // gR } impl FromOverride for Users { fn from(value: UsersOverride, default: Self) -> Self { Users { user_you: FromOverride::from(value.user_you, default.user_you), user_root: FromOverride::from(value.user_root, default.user_root), user_other: FromOverride::from(value.user_other, default.user_other), group_yours: FromOverride::from(value.group_yours, default.group_yours), group_other: FromOverride::from(value.group_other, default.group_other), group_root: FromOverride::from(value.group_root, default.group_root), } } } #[rustfmt::skip] #[derive(Clone, Copy, Debug, Eq, Default, PartialEq, Serialize, Deserialize)] pub struct LinksOverride { pub normal: Option, // lc pub multi_link_file: Option, // lm } impl FromOverride for Links { fn from(value: LinksOverride, default: Self) -> Self { Links { normal: FromOverride::from(value.normal, default.normal), multi_link_file: FromOverride::from(value.multi_link_file, default.multi_link_file), } } } #[rustfmt::skip] #[derive(Clone, Copy, Debug,Eq, PartialEq, Serialize, Deserialize)] pub struct GitOverride { pub new: Option, // ga pub modified: Option, // gm pub deleted: Option, // gd pub renamed: Option, // gv pub typechange: Option, // gt pub ignored: Option, // gi pub conflicted: Option, // gc } impl FromOverride for Git { fn from(value: GitOverride, default: Self) -> Self { Git { new: FromOverride::from(value.new, default.new), modified: FromOverride::from(value.modified, default.modified), deleted: FromOverride::from(value.deleted, default.deleted), renamed: FromOverride::from(value.renamed, default.renamed), typechange: FromOverride::from(value.typechange, default.typechange), ignored: FromOverride::from(value.ignored, default.ignored), conflicted: FromOverride::from(value.conflicted, default.conflicted), } } } #[rustfmt::skip] #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct GitRepoOverride { pub branch_main: Option, //Gm pub branch_other: Option, //Go pub git_clean: Option, //Gc pub git_dirty: Option, //Gd } impl FromOverride for GitRepo { fn from(value: GitRepoOverride, default: Self) -> Self { GitRepo { branch_main: FromOverride::from(value.branch_main, default.branch_main), branch_other: FromOverride::from(value.branch_other, default.branch_other), git_clean: FromOverride::from(value.git_clean, default.git_clean), git_dirty: FromOverride::from(value.git_dirty, default.git_dirty), } } } #[derive(Clone, Copy, Debug, Eq, Default, PartialEq, Serialize, Deserialize)] pub struct SELinuxContextOverride { pub colon: Option, pub user: Option, // Su pub role: Option, // Sr pub typ: Option, // St pub range: Option, // Sl } impl FromOverride for SELinuxContext { fn from(value: SELinuxContextOverride, default: Self) -> Self { SELinuxContext { colon: FromOverride::from(value.colon, default.colon), user: FromOverride::from(value.user, default.user), role: FromOverride::from(value.role, default.role), typ: FromOverride::from(value.typ, default.typ), range: FromOverride::from(value.range, default.range), } } } #[rustfmt::skip] #[derive(Clone, Eq, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct SecurityContextOverride { pub none: Option, // Sn pub selinux: Option, } impl FromOverride for SecurityContext { fn from(value: SecurityContextOverride, default: Self) -> Self { SecurityContext { none: FromOverride::from(value.none, default.none), selinux: FromOverride::from(value.selinux, default.selinux), } } } #[rustfmt::skip] #[derive(Clone, Copy, Debug, Eq, Default, PartialEq, Serialize, Deserialize)] pub struct FileTypeOverride { pub image: Option, // im - image file pub video: Option, // vi - video file pub music: Option, // mu - lossy music pub lossless: Option, // lo - lossless music pub crypto: Option, // cr - related to cryptography pub document: Option, // do - document file pub compressed: Option, // co - compressed file pub temp: Option, // tm - temporary file pub compiled: Option, // cm - compilation artifact pub build: Option, // bu - file that is used to build a project pub source: Option, // sc - source code } impl FromOverride for FileType { fn from(value: FileTypeOverride, default: Self) -> Self { FileType { image: FromOverride::from(value.image, default.image), video: FromOverride::from(value.video, default.video), music: FromOverride::from(value.music, default.music), lossless: FromOverride::from(value.lossless, default.lossless), crypto: FromOverride::from(value.crypto, default.crypto), document: FromOverride::from(value.document, default.document), compressed: FromOverride::from(value.compressed, default.compressed), temp: FromOverride::from(value.temp, default.temp), compiled: FromOverride::from(value.compiled, default.compiled), build: FromOverride::from(value.build, default.build), source: FromOverride::from(value.source, default.source), } } } #[rustfmt::skip] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct UiStylesOverride { pub colourful: Option, pub filekinds: Option, pub perms: Option, pub size: Option, pub users: Option, pub links: Option, pub git: Option, pub git_repo: Option, pub security_context: Option, pub file_type: Option, pub punctuation: Option, // xx pub date: Option, // da pub inode: Option, // in pub blocks: Option, // bl pub header: Option, // hd pub octal: Option, // oc pub flags: Option, // ff pub symlink_path: Option, // lp pub control_char: Option, // cc pub broken_symlink: Option, // or pub broken_path_overlay: Option, // bO pub filenames: Option>, pub extensions: Option>, } impl FromOverride for UiStyles { fn from(value: UiStylesOverride, default: Self) -> Self { UiStyles { colourful: value.colourful, filekinds: FromOverride::from(value.filekinds, default.filekinds), perms: FromOverride::from(value.perms, default.perms), size: FromOverride::from(value.size, default.size), users: FromOverride::from(value.users, default.users), links: FromOverride::from(value.links, default.links), git: FromOverride::from(value.git, default.git), git_repo: FromOverride::from(value.git_repo, default.git_repo), security_context: FromOverride::from(value.security_context, default.security_context), file_type: FromOverride::from(value.file_type, default.file_type), punctuation: FromOverride::from(value.punctuation, default.punctuation), date: FromOverride::from(value.date, default.date), inode: FromOverride::from(value.inode, default.inode), blocks: FromOverride::from(value.blocks, default.blocks), header: FromOverride::from(value.header, default.header), octal: FromOverride::from(value.octal, default.octal), flags: FromOverride::from(value.flags, default.flags), symlink_path: FromOverride::from(value.symlink_path, default.symlink_path), control_char: FromOverride::from(value.control_char, default.control_char), broken_symlink: FromOverride::from(value.broken_symlink, default.broken_symlink), broken_path_overlay: FromOverride::from( value.broken_path_overlay, default.broken_path_overlay, ), filenames: FromOverride::from(value.filenames, default.filenames), extensions: FromOverride::from(value.extensions, default.extensions), } } } impl ThemeConfig { #[must_use] pub fn from_path(path: PathBuf) -> Self { ThemeConfig { location: path } } #[must_use] pub fn to_theme(&self) -> Option { let ui_styles_override: Option = { let file = std::fs::File::open(&self.location).ok()?; serde_norway::from_reader(&file).ok() }; FromOverride::from(ui_styles_override, Some(UiStyles::default())) } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_none_color_from_string() { for case in &["", "none", "None"] { assert_eq!(color_from_str(case), None); } } #[test] fn parse_default_color_from_string() { for case in &["default", "Default"] { assert_eq!(color_from_str(case), Some(Color::Default)); } } #[test] fn parse_fixed_color_from_string() { for case in &["black", "Black"] { assert_eq!(color_from_str(case), Some(Color::Black)); } } #[test] fn parse_long_hex_color_from_string() { for case in &["#ff00ff", "#FF00FF"] { assert_eq!(color_from_str(case), Some(Color::Rgb(255, 0, 255))); } } #[test] fn parse_short_hex_color_from_string() { for case in &["#f0f", "#F0F"] { assert_eq!(color_from_str(case), Some(Color::Rgb(255, 0, 255))); } } #[test] fn parse_color_code_from_string() { for (s, c) in &[("118", 118), ("10", 10), ("01", 1), ("1", 1), ("001", 1)] { assert_eq!(color_from_str(s), Some(Color::Fixed(*c))); } } } ================================================ FILE: src/options/dir_action.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT //! Parsing the options for `DirAction`. use crate::options::OptionsError; use crate::fs::dir_action::{DirAction, RecurseOptions}; use clap::ArgMatches; impl DirAction { /// Determine which action to perform when trying to list a directory. /// There are three possible actions, and they overlap somewhat: the /// `--tree` flag is another form of recursion, so those two are allowed /// to both be present, but the `--treat-dirs-as-files` flag is used /// separately. pub fn deduce( matches: &ArgMatches, can_tree: bool, strict: bool, ) -> Result { let recurse = matches.get_flag("recurse"); let as_file = matches.get_flag("treat-dirs-as-files"); let tree = matches.get_flag("tree"); if strict { // Early check for --level when it wouldn’t do anything if !recurse && !tree && matches.get_one::("level").is_some() { return Err(OptionsError::Useless2("level", "recurse", "tree")); } else if recurse && as_file { return Err(OptionsError::Conflict("recurse", "treat-dirs-as-files")); } else if tree && as_file { return Err(OptionsError::Conflict("tree", "treat-dirs-as-files")); } } if tree && can_tree { // Tree is only appropriate in details mode, so this has to // examine the View, which should have already been deduced by now Ok(Self::Recurse(RecurseOptions::deduce(matches, true))) } else if recurse { Ok(Self::Recurse(RecurseOptions::deduce(matches, false))) } else if as_file { Ok(Self::AsFile) } else { Ok(Self::List) } } } impl RecurseOptions { /// Determine which files should be recursed into, based on the `--level` /// flag’s value, and whether the `--tree` flag was passed, which was /// determined earlier. The maximum level should be a number, and this /// will fail with an `Err` if it isn’t. pub fn deduce(matches: &ArgMatches, tree: bool) -> Self { Self { tree, max_depth: matches.get_one("level").copied(), } } } #[cfg(test)] mod tests { use super::*; use crate::options::parser::test::{mock_cli, mock_cli_try}; #[test] fn deduce_dir_action_list() { assert_eq!( DirAction::deduce(&mock_cli(vec![""]), false, false), Ok(DirAction::List) ); } #[test] fn deduce_recurse_options_level() { assert_eq!( RecurseOptions::deduce(&mock_cli(vec!["--level", "3"]), false), RecurseOptions { tree: false, max_depth: Some(3), } ); } #[test] fn deduce_recurse_options_no_level() { assert_eq!( DirAction::deduce(&mock_cli(vec!["--recurse"]), true, true), Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: None, })) ); } #[test] fn deduce_dir_action_as_file() { assert_eq!( DirAction::deduce(&mock_cli(vec!["--treat-dirs-as-files"]), false, false), Ok(DirAction::AsFile) ); } #[test] fn deduce_dir_action_recurse() { assert_eq!( DirAction::deduce(&mock_cli(vec!["--recurse"]), false, false), Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: None, })) ); } #[test] fn deduce_dir_action_tree() { assert_eq!( DirAction::deduce(&mock_cli(vec!["--tree"]), true, false), Ok(DirAction::Recurse(RecurseOptions { tree: true, max_depth: None, })) ); } #[test] fn deduce_dir_action_tree_level() { assert_eq!( DirAction::deduce(&mock_cli(vec!["--tree", "--level", "3"]), true, false), Ok(DirAction::Recurse(RecurseOptions { tree: true, max_depth: Some(3), })) ); } #[test] fn deduce_dir_action_tree_level_conflict() { assert_eq!( DirAction::deduce(&mock_cli(vec!["--level", "3"]), false, true), Err(OptionsError::Useless2("level", "recurse", "tree")) ); } #[test] fn deduce_dir_action_recurse_as_file_conflict() { assert!(mock_cli_try(vec!["--recurse", "--treat-dirs-as-files"]).is_err()); } #[test] fn deduce_dir_action_tree_as_file_conflict() { assert!(mock_cli_try(vec!["--tree", "--treat-dirs-as-files"]).is_err()); } } ================================================ FILE: src/options/error.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT use std::ffi::OsString; use std::fmt; use std::num::ParseIntError; /// Something wrong with the combination of options the user has picked. #[derive(PartialEq, Eq, Debug)] pub enum OptionsError { /// The user supplied an illegal choice to an Argument. BadArgument(&'static str, OsString), /// The user supplied a set of options that are unsupported Unsupported(String), /// An option was given twice or more in strict mode. Duplicate(&'static str, &'static str), /// Two options were given that conflict with one another. Conflict(&'static str, &'static str), /// An option was given that does nothing when another one either is or /// isn’t present. Useless(&'static str, bool, &'static str), /// An option was given that does nothing when either of two other options /// are not present. Useless2(&'static str, &'static str, &'static str), /// A very specific edge case where --tree can’t be used with --all twice. TreeAllAll, /// A numeric option was given that failed to be parsed as a number. FailedParse(String, NumberSource, ParseIntError), /// A glob ignore was given that failed to be parsed as a pattern. FailedGlobPattern(String), } /// The source of a string that failed to be parsed as a number. #[derive(PartialEq, Eq, Debug)] pub enum NumberSource { /// It came... from a command-line argument! Arg(&'static str), /// It came... from the environment! Env(&'static str), } impl From for OptionsError { fn from(error: glob::PatternError) -> Self { Self::FailedGlobPattern(error.to_string()) } } impl fmt::Display for NumberSource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Arg(arg) => write!(f, "option {arg}"), Self::Env(env) => write!(f, "environment variable {env}"), } } } impl fmt::Display for OptionsError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { #[rustfmt::skip] return match self { Self::BadArgument(arg, _) => write!(f, "Bad argument for {arg}"), Self::Unsupported(e) => write!(f, "{e}"), Self::Conflict(a, b) => write!(f, "Option {a} conflicts with option {b}"), Self::Duplicate(a, b) if a == b => write!(f, "Flag {a} was given twice"), Self::Duplicate(a, b) => write!(f, "Flag {a} conflicts with flag {b}"), Self::Useless(a, false, b) => write!(f, "Option {a} is useless without option {b}"), Self::Useless(a, true, b) => write!(f, "Option {a} is useless given option {b}"), Self::Useless2(a, b1, b2) => write!(f, "Option {a} is useless without options {b1} or {b2}"), Self::TreeAllAll => write!(f, "Option --tree is useless given --all --all"), Self::FailedParse(s, n, e) => write!(f, "Value {s:?} not valid for {n}: {e}"), Self::FailedGlobPattern(e) => write!(f, "Failed to parse glob pattern: {e}"), }; } } ================================================ FILE: src/options/file_name.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT use crate::options::parser::ShowWhen; use crate::options::vars::{self, Vars}; use crate::options::{NumberSource, OptionsError}; use crate::output::file_name::{Classify, EmbedHyperlinks, Options, QuoteStyle, ShowIcons}; use clap::ArgMatches; impl Options { pub fn deduce( matches: &ArgMatches, vars: &V, is_a_tty: bool, ) -> Result { let classify = Classify::deduce(matches); let show_icons = ShowIcons::deduce(matches, vars)?; let quote_style = QuoteStyle::deduce(matches); let embed_hyperlinks = EmbedHyperlinks::deduce(matches); let absolute = *matches.get_one("absolute").unwrap(); Ok(Self { classify, show_icons, quote_style, embed_hyperlinks, absolute, is_a_tty, }) } } impl Classify { fn deduce(matches: &ArgMatches) -> Self { match matches.get_one("classify") { Some(ShowWhen::Auto) => Self::AutomaticAddFileIndicators, Some(ShowWhen::Always) => Self::AddFileIndicators, None | Some(ShowWhen::Never) => Self::JustFilenames, } } } impl ShowIcons { pub fn deduce(matches: &ArgMatches, vars: &V) -> Result { let force_icons = vars.get(vars::EZA_ICONS_AUTO).is_some(); let mode_opt = &matches.get_one("icons"); if !force_icons && mode_opt.is_none() { return Ok(Self::Never); } match mode_opt { Some(ShowWhen::Never) => Ok(Self::Never), Some(ShowWhen::Always) => Ok(Self::Always(Self::get_width(vars)?)), Some(ShowWhen::Auto) | None => Ok(Self::Automatic(Self::get_width(vars)?)), } } fn get_width(vars: &V) -> Result { if let Some(columns) = vars .get_with_fallback(vars::EXA_ICON_SPACING, vars::EZA_ICON_SPACING) .map(|s| s.to_string_lossy().to_string()) { match columns.parse() { Ok(width) => Ok(width), Err(e) => { let source = NumberSource::Env( vars.source(vars::EXA_ICON_SPACING, vars::EZA_ICON_SPACING) .unwrap_or("1"), ); Err(OptionsError::FailedParse(columns.clone(), source, e)) } } } else { Ok(1) } } } impl QuoteStyle { pub fn deduce(matches: &ArgMatches) -> Self { if matches.get_flag("no-quotes") { Self::NoQuotes } else { Self::QuoteSpaces } } } impl EmbedHyperlinks { fn deduce(matches: &ArgMatches) -> Self { if matches.get_flag("hyperlink") { Self::On } else { Self::Off } } } #[cfg(test)] mod tests { use std::ffi::OsString; use std::num::ParseIntError; use super::*; use crate::options::parser::ShowWhen; use crate::options::parser::test::mock_cli; use crate::options::vars::test::MockVars; use crate::output::file_name::Absolute; use clap::ValueEnum; #[test] fn deduce_classify_file_indicators() { assert_eq!( Classify::deduce(&mock_cli(vec!["--classify"])), Classify::AutomaticAddFileIndicators ); } #[test] fn deduce_classify_just_filenames() { assert_eq!( Classify::deduce(&mock_cli(vec![""])), Classify::JustFilenames ); } #[test] fn deduce_quote_style_no_quotes() { assert_eq!( QuoteStyle::deduce(&mock_cli(vec!["--no-quotes"])), QuoteStyle::NoQuotes ); } #[test] fn deduce_quote_style_quote_spaces() { assert_eq!( QuoteStyle::deduce(&mock_cli(vec![""])), QuoteStyle::QuoteSpaces ); } #[test] fn deduce_embed_hyperlinks_on() { assert_eq!( EmbedHyperlinks::deduce(&mock_cli(vec!["--hyperlink"])), EmbedHyperlinks::On ); } #[test] fn deduce_embed_hyperlinks_off() { assert_eq!( EmbedHyperlinks::deduce(&mock_cli(vec![""])), EmbedHyperlinks::Off ); } #[test] fn deduce_show_icons_never_no_arg() { assert_eq!( ShowIcons::deduce(&mock_cli(vec![""]), &MockVars::default()), Ok(ShowIcons::Never) ); } #[test] fn deduce_show_icons_never_no_arg_env() { let mut vars = MockVars::default(); vars.set(vars::EZA_ICONS_AUTO, &OsString::from("1")); assert_eq!( ShowIcons::deduce(&mock_cli(vec![""]), &vars), Ok(ShowIcons::Automatic(1)) ); } #[test] fn deduce_show_icon_always() { assert_eq!( ShowIcons::deduce(&mock_cli(vec!["--icons", "always"]), &MockVars::default()), Ok(ShowIcons::Always(1)), ); } #[test] fn deduce_show_icons_never() { assert_eq!( ShowIcons::deduce(&mock_cli(vec!["--icons", "never"]), &MockVars::default()), Ok(ShowIcons::Never) ); } #[test] fn deduce_show_icons_auto() { assert_eq!( ShowIcons::deduce(&mock_cli(vec!["--icons", "auto"]), &MockVars::default()), Ok(ShowIcons::Automatic(1)) ); } #[test] fn deduce_show_icons_error() { assert_eq!( ShowWhen::from_str("foo", false) .map_err(|err| OptionsError::BadArgument("icons", err.into())), Err(OptionsError::BadArgument("icons", OsString::from("foo"))) ); } #[test] fn deduce_show_icons_width() { let mut vars = MockVars::default(); vars.set(vars::EZA_ICON_SPACING, &OsString::from("3")); assert_eq!( ShowIcons::deduce(&mock_cli(vec!["--icons"]), &vars), Ok(ShowIcons::Automatic(3)) ); } #[test] fn deduce_show_icons_width_error() { let mut vars = MockVars::default(); vars.set(vars::EZA_ICON_SPACING, &OsString::from("foo")); let e: Result = vars .get(vars::EZA_ICON_SPACING) .unwrap() .to_string_lossy() .parse(); assert_eq!( ShowIcons::deduce(&mock_cli(vec!["--icons", "auto"]), &vars), Err(OptionsError::FailedParse( String::from("foo"), NumberSource::Env(vars::EXA_ICON_SPACING), e.unwrap_err() )) ); } #[test] fn deduce_options() { assert_eq!( Options::deduce(&mock_cli(vec![""]), &MockVars::default(), true), Ok(Options { classify: Classify::JustFilenames, show_icons: ShowIcons::Never, quote_style: QuoteStyle::QuoteSpaces, embed_hyperlinks: EmbedHyperlinks::Off, absolute: Absolute::Off, is_a_tty: true, }) ); } } ================================================ FILE: src/options/filter.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT //! Parsing the options for `FileFilter`. use clap::ArgMatches; use crate::fs::DotFilter; use crate::fs::filter::{ FileFilter, FileFilterFlags, GitIgnore, IgnorePatterns, SortCase, SortField, }; use crate::options::OptionsError; impl FileFilter { /// Determines which of all the file filter options to use. pub fn deduce(matches: &ArgMatches, strict: bool) -> Result { use FileFilterFlags as FFF; let mut filter_flags: Vec = vec![]; for (flag, filter_flag) in &[ ("reverse", FFF::Reverse), ("only-dirs", FFF::OnlyDirs), ("only-files", FFF::OnlyFiles), ("no-symlinks", FFF::NoSymlinks), ("show-symlinks", FFF::ShowSymlinks), ("dirs-last", FFF::ListDirsLast), ("dirs-first", FFF::ListDirsFirst), ] { if matches.get_flag(flag) { filter_flags.push(filter_flag.clone()); } } Ok(Self { no_symlinks: matches.get_flag("no-symlinks"), show_symlinks: matches.get_flag("show-symlinks"), flags: filter_flags, sort_field: *matches.get_one("sort").unwrap(), dot_filter: DotFilter::deduce(matches, strict)?, ignore_patterns: IgnorePatterns::deduce(matches)?, git_ignore: GitIgnore::deduce(matches), }) } } // I’ve gone back and forth between whether to sort case-sensitively or // insensitively by default. The default string sort in most programming // languages takes each character’s ASCII value into account, sorting // “Documents” before “apps”, but there’s usually an option to ignore // characters’ case, putting “apps” before “Documents”. // // The argument for following case is that it’s easy to forget whether an item // begins with an uppercase or lowercase letter and end up having to scan both // the uppercase and lowercase sub-lists to find the item you want. If you // happen to pick the sublist it’s not in, it looks like it’s missing, which // is worse than if you just take longer to find it. // (https://ux.stackexchange.com/a/79266) // // The argument for ignoring case is that it makes exa sort files differently // from shells. A user would expect a directory’s files to be in the same // order if they used “exa ~/directory” or “exa ~/directory/*”, but exa sorts // them in the first case, and the shell in the second case, so they wouldn’t // be exactly the same if exa does something non-conventional. // // However, exa already sorts files differently: it uses natural sorting from // the natord crate, sorting the string “2” before “10” because the number’s // smaller, because that’s usually what the user expects to happen. Users will // name their files with numbers expecting them to be treated like numbers, // rather than lists of numeric characters. // // In the same way, users will name their files with letters expecting the // order of the letters to matter, rather than each letter’s character’s ASCII // value. So exa breaks from tradition and ignores case while sorting: // “apps” first, then “Documents”. // // You can get the old behaviour back by sorting with `--sort=Name`. impl Default for SortField { fn default() -> Self { Self::Name(SortCase::AaBbCc) } } impl DotFilter { /// Determines the dot filter based on how many `--all` options were /// given: one will show dotfiles, but two will show `.` and `..` too. /// --almost-all is equivalent to --all, included for compatibility with /// `ls -A`. /// /// It also checks for the `--tree` option, because of a special case /// where `--tree --all --all` won’t work: listing the parent directory /// in tree mode would loop onto itself! /// /// `--almost-all` binds stronger than multiple `--all` as we currently do not take the order /// of arguments into account and it is the safer option (does not clash with `--tree`) pub fn deduce(matches: &ArgMatches, strict: bool) -> Result { let all_count = matches.get_count("all"); let has_almost_all = matches.get_flag("almost-all"); match (all_count, has_almost_all) { (0, false) => Ok(Self::JustFiles), // either a single --all or at least one --almost-all is given (1, _) | (0, true) => Ok(Self::Dotfiles), // more than one --all (c, _) => { if matches.get_flag("tree") { Err(OptionsError::TreeAllAll) } else if strict && c > 2 { Err(OptionsError::Conflict("all", "all")) } else { Ok(Self::DotfilesAndDots) } } } } } impl IgnorePatterns { /// Determines the set of glob patterns to use based on the /// `--ignore-glob` argument’s value. This is a list of strings /// separated by pipe (`|`) characters, given in any order. pub fn deduce(matches: &ArgMatches) -> Result { // If there are no inputs, we return a set of patterns that doesn’t // match anything, rather than, say, `None`. let Some(inputs) = matches.get_one::("ignore-glob") else { return Ok(Self::empty()); }; // Awkwardly, though, a glob pattern can be invalid, and we need to // deal with invalid patterns somehow. let (patterns, mut errors) = Self::parse_from_iter(inputs.split('|')); // It can actually return more than one glob error, // but we only use one. (TODO) match errors.pop() { Some(e) => Err(e.into()), None => Ok(patterns), } } } impl GitIgnore { pub fn deduce(matches: &ArgMatches) -> Self { if matches.get_flag("git-ignore") { Self::CheckAndIgnore } else { Self::Off } } } #[cfg(test)] mod tests { use std::ffi::OsString; use super::*; use crate::options::parser::test::{mock_cli, mock_cli_try}; #[test] fn deduce_git_ignore_off() { assert_eq!(GitIgnore::deduce(&mock_cli(vec![""])), GitIgnore::Off); } #[test] fn deduce_git_ignore_on() { assert_eq!( GitIgnore::deduce(&mock_cli(vec!["--git-ignore"])), GitIgnore::CheckAndIgnore ); } #[test] fn deduce_ignore_patterns_empty() { assert_eq!( IgnorePatterns::deduce(&mock_cli(vec![""])), Ok(IgnorePatterns::empty()) ); } #[test] fn deduce_ignore_patterns_one() { let pattern = OsString::from("*.o"); let (res, _) = IgnorePatterns::parse_from_iter(pattern.to_string_lossy().split('|')); assert_eq!( IgnorePatterns::deduce(&mock_cli(vec!["--ignore-glob", "*.o"])), Ok(res) ); } #[test] fn deduce_ignore_patterns_error() { let pattern = OsString::from("["); let (_, mut e) = IgnorePatterns::parse_from_iter(pattern.to_string_lossy().split('|')); assert_eq!( IgnorePatterns::deduce(&mock_cli(vec!["--ignore-glob", "["])), Err(e.pop().unwrap().into()) ); } #[test] fn deduce_dot_filter_just_files() { assert_eq!( DotFilter::deduce(&mock_cli(vec![""]), false), Ok(DotFilter::JustFiles) ); } #[test] fn deduce_dot_filter_dotfiles() { assert_eq!( DotFilter::deduce(&mock_cli(vec!["--all"]), false), Ok(DotFilter::Dotfiles) ); } #[test] fn deduce_dot_filter_dotfiles_and_dots() { assert_eq!( DotFilter::deduce(&mock_cli(vec!["--all", "--all"]), false), Ok(DotFilter::DotfilesAndDots) ); } #[test] fn deduce_dot_filter_tree_all_all() { assert_eq!( DotFilter::deduce(&mock_cli(vec!["--all", "--all", "--tree"]), false), Err(OptionsError::TreeAllAll) ); } #[test] fn deduce_dot_filter_all_all() { assert_eq!( DotFilter::deduce(&mock_cli(vec!["--all", "--all", "--all"]), true), Err(OptionsError::Conflict("all", "all")) ); } #[test] fn deduce_dot_filter_almost_all() { assert_eq!( DotFilter::deduce(&mock_cli(vec!["--almost-all"]), false), Ok(DotFilter::Dotfiles) ); } #[test] fn deduce_sort_field_default() { assert_eq!( mock_cli(vec![""]).get_one::("sort"), Some(&SortField::default()) ); } #[test] fn deduce_sort_field_name() { assert_eq!( mock_cli(vec!["--sort", "name"]).get_one::("sort"), Some(&SortField::Name(SortCase::AaBbCc)) ); } #[test] fn deduce_sort_field_name_case() { assert_eq!( mock_cli(vec!["--sort", "Name"]).get_one::("sort"), Some(&SortField::Name(SortCase::ABCabc)) ); } #[test] fn deduce_sort_field_name_mix_hidden() { assert_eq!( mock_cli(vec!["--sort", ".name"]).get_one::("sort"), Some(&SortField::NameMixHidden(SortCase::AaBbCc)) ); } #[test] fn deduce_sort_field_name_mix_hidden_case() { assert_eq!( mock_cli(vec!["--sort", ".Name"]).get_one::("sort"), Some(&SortField::NameMixHidden(SortCase::ABCabc)) ); } #[test] fn deduce_sort_field_size() { assert_eq!( mock_cli(vec!["--sort", "size"]).get_one::("sort"), Some(&SortField::Size) ); } #[test] fn deduce_sort_field_extension() { assert_eq!( mock_cli(vec!["--sort", "ext"]).get_one::("sort"), Some(&SortField::Extension(SortCase::AaBbCc)) ); } #[test] fn deduce_sort_field_extension_case() { assert_eq!( mock_cli(vec!["--sort", "Ext"]).get_one::("sort"), Some(&SortField::Extension(SortCase::ABCabc)) ); } #[test] fn deduce_sort_field_date() { assert_eq!( mock_cli(vec!["--sort", "date"]).get_one::("sort"), Some(&SortField::ModifiedDate) ); } #[test] fn deduce_sort_field_time() { assert_eq!( mock_cli(vec!["--sort", "time"]).get_one::("sort"), Some(&SortField::ModifiedDate) ); } #[test] fn deduce_sort_field_age() { assert_eq!( mock_cli(vec!["--sort", "age"]).get_one::("sort"), Some(&SortField::ModifiedAge) ); } #[test] fn deduce_sort_field_old() { assert_eq!( mock_cli(vec!["--sort", "old"]).get_one::("sort"), Some(&SortField::ModifiedAge) ); } #[test] fn deduce_sort_field_ch() { assert_eq!( mock_cli(vec!["--sort", "ch"]).get_one::("sort"), Some(&SortField::ChangedDate) ); } #[test] fn deduce_sort_field_acc() { assert_eq!( mock_cli(vec!["--sort", "acc"]).get_one::("sort"), Some(&SortField::AccessedDate) ); } #[test] fn deduce_sort_field_cr() { assert_eq!( mock_cli(vec!["--sort", "cr"]).get_one::("sort"), Some(&SortField::CreatedDate) ); } #[test] fn deduce_sort_field_err() { assert!(mock_cli_try(vec!["--sort", "foo"]).is_err()); } #[test] fn deduce_file_filter_default() { assert_eq!( FileFilter::deduce(&mock_cli(vec![""]), false), Ok(FileFilter { flags: vec![], sort_field: SortField::default(), dot_filter: DotFilter::JustFiles, ignore_patterns: IgnorePatterns::empty(), git_ignore: GitIgnore::Off, no_symlinks: false, show_symlinks: false, }) ); } #[test] fn deduce_file_filter_reverse() { assert_eq!( FileFilter::deduce(&mock_cli(vec!["--reverse"]), false), Ok(FileFilter { flags: vec![FileFilterFlags::Reverse], sort_field: SortField::default(), dot_filter: DotFilter::JustFiles, ignore_patterns: IgnorePatterns::empty(), git_ignore: GitIgnore::Off, no_symlinks: false, show_symlinks: false, }) ); } #[test] fn deduce_file_filter_only_dirs() { assert_eq!( FileFilter::deduce(&mock_cli(vec!["--only-dirs"]), false), Ok(FileFilter { flags: vec![FileFilterFlags::OnlyDirs], sort_field: SortField::default(), dot_filter: DotFilter::JustFiles, ignore_patterns: IgnorePatterns::empty(), git_ignore: GitIgnore::Off, no_symlinks: false, show_symlinks: false, }) ); } #[test] fn deduce_file_filter_only_files() { assert_eq!( FileFilter::deduce(&mock_cli(vec!["--only-files"]), false), Ok(FileFilter { flags: vec![FileFilterFlags::OnlyFiles], sort_field: SortField::default(), dot_filter: DotFilter::JustFiles, ignore_patterns: IgnorePatterns::empty(), git_ignore: GitIgnore::Off, no_symlinks: false, show_symlinks: false, }) ); } } ================================================ FILE: src/options/mod.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT //! Parsing command-line strings into exa options. //! //! This module imports exa’s configuration types, such as `View` (the details //! of displaying multiple files) and `DirAction` (what to do when encountering //! a directory), and implements `deduce` methods on them so they can be //! configured using command-line options. //! //! //! ## Useless and overridden options //! //! Let’s say exa was invoked with just one argument: `exa --inode`. The //! `--inode` option is used in the details view, where it adds the inode //! column to the output. But because the details view is *only* activated with //! the `--long` argument, adding `--inode` without it would not have any //! effect. //! //! For a long time, exa’s philosophy was that the user should be warned //! whenever they could be mistaken like this. If you tell exa to display the //! inode, and it *doesn’t* display the inode, isn’t that more annoying than //! having it throw an error back at you? //! //! However, this doesn’t take into account *configuration*. Say a user wants //! to configure exa so that it lists inodes in the details view, but otherwise //! functions normally. A common way to do this for command-line programs is to //! define a shell alias that specifies the details they want to use every //! time. For the inode column, the alias would be: //! //! `alias exa="exa --inode"` //! //! Using this alias means that although the inode column will be shown in the //! details view, you’re now *only* allowed to use the details view, as any //! other view type will result in an error. Oops! //! //! Another example is when an option is specified twice, such as `exa //! --sort=Name --sort=size`. Did the user change their mind about sorting, and //! accidentally specify the option twice? //! //! Again, exa rejected this case, throwing an error back to the user instead //! of trying to guess how they want their output sorted. And again, this //! doesn’t take into account aliases being used to set defaults. A user who //! wants their files to be sorted case-insensitively may configure their shell //! with the following: //! //! `alias exa="exa --sort=Name"` //! //! Just like the earlier example, the user now can’t use any other sort order, //! because exa refuses to guess which one they meant. It’s *more* annoying to //! have to go back and edit the command than if there were no error. //! //! Fortunately, there’s a heuristic for telling which options came from an //! alias and which came from the actual command-line: aliased options are //! nearer the beginning of the options array, and command-line options are //! nearer the end. This means that after the options have been parsed, exa //! needs to traverse them *backwards* to find the last-most-specified one. //! //! For example, invoking exa with `exa --sort=size` when that alias is present //! would result in a full command-line of: //! //! `exa --sort=Name --sort=size` //! //! `--sort=size` should override `--sort=Name` because it’s closer to the end //! of the arguments array. In fact, because there’s no way to tell where the //! arguments came from — it’s just a heuristic — this will still work even //! if no aliases are being used! //! //! Finally, this isn’t just useful when options could override each other. //! Creating an alias `exal="exa --long --inode --header"` then invoking `exal //! --grid --long` shouldn’t complain about `--long` being given twice when //! it’s clear what the user wants. use clap::ArgMatches; use crate::fs::dir_action::DirAction; use crate::fs::filter::{FileFilter, GitIgnore}; use crate::options::stdin::FilesInput; use crate::output::{Mode, View, details, grid_details}; use crate::theme::Options as ThemeOptions; mod dir_action; mod error; mod file_name; mod filter; #[rustfmt::skip] // this module becomes unreadable with rustfmt mod theme; mod view; pub use self::error::{NumberSource, OptionsError}; pub mod parser; pub mod vars; pub use self::vars::Vars; pub mod config; pub mod stdin; /// These **options** represent a parsed, error-checked versions of the /// user’s command-line options. #[derive(Debug)] pub struct Options { /// The action to perform when encountering a directory rather than a /// regular file. pub dir_action: DirAction, /// How to sort and filter files before outputting them. pub filter: FileFilter, /// The user’s preference of view to use (lines, grid, details, or /// grid-details) along with the options on how to render file names. /// If the view requires the terminal to have a width, and there is no /// width, then the view will be downgraded. pub view: View, /// The options to make up the styles of the UI and file names. pub theme: ThemeOptions, /// Whether to read file names from stdin instead of the command-line pub stdin: FilesInput, } impl Options { /// Whether the View specified in this set of options includes a Git /// status column. It’s only worth trying to discover a repository if the /// results will end up being displayed. #[must_use] pub fn should_scan_for_git(&self) -> bool { if self.filter.git_ignore == GitIgnore::CheckAndIgnore { return true; } match self.view.mode { Mode::Details(details::Options { table: Some(ref table), .. }) | Mode::GridDetails(grid_details::Options { details: details::Options { table: Some(ref table), .. }, .. }) => table.columns.git, _ => false, } } /// Determines the complete set of options based on the given command-line /// arguments, after they’ve been parsed. pub fn deduce(matches: &ArgMatches, vars: &V) -> Result { if cfg!(not(feature = "git")) && (matches.get_flag("git") || matches.get_flag("git-ignore")) { return Err(OptionsError::Unsupported(String::from( "Options --git and --git-ignore can't be used because `git` feature was disabled in this build of exa", ))); } let strict = vars .get_with_fallback(vars::EXA_STRICT, vars::EZA_STRICT) .is_some(); let view = View::deduce(matches, vars, strict)?; let dir_action = DirAction::deduce(matches, matches!(view.mode, Mode::Details(_)), strict)?; let filter = FileFilter::deduce(matches, strict)?; let theme = ThemeOptions::deduce(matches, vars); let stdin = FilesInput::deduce(matches, vars); Ok(Self { dir_action, filter, view, theme, stdin, }) } } ================================================ FILE: src/options/parser.rs ================================================ // SPDX-FileCopyrightText: 2024 Christina Sørensen // SPDX-License-Identifier: EUPL-1.2 // // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors // SPDX-FileCopyrightText: 2014 Benjamin Sago // SPDX-License-Identifier: MIT use std::ffi::OsString; use clap::{Error, ValueEnum, arg, builder::PossibleValue, value_parser}; use crate::{ fs::filter::{SortCase, SortField}, output::{file_name::Absolute, time::TimeFormat}, }; const SORT_FIELDS_HELP: &str = "[default: name] [possible values: name, Name, .name, .Name, ext, ext, created, date, age, accessed, changed, size, inode, type, none]"; const TIME_FIELDS_HELP: &str = "[possible values: mod|modified, acc|accessed, ch|changed, cr|created]"; const FORMAT_STYLE_FIELDS_HELP: &str = "[possible values: default, iso, long-iso, full-iso, relative, \"+\"]"; pub fn get_command() -> clap::Command { clap::Command::new(clap::crate_name!()) .author(clap::crate_authors!()) .about(clap::crate_description!()) .version(include_str!(concat!(env!("OUT_DIR"), "/version_string.txt"))) .disable_help_flag(true) .disable_version_flag(true) .args_override_self(true) .arg(arg!([FILE]...).value_parser(clap::value_parser!(OsString)).hide_short_help(true)) .next_help_heading("META OPTIONS") .arg(arg!(--stdin "read file names from stdin")) .arg(arg!(-'?' --help "Print help").action(clap::ArgAction::HelpShort)) .arg(arg!(-v --version "Print help").action(clap::ArgAction::Version)) .next_help_heading("LAYOUT OPTIONS") .arg(arg!(-'1' --oneline "display one entry per line")) .arg(arg!(-l --long "display extended file metadata as a table")) .arg(arg!(-G --grid "display entries as a grid (default)")) .arg(arg!(-x --across "sort the grid across, rather than downwards")) .arg(arg!(-R --recurse "recurse into directories")) .arg(arg!(-T --tree "recurse into directories as a tree")) .arg(arg!(-L --level "limit the depth of recursion") .value_parser(value_parser!(usize))) .arg(arg!(--"follow-symlinks" "drill down into symbolic links that point to directories")) .arg(arg!(-w --width "set screen width in columns") .value_parser(value_parser!(usize))) .next_help_heading("DISPLAY OPTIONS") .arg(arg!(-F --classify "display type indicator by file names") .num_args(0..=1) .value_parser(value_parser!(ShowWhen)) .default_missing_value("auto")) .arg(arg!(-X --dereference "dereference symbolic links when displaying information")) .arg(arg!(--absolute "display entries with their absolute path") .num_args(0..=1) .action(clap::ArgAction::Set) .value_parser(value_parser!(Absolute)) .default_missing_value("on") .default_value("off") .hide_default_value(true)) .arg(arg!(--color "When to use colours.") .alias("colour") .num_args(0..=1) .value_parser(value_parser!(ShowWhen)) .default_missing_value("auto") .default_value("auto")) .arg(arg!(--"color-scale" "highlight value of FIELDS distinctly") .num_args(0..) .value_parser(value_parser!(ColorScaleArgs)) .default_missing_value("all") .value_delimiter(',')) .arg(arg!(--"color-scale-mode" "mode for --color-scale") .num_args(1) .value_parser(value_parser!(ColorScaleModeArgs)) .default_value("gradient")) .arg(arg!(--icons "when to display icons") .num_args(0..=1) .value_parser(value_parser!(ShowWhen)) .default_missing_value("auto")) .arg(arg!(--hyperlink "display entries as hyperlinks")) .arg(arg!(--"no-quotes" "don't quote file names with spaces")) .next_help_heading("FILTERING OPTIONS") .arg(arg!(-a --all... "show hidden files. Use this twice to also show the '.' and '..' directories")) .arg(arg!(-A --"almost-all" "equivalent to --all; included for compatibility with `ls -A`")) .arg(arg!(-d --"treat-dirs-as-files" "treat directories as files; don't list their contents") .alias("list-dirs") // TODO: compat alias to remove (above flag published in v0.23.4 / 2025-10-03) .conflicts_with_all(["recurse", "tree"])) .arg(arg!(-D --"only-dirs" "list only directories")) .arg(arg!(-f --"only-files" "list only files")) .arg(arg!(--"show-symlinks" "explicitly show symbolic links (with --only-dirs and --only-files)")) .arg(arg!(--"no-symlinks" "do not show symbolic links")) .arg(arg!(-I --"ignore-glob" "glob patterns (pipe-separated) of files to ignore")) .arg(arg!(--"git-ignore" "ignore files mentioned in '.gitignore'")) .next_help_heading("SORTING OPTIONS") .arg(arg!(--"group-directories-first" "list directories before other files").id("dirs-first")) .arg(arg!(--"group-directories-last" "list directories after other files").id("dirs-last")) .arg(arg!(-s --sort ) .help(format!("which field to sort by {SORT_FIELDS_HELP}")) .value_parser(value_parser!(SortField)) .default_value("name") .hide_default_value(true) .hide_possible_values(true)) .arg(arg!(-r --reverse "reverse the sort order")) .next_help_heading("LONG VIEW OPTIONS") .arg(arg!(-h --header "add a header row to each column")) .arg(arg!(-i --inode "list each file's inode number")) .arg(arg!(-o --"octal-permissions" "list each file's permission in octal format")) .arg(arg!(-H --links "list each file's number of hard links")) .arg(arg!(-b --binary "show file sizes with binary prefixes")) .arg(arg!(-B --bytes "show file sizes in bytes, without any prefixes")) .arg(arg!(--"total-size" "show the size of a directory as the one of its content (unix only)")) .arg(arg!(-S --blocksize "list size of allocated file system blocks")) .arg(arg!(-g --group "list each file's group")) .arg(arg!(--"smart-group" "only show group if it has a different name from owner")) .arg(arg!(-n --numeric "show user and group as their numeric IDs")) .arg(arg!(-t --time ).help(format!("which timestamp field to show {TIME_FIELDS_HELP}")) .value_parser(value_parser!(TimeArgs)) .conflicts_with_all(["modified", "accessed", "changed", "created"]) .hide_possible_values(true)) .arg(arg!(-m --modified "show the modified timestamp field (replace default field, combinable)")) .arg(arg!(-u --accessed "show the accessed timestamp field (replace default field, combinable)")) .arg(arg!(--changed "show the changed timestamp field (replace default field, combinable)")) .arg(arg!(-U --created "show the created timestamp field (replace default field, combinable)")) .arg(arg!(--"time-style"