├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE └── workflows │ └── CICD.yml ├── .gitignore ├── .release.toml ├── CODEOWNERS ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── OWNERS ├── README.md ├── build.rs ├── ci ├── before_deploy.bash ├── before_install.bash └── script.bash ├── doc └── lsd.md ├── lsd.spec ├── rustfmt.toml ├── src ├── app.rs ├── color.rs ├── config_file.rs ├── core.rs ├── display.rs ├── flags.rs ├── flags │ ├── blocks.rs │ ├── color.rs │ ├── date.rs │ ├── dereference.rs │ ├── display.rs │ ├── header.rs │ ├── hyperlink.rs │ ├── icons.rs │ ├── ignore_globs.rs │ ├── indicators.rs │ ├── layout.rs │ ├── literal.rs │ ├── permission.rs │ ├── recursion.rs │ ├── size.rs │ ├── sorting.rs │ ├── symlink_arrow.rs │ ├── symlinks.rs │ ├── total_size.rs │ └── truncate_owner.rs ├── git.rs ├── git_theme.rs ├── icon.rs ├── main.rs ├── meta │ ├── access_control.rs │ ├── date.rs │ ├── filetype.rs │ ├── git_file_status.rs │ ├── indicator.rs │ ├── inode.rs │ ├── links.rs │ ├── locale.rs │ ├── mod.rs │ ├── name.rs │ ├── owner.rs │ ├── permissions.rs │ ├── permissions_or_attributes.rs │ ├── size.rs │ ├── symlink.rs │ ├── windows_attributes.rs │ └── windows_utils.rs ├── sort.rs ├── theme.rs └── theme │ ├── color.rs │ ├── git.rs │ └── icon.rs └── tests └── integration.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: zwpaper 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: kweizh 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | custom: # Replace with a single custom sponsorship URL 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report Form 2 | description: Create a report to help us improve, by the new GitHub form 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | assignees: 6 | - zwpaper 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this bug report! 12 | - type: checkboxes 13 | id: latest-version 14 | attributes: 15 | label: Version 16 | description: Please make sure you can reproduce in the [latest release](https://github.com/lsd-rs/lsd/releases/latest) 17 | options: 18 | - label: latest 19 | required: true 20 | - type: textarea 21 | id: version 22 | attributes: 23 | label: version 24 | description: "`lsd --version` output" 25 | placeholder: lsd --version 26 | validations: 27 | required: true 28 | - type: dropdown 29 | id: os 30 | attributes: 31 | label: What OS are you seeing the problem on? 32 | multiple: true 33 | options: 34 | - Windows 35 | - Linux 36 | - macOS 37 | - Others 38 | - type: textarea 39 | id: installation 40 | attributes: 41 | label: installation 42 | description: "how do you install lsd?" 43 | placeholder: "how do you install lsd?" 44 | validations: 45 | required: true 46 | - type: textarea 47 | id: term 48 | attributes: 49 | label: term 50 | description: "`echo $TERM` output" 51 | placeholder: echo $TERM 52 | validations: 53 | required: false 54 | - type: textarea 55 | id: ls-colors 56 | attributes: 57 | label: ls-colors 58 | description: "`echo $LS_COLORS` output" 59 | placeholder: echo $LS_COLORS 60 | validations: 61 | required: false 62 | - type: textarea 63 | id: what-happened 64 | attributes: 65 | label: What happened? 66 | description: Tell us what happen? 67 | placeholder: | 68 | If applicable, add the output of the classic ls command (`\ls -la`) in order to show the buggy file/directory. 69 | render: markdown 70 | validations: 71 | required: true 72 | - type: textarea 73 | id: what-expected 74 | attributes: 75 | label: What expected? 76 | description: What did you expect to happen? 77 | placeholder: | 78 | If the application panics run the command with the trace (`RUST_BACKTRACE=1 lsd ...`). 79 | In case of graphical errors, add a screenshot if possible." 80 | render: markdown 81 | validations: 82 | required: true 83 | - type: textarea 84 | id: others 85 | attributes: 86 | label: What else? 87 | description: Is there anything else you want to tell us? 88 | placeholder: "Others" 89 | render: markdown 90 | validations: 91 | required: false 92 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | - os: 11 | - `lsd --version`: 12 | - `echo $TERM`: 13 | - `echo $LS_COLORS`: 14 | 15 | ## Expected behavior 16 | If applicable, add the output of the classic ls command (`\ls -la`) in order to show the buggy file/directory. 17 | 18 | ## Actual behavior 19 | If the application panics run the command with the trace (`RUST_BACKTRACE=1 lsd ...`). 20 | In case of graphical errors, add a screenshot if possible. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | --- 5 | #### TODO 6 | 7 | - [ ] Use `cargo fmt` 8 | - [ ] Add necessary tests 9 | - [ ] Update default config/theme in README (if applicable) 10 | - [ ] Update man page at lsd/doc/lsd.md (if applicable) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | out.md 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /.release.toml: -------------------------------------------------------------------------------- 1 | sign-commit = true 2 | sign-tag = true 3 | dev-version = false 4 | pre-release-commit-message = "Release {{version}}" 5 | tag-prefix = "" 6 | tag-name = "{{version}}" 7 | pre-release-replacements = [ 8 | {file="CHANGELOG.md", search="## \\[Unreleased\\]", replace="## [Unreleased]\n\n## [{{version}}] - {{date}}"}, 9 | {file="CHANGELOG.md", search="HEAD", replace="{{version}}"}, 10 | {file="CHANGELOG.md", search="\\[Unreleased\\]:", replace="[Unreleased]: https://github.com/Peltoche/lsd/compare/{{version}}...HEAD\n[{{version}}]: "}, 11 | {file="README.md", search="lsd_[0-9\\.]+_amd64.deb", replace="lsd_{{version}}_amd64.deb"}, 12 | ] 13 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @zwpaper 2 | 3 | # Retired 4 | # @meain @Peltoche 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Peltoche "] 3 | build = "build.rs" 4 | categories = ["command-line-utilities"] 5 | description = "An ls command with a lot of pretty colors and some other stuff." 6 | keywords = ["ls"] 7 | license = "Apache-2.0" 8 | name = "lsd" 9 | readme = "./README.md" 10 | repository = "https://github.com/lsd-rs/lsd" 11 | version = "1.1.5" 12 | edition = "2021" 13 | rust-version = "1.74" 14 | 15 | [[bin]] 16 | name = "lsd" 17 | path = "src/main.rs" 18 | 19 | [build-dependencies] 20 | clap = { version = "4.3.*", features = ["derive"] } 21 | clap_complete = "4.3" 22 | version_check = "0.9.*" 23 | 24 | [dependencies] 25 | crossterm = { version = "0.27.0", features = ["serde"] } 26 | dirs = "5" 27 | libc = "0.2.*" 28 | human-sort = "0.2.2" 29 | # should stick to 0.1, the 0.2 needs some adaptation 30 | # check https://github.com/lsd-rs/lsd/issues/1014 31 | term_grid = "0.1" 32 | terminal_size = "0.3" 33 | thiserror = "1.0" 34 | sys-locale = "0.3" 35 | once_cell = "1.17.1" 36 | chrono = { version = "0.4.19", features = ["unstable-locales"] } 37 | chrono-humanize = "0.2" 38 | # incompatible with v0.1.11 39 | unicode-width = "0.1.13" 40 | lscolors = "0.16.0" 41 | wild = "2.0" 42 | globset = "0.4.*" 43 | yaml-rust = "0.4.*" 44 | serde = { version = "1.0", features = ["derive"] } 45 | serde_yaml = "0.9" 46 | url = "2.5.4" 47 | vsort = "0.2" 48 | xdg = "2.5" 49 | 50 | [target."cfg(not(all(windows, target_arch = \"x86\", target_env = \"gnu\")))".dependencies] 51 | # if ssl feature is enabled compilation will fail on arm-unknown-linux-gnueabihf and i686-pc-windows-gnu 52 | git2 = { version = "0.18", optional = true, default-features = false } 53 | 54 | [target.'cfg(unix)'.dependencies] 55 | users = { version = "0.11.3", package = "uzers" } 56 | xattr = "1" 57 | 58 | [target.'cfg(windows)'.dependencies] 59 | windows = { version = "0.43.0", features = ["Win32_Foundation", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Memory"] } 60 | 61 | [dependencies.clap] 62 | features = ["derive", "wrap_help"] 63 | version = "4.3.*" 64 | 65 | [dev-dependencies] 66 | assert_cmd = "2" 67 | assert_fs = "1" 68 | predicates = "3" 69 | tempfile = "3" 70 | serial_test = "2.0" 71 | 72 | [features] 73 | default = ["git2"] 74 | sudo = [] 75 | no-git = [] # force disabling git even if available by default 76 | 77 | [profile.release] 78 | lto = true 79 | codegen-units = 1 80 | strip = true 81 | debug = false 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright [yyyy] [name of copyright owner] 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | approvers: 4 | - zwpaper 5 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 fd developers 2 | // Licensed under the Apache License, Version 2.0 3 | // 4 | // or the MIT license , 5 | // at your option. All files in the project carrying such 6 | // notice may not be copied, modified, or distributed except 7 | // according to those terms. 8 | 9 | use clap::CommandFactory; 10 | use clap_complete::generate_to; 11 | use clap_complete::shells::*; 12 | use std::fs; 13 | use std::process::exit; 14 | 15 | include!("src/app.rs"); 16 | 17 | fn main() { 18 | let outdir = std::env::var_os("SHELL_COMPLETIONS_DIR") 19 | .or_else(|| std::env::var_os("OUT_DIR")) 20 | .unwrap_or_else(|| exit(0)); 21 | 22 | fs::create_dir_all(&outdir).unwrap(); 23 | 24 | let mut app = Cli::command(); 25 | let bin_name = "lsd"; 26 | generate_to(Bash, &mut app, bin_name, &outdir).expect("Failed to generate Bash completions"); 27 | generate_to(Fish, &mut app, bin_name, &outdir).expect("Failed to generate Fish completions"); 28 | generate_to(Zsh, &mut app, bin_name, &outdir).expect("Failed to generate Zsh completions"); 29 | generate_to(PowerShell, &mut app, bin_name, &outdir) 30 | .expect("Failed to generate PowerShell completions"); 31 | 32 | // Disable git feature for these target where git2 is not well supported 33 | if !std::env::var("CARGO_FEATURE_GIT2") 34 | .map(|flag| flag == "1") 35 | .unwrap_or(false) 36 | || std::env::var("TARGET") 37 | .map(|target| target == "i686-pc-windows-gnu") 38 | .unwrap_or(false) 39 | { 40 | println!(r#"cargo:rustc-cfg=feature="no-git""#); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ci/before_deploy.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Building and packaging for release 3 | 4 | set -ex 5 | 6 | build() { 7 | cargo build --target "$TARGET" --features="$FEATURES" --release --verbose 8 | } 9 | 10 | pack() { 11 | local tempdir 12 | local out_dir 13 | local package_name 14 | local gcc_prefix 15 | 16 | tempdir=$(mktemp -d 2>/dev/null || mktemp -d -t tmp) 17 | out_dir=$(pwd) 18 | package_name="$PROJECT_NAME-$TRAVIS_TAG-$TARGET" 19 | 20 | if [[ $TARGET == arm-unknown-linux-* ]]; then 21 | gcc_prefix="arm-linux-gnueabihf-" 22 | else 23 | gcc_prefix="" 24 | fi 25 | 26 | # create a "staging" directory 27 | mkdir "$tempdir/$package_name" 28 | mkdir "$tempdir/$package_name/autocomplete" 29 | 30 | # copying the main binary 31 | cp "target/$TARGET/release/$PROJECT_NAME" "$tempdir/$package_name/" 32 | "${gcc_prefix}"strip "$tempdir/$package_name/$PROJECT_NAME" 33 | 34 | # manpage, readme and license 35 | cp README.md "$tempdir/$package_name" 36 | cp LICENSE "$tempdir/$package_name" 37 | 38 | # various autocomplete 39 | cp target/"$TARGET"/release/build/"$PROJECT_NAME"-*/out/"$PROJECT_NAME".bash "$tempdir/$package_name/autocomplete/${PROJECT_NAME}.bash-completion" 40 | cp target/"$TARGET"/release/build/"$PROJECT_NAME"-*/out/"$PROJECT_NAME".fish "$tempdir/$package_name/autocomplete" 41 | cp target/"$TARGET"/release/build/"$PROJECT_NAME"-*/out/_"$PROJECT_NAME" "$tempdir/$package_name/autocomplete" 42 | 43 | # archiving 44 | pushd "$tempdir" 45 | tar czf "$out_dir/$package_name.tar.gz" "$package_name"/* 46 | popd 47 | rm -r "$tempdir" 48 | } 49 | 50 | make_deb() { 51 | local tempdir 52 | local architecture 53 | local version 54 | local dpkgname 55 | local conflictname 56 | 57 | case $TARGET in 58 | x86_64*) 59 | architecture=amd64 60 | ;; 61 | i686*) 62 | architecture=i386 63 | ;; 64 | *) 65 | echo "make_deb: skipping target '${TARGET}'" >&2 66 | return 0 67 | ;; 68 | esac 69 | version=${TRAVIS_TAG#v} 70 | if [[ $TARGET = *musl* ]]; then 71 | dpkgname=$PROJECT_NAME-musl 72 | conflictname=$PROJECT_NAME 73 | else 74 | dpkgname=$PROJECT_NAME 75 | conflictname=$PROJECT_NAME-musl 76 | fi 77 | 78 | tempdir=$(mktemp -d 2>/dev/null || mktemp -d -t tmp) 79 | 80 | # copy the main binary 81 | install -Dm755 "target/$TARGET/release/$PROJECT_NAME" "$tempdir/usr/bin/$PROJECT_NAME" 82 | strip "$tempdir/usr/bin/$PROJECT_NAME" 83 | 84 | # readme and license 85 | install -Dm644 README.md "$tempdir/usr/share/doc/$PROJECT_NAME/README.md" 86 | install -Dm644 LICENSE "$tempdir/usr/share/doc/$PROJECT_NAME/LICENSE" 87 | 88 | # completions 89 | install -Dm644 target/$TARGET/release/build/$PROJECT_NAME-*/out/$PROJECT_NAME.bash "$tempdir/usr/share/bash-completion/completions/${PROJECT_NAME}" 90 | install -Dm644 target/$TARGET/release/build/$PROJECT_NAME-*/out/$PROJECT_NAME.fish "$tempdir/usr/share/fish/completions/$PROJECT_NAME.fish" 91 | install -Dm644 target/$TARGET/release/build/$PROJECT_NAME-*/out/_$PROJECT_NAME "$tempdir/usr/share/zsh/vendor-completions/_$PROJECT_NAME" 92 | 93 | # Control file 94 | mkdir "$tempdir/DEBIAN" 95 | cat > "$tempdir/DEBIAN/control" < 101 | Architecture: $architecture 102 | Provides: $PROJECT_NAME 103 | Conflicts: $conflictname 104 | Description: A ls command with a lot of pretty colors. 105 | EOF 106 | 107 | fakeroot dpkg-deb --build "$tempdir" "${dpkgname}_${version}_${architecture}.deb" 108 | } 109 | 110 | 111 | main() { 112 | build 113 | pack 114 | if [[ $TARGET = *linux* ]]; then 115 | make_deb 116 | fi 117 | } 118 | 119 | main 120 | -------------------------------------------------------------------------------- /ci/before_install.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | if [ "$TRAVIS_OS_NAME" != linux ]; then 6 | exit 0 7 | fi 8 | 9 | sudo apt-get update 10 | 11 | # needed to build deb packages 12 | sudo apt-get install -y fakeroot 13 | 14 | # needed for i686 linux gnu target 15 | if [[ $TARGET == i686-unknown-linux-gnu ]]; then 16 | sudo apt-get install -y gcc-multilib 17 | fi 18 | 19 | # needed for cross-compiling for arm 20 | if [[ $TARGET == arm-unknown-linux-* ]]; then 21 | sudo apt-get install -y \ 22 | gcc-4.8-arm-linux-gnueabihf \ 23 | binutils-arm-linux-gnueabihf \ 24 | libc6-armhf-cross \ 25 | libc6-dev-armhf-cross 26 | fi 27 | -------------------------------------------------------------------------------- /ci/script.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | # Incorporate TARGET env var to the build and test process 6 | cargo build --target "$TARGET" --verbose 7 | 8 | # We cannot run arm executables on linux 9 | if [[ $TARGET != arm-unknown-linux-* ]]; then 10 | cargo test --target "$TARGET" --verbose 11 | fi 12 | -------------------------------------------------------------------------------- /doc/lsd.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: lsd 3 | section: 1 4 | header: User Manual 5 | footer: lsd 6 | date: 7 | --- 8 | 9 | # NAME 10 | 11 | lsd - LSDeluxe 12 | 13 | # SYNOPSIS 14 | 15 | `lsd [FLAGS] [OPTIONS] [--] [FILE]...` 16 | 17 | # DESCRIPTION 18 | 19 | lsd is a ls command with a lot of pretty colours and some other stuff to enrich and enhance the directory listing experience. 20 | 21 | # OPTIONS 22 | 23 | `-a`, `--all` 24 | : Do not ignore entries starting with **.** 25 | 26 | `-A`, `--almost-all` 27 | : Do not list implied **.** and **..** 28 | 29 | `--classic` 30 | : Enable classic mode (no colours or icons) 31 | 32 | `-L`, `--dereference` 33 | : When showing file information for a symbolic link, show information for the file the link references rather than for the link itself 34 | 35 | `-d`, `--directory-only` 36 | : Display directories themselves, and not their contents (recursively when used with --tree) 37 | 38 | `-X`, `--extensionsort` 39 | : Sort by file extension 40 | 41 | `--git` 42 | : Display git status. Directory git status is a reduction of included file statuses (recursively). 43 | 44 | `--help` 45 | : Prints help information 46 | 47 | `-h`, `--human-readable` 48 | : For ls compatibility purposes ONLY, currently set by default 49 | 50 | `--ignore-config` 51 | : Ignore the configuration file 52 | 53 | `--config-file ` 54 | : Provide the config file from a custom location 55 | 56 | `-F`, `--classify` 57 | : Append indicator (one of \*/=>@|) at the end of the file names 58 | 59 | `-i`, `--inode` 60 | : Display the index number of each file 61 | 62 | `-l`, `--long` 63 | : Display extended file metadata as a table 64 | 65 | `--no-symlink` 66 | : Do not display symlink target 67 | 68 | `-1`, `--oneline` 69 | : Display one entry per line 70 | 71 | `-R`, `--recursive` 72 | : Recurse into directories 73 | 74 | `-r`, `--reverse` 75 | : Reverse the order of the sort 76 | 77 | `-S`, `--sizesort` 78 | : Sort by size 79 | 80 | `-t`, `--timesort` 81 | : Sort by time modified 82 | 83 | `--total-size` 84 | : Display the total size of directories 85 | 86 | `--tree` 87 | : Recurse into directories and present the result as a tree 88 | 89 | `-V`, `--version` 90 | : Prints version information 91 | 92 | `-v`, `--versionsort` 93 | : Natural sort of (version) numbers within text 94 | 95 | `--blocks ...` 96 | : Specify the blocks that will be displayed and in what order [possible values: permission, user, group, size, date, name, inode, git] 97 | 98 | `--color ...` 99 | : When to use terminal colours [default: auto] [possible values: always, auto, never] 100 | 101 | `--date ...` 102 | : How to display date [possible values: date, locale, relative, +date-time-format] [default: date] 103 | 104 | `--depth ...` 105 | : Stop recursing into directories after reaching specified depth 106 | 107 | `--group-dirs ...` 108 | : Sort the directories then the files [default: none] [possible values: none, first, last] 109 | 110 | `--group-directories-first` 111 | : Groups the directories at the top before the files. Same as `--group-dirs=first` 112 | 113 | `--hyperlink ...` 114 | : Attach hyperlink to filenames [default: never] [possible values: always, auto, never] 115 | 116 | `--icon ...` 117 | : When to print the icons [default: auto] [possible values: always, auto, never] 118 | 119 | `--icon-theme ...` 120 | : Whether to use fancy or unicode icons [default: fancy] [possible values: fancy, unicode] 121 | 122 | `-I, --ignore-glob ...` 123 | : Do not display files/directories with names matching the glob pattern(s). More than one can be specified by repeating the argument [default: ] 124 | 125 | `--permission ...` 126 | : How to display permissions [default: rwx for linux, attributes for windows] [possible values: rwx, octal, attributes, disable] 127 | 128 | `--size ...` 129 | : How to display size [default: default] [possible values: default, short, bytes] 130 | 131 | `--sort ...` 132 | : Sort by WORD instead of name [possible values: size, time, version, extension, git] 133 | 134 | `-U`, `--no-sort` 135 | : Do not sort. List entries in directory order 136 | 137 | `-Z` `--context` 138 | : Display SELinux or SMACK security context 139 | 140 | `--header` 141 | : Display block headers 142 | 143 | `-N --literal` 144 | : Print entry names without quoting 145 | 146 | `--truncate-owner-after` 147 | : Truncate the user and group names if they exceed a certain number of characters 148 | 149 | `--truncate-owner-marker` 150 | : Truncation marker appended to a truncated user or group name 151 | 152 | # ARGS 153 | 154 | `...` 155 | : A file or directory to list [default: .] 156 | 157 | # EXAMPLES 158 | 159 | `lsd` 160 | : Display listing for current directory 161 | 162 | `lsd /etc` 163 | : Display listing of /etc 164 | 165 | `lsd -la` 166 | : Display listing of current directory, including files starting with `.` and the current directory's entry. 167 | 168 | # ENVIRONMENT 169 | 170 | `LS_COLORS` 171 | : Used to determine color for displaying filenames. See **dir_colors**. 172 | 173 | `XDG_CONFIG_HOME` 174 | : Used to locate optional config file. If `XDG_CONFIG_HOME` is set, use `$XDG_CONFIG_HOME/lsd/config.yaml` else `$HOME/.config/lsd/config.yaml`. 175 | 176 | `SHELL_COMPLETIONS_DIR` or `OUT_DIR` 177 | : Used to specify the directory for generating a shell completions file. If neither are set, no completions file will be generated. The directory will be created if it does not exist. 178 | -------------------------------------------------------------------------------- /lsd.spec: -------------------------------------------------------------------------------- 1 | Name: lsd 2 | Version: 1.1.5 3 | Release: 1%{?dist} 4 | Summary: The next gen ls command 5 | 6 | License: MIT 7 | URL: https://github.com/lsd-rs/lsd 8 | Source0: https://github.com/lsd-rs/lsd/archive/refs/tags/v%{version}.tar.gz 9 | 10 | BuildRequires: rust 11 | BuildRequires: cargo 12 | 13 | %description 14 | This project is a rewrite of GNU ls with lots of added features like colors, icons, tree-view, more formatting options etc. The project is heavily inspired by the super colorls project. 15 | 16 | %global debug_package %{nil} 17 | 18 | %prep 19 | %setup -q 20 | 21 | %build 22 | cargo build --release 23 | 24 | %install 25 | %global _build_id_links none 26 | mkdir -p %{buildroot}/%{_bindir} 27 | # upx "target/release/lsd" 28 | install -m 755 target/release/%{name} %{buildroot}/%{_bindir}/%{name} 29 | 30 | %files 31 | %defattr(-,root,root,-) 32 | %{_bindir}/%{name} 33 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # use empty config file to ensure that `rustfmt` will always use 2 | # the default configuration when formatting code 3 | # even if there is a `rustfmt.toml` file in parent directories 4 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{ArgAction, Parser, ValueHint}; 4 | 5 | #[derive(Debug, Parser)] 6 | #[command(about, version, args_override_self = true, disable_help_flag = true)] 7 | pub struct Cli { 8 | #[arg(value_name = "FILE", default_value = ".", value_hint = ValueHint::AnyPath)] 9 | pub inputs: Vec, 10 | 11 | /// Do not ignore entries starting with . . 12 | #[arg(short, long, overrides_with = "almost_all")] 13 | pub all: bool, 14 | 15 | /// Do not list implied . and .. 16 | #[arg(short = 'A', long)] 17 | pub almost_all: bool, 18 | 19 | /// When to use terminal colours [default: auto] 20 | #[arg(long, value_name = "MODE", value_parser = ["always", "auto", "never"])] 21 | pub color: Option, 22 | 23 | /// When to print the icons [default: auto] 24 | #[arg(long, value_name = "MODE", value_parser = ["always", "auto", "never"])] 25 | pub icon: Option, 26 | 27 | /// Whether to use fancy or unicode icons [default: fancy] 28 | #[arg(long, value_name = "THEME", value_parser = ["fancy", "unicode"])] 29 | pub icon_theme: Option, 30 | 31 | /// Append indicator (one of */=>@|) at the end of the file names 32 | #[arg(short = 'F', long = "classify")] 33 | pub indicators: bool, 34 | 35 | /// Display extended file metadata as a table 36 | #[arg(short, long)] 37 | pub long: bool, 38 | 39 | /// Ignore the configuration file 40 | #[arg(long)] 41 | pub ignore_config: bool, 42 | 43 | /// Provide a custom lsd configuration file 44 | #[arg(long, value_name = "PATH")] 45 | pub config_file: Option, 46 | 47 | /// Display one entry per line 48 | #[arg(short = '1', long)] 49 | pub oneline: bool, 50 | 51 | /// Recurse into directories 52 | #[arg(short = 'R', long, conflicts_with = "tree")] 53 | pub recursive: bool, 54 | 55 | /// For ls compatibility purposes ONLY, currently set by default 56 | #[arg(short, long)] 57 | human_readable: bool, 58 | 59 | /// Recurse into directories and present the result as a tree 60 | #[arg(long)] 61 | pub tree: bool, 62 | 63 | /// Stop recursing into directories after reaching specified depth 64 | #[arg(long, value_name = "NUM")] 65 | pub depth: Option, 66 | 67 | /// Display directories themselves, and not their contents (recursively when used with --tree) 68 | #[arg(short, long, conflicts_with = "recursive")] 69 | pub directory_only: bool, 70 | 71 | /// How to display permissions [default: rwx for linux, attributes for windows] 72 | #[arg(long, value_name = "MODE", value_parser = ["rwx", "octal", "attributes", "disable"])] 73 | pub permission: Option, 74 | 75 | /// How to display size [default: default] 76 | #[arg(long, value_name = "MODE", value_parser = ["default", "short", "bytes"])] 77 | pub size: Option, 78 | 79 | /// Display the total size of directories 80 | #[arg(long)] 81 | pub total_size: bool, 82 | 83 | /// How to display date [default: date] [possible values: date, locale, relative, +date-time-format] 84 | #[arg(long, value_parser = validate_date_argument)] 85 | pub date: Option, 86 | 87 | /// Sort by time modified 88 | #[arg(short = 't', long)] 89 | pub timesort: bool, 90 | 91 | /// Sort by size 92 | #[arg(short = 'S', long)] 93 | pub sizesort: bool, 94 | 95 | /// Sort by file extension 96 | #[arg(short = 'X', long)] 97 | pub extensionsort: bool, 98 | 99 | /// Sort by git status 100 | #[arg(short = 'G', long)] 101 | pub gitsort: bool, 102 | 103 | /// Natural sort of (version) numbers within text 104 | #[arg(short = 'v', long)] 105 | pub versionsort: bool, 106 | 107 | /// Sort by TYPE instead of name 108 | #[arg( 109 | long, 110 | value_name = "TYPE", 111 | value_parser = ["size", "time", "version", "extension", "git", "none"], 112 | overrides_with_all = ["timesort", "sizesort", "extensionsort", "versionsort", "gitsort", "no_sort"] 113 | )] 114 | pub sort: Option, 115 | 116 | /// Do not sort. List entries in directory order 117 | #[arg(short = 'U', long, overrides_with_all = ["timesort", "sizesort", "extensionsort", "versionsort", "gitsort", "sort"])] 118 | pub no_sort: bool, 119 | 120 | /// Reverse the order of the sort 121 | #[arg(short, long)] 122 | pub reverse: bool, 123 | 124 | /// Sort the directories then the files 125 | #[arg(long, value_name = "MODE", value_parser = ["none", "first", "last"])] 126 | pub group_dirs: Option, 127 | 128 | /// Groups the directories at the top before the files. Same as --group-dirs=first 129 | #[arg(long)] 130 | pub group_directories_first: bool, 131 | 132 | /// Specify the blocks that will be displayed and in what order 133 | #[arg( 134 | long, 135 | value_delimiter = ',', 136 | value_parser = ["permission", "user", "group", "context", "size", "date", "name", "inode", "links", "git"], 137 | )] 138 | pub blocks: Vec, 139 | 140 | /// Enable classic mode (display output similar to ls) 141 | #[arg(long)] 142 | pub classic: bool, 143 | 144 | /// Do not display symlink target 145 | #[arg(long)] 146 | pub no_symlink: bool, 147 | 148 | /// Do not display files/directories with names matching the glob pattern(s). 149 | /// More than one can be specified by repeating the argument 150 | #[arg(short = 'I', long, value_name = "PATTERN")] 151 | pub ignore_glob: Vec, 152 | 153 | /// Display the index number of each file 154 | #[arg(short, long)] 155 | pub inode: bool, 156 | 157 | /// Show git status on file and directory" 158 | /// Only when used with --long option 159 | #[arg(short, long)] 160 | pub git: bool, 161 | 162 | /// When showing file information for a symbolic link, 163 | /// show information for the file the link references rather than for the link itself 164 | #[arg(short = 'L', long)] 165 | pub dereference: bool, 166 | 167 | /// Print security context (label) of each file 168 | #[arg(short = 'Z', long)] 169 | pub context: bool, 170 | 171 | /// Attach hyperlink to filenames [default: never] 172 | #[arg(long, value_name = "MODE", value_parser = ["always", "auto", "never"])] 173 | pub hyperlink: Option, 174 | 175 | /// Display block headers 176 | #[arg(long)] 177 | pub header: bool, 178 | 179 | /// Truncate the user and group names if they exceed a certain number of characters 180 | #[arg(long, value_name = "NUM")] 181 | pub truncate_owner_after: Option, 182 | 183 | /// Truncation marker appended to a truncated user or group name 184 | #[arg(long, value_name = "STR")] 185 | pub truncate_owner_marker: Option, 186 | 187 | /// Includes files with the windows system protection flag set. 188 | /// This is the same as --all on other platforms 189 | #[arg(long, hide = !cfg!(windows))] 190 | pub system_protected: bool, 191 | 192 | /// Print entry names without quoting 193 | #[arg(short = 'N', long)] 194 | pub literal: bool, 195 | 196 | /// Print help information 197 | #[arg(long, action = ArgAction::Help)] 198 | help: (), 199 | } 200 | 201 | fn validate_date_argument(arg: &str) -> Result { 202 | if arg.starts_with('+') { 203 | validate_time_format(arg) 204 | } else if arg == "date" || arg == "relative" || arg == "locale" { 205 | Result::Ok(arg.to_owned()) 206 | } else { 207 | Result::Err("possible values: date, locale, relative, +date-time-format".to_owned()) 208 | } 209 | } 210 | 211 | pub fn validate_time_format(formatter: &str) -> Result { 212 | let mut chars = formatter.chars(); 213 | loop { 214 | match chars.next() { 215 | Some('%') => match chars.next() { 216 | Some('.') => match chars.next() { 217 | Some('f') => (), 218 | Some(n @ ('3' | '6' | '9')) => match chars.next() { 219 | Some('f') => (), 220 | Some(c) => return Err(format!("invalid format specifier: %.{n}{c}")), 221 | None => return Err("missing format specifier".to_owned()), 222 | }, 223 | Some(c) => return Err(format!("invalid format specifier: %.{c}")), 224 | None => return Err("missing format specifier".to_owned()), 225 | }, 226 | Some(n @ (':' | '#')) => match chars.next() { 227 | Some('z') => (), 228 | Some(c) => return Err(format!("invalid format specifier: %{n}{c}")), 229 | None => return Err("missing format specifier".to_owned()), 230 | }, 231 | Some(n @ ('-' | '_' | '0')) => match chars.next() { 232 | Some( 233 | 'C' | 'd' | 'e' | 'f' | 'G' | 'g' | 'H' | 'I' | 'j' | 'k' | 'l' | 'M' | 'm' 234 | | 'S' | 's' | 'U' | 'u' | 'V' | 'W' | 'w' | 'Y' | 'y', 235 | ) => (), 236 | Some(c) => return Err(format!("invalid format specifier: %{n}{c}")), 237 | None => return Err("missing format specifier".to_owned()), 238 | }, 239 | Some( 240 | 'A' | 'a' | 'B' | 'b' | 'C' | 'c' | 'D' | 'd' | 'e' | 'F' | 'f' | 'G' | 'g' 241 | | 'H' | 'h' | 'I' | 'j' | 'k' | 'l' | 'M' | 'm' | 'n' | 'P' | 'p' | 'R' | 'r' 242 | | 'S' | 's' | 'T' | 't' | 'U' | 'u' | 'V' | 'v' | 'W' | 'w' | 'X' | 'x' | 'Y' 243 | | 'y' | 'Z' | 'z' | '+' | '%', 244 | ) => (), 245 | Some(n @ ('3' | '6' | '9')) => match chars.next() { 246 | Some('f') => (), 247 | Some(c) => return Err(format!("invalid format specifier: %{n}{c}")), 248 | None => return Err("missing format specifier".to_owned()), 249 | }, 250 | Some(c) => return Err(format!("invalid format specifier: %{c}")), 251 | None => return Err("missing format specifier".to_owned()), 252 | }, 253 | None => break, 254 | _ => continue, 255 | } 256 | } 257 | Ok(formatter.to_owned()) 258 | } 259 | 260 | // Wrapper for value_parser to simply remove non supported option (mainly git flag) 261 | // required since value_parser requires impl Into that Vec do not support 262 | // should be located here, since this file is included by build.rs 263 | struct LabelFilter bool, const C: usize>([&'static str; C], Filter); 264 | 265 | impl bool, const C: usize> From> 266 | for clap::builder::ValueParser 267 | { 268 | fn from(label_filter: LabelFilter) -> Self { 269 | let filter = label_filter.1; 270 | let values = label_filter.0.into_iter().filter(|x| filter(x)); 271 | let inner = clap::builder::PossibleValuesParser::from(values); 272 | Self::from(inner) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | use crate::color::Colors; 2 | use crate::display; 3 | use crate::flags::{ 4 | ColorOption, Display, Flags, HyperlinkOption, Layout, Literal, SortOrder, ThemeOption, 5 | }; 6 | use crate::git::GitCache; 7 | use crate::icon::Icons; 8 | 9 | use crate::meta::Meta; 10 | use crate::{print_error, print_output, sort, ExitCode}; 11 | use std::path::PathBuf; 12 | 13 | #[cfg(not(target_os = "windows"))] 14 | use std::io; 15 | #[cfg(not(target_os = "windows"))] 16 | use std::os::unix::io::AsRawFd; 17 | 18 | use crate::flags::blocks::Block; 19 | use crate::git_theme::GitTheme; 20 | #[cfg(target_os = "windows")] 21 | use terminal_size::terminal_size; 22 | 23 | pub struct Core { 24 | flags: Flags, 25 | icons: Icons, 26 | colors: Colors, 27 | git_theme: GitTheme, 28 | sorters: Vec<(SortOrder, sort::SortFn)>, 29 | } 30 | 31 | impl Core { 32 | pub fn new(mut flags: Flags) -> Self { 33 | // Check through libc if stdout is a tty. Unix specific so not on windows. 34 | // Determine color output availability (and initialize color output (for Windows 10)) 35 | #[cfg(not(target_os = "windows"))] 36 | let tty_available = unsafe { libc::isatty(io::stdout().as_raw_fd()) == 1 }; 37 | 38 | #[cfg(not(target_os = "windows"))] 39 | let console_color_ok = true; 40 | 41 | #[cfg(target_os = "windows")] 42 | let tty_available = terminal_size().is_some(); // terminal_size allows us to know if the stdout is a tty or not. 43 | 44 | #[cfg(target_os = "windows")] 45 | let console_color_ok = crossterm::ansi_support::supports_ansi(); 46 | 47 | let color_theme = match (tty_available && console_color_ok, flags.color.when) { 48 | (_, ColorOption::Never) | (false, ColorOption::Auto) => ThemeOption::NoColor, 49 | _ => flags.color.theme.clone(), 50 | }; 51 | 52 | let icon_when = flags.icons.when; 53 | let icon_theme = flags.icons.theme.clone(); 54 | 55 | // TODO: Rework this so that flags passed downstream does not 56 | // have Auto option for any (icon, color, hyperlink). 57 | if matches!(flags.hyperlink, HyperlinkOption::Auto) { 58 | flags.hyperlink = if tty_available { 59 | HyperlinkOption::Always 60 | } else { 61 | HyperlinkOption::Never 62 | } 63 | } 64 | 65 | let icon_separator = flags.icons.separator.0.clone(); 66 | 67 | // The output is not a tty, this means the command is piped. e.g. 68 | // 69 | // lsd -l | less 70 | // 71 | // Most of the programs does not handle correctly the ansi colors 72 | // or require a raw output (like the `wc` command). 73 | if !tty_available { 74 | // we should not overwrite the tree layout 75 | if flags.layout != Layout::Tree { 76 | flags.layout = Layout::OneLine; 77 | } 78 | 79 | flags.literal = Literal(true); 80 | }; 81 | 82 | let sorters = sort::assemble_sorters(&flags); 83 | 84 | Self { 85 | flags, 86 | colors: Colors::new(color_theme), 87 | icons: Icons::new(tty_available, icon_when, icon_theme, icon_separator), 88 | git_theme: GitTheme::new(), 89 | sorters, 90 | } 91 | } 92 | 93 | pub fn run(self, paths: Vec) -> ExitCode { 94 | let (mut meta_list, exit_code) = self.fetch(paths); 95 | 96 | self.sort(&mut meta_list); 97 | self.display(&meta_list); 98 | exit_code 99 | } 100 | 101 | fn fetch(&self, paths: Vec) -> (Vec, ExitCode) { 102 | let mut exit_code = ExitCode::OK; 103 | let mut meta_list = Vec::with_capacity(paths.len()); 104 | let depth = match self.flags.layout { 105 | Layout::Tree { .. } => self.flags.recursion.depth, 106 | _ if self.flags.recursion.enabled => self.flags.recursion.depth, 107 | _ => 1, 108 | }; 109 | 110 | #[cfg(target_os = "windows")] 111 | use crate::config_file; 112 | #[cfg(target_os = "windows")] 113 | let paths: Vec = paths 114 | .into_iter() 115 | .filter_map(config_file::expand_home) 116 | .collect(); 117 | 118 | for path in paths { 119 | let mut meta = 120 | match Meta::from_path(&path, self.flags.dereference.0, self.flags.permission) { 121 | Ok(meta) => meta, 122 | Err(err) => { 123 | print_error!("{}: {}.", path.display(), err); 124 | exit_code.set_if_greater(ExitCode::MajorIssue); 125 | continue; 126 | } 127 | }; 128 | 129 | let cache = if self.flags.blocks.0.contains(&Block::GitStatus) { 130 | Some(GitCache::new(&path)) 131 | } else { 132 | None 133 | }; 134 | 135 | let recurse = 136 | self.flags.layout == Layout::Tree || self.flags.display != Display::DirectoryOnly; 137 | if recurse { 138 | match meta.recurse_into(depth, &self.flags, cache.as_ref()) { 139 | Ok((content, path_exit_code)) => { 140 | meta.content = content; 141 | meta.git_status = cache.and_then(|cache| cache.get(&meta.path, true)); 142 | meta_list.push(meta); 143 | exit_code.set_if_greater(path_exit_code); 144 | } 145 | Err(err) => { 146 | print_error!("lsd: {}: {}\n", path.display(), err); 147 | exit_code.set_if_greater(ExitCode::MinorIssue); 148 | continue; 149 | } 150 | }; 151 | } else { 152 | meta.git_status = cache.and_then(|cache| cache.get(&meta.path, true)); 153 | meta_list.push(meta); 154 | }; 155 | } 156 | // Only calculate the total size of a directory if it will be displayed 157 | if self.flags.total_size.0 && self.flags.blocks.displays_size() { 158 | for meta in &mut meta_list.iter_mut() { 159 | meta.calculate_total_size(); 160 | } 161 | } 162 | 163 | (meta_list, exit_code) 164 | } 165 | 166 | fn sort(&self, metas: &mut Vec) { 167 | metas.sort_unstable_by(|a, b| sort::by_meta(&self.sorters, a, b)); 168 | 169 | for meta in metas { 170 | if let Some(ref mut content) = meta.content { 171 | self.sort(content); 172 | } 173 | } 174 | } 175 | 176 | fn display(&self, metas: &[Meta]) { 177 | let output = if self.flags.layout == Layout::Tree { 178 | display::tree( 179 | metas, 180 | &self.flags, 181 | &self.colors, 182 | &self.icons, 183 | &self.git_theme, 184 | ) 185 | } else { 186 | display::grid( 187 | metas, 188 | &self.flags, 189 | &self.colors, 190 | &self.icons, 191 | &self.git_theme, 192 | ) 193 | }; 194 | 195 | print_output!("{}", output); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/flags.rs: -------------------------------------------------------------------------------- 1 | pub mod blocks; 2 | pub mod color; 3 | pub mod date; 4 | pub mod dereference; 5 | pub mod display; 6 | pub mod header; 7 | pub mod hyperlink; 8 | pub mod icons; 9 | pub mod ignore_globs; 10 | pub mod indicators; 11 | pub mod layout; 12 | pub mod literal; 13 | pub mod permission; 14 | pub mod recursion; 15 | pub mod size; 16 | pub mod sorting; 17 | pub mod symlink_arrow; 18 | pub mod symlinks; 19 | pub mod total_size; 20 | pub mod truncate_owner; 21 | 22 | pub use blocks::Blocks; 23 | pub use color::Color; 24 | pub use color::{ColorOption, ThemeOption}; 25 | pub use date::DateFlag; 26 | pub use dereference::Dereference; 27 | pub use display::Display; 28 | pub use header::Header; 29 | pub use hyperlink::HyperlinkOption; 30 | pub use icons::IconOption; 31 | pub use icons::IconTheme; 32 | pub use icons::Icons; 33 | pub use ignore_globs::IgnoreGlobs; 34 | pub use indicators::Indicators; 35 | pub use layout::Layout; 36 | pub use literal::Literal; 37 | pub use permission::PermissionFlag; 38 | pub use recursion::Recursion; 39 | pub use size::SizeFlag; 40 | pub use sorting::DirGrouping; 41 | pub use sorting::SortColumn; 42 | pub use sorting::SortOrder; 43 | pub use sorting::Sorting; 44 | pub use symlink_arrow::SymlinkArrow; 45 | pub use symlinks::NoSymlink; 46 | pub use total_size::TotalSize; 47 | pub use truncate_owner::TruncateOwner; 48 | 49 | use crate::app::Cli; 50 | use crate::config_file::Config; 51 | 52 | use clap::Error; 53 | 54 | #[cfg(doc)] 55 | use yaml_rust::Yaml; 56 | 57 | /// A struct to hold all set configuration flags for the application. 58 | #[derive(Clone, Debug, Default)] 59 | pub struct Flags { 60 | pub blocks: Blocks, 61 | pub color: Color, 62 | pub date: DateFlag, 63 | pub dereference: Dereference, 64 | pub display: Display, 65 | pub display_indicators: Indicators, 66 | pub icons: Icons, 67 | pub ignore_globs: IgnoreGlobs, 68 | pub layout: Layout, 69 | pub no_symlink: NoSymlink, 70 | pub recursion: Recursion, 71 | pub size: SizeFlag, 72 | pub permission: PermissionFlag, 73 | pub sorting: Sorting, 74 | pub total_size: TotalSize, 75 | pub symlink_arrow: SymlinkArrow, 76 | pub hyperlink: HyperlinkOption, 77 | pub header: Header, 78 | pub literal: Literal, 79 | pub truncate_owner: TruncateOwner, 80 | } 81 | 82 | impl Flags { 83 | /// Set up the `Flags` from either [Cli], a [Config] or its [Default] value. 84 | /// 85 | /// # Errors 86 | /// 87 | /// This can return an [Error], when either the building of the ignore globs or the parsing of 88 | /// the recursion depth parameter fails. 89 | pub fn configure_from(cli: &Cli, config: &Config) -> Result { 90 | Ok(Self { 91 | blocks: Blocks::configure_from(cli, config), 92 | color: Color::configure_from(cli, config), 93 | date: DateFlag::configure_from(cli, config), 94 | dereference: Dereference::configure_from(cli, config), 95 | display: Display::configure_from(cli, config), 96 | layout: Layout::configure_from(cli, config), 97 | size: SizeFlag::configure_from(cli, config), 98 | permission: PermissionFlag::configure_from(cli, config), 99 | display_indicators: Indicators::configure_from(cli, config), 100 | icons: Icons::configure_from(cli, config), 101 | ignore_globs: IgnoreGlobs::configure_from(cli, config)?, 102 | no_symlink: NoSymlink::configure_from(cli, config), 103 | recursion: Recursion::configure_from(cli, config), 104 | sorting: Sorting::configure_from(cli, config), 105 | total_size: TotalSize::configure_from(cli, config), 106 | symlink_arrow: SymlinkArrow::configure_from(cli, config), 107 | hyperlink: HyperlinkOption::configure_from(cli, config), 108 | header: Header::configure_from(cli, config), 109 | literal: Literal::configure_from(cli, config), 110 | truncate_owner: TruncateOwner::configure_from(cli, config), 111 | }) 112 | } 113 | } 114 | 115 | /// A trait to allow a type to be configured by either command line parameters, a configuration 116 | /// file or a [Default] value. 117 | pub trait Configurable 118 | where 119 | T: std::default::Default, 120 | { 121 | /// Returns a value from either [Cli], a [Config], a [Default] or the environment value. 122 | /// The first value that is not [None] is used. The order of precedence for the value used is: 123 | /// - [from_cli](Configurable::from_cli) 124 | /// - [from_environment](Configurable::from_environment) 125 | /// - [from_config](Configurable::from_config) 126 | /// - [Default::default] 127 | /// 128 | /// # Note 129 | /// 130 | /// The configuration file's Yaml is read in any case, to be able to check for errors and print 131 | /// out warnings. 132 | fn configure_from(cli: &Cli, config: &Config) -> T { 133 | if let Some(value) = Self::from_cli(cli) { 134 | return value; 135 | } 136 | 137 | if let Some(value) = Self::from_environment() { 138 | return value; 139 | } 140 | 141 | if let Some(value) = Self::from_config(config) { 142 | return value; 143 | } 144 | 145 | Default::default() 146 | } 147 | 148 | /// The method to implement the value fetching from command line parameters. 149 | fn from_cli(cli: &Cli) -> Option; 150 | 151 | /// The method to implement the value fetching from a configuration file. This should return 152 | /// [None], if the [Config] does not have a [Yaml]. 153 | fn from_config(config: &Config) -> Option; 154 | 155 | /// The method to implement the value fetching from environment variables. 156 | fn from_environment() -> Option { 157 | None 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/flags/color.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [Color]. To set it up from [Cli], a [Config] and its [Default] 2 | //! value, use its [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::Cli; 7 | use crate::config_file::Config; 8 | 9 | use serde::de::{self, Deserializer, Visitor}; 10 | use serde::Deserialize; 11 | use std::env; 12 | use std::fmt; 13 | 14 | /// A collection of flags on how to use colors. 15 | #[derive(Clone, Debug, Default)] 16 | pub struct Color { 17 | /// When to use color. 18 | pub when: ColorOption, 19 | pub theme: ThemeOption, 20 | } 21 | 22 | impl Color { 23 | /// Get a `Color` struct from [Cli], a [Config] or the [Default] values. 24 | /// 25 | /// The [ColorOption] is configured with their respective [Configurable] implementation. 26 | pub fn configure_from(cli: &Cli, config: &Config) -> Self { 27 | let when = ColorOption::configure_from(cli, config); 28 | let theme = ThemeOption::from_config(config); 29 | Self { when, theme } 30 | } 31 | } 32 | 33 | /// ThemeOption could be one of the following: 34 | /// Custom(*.yaml): use the YAML theme file as theme file 35 | /// if error happened, use the default theme 36 | #[derive(PartialEq, Eq, Debug, Clone, Default)] 37 | pub enum ThemeOption { 38 | NoColor, 39 | #[default] 40 | Default, 41 | #[allow(dead_code)] 42 | NoLscolors, 43 | CustomLegacy(String), 44 | Custom, 45 | } 46 | 47 | impl ThemeOption { 48 | fn from_config(config: &Config) -> ThemeOption { 49 | if config.classic == Some(true) { 50 | ThemeOption::NoColor 51 | } else { 52 | config 53 | .color 54 | .as_ref() 55 | .and_then(|c| c.theme.clone()) 56 | .unwrap_or_default() 57 | } 58 | } 59 | } 60 | 61 | impl<'de> de::Deserialize<'de> for ThemeOption { 62 | fn deserialize(deserializer: D) -> Result 63 | where 64 | D: Deserializer<'de>, 65 | { 66 | struct ThemeOptionVisitor; 67 | 68 | impl<'de> Visitor<'de> for ThemeOptionVisitor { 69 | type Value = ThemeOption; 70 | 71 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 72 | formatter.write_str("`default` or ") 73 | } 74 | 75 | fn visit_str(self, value: &str) -> Result 76 | where 77 | E: de::Error, 78 | { 79 | match value { 80 | "default" => Ok(ThemeOption::Default), 81 | "custom" => Ok(ThemeOption::Custom), 82 | str => Ok(ThemeOption::CustomLegacy(str.to_string())), 83 | } 84 | } 85 | } 86 | 87 | deserializer.deserialize_identifier(ThemeOptionVisitor) 88 | } 89 | } 90 | 91 | /// The flag showing when to use colors in the output. 92 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] 93 | #[serde(rename_all = "kebab-case")] 94 | pub enum ColorOption { 95 | Always, 96 | #[default] 97 | Auto, 98 | Never, 99 | } 100 | 101 | impl ColorOption { 102 | fn from_arg_str(value: &str) -> Self { 103 | match value { 104 | "always" => Self::Always, 105 | "auto" => Self::Auto, 106 | "never" => Self::Never, 107 | // Invalid value should be handled by `clap` when building an `Cli` 108 | other => unreachable!("Invalid value '{other}' for 'color'"), 109 | } 110 | } 111 | } 112 | 113 | impl Configurable for ColorOption { 114 | /// Get a potential `ColorOption` variant from [Cli]. 115 | /// 116 | /// If the "classic" argument is passed, then this returns the [ColorOption::Never] variant in 117 | /// a [Some]. Otherwise if the argument is passed, this returns the variant corresponding to 118 | /// its parameter in a [Some]. Otherwise this returns [None]. 119 | fn from_cli(cli: &Cli) -> Option { 120 | if cli.classic { 121 | Some(Self::Never) 122 | } else { 123 | cli.color.as_deref().map(Self::from_arg_str) 124 | } 125 | } 126 | 127 | /// Get a potential `ColorOption` variant from a [Config]. 128 | /// 129 | /// If the `Config::classic` is `true` then this returns the Some(ColorOption::Never), 130 | /// Otherwise if the `Config::color::when` has value and is one of "always", "auto" or "never" 131 | /// this returns its corresponding variant in a [Some]. Otherwise this returns [None]. 132 | fn from_config(config: &Config) -> Option { 133 | if config.classic == Some(true) { 134 | Some(Self::Never) 135 | } else { 136 | config.color.as_ref().and_then(|c| c.when) 137 | } 138 | } 139 | 140 | fn from_environment() -> Option { 141 | if env::var("NO_COLOR").is_ok() { 142 | Some(Self::Never) 143 | } else { 144 | None 145 | } 146 | } 147 | } 148 | 149 | #[cfg(test)] 150 | mod test_color_option { 151 | use clap::Parser; 152 | 153 | use super::ColorOption; 154 | 155 | use crate::app::Cli; 156 | use crate::config_file::{self, Config}; 157 | use crate::flags::Configurable; 158 | 159 | use std::env::set_var; 160 | 161 | #[test] 162 | fn test_from_cli_none() { 163 | let argv = ["lsd"]; 164 | let cli = Cli::try_parse_from(argv).unwrap(); 165 | assert_eq!(None, ColorOption::from_cli(&cli)); 166 | } 167 | 168 | #[test] 169 | fn test_from_cli_always() { 170 | let argv = ["lsd", "--color", "always"]; 171 | let cli = Cli::try_parse_from(argv).unwrap(); 172 | assert_eq!(Some(ColorOption::Always), ColorOption::from_cli(&cli)); 173 | } 174 | 175 | #[test] 176 | fn test_from_cli_auto() { 177 | let argv = ["lsd", "--color", "auto"]; 178 | let cli = Cli::try_parse_from(argv).unwrap(); 179 | assert_eq!(Some(ColorOption::Auto), ColorOption::from_cli(&cli)); 180 | } 181 | 182 | #[test] 183 | fn test_from_cli_never() { 184 | let argv = ["lsd", "--color", "never"]; 185 | let cli = Cli::try_parse_from(argv).unwrap(); 186 | assert_eq!(Some(ColorOption::Never), ColorOption::from_cli(&cli)); 187 | } 188 | 189 | #[test] 190 | fn test_from_env_no_color() { 191 | set_var("NO_COLOR", "true"); 192 | assert_eq!(Some(ColorOption::Never), ColorOption::from_environment()); 193 | } 194 | 195 | #[test] 196 | fn test_from_cli_classic_mode() { 197 | let argv = ["lsd", "--color", "always", "--classic"]; 198 | let cli = Cli::try_parse_from(argv).unwrap(); 199 | assert_eq!(Some(ColorOption::Never), ColorOption::from_cli(&cli)); 200 | } 201 | 202 | #[test] 203 | fn test_from_cli_color_multiple() { 204 | let argv = ["lsd", "--color", "always", "--color", "never"]; 205 | let cli = Cli::try_parse_from(argv).unwrap(); 206 | assert_eq!(Some(ColorOption::Never), ColorOption::from_cli(&cli)); 207 | } 208 | 209 | #[test] 210 | fn test_from_config_none() { 211 | assert_eq!(None, ColorOption::from_config(&Config::with_none())); 212 | } 213 | 214 | #[test] 215 | fn test_from_config_always() { 216 | let mut c = Config::with_none(); 217 | c.color = Some(config_file::Color { 218 | when: Some(ColorOption::Always), 219 | theme: None, 220 | }); 221 | 222 | assert_eq!(Some(ColorOption::Always), ColorOption::from_config(&c)); 223 | } 224 | 225 | #[test] 226 | fn test_from_config_auto() { 227 | let mut c = Config::with_none(); 228 | c.color = Some(config_file::Color { 229 | when: Some(ColorOption::Auto), 230 | theme: None, 231 | }); 232 | assert_eq!(Some(ColorOption::Auto), ColorOption::from_config(&c)); 233 | } 234 | 235 | #[test] 236 | fn test_from_config_never() { 237 | let mut c = Config::with_none(); 238 | c.color = Some(config_file::Color { 239 | when: Some(ColorOption::Never), 240 | theme: None, 241 | }); 242 | assert_eq!(Some(ColorOption::Never), ColorOption::from_config(&c)); 243 | } 244 | 245 | #[test] 246 | fn test_from_config_classic_mode() { 247 | let mut c = Config::with_none(); 248 | c.color = Some(config_file::Color { 249 | when: Some(ColorOption::Always), 250 | theme: None, 251 | }); 252 | c.classic = Some(true); 253 | assert_eq!(Some(ColorOption::Never), ColorOption::from_config(&c)); 254 | } 255 | } 256 | 257 | #[cfg(test)] 258 | mod test_theme_option { 259 | use super::ThemeOption; 260 | use crate::config_file::{self, Config}; 261 | 262 | #[test] 263 | fn test_from_config_none_default() { 264 | assert_eq!( 265 | ThemeOption::Default, 266 | ThemeOption::from_config(&Config::with_none()) 267 | ); 268 | } 269 | 270 | #[test] 271 | fn test_from_config_default() { 272 | let mut c = Config::with_none(); 273 | c.color = Some(config_file::Color { 274 | when: None, 275 | theme: Some(ThemeOption::Default), 276 | }); 277 | 278 | assert_eq!(ThemeOption::Default, ThemeOption::from_config(&c)); 279 | } 280 | 281 | #[test] 282 | fn test_from_config_no_color() { 283 | let mut c = Config::with_none(); 284 | c.color = Some(config_file::Color { 285 | when: None, 286 | theme: Some(ThemeOption::NoColor), 287 | }); 288 | assert_eq!(ThemeOption::NoColor, ThemeOption::from_config(&c)); 289 | } 290 | 291 | #[test] 292 | fn test_from_config_no_lscolor() { 293 | let mut c = Config::with_none(); 294 | c.color = Some(config_file::Color { 295 | when: None, 296 | theme: Some(ThemeOption::NoLscolors), 297 | }); 298 | assert_eq!(ThemeOption::NoLscolors, ThemeOption::from_config(&c)); 299 | } 300 | 301 | #[test] 302 | fn test_from_config_bad_file_flag() { 303 | let mut c = Config::with_none(); 304 | c.color = Some(config_file::Color { 305 | when: None, 306 | theme: Some(ThemeOption::CustomLegacy("not-existed".to_string())), 307 | }); 308 | assert_eq!( 309 | ThemeOption::CustomLegacy("not-existed".to_string()), 310 | ThemeOption::from_config(&c) 311 | ); 312 | } 313 | 314 | #[test] 315 | fn test_from_config_classic_mode() { 316 | let mut c = Config::with_none(); 317 | c.color = Some(config_file::Color { 318 | when: None, 319 | theme: Some(ThemeOption::Default), 320 | }); 321 | c.classic = Some(true); 322 | assert_eq!(ThemeOption::NoColor, ThemeOption::from_config(&c)); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/flags/date.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [DateFlag]. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use its [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::{self, Cli}; 7 | use crate::config_file::Config; 8 | use crate::print_error; 9 | 10 | /// The flag showing which kind of time stamps to display. 11 | #[derive(Clone, Debug, PartialEq, Eq, Default)] 12 | pub enum DateFlag { 13 | #[default] 14 | Date, 15 | Locale, 16 | Relative, 17 | Iso, 18 | Formatted(String), 19 | } 20 | 21 | impl DateFlag { 22 | /// Get a value from a date format string 23 | fn from_format_string(value: &str) -> Option { 24 | if app::validate_time_format(value).is_ok() { 25 | Some(Self::Formatted(value[1..].to_string())) 26 | } else { 27 | print_error!("Not a valid date format: {}.", value); 28 | None 29 | } 30 | } 31 | 32 | /// Get a value from a str. 33 | fn from_str>(value: S) -> Option { 34 | let value = value.as_ref(); 35 | match value { 36 | "date" => Some(Self::Date), 37 | "locale" => Some(Self::Locale), 38 | "relative" => Some(Self::Relative), 39 | _ if value.starts_with('+') => Self::from_format_string(value), 40 | _ => { 41 | print_error!("Not a valid date value: {}.", value); 42 | None 43 | } 44 | } 45 | } 46 | } 47 | 48 | impl Configurable for DateFlag { 49 | /// Get a potential `DateFlag` variant from [Cli]. 50 | /// 51 | /// If the "classic" argument is passed, then this returns the [DateFlag::Date] variant in a 52 | /// [Some]. Otherwise if the argument is passed, this returns the variant corresponding to its 53 | /// parameter in a [Some]. Otherwise this returns [None]. 54 | fn from_cli(cli: &Cli) -> Option { 55 | if cli.classic { 56 | Some(Self::Date) 57 | } else { 58 | cli.date.as_deref().and_then(Self::from_str) 59 | } 60 | } 61 | 62 | /// Get a potential `DateFlag` variant from a [Config]. 63 | /// 64 | /// If the `Config::classic` is `true` then this returns the Some(DateFlag::Date), 65 | /// Otherwise if the `Config::date` has value and is one of "date", "locale" or "relative", 66 | /// this returns its corresponding variant in a [Some]. 67 | /// Otherwise this returns [None]. 68 | fn from_config(config: &Config) -> Option { 69 | if config.classic == Some(true) { 70 | Some(Self::Date) 71 | } else { 72 | config.date.as_ref().and_then(Self::from_str) 73 | } 74 | } 75 | 76 | /// Get a potential `DateFlag` variant from the environment. 77 | fn from_environment() -> Option { 78 | if let Ok(value) = std::env::var("TIME_STYLE") { 79 | match value.as_str() { 80 | "full-iso" => Some(Self::Formatted("%F %T.%f %z".into())), 81 | "long-iso" => Some(Self::Formatted("%F %R".into())), 82 | "locale" => Some(Self::Locale), 83 | "iso" => Some(Self::Iso), 84 | _ if value.starts_with('+') => Self::from_format_string(&value), 85 | _ => { 86 | print_error!("Not a valid date value: {}.", value); 87 | None 88 | } 89 | } 90 | } else { 91 | None 92 | } 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod test { 98 | use clap::Parser; 99 | 100 | use super::DateFlag; 101 | 102 | use crate::app::Cli; 103 | use crate::config_file::Config; 104 | use crate::flags::Configurable; 105 | 106 | #[test] 107 | fn test_from_cli_none() { 108 | let argv = ["lsd"]; 109 | let cli = Cli::try_parse_from(argv).unwrap(); 110 | assert_eq!(None, DateFlag::from_cli(&cli)); 111 | } 112 | 113 | #[test] 114 | fn test_from_cli_date() { 115 | let argv = ["lsd", "--date", "date"]; 116 | let cli = Cli::try_parse_from(argv).unwrap(); 117 | assert_eq!(Some(DateFlag::Date), DateFlag::from_cli(&cli)); 118 | } 119 | 120 | #[test] 121 | fn test_from_cli_locale() { 122 | let argv = ["lsd", "--date", "locale"]; 123 | let cli = Cli::try_parse_from(argv).unwrap(); 124 | assert_eq!(Some(DateFlag::Locale), DateFlag::from_cli(&cli)); 125 | } 126 | 127 | #[test] 128 | fn test_from_cli_relative() { 129 | let argv = ["lsd", "--date", "relative"]; 130 | let cli = Cli::try_parse_from(argv).unwrap(); 131 | assert_eq!(Some(DateFlag::Relative), DateFlag::from_cli(&cli)); 132 | } 133 | 134 | #[test] 135 | fn test_from_cli_format() { 136 | let argv = ["lsd", "--date", "+%F"]; 137 | let cli = Cli::try_parse_from(argv).unwrap(); 138 | assert_eq!( 139 | Some(DateFlag::Formatted("%F".to_string())), 140 | DateFlag::from_cli(&cli) 141 | ); 142 | } 143 | 144 | #[test] 145 | #[should_panic(expected = "invalid format specifier: %J")] 146 | fn test_from_cli_format_invalid() { 147 | let argv = ["lsd", "--date", "+%J"]; 148 | let cli = Cli::try_parse_from(argv).unwrap(); 149 | DateFlag::from_cli(&cli); 150 | } 151 | 152 | #[test] 153 | fn test_from_cli_classic_mode() { 154 | let argv = ["lsd", "--date", "date", "--classic"]; 155 | let cli = Cli::try_parse_from(argv).unwrap(); 156 | assert_eq!(Some(DateFlag::Date), DateFlag::from_cli(&cli)); 157 | } 158 | 159 | #[test] 160 | fn test_from_cli_date_multi() { 161 | let argv = ["lsd", "--date", "relative", "--date", "date"]; 162 | let cli = Cli::try_parse_from(argv).unwrap(); 163 | assert_eq!(Some(DateFlag::Date), DateFlag::from_cli(&cli)); 164 | } 165 | 166 | #[test] 167 | fn test_from_config_none() { 168 | assert_eq!(None, DateFlag::from_config(&Config::with_none())); 169 | } 170 | 171 | #[test] 172 | fn test_from_config_date() { 173 | let mut c = Config::with_none(); 174 | c.date = Some("date".into()); 175 | 176 | assert_eq!(Some(DateFlag::Date), DateFlag::from_config(&c)); 177 | } 178 | 179 | #[test] 180 | fn test_from_config_relative() { 181 | let mut c = Config::with_none(); 182 | c.date = Some("relative".into()); 183 | assert_eq!(Some(DateFlag::Relative), DateFlag::from_config(&c)); 184 | } 185 | 186 | #[test] 187 | fn test_from_config_format() { 188 | let mut c = Config::with_none(); 189 | c.date = Some("+%F".into()); 190 | assert_eq!( 191 | Some(DateFlag::Formatted("%F".to_string())), 192 | DateFlag::from_config(&c) 193 | ); 194 | } 195 | 196 | #[test] 197 | fn test_from_config_format_invalid() { 198 | let mut c = Config::with_none(); 199 | c.date = Some("+%J".into()); 200 | assert_eq!(None, DateFlag::from_config(&c)); 201 | } 202 | 203 | #[test] 204 | fn test_from_config_classic_mode() { 205 | let mut c = Config::with_none(); 206 | c.date = Some("relative".into()); 207 | c.classic = Some(true); 208 | assert_eq!(Some(DateFlag::Date), DateFlag::from_config(&c)); 209 | } 210 | 211 | #[test] 212 | #[serial_test::serial] 213 | fn test_from_environment_none() { 214 | std::env::set_var("TIME_STYLE", ""); 215 | assert_eq!(None, DateFlag::from_environment()); 216 | } 217 | 218 | #[test] 219 | #[serial_test::serial] 220 | fn test_from_environment_full_iso() { 221 | std::env::set_var("TIME_STYLE", "full-iso"); 222 | assert_eq!( 223 | Some(DateFlag::Formatted("%F %T.%f %z".into())), 224 | DateFlag::from_environment() 225 | ); 226 | } 227 | 228 | #[test] 229 | #[serial_test::serial] 230 | fn test_from_environment_long_iso() { 231 | std::env::set_var("TIME_STYLE", "long-iso"); 232 | assert_eq!( 233 | Some(DateFlag::Formatted("%F %R".into())), 234 | DateFlag::from_environment() 235 | ); 236 | } 237 | 238 | #[test] 239 | #[serial_test::serial] 240 | fn test_from_environment_iso() { 241 | std::env::set_var("TIME_STYLE", "iso"); 242 | assert_eq!(Some(DateFlag::Iso), DateFlag::from_environment()); 243 | } 244 | 245 | #[test] 246 | #[serial_test::serial] 247 | fn test_from_environment_format() { 248 | std::env::set_var("TIME_STYLE", "+%F"); 249 | assert_eq!( 250 | Some(DateFlag::Formatted("%F".into())), 251 | DateFlag::from_environment() 252 | ); 253 | } 254 | 255 | #[test] 256 | #[serial_test::serial] 257 | fn test_parsing_order_arg() { 258 | std::env::set_var("TIME_STYLE", "+%R"); 259 | let argv = ["lsd", "--date", "+%F"]; 260 | let cli = Cli::try_parse_from(argv).unwrap(); 261 | let mut config = Config::with_none(); 262 | config.date = Some("+%c".into()); 263 | assert_eq!( 264 | DateFlag::Formatted("%F".into()), 265 | DateFlag::configure_from(&cli, &config) 266 | ); 267 | } 268 | 269 | #[test] 270 | #[serial_test::serial] 271 | fn test_parsing_order_env() { 272 | std::env::set_var("TIME_STYLE", "+%R"); 273 | let argv = ["lsd"]; 274 | let cli = Cli::try_parse_from(argv).unwrap(); 275 | let mut config = Config::with_none(); 276 | config.date = Some("+%c".into()); 277 | assert_eq!( 278 | DateFlag::Formatted("%R".into()), 279 | DateFlag::configure_from(&cli, &config) 280 | ); 281 | } 282 | 283 | #[test] 284 | #[serial_test::serial] 285 | fn test_parsing_order_config() { 286 | std::env::set_var("TIME_STYLE", ""); 287 | let argv = ["lsd"]; 288 | let cli = Cli::try_parse_from(argv).unwrap(); 289 | let mut config = Config::with_none(); 290 | config.date = Some("+%c".into()); 291 | assert_eq!( 292 | DateFlag::Formatted("%c".into()), 293 | DateFlag::configure_from(&cli, &config) 294 | ); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/flags/dereference.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [Dereference] flag. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use the [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::Cli; 7 | use crate::config_file::Config; 8 | 9 | /// The flag showing whether to dereference symbolic links. 10 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] 11 | pub struct Dereference(pub bool); 12 | 13 | impl Configurable for Dereference { 14 | /// Get a potential `Dereference` value from [Cli]. 15 | /// 16 | /// If the "dereference" argument is passed, this returns a `Dereference` with value `true` in 17 | /// a [Some]. Otherwise this returns [None]. 18 | fn from_cli(cli: &Cli) -> Option { 19 | if cli.dereference { 20 | Some(Self(true)) 21 | } else { 22 | None 23 | } 24 | } 25 | 26 | /// Get a potential `Dereference` value from a [Config]. 27 | /// 28 | /// If the `Config::dereference` has value, this returns its value 29 | /// as the value of the `Dereference`, in a [Some], Otherwise this returns [None]. 30 | fn from_config(config: &Config) -> Option { 31 | config.dereference.map(Self) 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod test { 37 | use clap::Parser; 38 | 39 | use super::Dereference; 40 | 41 | use crate::app::Cli; 42 | use crate::config_file::Config; 43 | use crate::flags::Configurable; 44 | 45 | #[test] 46 | fn test_from_cli_none() { 47 | let argv = ["lsd"]; 48 | let cli = Cli::try_parse_from(argv).unwrap(); 49 | assert_eq!(None, Dereference::from_cli(&cli)); 50 | } 51 | 52 | #[test] 53 | fn test_from_cli_true() { 54 | let argv = ["lsd", "--dereference"]; 55 | let cli = Cli::try_parse_from(argv).unwrap(); 56 | assert_eq!(Some(Dereference(true)), Dereference::from_cli(&cli)); 57 | } 58 | 59 | #[test] 60 | fn test_from_config_none() { 61 | assert_eq!(None, Dereference::from_config(&Config::with_none())); 62 | } 63 | 64 | #[test] 65 | fn test_from_config_true() { 66 | let mut c = Config::with_none(); 67 | c.dereference = Some(true); 68 | assert_eq!(Some(Dereference(true)), Dereference::from_config(&c)); 69 | } 70 | 71 | #[test] 72 | fn test_from_config_false() { 73 | let mut c = Config::with_none(); 74 | c.dereference = Some(false); 75 | assert_eq!(Some(Dereference(false)), Dereference::from_config(&c)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/flags/display.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [Display] flag. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use its [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::Cli; 7 | use crate::config_file::Config; 8 | 9 | use serde::Deserialize; 10 | 11 | /// The flag showing which file system nodes to display. 12 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] 13 | #[serde(rename_all = "kebab-case")] 14 | pub enum Display { 15 | /// windows only, used to show files with system protected flag 16 | SystemProtected, 17 | All, 18 | AlmostAll, 19 | DirectoryOnly, 20 | #[default] 21 | VisibleOnly, 22 | } 23 | 24 | impl Configurable for Display { 25 | /// Get a potential `Display` variant from [Cli]. 26 | /// 27 | /// If any of the "all", "almost-all" or "directory-only" arguments is passed, this returns the 28 | /// corresponding `Display` variant in a [Some]. If neither of them is passed, this returns 29 | /// [None]. 30 | fn from_cli(cli: &Cli) -> Option { 31 | if cli.directory_only { 32 | Some(Self::DirectoryOnly) 33 | } else if cli.almost_all { 34 | Some(Self::AlmostAll) 35 | } else if cli.all { 36 | Some(Self::All) 37 | } else if cli.system_protected { 38 | #[cfg(windows)] 39 | return Some(Self::SystemProtected); 40 | 41 | #[cfg(not(windows))] 42 | return Some(Self::All); 43 | } else { 44 | None 45 | } 46 | } 47 | 48 | /// Get a potential `Display` variant from a [Config]. 49 | /// 50 | /// If the `Config::display` has value and is one of 51 | /// "all", "almost-all", "directory-only" or `visible-only`, 52 | /// this returns the corresponding `Display` variant in a [Some]. 53 | /// Otherwise this returns [None]. 54 | fn from_config(config: &Config) -> Option { 55 | config.display 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod test { 61 | use clap::Parser; 62 | 63 | use super::Display; 64 | 65 | use crate::app::Cli; 66 | use crate::config_file::Config; 67 | use crate::flags::Configurable; 68 | 69 | #[test] 70 | fn test_from_cli_none() { 71 | let argv = ["lsd"]; 72 | let cli = Cli::try_parse_from(argv).unwrap(); 73 | assert_eq!(None, Display::from_cli(&cli)); 74 | } 75 | 76 | #[test] 77 | fn test_from_cli_system_protected() { 78 | let argv = ["lsd", "--system-protected"]; 79 | let cli = Cli::try_parse_from(argv).unwrap(); 80 | #[cfg(windows)] 81 | assert_eq!(Some(Display::SystemProtected), Display::from_cli(&cli)); 82 | 83 | #[cfg(not(windows))] 84 | assert_eq!(Some(Display::All), Display::from_cli(&cli)); 85 | } 86 | 87 | #[test] 88 | fn test_from_cli_all() { 89 | let argv = ["lsd", "--all"]; 90 | let cli = Cli::try_parse_from(argv).unwrap(); 91 | assert_eq!(Some(Display::All), Display::from_cli(&cli)); 92 | } 93 | 94 | #[test] 95 | fn test_from_cli_almost_all() { 96 | let argv = ["lsd", "--almost-all"]; 97 | let cli = Cli::try_parse_from(argv).unwrap(); 98 | assert_eq!(Some(Display::AlmostAll), Display::from_cli(&cli)); 99 | } 100 | 101 | #[test] 102 | fn test_from_cli_directory_only() { 103 | let argv = ["lsd", "--directory-only"]; 104 | let cli = Cli::try_parse_from(argv).unwrap(); 105 | assert_eq!(Some(Display::DirectoryOnly), Display::from_cli(&cli)); 106 | } 107 | 108 | #[test] 109 | fn test_from_config_none() { 110 | assert_eq!(None, Display::from_config(&Config::with_none())); 111 | } 112 | 113 | #[test] 114 | fn test_from_config_all() { 115 | let mut c = Config::with_none(); 116 | c.display = Some(Display::All); 117 | assert_eq!(Some(Display::All), Display::from_config(&c)); 118 | } 119 | 120 | #[test] 121 | fn test_from_config_almost_all() { 122 | let mut c = Config::with_none(); 123 | c.display = Some(Display::AlmostAll); 124 | assert_eq!(Some(Display::AlmostAll), Display::from_config(&c)); 125 | } 126 | 127 | #[test] 128 | fn test_from_config_directory_only() { 129 | let mut c = Config::with_none(); 130 | c.display = Some(Display::DirectoryOnly); 131 | assert_eq!(Some(Display::DirectoryOnly), Display::from_config(&c)); 132 | } 133 | 134 | #[test] 135 | fn test_from_config_visible_only() { 136 | let mut c = Config::with_none(); 137 | c.display = Some(Display::VisibleOnly); 138 | assert_eq!(Some(Display::VisibleOnly), Display::from_config(&c)); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/flags/header.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [Header] flag. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use the [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::Cli; 7 | use crate::config_file::Config; 8 | 9 | /// The flag showing whether to display block headers. 10 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] 11 | pub struct Header(pub bool); 12 | 13 | impl Configurable for Header { 14 | /// Get a potential `Header` value from [Cli]. 15 | /// 16 | /// If the "header" argument is passed, this returns a `Header` with value `true` in a 17 | /// [Some]. Otherwise this returns [None]. 18 | fn from_cli(cli: &Cli) -> Option { 19 | if cli.header { 20 | Some(Self(true)) 21 | } else { 22 | None 23 | } 24 | } 25 | 26 | /// Get a potential `Header` value from a [Config]. 27 | /// 28 | /// If the `Config::header` has value, 29 | /// this returns it as the value of the `Header`, in a [Some]. 30 | /// Otherwise this returns [None]. 31 | fn from_config(config: &Config) -> Option { 32 | config.header.map(Self) 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod test { 38 | use clap::Parser; 39 | 40 | use super::Header; 41 | 42 | use crate::app::Cli; 43 | use crate::config_file::Config; 44 | use crate::flags::Configurable; 45 | 46 | #[test] 47 | fn test_from_cli_none() { 48 | let argv = ["lsd"]; 49 | let cli = Cli::try_parse_from(argv).unwrap(); 50 | assert_eq!(None, Header::from_cli(&cli)); 51 | } 52 | 53 | #[test] 54 | fn test_from_cli_true() { 55 | let argv = ["lsd", "--header"]; 56 | let cli = Cli::try_parse_from(argv).unwrap(); 57 | assert_eq!(Some(Header(true)), Header::from_cli(&cli)); 58 | } 59 | 60 | #[test] 61 | fn test_from_config_none() { 62 | assert_eq!(None, Header::from_config(&Config::with_none())); 63 | } 64 | 65 | #[test] 66 | fn test_from_config_true() { 67 | let mut c = Config::with_none(); 68 | c.header = Some(true); 69 | assert_eq!(Some(Header(true)), Header::from_config(&c)); 70 | } 71 | 72 | #[test] 73 | fn test_from_config_false() { 74 | let mut c = Config::with_none(); 75 | c.header = Some(false); 76 | assert_eq!(Some(Header(false)), Header::from_config(&c)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/flags/hyperlink.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [HyperlinkOption]. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use its [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::Cli; 7 | use crate::config_file::Config; 8 | 9 | use serde::Deserialize; 10 | 11 | /// The flag showing when to use hyperlink in the output. 12 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] 13 | #[serde(rename_all = "kebab-case")] 14 | pub enum HyperlinkOption { 15 | Always, 16 | Auto, 17 | #[default] 18 | Never, 19 | } 20 | 21 | impl HyperlinkOption { 22 | fn from_arg_str(value: &str) -> Self { 23 | match value { 24 | "always" => Self::Always, 25 | "auto" => Self::Auto, 26 | "never" => Self::Never, 27 | // Invalid value should be handled by `clap` when building an `Cli` 28 | other => unreachable!("Invalid value '{other}' for 'hyperlink'"), 29 | } 30 | } 31 | } 32 | 33 | impl Configurable for HyperlinkOption { 34 | /// Get a potential `HyperlinkOption` variant from [Cli]. 35 | /// 36 | /// If the "classic" argument is passed, then this returns the [HyperlinkOption::Never] variant in 37 | /// a [Some]. Otherwise if the argument is passed, this returns the variant corresponding to 38 | /// its parameter in a [Some]. Otherwise this returns [None]. 39 | fn from_cli(cli: &Cli) -> Option { 40 | if cli.classic { 41 | Some(Self::Never) 42 | } else { 43 | cli.hyperlink.as_deref().map(Self::from_arg_str) 44 | } 45 | } 46 | 47 | /// Get a potential `HyperlinkOption` variant from a [Config]. 48 | /// 49 | /// If the `Configs::classic` has value and is "true" then this returns Some(HyperlinkOption::Never). 50 | /// Otherwise if the `Config::hyperlink::when` has value and is one of "always", "auto" or "never", 51 | /// this returns its corresponding variant in a [Some]. 52 | /// Otherwise this returns [None]. 53 | fn from_config(config: &Config) -> Option { 54 | if config.classic == Some(true) { 55 | Some(Self::Never) 56 | } else { 57 | config.hyperlink 58 | } 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod test_hyperlink_option { 64 | use clap::Parser; 65 | 66 | use super::HyperlinkOption; 67 | 68 | use crate::app::Cli; 69 | use crate::config_file::Config; 70 | use crate::flags::Configurable; 71 | 72 | #[test] 73 | fn test_from_cli_none() { 74 | let argv = ["lsd"]; 75 | let cli = Cli::try_parse_from(argv).unwrap(); 76 | assert_eq!(None, HyperlinkOption::from_cli(&cli)); 77 | } 78 | 79 | #[test] 80 | fn test_from_cli_always() { 81 | let argv = ["lsd", "--hyperlink", "always"]; 82 | let cli = Cli::try_parse_from(argv).unwrap(); 83 | assert_eq!( 84 | Some(HyperlinkOption::Always), 85 | HyperlinkOption::from_cli(&cli) 86 | ); 87 | } 88 | 89 | #[test] 90 | fn test_from_cli_auto() { 91 | let argv = ["lsd", "--hyperlink", "auto"]; 92 | let cli = Cli::try_parse_from(argv).unwrap(); 93 | assert_eq!(Some(HyperlinkOption::Auto), HyperlinkOption::from_cli(&cli)); 94 | } 95 | 96 | #[test] 97 | fn test_from_cli_never() { 98 | let argv = ["lsd", "--hyperlink", "never"]; 99 | let cli = Cli::try_parse_from(argv).unwrap(); 100 | assert_eq!( 101 | Some(HyperlinkOption::Never), 102 | HyperlinkOption::from_cli(&cli) 103 | ); 104 | } 105 | 106 | #[test] 107 | fn test_from_cli_classic_mode() { 108 | let argv = ["lsd", "--hyperlink", "always", "--classic"]; 109 | let cli = Cli::try_parse_from(argv).unwrap(); 110 | assert_eq!( 111 | Some(HyperlinkOption::Never), 112 | HyperlinkOption::from_cli(&cli) 113 | ); 114 | } 115 | 116 | #[test] 117 | fn test_from_cli_hyperlink_when_multi() { 118 | let argv = ["lsd", "--hyperlink", "always", "--hyperlink", "never"]; 119 | let cli = Cli::try_parse_from(argv).unwrap(); 120 | assert_eq!( 121 | Some(HyperlinkOption::Never), 122 | HyperlinkOption::from_cli(&cli) 123 | ); 124 | } 125 | 126 | #[test] 127 | fn test_from_config_none() { 128 | assert_eq!(None, HyperlinkOption::from_config(&Config::with_none())); 129 | } 130 | 131 | #[test] 132 | fn test_from_config_always() { 133 | let mut c = Config::with_none(); 134 | c.hyperlink = Some(HyperlinkOption::Always); 135 | assert_eq!( 136 | Some(HyperlinkOption::Always), 137 | HyperlinkOption::from_config(&c) 138 | ); 139 | } 140 | 141 | #[test] 142 | fn test_from_config_auto() { 143 | let mut c = Config::with_none(); 144 | c.hyperlink = Some(HyperlinkOption::Auto); 145 | assert_eq!( 146 | Some(HyperlinkOption::Auto), 147 | HyperlinkOption::from_config(&c) 148 | ); 149 | } 150 | 151 | #[test] 152 | fn test_from_config_never() { 153 | let mut c = Config::with_none(); 154 | c.hyperlink = Some(HyperlinkOption::Never); 155 | assert_eq!( 156 | Some(HyperlinkOption::Never), 157 | HyperlinkOption::from_config(&c) 158 | ); 159 | } 160 | 161 | #[test] 162 | fn test_from_config_classic_mode() { 163 | let mut c = Config::with_none(); 164 | c.classic = Some(true); 165 | c.hyperlink = Some(HyperlinkOption::Always); 166 | assert_eq!( 167 | Some(HyperlinkOption::Never), 168 | HyperlinkOption::from_config(&c) 169 | ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/flags/icons.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [IconOption]. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use its [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::Cli; 7 | use crate::config_file::Config; 8 | 9 | use serde::Deserialize; 10 | 11 | /// A collection of flags on how to use icons. 12 | #[derive(Clone, Debug, PartialEq, Eq, Default)] 13 | pub struct Icons { 14 | /// When to use icons. 15 | pub when: IconOption, 16 | /// Which icon theme to use. 17 | pub theme: IconTheme, 18 | /// String between icon and name. 19 | pub separator: IconSeparator, 20 | } 21 | 22 | impl Icons { 23 | /// Get an `Icons` struct from [Cli], a [Config] or the [Default] values. 24 | /// 25 | /// The [IconOption] and [IconTheme] are configured with their respective [Configurable] 26 | /// implementation. 27 | pub fn configure_from(cli: &Cli, config: &Config) -> Self { 28 | let when = IconOption::configure_from(cli, config); 29 | let theme = IconTheme::configure_from(cli, config); 30 | let separator = IconSeparator::configure_from(cli, config); 31 | Self { 32 | when, 33 | theme, 34 | separator, 35 | } 36 | } 37 | } 38 | 39 | /// The flag showing when to use icons in the output. 40 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] 41 | #[serde(rename_all = "kebab-case")] 42 | pub enum IconOption { 43 | Always, 44 | #[default] 45 | Auto, 46 | Never, 47 | } 48 | 49 | impl IconOption { 50 | fn from_arg_str(value: &str) -> Self { 51 | match value { 52 | "always" => Self::Always, 53 | "auto" => Self::Auto, 54 | "never" => Self::Never, 55 | // Invalid value should be handled by `clap` when building an `Cli` 56 | other => unreachable!("Invalid value '{other}' for 'icon'"), 57 | } 58 | } 59 | } 60 | 61 | impl Configurable for IconOption { 62 | /// Get a potential `IconOption` variant from [Cli]. 63 | /// 64 | /// If the "classic" argument is passed, then this returns the [IconOption::Never] variant in 65 | /// a [Some]. Otherwise if the argument is passed, this returns the variant corresponding to 66 | /// its parameter in a [Some]. Otherwise this returns [None]. 67 | fn from_cli(cli: &Cli) -> Option { 68 | if cli.classic { 69 | Some(Self::Never) 70 | } else { 71 | cli.icon.as_deref().map(Self::from_arg_str) 72 | } 73 | } 74 | 75 | /// Get a potential `IconOption` variant from a [Config]. 76 | /// 77 | /// If the `Configs::classic` has value and is "true" then this returns Some(IconOption::Never). 78 | /// Otherwise if the `Config::icon::when` has value and is one of "always", "auto" or "never", 79 | /// this returns its corresponding variant in a [Some]. 80 | /// Otherwise this returns [None]. 81 | fn from_config(config: &Config) -> Option { 82 | if config.classic == Some(true) { 83 | Some(Self::Never) 84 | } else { 85 | config.icons.as_ref().and_then(|icon| icon.when) 86 | } 87 | } 88 | } 89 | 90 | /// The flag showing which icon theme to use. 91 | #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)] 92 | #[serde(rename_all = "kebab-case")] 93 | pub enum IconTheme { 94 | Unicode, 95 | #[default] 96 | Fancy, 97 | } 98 | 99 | impl IconTheme { 100 | fn from_arg_str(value: &str) -> Self { 101 | match value { 102 | "fancy" => Self::Fancy, 103 | "unicode" => Self::Unicode, 104 | // Invalid value should be handled by `clap` when building an `Cli` 105 | other => unreachable!("Invalid value '{other}' for 'icon-theme'"), 106 | } 107 | } 108 | } 109 | 110 | impl Configurable for IconTheme { 111 | /// Get a potential `IconTheme` variant from [Cli]. 112 | /// 113 | /// If the argument is passed, this returns the variant corresponding to its parameter in a 114 | /// [Some]. Otherwise this returns [None]. 115 | fn from_cli(cli: &Cli) -> Option { 116 | cli.icon_theme.as_deref().map(Self::from_arg_str) 117 | } 118 | 119 | /// Get a potential `IconTheme` variant from a [Config]. 120 | /// 121 | /// If the `Config::icons::theme` has value and is one of "fancy" or "unicode", 122 | /// this returns its corresponding variant in a [Some]. 123 | /// Otherwise this returns [None]. 124 | fn from_config(config: &Config) -> Option { 125 | config.icons.as_ref().and_then(|icon| icon.theme.clone()) 126 | } 127 | } 128 | 129 | #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] 130 | #[serde(rename_all = "kebab-case")] 131 | pub struct IconSeparator(pub String); 132 | 133 | impl Configurable for IconSeparator { 134 | /// Get a potential `IconSeparator` variant from [Cli]. 135 | /// 136 | /// If the argument is passed, this returns the variant corresponding to its parameter in a 137 | /// [Some]. Otherwise this returns [None]. 138 | fn from_cli(_cli: &Cli) -> Option { 139 | None 140 | } 141 | 142 | /// Get a potential `IconSeparator` variant from a [Config]. 143 | /// 144 | /// This returns its corresponding variant in a [Some]. 145 | /// Otherwise this returns [None]. 146 | fn from_config(config: &Config) -> Option { 147 | if let Some(icon) = &config.icons { 148 | if let Some(separator) = icon.separator.clone() { 149 | return Some(IconSeparator(separator)); 150 | } 151 | } 152 | None 153 | } 154 | } 155 | 156 | /// The default value for `IconSeparator` is [" "]. 157 | impl Default for IconSeparator { 158 | fn default() -> Self { 159 | IconSeparator(" ".to_string()) 160 | } 161 | } 162 | 163 | #[cfg(test)] 164 | mod test_icon_option { 165 | use clap::Parser; 166 | 167 | use super::IconOption; 168 | 169 | use crate::app::Cli; 170 | use crate::config_file::{Config, Icons}; 171 | use crate::flags::Configurable; 172 | 173 | #[test] 174 | fn test_from_cli_none() { 175 | let argv = ["lsd"]; 176 | let cli = Cli::try_parse_from(argv).unwrap(); 177 | assert_eq!(None, IconOption::from_cli(&cli)); 178 | } 179 | 180 | #[test] 181 | fn test_from_cli_always() { 182 | let argv = ["lsd", "--icon", "always"]; 183 | let cli = Cli::try_parse_from(argv).unwrap(); 184 | assert_eq!(Some(IconOption::Always), IconOption::from_cli(&cli)); 185 | } 186 | 187 | #[test] 188 | fn test_from_cli_auto() { 189 | let argv = ["lsd", "--icon", "auto"]; 190 | let cli = Cli::try_parse_from(argv).unwrap(); 191 | assert_eq!(Some(IconOption::Auto), IconOption::from_cli(&cli)); 192 | } 193 | 194 | #[test] 195 | fn test_from_cli_never() { 196 | let argv = ["lsd", "--icon", "never"]; 197 | let cli = Cli::try_parse_from(argv).unwrap(); 198 | assert_eq!(Some(IconOption::Never), IconOption::from_cli(&cli)); 199 | } 200 | 201 | #[test] 202 | fn test_from_cli_classic_mode() { 203 | let argv = ["lsd", "--icon", "always", "--classic"]; 204 | let cli = Cli::try_parse_from(argv).unwrap(); 205 | assert_eq!(Some(IconOption::Never), IconOption::from_cli(&cli)); 206 | } 207 | 208 | #[test] 209 | fn test_from_cli_icon_when_multi() { 210 | let argv = ["lsd", "--icon", "always", "--icon", "never"]; 211 | let cli = Cli::try_parse_from(argv).unwrap(); 212 | assert_eq!(Some(IconOption::Never), IconOption::from_cli(&cli)); 213 | } 214 | 215 | #[test] 216 | fn test_from_config_none() { 217 | assert_eq!(None, IconOption::from_config(&Config::with_none())); 218 | } 219 | 220 | #[test] 221 | fn test_from_config_always() { 222 | let mut c = Config::with_none(); 223 | c.icons = Some(Icons { 224 | when: Some(IconOption::Always), 225 | theme: None, 226 | separator: None, 227 | }); 228 | assert_eq!(Some(IconOption::Always), IconOption::from_config(&c)); 229 | } 230 | 231 | #[test] 232 | fn test_from_config_auto() { 233 | let mut c = Config::with_none(); 234 | c.icons = Some(Icons { 235 | when: Some(IconOption::Auto), 236 | theme: None, 237 | separator: None, 238 | }); 239 | assert_eq!(Some(IconOption::Auto), IconOption::from_config(&c)); 240 | } 241 | 242 | #[test] 243 | fn test_from_config_never() { 244 | let mut c = Config::with_none(); 245 | c.icons = Some(Icons { 246 | when: Some(IconOption::Never), 247 | theme: None, 248 | separator: None, 249 | }); 250 | assert_eq!(Some(IconOption::Never), IconOption::from_config(&c)); 251 | } 252 | 253 | #[test] 254 | fn test_from_config_classic_mode() { 255 | let mut c = Config::with_none(); 256 | c.classic = Some(true); 257 | c.icons = Some(Icons { 258 | when: Some(IconOption::Always), 259 | theme: None, 260 | separator: None, 261 | }); 262 | assert_eq!(Some(IconOption::Never), IconOption::from_config(&c)); 263 | } 264 | } 265 | 266 | #[cfg(test)] 267 | mod test_icon_theme { 268 | use clap::Parser; 269 | 270 | use super::IconTheme; 271 | 272 | use crate::app::Cli; 273 | use crate::config_file::{Config, Icons}; 274 | use crate::flags::Configurable; 275 | 276 | #[test] 277 | fn test_from_cli_none() { 278 | let argv = ["lsd"]; 279 | let cli = Cli::try_parse_from(argv).unwrap(); 280 | assert_eq!(None, IconTheme::from_cli(&cli)); 281 | } 282 | 283 | #[test] 284 | fn test_from_cli_fancy() { 285 | let argv = ["lsd", "--icon-theme", "fancy"]; 286 | let cli = Cli::try_parse_from(argv).unwrap(); 287 | assert_eq!(Some(IconTheme::Fancy), IconTheme::from_cli(&cli)); 288 | } 289 | 290 | #[test] 291 | fn test_from_cli_unicode() { 292 | let argv = ["lsd", "--icon-theme", "unicode"]; 293 | let cli = Cli::try_parse_from(argv).unwrap(); 294 | assert_eq!(Some(IconTheme::Unicode), IconTheme::from_cli(&cli)); 295 | } 296 | 297 | #[test] 298 | fn test_from_cli_icon_multi() { 299 | let argv = ["lsd", "--icon-theme", "fancy", "--icon-theme", "unicode"]; 300 | let cli = Cli::try_parse_from(argv).unwrap(); 301 | assert_eq!(Some(IconTheme::Unicode), IconTheme::from_cli(&cli)); 302 | } 303 | 304 | #[test] 305 | fn test_from_config_none() { 306 | assert_eq!(None, IconTheme::from_config(&Config::with_none())); 307 | } 308 | 309 | #[test] 310 | fn test_from_config_fancy() { 311 | let mut c = Config::with_none(); 312 | c.icons = Some(Icons { 313 | when: None, 314 | theme: Some(IconTheme::Fancy), 315 | separator: None, 316 | }); 317 | assert_eq!(Some(IconTheme::Fancy), IconTheme::from_config(&c)); 318 | } 319 | 320 | #[test] 321 | fn test_from_config_unicode() { 322 | let mut c = Config::with_none(); 323 | c.icons = Some(Icons { 324 | when: None, 325 | theme: Some(IconTheme::Unicode), 326 | separator: None, 327 | }); 328 | assert_eq!(Some(IconTheme::Unicode), IconTheme::from_config(&c)); 329 | } 330 | } 331 | 332 | #[cfg(test)] 333 | mod test_icon_separator { 334 | use super::IconSeparator; 335 | 336 | use crate::config_file::{Config, Icons}; 337 | use crate::flags::Configurable; 338 | 339 | #[test] 340 | fn test_from_config_default() { 341 | let mut c = Config::with_none(); 342 | c.icons = Some(Icons { 343 | when: None, 344 | theme: None, 345 | separator: Some(" ".to_string()), 346 | }); 347 | let expected = Some(IconSeparator(" ".to_string())); 348 | assert_eq!(expected, IconSeparator::from_config(&c)); 349 | } 350 | 351 | #[test] 352 | fn test_from_config_custom() { 353 | let mut c = Config::with_none(); 354 | c.icons = Some(Icons { 355 | when: None, 356 | theme: None, 357 | separator: Some(" |".to_string()), 358 | }); 359 | let expected = Some(IconSeparator(" |".to_string())); 360 | assert_eq!(expected, IconSeparator::from_config(&c)); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/flags/ignore_globs.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [IgnoreGlobs]. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use the [configure_from](IgnoreGlobs::configure_from) method. 3 | 4 | use crate::app::Cli; 5 | use crate::config_file::Config; 6 | 7 | use clap::error::ErrorKind; 8 | use clap::Error; 9 | use globset::{Glob, GlobSet, GlobSetBuilder}; 10 | 11 | /// The struct holding a [GlobSet] and methods to build it. 12 | #[derive(Clone, Debug)] 13 | pub struct IgnoreGlobs(pub GlobSet); 14 | 15 | impl IgnoreGlobs { 16 | /// Returns a value from either [Cli], a [Config] or a [Default] value. The first value 17 | /// that is not [None] is used. The order of precedence for the value used is: 18 | /// - [from_cli](IgnoreGlobs::from_cli) 19 | /// - [from_config](IgnoreGlobs::from_config) 20 | /// - [Default::default] 21 | /// 22 | /// # Errors 23 | /// 24 | /// If either of the [Glob::new] or [GlobSetBuilder.build] methods return an [Err]. 25 | pub fn configure_from(cli: &Cli, config: &Config) -> Result { 26 | if let Some(value) = Self::from_cli(cli) { 27 | return value; 28 | } 29 | 30 | if let Some(value) = Self::from_config(config) { 31 | return value; 32 | } 33 | 34 | Ok(Default::default()) 35 | } 36 | 37 | /// Get a potential [IgnoreGlobs] from [Cli]. 38 | /// 39 | /// If the "ignore-glob" argument has been passed, this returns a [Result] in a [Some] with 40 | /// either the built [IgnoreGlobs] or an [Error], if any error was encountered while creating the 41 | /// [IgnoreGlobs]. If the argument has not been passed, this returns [None]. 42 | fn from_cli(cli: &Cli) -> Option> { 43 | if cli.ignore_glob.is_empty() { 44 | return None; 45 | } 46 | 47 | let mut glob_set_builder = GlobSetBuilder::new(); 48 | 49 | for value in &cli.ignore_glob { 50 | match Self::create_glob(value) { 51 | Ok(glob) => { 52 | glob_set_builder.add(glob); 53 | } 54 | Err(err) => return Some(Err(err)), 55 | } 56 | } 57 | 58 | Some(Self::create_glob_set(&glob_set_builder).map(Self)) 59 | } 60 | 61 | /// Get a potential [IgnoreGlobs] from a [Config]. 62 | /// 63 | /// If the `Config::ignore-globs` contains an Array of Strings, 64 | /// each of its values is used to build the [GlobSet]. If the building 65 | /// succeeds, the [IgnoreGlobs] is returned in the [Result] in a [Some]. If any error is 66 | /// encountered while building, an [Error] is returned in the Result instead. If the Config does 67 | /// not contain such a key, this returns [None]. 68 | fn from_config(config: &Config) -> Option> { 69 | let globs = config.ignore_globs.as_ref()?; 70 | let mut glob_set_builder = GlobSetBuilder::new(); 71 | 72 | for glob in globs { 73 | match Self::create_glob(glob) { 74 | Ok(glob) => { 75 | glob_set_builder.add(glob); 76 | } 77 | Err(err) => return Some(Err(err)), 78 | } 79 | } 80 | 81 | Some(Self::create_glob_set(&glob_set_builder).map(Self)) 82 | } 83 | 84 | /// Create a [Glob] from a provided pattern. 85 | /// 86 | /// This method is mainly a helper to wrap the handling of potential errors. 87 | fn create_glob(pattern: &str) -> Result { 88 | Glob::new(pattern).map_err(|err| Error::raw(ErrorKind::ValueValidation, err)) 89 | } 90 | 91 | /// Create a [GlobSet] from a provided [GlobSetBuilder]. 92 | /// 93 | /// This method is mainly a helper to wrap the handling of potential errors. 94 | fn create_glob_set(builder: &GlobSetBuilder) -> Result { 95 | builder 96 | .build() 97 | .map_err(|err| Error::raw(ErrorKind::ValueValidation, err)) 98 | } 99 | } 100 | 101 | /// The default value of `IgnoreGlobs` is the empty [GlobSet], returned by [GlobSet::empty()]. 102 | impl Default for IgnoreGlobs { 103 | fn default() -> Self { 104 | Self(GlobSet::empty()) 105 | } 106 | } 107 | 108 | #[cfg(test)] 109 | mod test { 110 | use clap::Parser; 111 | 112 | use super::IgnoreGlobs; 113 | 114 | use crate::app::Cli; 115 | use crate::config_file::Config; 116 | 117 | // The following tests are implemented using match expressions instead of the assert_eq macro, 118 | // because clap::Error does not implement PartialEq. 119 | // 120 | // Further no tests for actually returned GlobSets are implemented, because GlobSet does not 121 | // even implement PartialEq and thus can not be easily compared. 122 | 123 | #[test] 124 | fn test_configuration_from_none() { 125 | let argv = ["lsd"]; 126 | let cli = Cli::try_parse_from(argv).unwrap(); 127 | assert!(matches!( 128 | IgnoreGlobs::configure_from(&cli, &Config::with_none()), 129 | Ok(..) 130 | )); 131 | } 132 | 133 | #[test] 134 | fn test_configuration_from_args() { 135 | let argv = ["lsd", "--ignore-glob", ".git"]; 136 | let cli = Cli::try_parse_from(argv).unwrap(); 137 | assert!(matches!( 138 | IgnoreGlobs::configure_from(&cli, &Config::with_none()), 139 | Ok(..) 140 | )); 141 | } 142 | 143 | #[test] 144 | fn test_configuration_from_config() { 145 | let argv = ["lsd"]; 146 | let cli = Cli::try_parse_from(argv).unwrap(); 147 | let mut c = Config::with_none(); 148 | c.ignore_globs = Some(vec![".git".into()]); 149 | assert!(matches!(IgnoreGlobs::configure_from(&cli, &c), Ok(..))); 150 | } 151 | 152 | #[test] 153 | fn test_from_cli_none() { 154 | let argv = ["lsd"]; 155 | let cli = Cli::try_parse_from(argv).unwrap(); 156 | assert!(IgnoreGlobs::from_cli(&cli).is_none()); 157 | } 158 | 159 | #[test] 160 | fn test_from_config_none() { 161 | assert!(IgnoreGlobs::from_config(&Config::with_none()).is_none()); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/flags/indicators.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [Indicators] flag. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use the [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::Cli; 7 | use crate::config_file::Config; 8 | 9 | /// The flag showing whether to print file type indicators. 10 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] 11 | pub struct Indicators(pub bool); 12 | 13 | impl Configurable for Indicators { 14 | /// Get a potential `Indicators` value from [Cli]. 15 | /// 16 | /// If the "indicators" argument is passed, this returns an `Indicators` with value `true` in a 17 | /// [Some]. Otherwise this returns [None]. 18 | fn from_cli(cli: &Cli) -> Option { 19 | if cli.indicators { 20 | Some(Self(true)) 21 | } else { 22 | None 23 | } 24 | } 25 | 26 | /// Get a potential `Indicators` value from a [Config]. 27 | /// 28 | /// If the `Config::indicators` has value, 29 | /// this returns its value as the value of the `Indicators`, in a [Some]. 30 | /// Otherwise this returns [None]. 31 | fn from_config(config: &Config) -> Option { 32 | config.indicators.as_ref().map(|ind| Self(*ind)) 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod test { 38 | use clap::Parser; 39 | 40 | use super::Indicators; 41 | 42 | use crate::app::Cli; 43 | use crate::config_file::Config; 44 | use crate::flags::Configurable; 45 | 46 | #[test] 47 | fn test_from_cli_none() { 48 | let argv = ["lsd"]; 49 | let cli = Cli::try_parse_from(argv).unwrap(); 50 | assert_eq!(None, Indicators::from_cli(&cli)); 51 | } 52 | 53 | #[test] 54 | fn test_from_cli_true() { 55 | let argv = ["lsd", "--classify"]; 56 | let cli = Cli::try_parse_from(argv).unwrap(); 57 | assert_eq!(Some(Indicators(true)), Indicators::from_cli(&cli)); 58 | } 59 | 60 | #[test] 61 | fn test_from_config_none() { 62 | assert_eq!(None, Indicators::from_config(&Config::with_none())); 63 | } 64 | 65 | #[test] 66 | fn test_from_config_true() { 67 | let mut c = Config::with_none(); 68 | c.indicators = Some(true); 69 | assert_eq!(Some(Indicators(true)), Indicators::from_config(&c)); 70 | } 71 | 72 | #[test] 73 | fn test_from_config_false() { 74 | let mut c = Config::with_none(); 75 | c.indicators = Some(false); 76 | assert_eq!(Some(Indicators(false)), Indicators::from_config(&c)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/flags/layout.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [Layout] flag. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use its [configure_from](Configurable::configure_from) method. 3 | 4 | use crate::app::Cli; 5 | use crate::config_file::Config; 6 | 7 | use super::Configurable; 8 | 9 | use serde::Deserialize; 10 | 11 | /// The flag showing which output layout to print. 12 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] 13 | #[serde(rename_all = "lowercase")] 14 | pub enum Layout { 15 | #[default] 16 | Grid, 17 | Tree, 18 | OneLine, 19 | } 20 | 21 | impl Configurable for Layout { 22 | /// Get a potential `Layout` variant from [Cli]. 23 | /// 24 | /// If any of the "tree", "long" or "oneline" arguments is passed, this returns the 25 | /// corresponding `Layout` variant in a [Some]. Otherwise if the number of passed "blocks" 26 | /// arguments is greater than 1, this also returns the [OneLine](Layout::OneLine) variant. 27 | /// Finally if neither of them is passed, this returns [None]. 28 | fn from_cli(cli: &Cli) -> Option { 29 | if cli.tree { 30 | Some(Self::Tree) 31 | } else if cli.long || cli.oneline || cli.inode || cli.context || cli.blocks.len() > 1 32 | // TODO: handle this differently 33 | { 34 | Some(Self::OneLine) 35 | } else { 36 | None 37 | } 38 | } 39 | 40 | /// Get a potential Layout variant from a [Config]. 41 | /// 42 | /// If the `Config::layout` has value and is one of "tree", "oneline" or "grid", 43 | /// this returns the corresponding `Layout` variant in a [Some]. 44 | /// Otherwise this returns [None]. 45 | fn from_config(config: &Config) -> Option { 46 | config.layout 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | mod test { 52 | use clap::Parser; 53 | 54 | use super::Layout; 55 | 56 | use crate::app::Cli; 57 | use crate::config_file::Config; 58 | use crate::flags::Configurable; 59 | 60 | #[test] 61 | fn test_from_cli_none() { 62 | let argv = ["lsd"]; 63 | let cli = Cli::try_parse_from(argv).unwrap(); 64 | assert_eq!(None, Layout::from_cli(&cli)); 65 | } 66 | 67 | #[test] 68 | fn test_from_cli_tree() { 69 | let argv = ["lsd", "--tree"]; 70 | let cli = Cli::try_parse_from(argv).unwrap(); 71 | assert_eq!(Some(Layout::Tree), Layout::from_cli(&cli)); 72 | } 73 | 74 | #[test] 75 | fn test_from_cli_oneline() { 76 | let argv = ["lsd", "--oneline"]; 77 | let cli = Cli::try_parse_from(argv).unwrap(); 78 | assert_eq!(Some(Layout::OneLine), Layout::from_cli(&cli)); 79 | } 80 | 81 | #[test] 82 | fn test_from_cli_oneline_through_long() { 83 | let argv = ["lsd", "--long"]; 84 | let cli = Cli::try_parse_from(argv).unwrap(); 85 | assert_eq!(Some(Layout::OneLine), Layout::from_cli(&cli)); 86 | } 87 | 88 | #[test] 89 | fn test_from_cli_oneline_through_blocks() { 90 | let argv = ["lsd", "--blocks", "permission,name"]; 91 | let cli = Cli::try_parse_from(argv).unwrap(); 92 | assert_eq!(Some(Layout::OneLine), Layout::from_cli(&cli)); 93 | } 94 | 95 | #[test] 96 | fn test_from_config_none() { 97 | assert_eq!(None, Layout::from_config(&Config::with_none())); 98 | } 99 | 100 | #[test] 101 | fn test_from_config_tree() { 102 | let mut c = Config::with_none(); 103 | c.layout = Some(Layout::Tree); 104 | assert_eq!(Some(Layout::Tree), Layout::from_config(&c)); 105 | } 106 | 107 | #[test] 108 | fn test_from_config_oneline() { 109 | let mut c = Config::with_none(); 110 | c.layout = Some(Layout::OneLine); 111 | assert_eq!(Some(Layout::OneLine), Layout::from_config(&c)); 112 | } 113 | 114 | #[test] 115 | fn test_from_config_grid() { 116 | let mut c = Config::with_none(); 117 | c.layout = Some(Layout::Grid); 118 | assert_eq!(Some(Layout::Grid), Layout::from_config(&c)); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/flags/literal.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [Literal]. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use its [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::Cli; 7 | use crate::config_file::Config; 8 | 9 | /// The flag to set in order to show literal file names without quotes. 10 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] 11 | pub struct Literal(pub bool); 12 | 13 | impl Configurable for Literal { 14 | /// Get a potential `Literal` value from [Cli]. 15 | /// 16 | /// If the "literal" argument is passed, this returns a `Literal` with value `true` in a 17 | /// [Some]. Otherwise this returns [None]. 18 | fn from_cli(cli: &Cli) -> Option { 19 | if cli.literal { 20 | Some(Self(true)) 21 | } else { 22 | None 23 | } 24 | } 25 | 26 | /// Get a potential `Literal` value from a [Config]. 27 | /// 28 | /// If the `Config::indicators` has value, 29 | /// this returns its value as the value of the `Literal`, in a [Some]. 30 | /// Otherwise this returns [None]. 31 | fn from_config(config: &Config) -> Option { 32 | config.literal.map(Self) 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod test { 38 | use clap::Parser; 39 | 40 | use super::Literal; 41 | 42 | use crate::app::Cli; 43 | use crate::config_file::Config; 44 | use crate::flags::Configurable; 45 | 46 | #[test] 47 | fn test_from_cli_none() { 48 | let argv = ["lsd"]; 49 | let cli = Cli::try_parse_from(argv).unwrap(); 50 | assert_eq!(None, Literal::from_cli(&cli)); 51 | } 52 | 53 | #[test] 54 | fn test_from_cli_literal() { 55 | let argv = ["lsd", "--literal"]; 56 | let cli = Cli::try_parse_from(argv).unwrap(); 57 | assert_eq!(Some(Literal(true)), Literal::from_cli(&cli)); 58 | } 59 | 60 | #[test] 61 | fn test_from_config_none() { 62 | assert_eq!(None, Literal::from_config(&Config::with_none())); 63 | } 64 | 65 | #[test] 66 | fn test_from_config_true() { 67 | let mut c = Config::with_none(); 68 | c.literal = Some(true); 69 | assert_eq!(Some(Literal(true)), Literal::from_config(&c)); 70 | } 71 | 72 | #[test] 73 | fn test_from_config_false() { 74 | let mut c = Config::with_none(); 75 | c.literal = Some(false); 76 | assert_eq!(Some(Literal(false)), Literal::from_config(&c)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/flags/permission.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [PermissionFlag]. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use its [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::Cli; 7 | use crate::config_file::Config; 8 | 9 | use serde::Deserialize; 10 | 11 | /// The flag showing which file permissions units to use. 12 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] 13 | #[serde(rename_all = "kebab-case")] 14 | pub enum PermissionFlag { 15 | /// The variant to show file permissions in rwx format 16 | #[cfg_attr(not(target_os = "windows"), default)] 17 | Rwx, 18 | /// The variant to show file permissions in octal format 19 | Octal, 20 | /// (windows only): Attributes from powershell's `Get-ChildItem` 21 | #[cfg_attr(target_os = "windows", default)] 22 | Attributes, 23 | /// Disable the display of owner and permissions, may be used to speed up in Windows 24 | Disable, 25 | } 26 | 27 | impl PermissionFlag { 28 | fn from_arg_str(value: &str) -> Self { 29 | match value { 30 | "rwx" => Self::Rwx, 31 | "octal" => Self::Octal, 32 | "attributes" => Self::Attributes, 33 | "disable" => Self::Disable, 34 | // Invalid value should be handled by `clap` when building an `Cli` 35 | other => unreachable!("Invalid value '{other}' for 'permission'"), 36 | } 37 | } 38 | } 39 | 40 | impl Configurable for PermissionFlag { 41 | /// Get a potential `PermissionFlag` variant from [Cli]. 42 | /// 43 | /// If any of the "rwx" or "octal" arguments is passed, the corresponding 44 | /// `PermissionFlag` variant is returned in a [Some]. If neither of them is passed, 45 | /// this returns [None]. 46 | /// Sets permissions to rwx if classic flag is enabled. 47 | fn from_cli(cli: &Cli) -> Option { 48 | if cli.classic { 49 | Some(Self::Rwx) 50 | } else { 51 | cli.permission.as_deref().map(Self::from_arg_str) 52 | } 53 | } 54 | 55 | /// Get a potential `PermissionFlag` variant from a [Config]. 56 | /// 57 | /// If the `Config::permissions` has value and is one of "rwx" or "octal", 58 | /// this returns the corresponding `PermissionFlag` variant in a [Some]. 59 | /// Otherwise this returns [None]. 60 | /// Sets permissions to rwx if classic flag is enabled. 61 | fn from_config(config: &Config) -> Option { 62 | if config.classic == Some(true) { 63 | Some(Self::Rwx) 64 | } else { 65 | config.permission 66 | } 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | mod test { 72 | use clap::Parser; 73 | 74 | use super::PermissionFlag; 75 | 76 | use crate::app::Cli; 77 | use crate::config_file::Config; 78 | use crate::flags::Configurable; 79 | 80 | #[test] 81 | fn test_default() { 82 | let expected = if cfg!(target_os = "windows") { 83 | PermissionFlag::Attributes 84 | } else { 85 | PermissionFlag::Rwx 86 | }; 87 | assert_eq!(expected, PermissionFlag::default()); 88 | } 89 | 90 | #[test] 91 | fn test_from_cli_none() { 92 | let argv = ["lsd"]; 93 | let cli = Cli::try_parse_from(argv).unwrap(); 94 | assert_eq!(None, PermissionFlag::from_cli(&cli)); 95 | } 96 | 97 | #[test] 98 | fn test_from_cli_default() { 99 | let argv = ["lsd", "--permission", "rwx"]; 100 | let cli = Cli::try_parse_from(argv).unwrap(); 101 | assert_eq!(Some(PermissionFlag::Rwx), PermissionFlag::from_cli(&cli)); 102 | } 103 | 104 | #[test] 105 | fn test_from_cli_short() { 106 | let argv = ["lsd", "--permission", "octal"]; 107 | let cli = Cli::try_parse_from(argv).unwrap(); 108 | assert_eq!(Some(PermissionFlag::Octal), PermissionFlag::from_cli(&cli)); 109 | } 110 | 111 | #[test] 112 | fn test_from_cli_attributes() { 113 | let argv = ["lsd", "--permission", "attributes"]; 114 | let cli = Cli::try_parse_from(argv).unwrap(); 115 | assert_eq!( 116 | Some(PermissionFlag::Attributes), 117 | PermissionFlag::from_cli(&cli) 118 | ); 119 | } 120 | 121 | #[test] 122 | fn test_from_cli_permissions_disable() { 123 | let argv = ["lsd", "--permission", "disable"]; 124 | let cli = Cli::try_parse_from(argv).unwrap(); 125 | assert_eq!( 126 | Some(PermissionFlag::Disable), 127 | PermissionFlag::from_cli(&cli) 128 | ); 129 | } 130 | 131 | #[test] 132 | #[should_panic] 133 | fn test_from_cli_unknown() { 134 | let argv = ["lsd", "--permission", "unknown"]; 135 | let _ = Cli::try_parse_from(argv).unwrap(); 136 | } 137 | #[test] 138 | fn test_from_cli_permissions_multi() { 139 | let argv = ["lsd", "--permission", "octal", "--permission", "rwx"]; 140 | let cli = Cli::try_parse_from(argv).unwrap(); 141 | assert_eq!(Some(PermissionFlag::Rwx), PermissionFlag::from_cli(&cli)); 142 | } 143 | 144 | #[test] 145 | fn test_from_cli_permissions_classic() { 146 | let argv = ["lsd", "--permission", "rwx", "--classic"]; 147 | let cli = Cli::try_parse_from(argv).unwrap(); 148 | assert_eq!(Some(PermissionFlag::Rwx), PermissionFlag::from_cli(&cli)); 149 | } 150 | 151 | #[test] 152 | fn test_from_config_none() { 153 | assert_eq!(None, PermissionFlag::from_config(&Config::with_none())); 154 | } 155 | 156 | #[test] 157 | fn test_from_config_rwx() { 158 | let mut c = Config::with_none(); 159 | c.permission = Some(PermissionFlag::Rwx); 160 | assert_eq!(Some(PermissionFlag::Rwx), PermissionFlag::from_config(&c)); 161 | } 162 | 163 | #[test] 164 | fn test_from_config_octal() { 165 | let mut c = Config::with_none(); 166 | c.permission = Some(PermissionFlag::Octal); 167 | assert_eq!(Some(PermissionFlag::Octal), PermissionFlag::from_config(&c)); 168 | } 169 | 170 | #[test] 171 | fn test_from_config_classic_mode() { 172 | let mut c = Config::with_none(); 173 | c.classic = Some(true); 174 | assert_eq!(Some(PermissionFlag::Rwx), PermissionFlag::from_config(&c)); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/flags/recursion.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [Recursion] options. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use the [configure_from](Recursion::configure_from) method. 3 | 4 | use crate::app::Cli; 5 | use crate::config_file::Config; 6 | 7 | /// The options relating to recursion. 8 | #[derive(Clone, Debug, Copy, PartialEq, Eq)] 9 | pub struct Recursion { 10 | /// Whether the recursion into directories is enabled. 11 | pub enabled: bool, 12 | /// The depth for how far to recurse into directories. 13 | pub depth: usize, 14 | } 15 | 16 | impl Recursion { 17 | /// Get the Recursion from either [Cli], a [Config] or the [Default] value. 18 | /// 19 | /// The "enabled" value is determined by [enabled_from](Recursion::enabled_from) and the depth 20 | /// value is determined by [depth_from](Recursion::depth_from). 21 | /// 22 | /// # Errors 23 | /// 24 | /// If [depth_from](Recursion::depth_from) returns an [Error], this returns it. 25 | pub fn configure_from(cli: &Cli, config: &Config) -> Self { 26 | let enabled = Self::enabled_from(cli, config); 27 | let depth = Self::depth_from(cli, config); 28 | Self { enabled, depth } 29 | } 30 | 31 | /// Get the "enabled" boolean from [Cli], a [Config] or the [Default] value. The first 32 | /// value that is not [None] is used. The order of precedence for the value used is: 33 | /// - [enabled_from_cli](Recursion::enabled_from_cli) 34 | /// - [Config.recursion.enabled] 35 | /// - [Default::default] 36 | fn enabled_from(cli: &Cli, config: &Config) -> bool { 37 | if let Some(value) = Self::enabled_from_cli(cli) { 38 | return value; 39 | } 40 | if let Some(recursion) = &config.recursion { 41 | if let Some(enabled) = recursion.enabled { 42 | return enabled; 43 | } 44 | } 45 | 46 | Default::default() 47 | } 48 | 49 | /// Get a potential "enabled" boolean from [Cli]. 50 | /// 51 | /// If the "recursive" argument is passed, this returns `true` in a [Some]. Otherwise this 52 | /// returns [None]. 53 | fn enabled_from_cli(cli: &Cli) -> Option { 54 | if cli.recursive { 55 | Some(true) 56 | } else { 57 | None 58 | } 59 | } 60 | 61 | /// Get the "depth" integer from [Cli], a [Config] or the [Default] value. The first 62 | /// value that is not [None] is used. The order of precedence for the value used is: 63 | /// - Cli::depth 64 | /// - [Config.recursion.depth] 65 | /// - [Default::default] 66 | /// 67 | /// # Note 68 | /// 69 | /// If both configuration file and Args is error, this will return a Max-Uint value. 70 | fn depth_from(cli: &Cli, config: &Config) -> usize { 71 | if let Some(value) = cli.depth { 72 | return value; 73 | } 74 | 75 | use crate::config_file::Recursion; 76 | if let Some(Recursion { 77 | depth: Some(value), .. 78 | }) = &config.recursion 79 | { 80 | return *value; 81 | } 82 | 83 | usize::MAX 84 | } 85 | } 86 | 87 | /// The default values for `Recursion` are the boolean default and [prim@usize::max_value()]. 88 | impl Default for Recursion { 89 | fn default() -> Self { 90 | Self { 91 | depth: usize::MAX, 92 | enabled: false, 93 | } 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod test { 99 | use clap::error::ErrorKind; 100 | use clap::Parser; 101 | 102 | use super::Recursion; 103 | 104 | use crate::app::Cli; 105 | use crate::config_file::{self, Config}; 106 | 107 | #[test] 108 | fn test_enabled_from_cli_empty() { 109 | let argv = ["lsd"]; 110 | let cli = Cli::try_parse_from(argv).unwrap(); 111 | assert_eq!(None, Recursion::enabled_from_cli(&cli)); 112 | } 113 | 114 | #[test] 115 | fn test_enabled_from_cli_true() { 116 | let argv = ["lsd", "--recursive"]; 117 | let cli = Cli::try_parse_from(argv).unwrap(); 118 | assert_eq!(Some(true), Recursion::enabled_from_cli(&cli)); 119 | } 120 | 121 | #[test] 122 | fn test_enabled_from_empty_matches_and_config() { 123 | let argv = ["lsd"]; 124 | assert!(!Recursion::enabled_from( 125 | &Cli::try_parse_from(argv).unwrap(), 126 | &Config::with_none() 127 | )); 128 | } 129 | 130 | #[test] 131 | fn test_enabled_from_matches_empty_and_config_true() { 132 | let argv = ["lsd"]; 133 | let mut c = Config::with_none(); 134 | c.recursion = Some(config_file::Recursion { 135 | enabled: Some(true), 136 | depth: None, 137 | }); 138 | assert!(Recursion::enabled_from( 139 | &Cli::try_parse_from(argv).unwrap(), 140 | &c 141 | )); 142 | } 143 | 144 | #[test] 145 | fn test_enabled_from_matches_empty_and_config_false() { 146 | let argv = ["lsd"]; 147 | let mut c = Config::with_none(); 148 | c.recursion = Some(config_file::Recursion { 149 | enabled: Some(false), 150 | depth: None, 151 | }); 152 | assert!(!Recursion::enabled_from( 153 | &Cli::try_parse_from(argv).unwrap(), 154 | &c 155 | )); 156 | } 157 | 158 | // The following depth_from_cli tests are implemented using match expressions instead 159 | // of the assert_eq macro, because clap::Error does not implement PartialEq. 160 | 161 | #[test] 162 | fn test_depth_from_cli_empty() { 163 | let argv = ["lsd"]; 164 | let cli = Cli::try_parse_from(argv).unwrap(); 165 | assert!(cli.depth.is_none()); 166 | } 167 | 168 | #[test] 169 | fn test_depth_from_cli_integer() { 170 | let argv = ["lsd", "--depth", "42"]; 171 | let cli = Cli::try_parse_from(argv).unwrap(); 172 | assert!(matches!(cli.depth, Some(42))); 173 | } 174 | 175 | #[test] 176 | fn test_depth_from_cli_depth_multi() { 177 | let argv = ["lsd", "--depth", "4", "--depth", "2"]; 178 | let cli = Cli::try_parse_from(argv).unwrap(); 179 | assert!(matches!(cli.depth, Some(2))); 180 | } 181 | 182 | #[test] 183 | fn test_depth_from_cli_neg_int() { 184 | let argv = ["lsd", "--depth", "\\-42"]; 185 | let cli = Cli::try_parse_from(argv); 186 | assert!(matches!(cli, Err(e) if e.kind() == ErrorKind::ValueValidation)); 187 | } 188 | 189 | #[test] 190 | fn test_depth_from_cli_non_int() { 191 | let argv = ["lsd", "--depth", "foo"]; 192 | let cli = Cli::try_parse_from(argv); 193 | assert!(matches!(cli, Err(e) if e.kind() == ErrorKind::ValueValidation)); 194 | } 195 | 196 | #[test] 197 | fn test_depth_from_config_none_max() { 198 | let argv = ["lsd"]; 199 | let cli = Cli::try_parse_from(argv).unwrap(); 200 | assert_eq!( 201 | usize::MAX, 202 | Recursion::depth_from(&cli, &Config::with_none()) 203 | ); 204 | } 205 | 206 | #[test] 207 | fn test_depth_from_config_pos_integer() { 208 | let argv = ["lsd"]; 209 | let mut c = Config::with_none(); 210 | c.recursion = Some(config_file::Recursion { 211 | enabled: None, 212 | depth: Some(42), 213 | }); 214 | assert_eq!( 215 | 42, 216 | Recursion::depth_from(&Cli::try_parse_from(argv).unwrap(), &c) 217 | ); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/flags/size.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [SizeFlag]. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use its [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::Cli; 7 | use crate::config_file::Config; 8 | 9 | use serde::Deserialize; 10 | 11 | /// The flag showing which file size units to use. 12 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] 13 | #[serde(rename_all = "kebab-case")] 14 | pub enum SizeFlag { 15 | /// The variant to show file size with SI unit prefix and a B for bytes. 16 | #[default] 17 | Default, 18 | /// The variant to show file size with only the SI unit prefix. 19 | Short, 20 | /// The variant to show file size in bytes. 21 | Bytes, 22 | } 23 | 24 | impl SizeFlag { 25 | fn from_arg_str(value: &str) -> Self { 26 | match value { 27 | "default" => Self::Default, 28 | "short" => Self::Short, 29 | "bytes" => Self::Bytes, 30 | // Invalid value should be handled by `clap` when building an `Cli` 31 | other => unreachable!("Invalid value '{other}' for 'size'"), 32 | } 33 | } 34 | } 35 | 36 | impl Configurable for SizeFlag { 37 | /// Get a potential `SizeFlag` variant from [Cli]. 38 | /// 39 | /// If any of the "default", "short" or "bytes" arguments is passed, the corresponding 40 | /// `SizeFlag` variant is returned in a [Some]. If neither of them is passed, this returns 41 | /// [None]. 42 | fn from_cli(cli: &Cli) -> Option { 43 | if cli.classic { 44 | Some(Self::Bytes) 45 | } else { 46 | cli.size.as_deref().map(Self::from_arg_str) 47 | } 48 | } 49 | 50 | /// Get a potential `SizeFlag` variant from a [Config]. 51 | /// 52 | /// If the `Config::size` has value and is one of "default", "short" or "bytes", 53 | /// this returns the corresponding `SizeFlag` variant in a [Some]. 54 | /// Otherwise this returns [None]. 55 | fn from_config(config: &Config) -> Option { 56 | if config.classic == Some(true) { 57 | Some(Self::Bytes) 58 | } else { 59 | config.size 60 | } 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod test { 66 | use clap::Parser; 67 | 68 | use super::SizeFlag; 69 | 70 | use crate::app::Cli; 71 | use crate::config_file::Config; 72 | use crate::flags::Configurable; 73 | 74 | #[test] 75 | fn test_default() { 76 | assert_eq!(SizeFlag::Default, SizeFlag::default()); 77 | } 78 | 79 | #[test] 80 | fn test_from_cli_none() { 81 | let argv = ["lsd"]; 82 | let cli = Cli::try_parse_from(argv).unwrap(); 83 | assert_eq!(None, SizeFlag::from_cli(&cli)); 84 | } 85 | 86 | #[test] 87 | fn test_from_cli_default() { 88 | let argv = ["lsd", "--size", "default"]; 89 | let cli = Cli::try_parse_from(argv).unwrap(); 90 | assert_eq!(Some(SizeFlag::Default), SizeFlag::from_cli(&cli)); 91 | } 92 | 93 | #[test] 94 | fn test_from_cli_short() { 95 | let argv = ["lsd", "--size", "short"]; 96 | let cli = Cli::try_parse_from(argv).unwrap(); 97 | assert_eq!(Some(SizeFlag::Short), SizeFlag::from_cli(&cli)); 98 | } 99 | 100 | #[test] 101 | fn test_from_cli_bytes() { 102 | let argv = ["lsd", "--size", "bytes"]; 103 | let cli = Cli::try_parse_from(argv).unwrap(); 104 | assert_eq!(Some(SizeFlag::Bytes), SizeFlag::from_cli(&cli)); 105 | } 106 | 107 | #[test] 108 | #[should_panic] 109 | fn test_from_cli_unknown() { 110 | let argv = ["lsd", "--size", "unknown"]; 111 | let _ = Cli::try_parse_from(argv).unwrap(); 112 | } 113 | #[test] 114 | fn test_from_cli_size_multi() { 115 | let argv = ["lsd", "--size", "bytes", "--size", "short"]; 116 | let cli = Cli::try_parse_from(argv).unwrap(); 117 | assert_eq!(Some(SizeFlag::Short), SizeFlag::from_cli(&cli)); 118 | } 119 | 120 | #[test] 121 | fn test_from_cli_size_classic() { 122 | let argv = ["lsd", "--size", "short", "--classic"]; 123 | let cli = Cli::try_parse_from(argv).unwrap(); 124 | assert_eq!(Some(SizeFlag::Bytes), SizeFlag::from_cli(&cli)); 125 | } 126 | 127 | #[test] 128 | fn test_from_config_none() { 129 | assert_eq!(None, SizeFlag::from_config(&Config::with_none())); 130 | } 131 | 132 | #[test] 133 | fn test_from_config_default() { 134 | let mut c = Config::with_none(); 135 | c.size = Some(SizeFlag::Default); 136 | assert_eq!(Some(SizeFlag::Default), SizeFlag::from_config(&c)); 137 | } 138 | 139 | #[test] 140 | fn test_from_config_short() { 141 | let mut c = Config::with_none(); 142 | c.size = Some(SizeFlag::Short); 143 | assert_eq!(Some(SizeFlag::Short), SizeFlag::from_config(&c)); 144 | } 145 | 146 | #[test] 147 | fn test_from_config_bytes() { 148 | let mut c = Config::with_none(); 149 | c.size = Some(SizeFlag::Bytes); 150 | assert_eq!(Some(SizeFlag::Bytes), SizeFlag::from_config(&c)); 151 | } 152 | 153 | #[test] 154 | fn test_from_config_classic_mode() { 155 | let mut c = Config::with_none(); 156 | c.classic = Some(true); 157 | assert_eq!(Some(SizeFlag::Bytes), SizeFlag::from_config(&c)); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/flags/symlink_arrow.rs: -------------------------------------------------------------------------------- 1 | use super::Configurable; 2 | 3 | use crate::app::Cli; 4 | use crate::config_file::Config; 5 | 6 | /// The flag showing how to display symbolic arrow. 7 | #[derive(Clone, Debug, Eq, PartialEq)] 8 | pub struct SymlinkArrow(String); 9 | 10 | impl Configurable for SymlinkArrow { 11 | /// `SymlinkArrow` can not be configured by [Cli] 12 | /// 13 | /// Return `None` 14 | fn from_cli(_: &Cli) -> Option { 15 | None 16 | } 17 | /// Get a potential `SymlinkArrow` value from a [Config]. 18 | /// 19 | /// If the `Config::symlink-arrow` has value, 20 | /// returns its value as the value of the `SymlinkArrow`, in a [Some]. 21 | /// Otherwise this returns [None]. 22 | fn from_config(config: &Config) -> Option { 23 | config 24 | .symlink_arrow 25 | .as_ref() 26 | .map(|arrow| SymlinkArrow(arrow.to_string())) 27 | } 28 | } 29 | 30 | /// The default value for the `SymlinkArrow` is `\u{21d2}(⇒)` 31 | impl Default for SymlinkArrow { 32 | fn default() -> Self { 33 | Self(String::from("\u{21d2}")) // ⇒ 34 | } 35 | } 36 | 37 | use std::fmt; 38 | impl fmt::Display for SymlinkArrow { 39 | // This trait requires `fmt` with this exact signature. 40 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 41 | write!(f, "{}", self.0) 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod test { 47 | use clap::Parser; 48 | 49 | use crate::app::Cli; 50 | use crate::config_file::Config; 51 | use crate::flags::Configurable; 52 | 53 | use super::SymlinkArrow; 54 | #[test] 55 | fn test_symlink_arrow_from_config_utf8() { 56 | let mut c = Config::with_none(); 57 | c.symlink_arrow = Some("↹".into()); 58 | assert_eq!( 59 | Some(SymlinkArrow(String::from("\u{21B9}"))), 60 | SymlinkArrow::from_config(&c) 61 | ); 62 | } 63 | 64 | #[test] 65 | fn test_symlink_arrow_from_args_none() { 66 | let argv = ["lsd"]; 67 | let cli = Cli::try_parse_from(argv).unwrap(); 68 | assert_eq!(None, SymlinkArrow::from_cli(&cli)); 69 | } 70 | 71 | #[test] 72 | fn test_symlink_arrow_default() { 73 | assert_eq!( 74 | SymlinkArrow(String::from("\u{21d2}")), 75 | SymlinkArrow::default() 76 | ); 77 | } 78 | 79 | #[test] 80 | fn test_symlink_display() { 81 | assert_eq!("⇒", format!("{}", SymlinkArrow::default())); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/flags/symlinks.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [NoSymlink] flag. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use the [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::Cli; 7 | use crate::config_file::Config; 8 | 9 | /// The flag showing whether to follow symbolic links. 10 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] 11 | pub struct NoSymlink(pub bool); 12 | 13 | impl Configurable for NoSymlink { 14 | /// Get a potential `NoSymlink` value from [Cli]. 15 | /// 16 | /// If the "no-symlink" argument is passed, this returns a `NoSymlink` with value `true` in a 17 | /// [Some]. Otherwise this returns [None]. 18 | fn from_cli(cli: &Cli) -> Option { 19 | if cli.no_symlink { 20 | Some(Self(true)) 21 | } else { 22 | None 23 | } 24 | } 25 | 26 | /// Get a potential `NoSymlink` value from a [Config]. 27 | /// 28 | /// If the `Config::no-symlink` has value, 29 | /// this returns it as the value of the `NoSymlink`, in a [Some]. 30 | /// Otherwise this returns [None]. 31 | fn from_config(config: &Config) -> Option { 32 | config.no_symlink.map(Self) 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod test { 38 | use clap::Parser; 39 | 40 | use super::NoSymlink; 41 | 42 | use crate::app::Cli; 43 | use crate::config_file::Config; 44 | use crate::flags::Configurable; 45 | 46 | #[test] 47 | fn test_from_cli_none() { 48 | let argv = ["lsd"]; 49 | let cli = Cli::try_parse_from(argv).unwrap(); 50 | assert_eq!(None, NoSymlink::from_cli(&cli)); 51 | } 52 | 53 | #[test] 54 | fn test_from_cli_true() { 55 | let argv = ["lsd", "--no-symlink"]; 56 | let cli = Cli::try_parse_from(argv).unwrap(); 57 | assert_eq!(Some(NoSymlink(true)), NoSymlink::from_cli(&cli)); 58 | } 59 | 60 | #[test] 61 | fn test_from_config_none() { 62 | assert_eq!(None, NoSymlink::from_config(&Config::with_none())); 63 | } 64 | 65 | #[test] 66 | fn test_from_config_true() { 67 | let mut c = Config::with_none(); 68 | c.no_symlink = Some(true); 69 | assert_eq!(Some(NoSymlink(true)), NoSymlink::from_config(&c)); 70 | } 71 | 72 | #[test] 73 | fn test_from_config_false() { 74 | let mut c = Config::with_none(); 75 | c.no_symlink = Some(false); 76 | assert_eq!(Some(NoSymlink(false)), NoSymlink::from_config(&c)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/flags/total_size.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [TotalSize] flag. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use the [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | 6 | use crate::app::Cli; 7 | use crate::config_file::Config; 8 | 9 | /// The flag showing whether to show the total size for directories. 10 | #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] 11 | pub struct TotalSize(pub bool); 12 | 13 | impl Configurable for TotalSize { 14 | /// Get a potential `TotalSize` value from [Cli]. 15 | /// 16 | /// If the "total-size" argument is passed, this returns a `TotalSize` with value `true` in a 17 | /// [Some]. Otherwise this returns [None]. 18 | fn from_cli(cli: &Cli) -> Option { 19 | if cli.total_size { 20 | Some(Self(true)) 21 | } else { 22 | None 23 | } 24 | } 25 | 26 | /// Get a potential `TotalSize` value from a [Config]. 27 | /// 28 | /// If the `Config::total-size` has value, 29 | /// this returns it as the value of the `TotalSize`, in a [Some]. 30 | /// Otherwise this returns [None]. 31 | fn from_config(config: &Config) -> Option { 32 | config.total_size.map(Self) 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod test { 38 | use clap::Parser; 39 | 40 | use super::TotalSize; 41 | 42 | use crate::app::Cli; 43 | use crate::config_file::Config; 44 | use crate::flags::Configurable; 45 | 46 | #[test] 47 | fn test_from_cli_none() { 48 | let argv = ["lsd"]; 49 | let cli = Cli::try_parse_from(argv).unwrap(); 50 | assert_eq!(None, TotalSize::from_cli(&cli)); 51 | } 52 | 53 | #[test] 54 | fn test_from_cli_true() { 55 | let argv = ["lsd", "--total-size"]; 56 | let cli = Cli::try_parse_from(argv).unwrap(); 57 | assert_eq!(Some(TotalSize(true)), TotalSize::from_cli(&cli)); 58 | } 59 | 60 | #[test] 61 | fn test_from_config_none() { 62 | assert_eq!(None, TotalSize::from_config(&Config::with_none())); 63 | } 64 | 65 | #[test] 66 | fn test_from_config_true() { 67 | let mut c = Config::with_none(); 68 | c.total_size = Some(true); 69 | assert_eq!(Some(TotalSize(true)), TotalSize::from_config(&c)); 70 | } 71 | 72 | #[test] 73 | fn test_from_config_false() { 74 | let mut c = Config::with_none(); 75 | c.total_size = Some(false); 76 | assert_eq!(Some(TotalSize(false)), TotalSize::from_config(&c)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/flags/truncate_owner.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [TruncateOwner] flag. To set it up from [Cli], a [Config] and its 2 | //! [Default] value, use the [configure_from](Configurable::configure_from) method. 3 | 4 | use super::Configurable; 5 | use crate::app::Cli; 6 | 7 | use crate::config_file::Config; 8 | 9 | /// The flag showing how to truncate user and group names. 10 | #[derive(Clone, Debug, PartialEq, Eq, Default)] 11 | pub struct TruncateOwner { 12 | pub after: Option, 13 | pub marker: Option, 14 | } 15 | 16 | impl Configurable for TruncateOwner { 17 | /// Get a potential `TruncateOwner` value from [Cli]. 18 | /// 19 | /// If the "header" argument is passed, this returns a `TruncateOwner` with value `true` in a 20 | /// [Some]. Otherwise this returns [None]. 21 | fn from_cli(cli: &Cli) -> Option { 22 | match (cli.truncate_owner_after, cli.truncate_owner_marker.clone()) { 23 | (None, None) => None, 24 | (after, marker) => Some(Self { after, marker }), 25 | } 26 | } 27 | 28 | /// Get a potential `TruncateOwner` value from a [Config]. 29 | /// 30 | /// If the `Config::truncate_owner` has value, 31 | /// this returns it as the value of the `TruncateOwner`, in a [Some]. 32 | /// Otherwise this returns [None]. 33 | fn from_config(config: &Config) -> Option { 34 | config.truncate_owner.as_ref().map(|c| Self { 35 | after: c.after, 36 | marker: c.marker.clone(), 37 | }) 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod test { 43 | use clap::Parser; 44 | 45 | use super::TruncateOwner; 46 | 47 | use crate::app::Cli; 48 | use crate::config_file::{self, Config}; 49 | use crate::flags::Configurable; 50 | 51 | #[test] 52 | fn test_from_cli_none() { 53 | let argv = ["lsd"]; 54 | let cli = Cli::try_parse_from(argv).unwrap(); 55 | assert_eq!(None, TruncateOwner::from_cli(&cli)); 56 | } 57 | 58 | #[test] 59 | fn test_from_cli_after_some() { 60 | let argv = ["lsd", "--truncate-owner-after", "1"]; 61 | let cli = Cli::try_parse_from(argv).unwrap(); 62 | assert_eq!( 63 | Some(TruncateOwner { 64 | after: Some(1), 65 | marker: None, 66 | }), 67 | TruncateOwner::from_cli(&cli) 68 | ); 69 | } 70 | 71 | #[test] 72 | fn test_from_cli_marker_some() { 73 | let argv = ["lsd", "--truncate-owner-marker", "…"]; 74 | let cli = Cli::try_parse_from(argv).unwrap(); 75 | assert_eq!( 76 | Some(TruncateOwner { 77 | after: None, 78 | marker: Some("…".to_string()), 79 | }), 80 | TruncateOwner::from_cli(&cli) 81 | ); 82 | } 83 | 84 | #[test] 85 | fn test_from_config_none() { 86 | assert_eq!(None, TruncateOwner::from_config(&Config::with_none())); 87 | } 88 | 89 | #[test] 90 | fn test_from_config_all_fields_none() { 91 | let mut c = Config::with_none(); 92 | c.truncate_owner = Some(config_file::TruncateOwner { 93 | after: None, 94 | marker: None, 95 | }); 96 | assert_eq!( 97 | Some(TruncateOwner { 98 | after: None, 99 | marker: None, 100 | }), 101 | TruncateOwner::from_config(&c) 102 | ); 103 | } 104 | 105 | #[test] 106 | fn test_from_config_all_fields_some() { 107 | let mut c = Config::with_none(); 108 | c.truncate_owner = Some(config_file::TruncateOwner { 109 | after: Some(1), 110 | marker: Some(">".to_string()), 111 | }); 112 | assert_eq!( 113 | Some(TruncateOwner { 114 | after: Some(1), 115 | marker: Some(">".to_string()), 116 | }), 117 | TruncateOwner::from_config(&c) 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/git_theme.rs: -------------------------------------------------------------------------------- 1 | use crate::git::GitStatus; 2 | use crate::theme::git::GitThemeSymbols; 3 | 4 | pub struct GitTheme { 5 | symbols: GitThemeSymbols, 6 | } 7 | 8 | impl GitTheme { 9 | pub fn new() -> GitTheme { 10 | let git_symbols = GitThemeSymbols::default(); 11 | Self { 12 | symbols: git_symbols, 13 | } 14 | } 15 | 16 | pub fn get_symbol(&self, status: &GitStatus) -> String { 17 | let symbol = match status { 18 | GitStatus::Default => &self.symbols.default, 19 | GitStatus::Unmodified => &self.symbols.unmodified, 20 | GitStatus::Ignored => &self.symbols.ignored, 21 | GitStatus::NewInIndex => &self.symbols.new_in_index, 22 | GitStatus::NewInWorkdir => &self.symbols.new_in_workdir, 23 | GitStatus::Typechange => &self.symbols.typechange, 24 | GitStatus::Deleted => &self.symbols.deleted, 25 | GitStatus::Renamed => &self.symbols.renamed, 26 | GitStatus::Modified => &self.symbols.modified, 27 | GitStatus::Conflicted => &self.symbols.conflicted, 28 | }; 29 | symbol.to_string() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/icon.rs: -------------------------------------------------------------------------------- 1 | use crate::flags::{IconOption, IconTheme as FlagTheme}; 2 | use crate::meta::{FileType, Name}; 3 | use crate::theme::{icon::IconTheme, Theme}; 4 | 5 | pub struct Icons { 6 | icon_separator: String, 7 | theme: Option, 8 | } 9 | 10 | // In order to add a new icon, write the unicode value like "\ue5fb" then 11 | // run the command below in vim: 12 | // 13 | // s#\\u[0-9a-f]*#\=eval('"'.submatch(0).'"')# 14 | impl Icons { 15 | pub fn new(tty: bool, when: IconOption, theme: FlagTheme, icon_separator: String) -> Self { 16 | let icon_theme = match (tty, when, theme) { 17 | (_, IconOption::Never, _) | (false, IconOption::Auto, _) => None, 18 | (_, _, FlagTheme::Fancy) => { 19 | if let Ok(t) = Theme::from_path::("icons") { 20 | Some(t) 21 | } else { 22 | Some(IconTheme::default()) 23 | } 24 | } 25 | (_, _, FlagTheme::Unicode) => Some(IconTheme::unicode()), 26 | }; 27 | 28 | Self { 29 | icon_separator, 30 | theme: icon_theme, 31 | } 32 | } 33 | 34 | pub fn get(&self, name: &Name) -> String { 35 | match &self.theme { 36 | None => String::new(), 37 | Some(t) => { 38 | // Check file types 39 | let file_type: FileType = name.file_type(); 40 | let icon = match file_type { 41 | FileType::SymLink { is_dir: true } => &t.filetype.symlink_dir, 42 | FileType::SymLink { is_dir: false } => &t.filetype.symlink_file, 43 | FileType::Socket => &t.filetype.socket, 44 | FileType::Pipe => &t.filetype.pipe, 45 | FileType::CharDevice => &t.filetype.device_char, 46 | FileType::BlockDevice => &t.filetype.device_block, 47 | FileType::Special => &t.filetype.special, 48 | _ => { 49 | if let Some(icon) = t.name.get(name.file_name().to_lowercase().as_str()) { 50 | icon 51 | } else if let Some(icon) = name 52 | .extension() 53 | .and_then(|ext| t.extension.get(ext.to_lowercase().as_str())) 54 | { 55 | icon 56 | } else { 57 | match file_type { 58 | FileType::Directory { .. } => &t.filetype.dir, 59 | // If a file has no extension and is executable, show an icon. 60 | // Except for Windows, it marks everything as an executable. 61 | #[cfg(not(windows))] 62 | FileType::File { exec: true, .. } => &t.filetype.executable, 63 | _ => &t.filetype.file, 64 | } 65 | } 66 | } 67 | }; 68 | 69 | format!("{}{}", icon, self.icon_separator) 70 | } 71 | } 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod test { 77 | use super::{IconTheme, Icons}; 78 | use crate::flags::{IconOption, IconTheme as FlagTheme, PermissionFlag}; 79 | use crate::meta::Meta; 80 | use std::fs::File; 81 | use tempfile::tempdir; 82 | 83 | #[test] 84 | fn get_no_icon_never_tty() { 85 | let tmp_dir = tempdir().expect("failed to create temp dir"); 86 | let file_path = tmp_dir.path().join("file.txt"); 87 | File::create(&file_path).expect("failed to create file"); 88 | let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); 89 | 90 | let icons = Icons::new(true, IconOption::Never, FlagTheme::Fancy, " ".to_string()); 91 | let icon = icons.get(&meta.name); 92 | 93 | assert_eq!(icon, ""); 94 | } 95 | #[test] 96 | fn get_no_icon_never_not_tty() { 97 | let tmp_dir = tempdir().expect("failed to create temp dir"); 98 | let file_path = tmp_dir.path().join("file.txt"); 99 | File::create(&file_path).expect("failed to create file"); 100 | let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); 101 | 102 | let icons = Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()); 103 | let icon = icons.get(&meta.name); 104 | 105 | assert_eq!(icon, ""); 106 | } 107 | 108 | #[test] 109 | fn get_no_icon_auto() { 110 | let tmp_dir = tempdir().expect("failed to create temp dir"); 111 | let file_path = tmp_dir.path().join("file.txt"); 112 | File::create(&file_path).expect("failed to create file"); 113 | let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); 114 | 115 | let icons = Icons::new(false, IconOption::Auto, FlagTheme::Fancy, " ".to_string()); 116 | let icon = icons.get(&meta.name); 117 | 118 | assert_eq!(icon, ""); 119 | } 120 | #[test] 121 | fn get_icon_auto_tty() { 122 | let tmp_dir = tempdir().expect("failed to create temp dir"); 123 | let file_path = tmp_dir.path().join("file.txt"); 124 | File::create(&file_path).expect("failed to create file"); 125 | let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); 126 | 127 | let icons = Icons::new(true, IconOption::Auto, FlagTheme::Fancy, " ".to_string()); 128 | let icon = icons.get(&meta.name); 129 | 130 | assert_eq!(icon, "\u{f15c} "); 131 | } 132 | 133 | #[test] 134 | fn get_icon_always_tty_default_file() { 135 | let tmp_dir = tempdir().expect("failed to create temp dir"); 136 | let file_path = tmp_dir.path().join("file"); 137 | File::create(&file_path).expect("failed to create file"); 138 | let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); 139 | 140 | let icon = Icons::new(true, IconOption::Always, FlagTheme::Fancy, " ".to_string()); 141 | let icon_str = icon.get(&meta.name); 142 | 143 | assert_eq!(icon_str, "\u{f016} "); //  144 | } 145 | 146 | #[test] 147 | fn get_icon_always_not_tty_default_file() { 148 | let tmp_dir = tempdir().expect("failed to create temp dir"); 149 | let file_path = tmp_dir.path().join("file"); 150 | File::create(&file_path).expect("failed to create file"); 151 | let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); 152 | 153 | let icon = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); 154 | let icon_str = icon.get(&meta.name); 155 | 156 | assert_eq!(icon_str, "\u{f016} "); //  157 | } 158 | 159 | #[test] 160 | fn get_icon_default_file_icon_unicode() { 161 | let tmp_dir = tempdir().expect("failed to create temp dir"); 162 | let file_path = tmp_dir.path().join("file"); 163 | File::create(&file_path).expect("failed to create file"); 164 | let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); 165 | 166 | let icon = Icons::new( 167 | false, 168 | IconOption::Always, 169 | FlagTheme::Unicode, 170 | " ".to_string(), 171 | ); 172 | let icon_str = icon.get(&meta.name); 173 | 174 | assert_eq!(icon_str, format!("{}{}", "\u{1f4c4}", icon.icon_separator)); 175 | } 176 | 177 | #[test] 178 | fn get_icon_default_directory() { 179 | let tmp_dir = tempdir().expect("failed to create temp dir"); 180 | let file_path = tmp_dir.path(); 181 | let meta = Meta::from_path(file_path, false, PermissionFlag::Rwx).unwrap(); 182 | 183 | let icon = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); 184 | let icon_str = icon.get(&meta.name); 185 | 186 | assert_eq!(icon_str, "\u{f115} "); //  187 | } 188 | 189 | #[test] 190 | fn get_icon_default_directory_unicode() { 191 | let tmp_dir = tempdir().expect("failed to create temp dir"); 192 | let file_path = tmp_dir.path(); 193 | let meta = Meta::from_path(file_path, false, PermissionFlag::Rwx).unwrap(); 194 | 195 | let icon = Icons::new( 196 | false, 197 | IconOption::Always, 198 | FlagTheme::Unicode, 199 | " ".to_string(), 200 | ); 201 | let icon_str = icon.get(&meta.name); 202 | 203 | assert_eq!(icon_str, format!("{}{}", "\u{1f4c2}", icon.icon_separator)); 204 | } 205 | 206 | #[test] 207 | fn get_icon_by_name() { 208 | let tmp_dir = tempdir().expect("failed to create temp dir"); 209 | 210 | for (file_name, file_icon) in &IconTheme::get_default_icons_by_name() { 211 | let file_path = tmp_dir.path().join(file_name); 212 | File::create(&file_path).expect("failed to create file"); 213 | let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); 214 | 215 | let icon = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); 216 | let icon_str = icon.get(&meta.name); 217 | 218 | assert_eq!(icon_str, format!("{}{}", file_icon, icon.icon_separator)); 219 | } 220 | } 221 | 222 | #[test] 223 | fn get_icon_by_extension() { 224 | let tmp_dir = tempdir().expect("failed to create temp dir"); 225 | 226 | for (ext, file_icon) in &IconTheme::get_default_icons_by_extension() { 227 | let file_path = tmp_dir.path().join(format!("file.{ext}")); 228 | File::create(&file_path).expect("failed to create file"); 229 | let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); 230 | 231 | let icon = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); 232 | let icon_str = icon.get(&meta.name); 233 | 234 | assert_eq!(icon_str, format!("{}{}", file_icon, icon.icon_separator)); 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | clippy::cast_precision_loss, 3 | clippy::cast_sign_loss, 4 | clippy::match_same_arms, 5 | clippy::cast_possible_wrap 6 | )] 7 | 8 | extern crate chrono; 9 | extern crate chrono_humanize; 10 | extern crate clap; 11 | extern crate dirs; 12 | extern crate libc; 13 | extern crate lscolors; 14 | #[cfg(test)] 15 | extern crate tempfile; 16 | extern crate term_grid; 17 | extern crate terminal_size; 18 | extern crate unicode_width; 19 | extern crate url; 20 | extern crate wild; 21 | extern crate yaml_rust; 22 | 23 | #[cfg(unix)] 24 | extern crate users; 25 | 26 | #[cfg(windows)] 27 | extern crate windows; 28 | 29 | mod app; 30 | mod color; 31 | mod config_file; 32 | mod core; 33 | mod display; 34 | mod flags; 35 | mod git; 36 | mod git_theme; 37 | mod icon; 38 | mod meta; 39 | mod sort; 40 | mod theme; 41 | 42 | use clap::Parser; 43 | 44 | use crate::app::Cli; 45 | use crate::config_file::Config; 46 | use crate::core::Core; 47 | use crate::flags::Flags; 48 | 49 | #[derive(PartialEq, Eq, PartialOrd, Copy, Clone)] 50 | pub enum ExitCode { 51 | OK, 52 | MinorIssue, 53 | MajorIssue, 54 | } 55 | impl ExitCode { 56 | pub fn set_if_greater(&mut self, code: ExitCode) { 57 | let self_i32 = *self as i32; 58 | let code_i32 = code as i32; 59 | if self_i32 < code_i32 { 60 | *self = code; 61 | } 62 | } 63 | } 64 | /// Macro used to avoid panicking when the lsd method is used with a pipe and 65 | /// stderr close before our program. 66 | #[macro_export] 67 | macro_rules! print_error { 68 | ($($arg:tt)*) => { 69 | { 70 | use std::io::Write; 71 | 72 | let stderr = std::io::stderr(); 73 | 74 | { 75 | let mut handle = stderr.lock(); 76 | // We can write on stderr, so we simply ignore the error and don't print 77 | // and stop with success. 78 | let res = handle.write_all(std::format!("lsd: {}\n\n", 79 | std::format!($($arg)*)).as_bytes()); 80 | if res.is_err() { 81 | std::process::exit(0); 82 | } 83 | } 84 | } 85 | }; 86 | } 87 | 88 | /// Macro used to avoid panicking when the lsd method is used with a pipe and 89 | /// stdout close before our program. 90 | #[macro_export] 91 | macro_rules! print_output { 92 | ($($arg:tt)*) => { 93 | use std::io::Write; 94 | 95 | let stderr = std::io::stdout(); 96 | 97 | 98 | { 99 | let mut handle = stderr.lock(); 100 | // We can write on stdout, so we simply ignore the error and don't print 101 | // and stop with success. 102 | let res = handle.write_all(std::format!($($arg)*).as_bytes()); 103 | if res.is_err() { 104 | std::process::exit(0); 105 | } 106 | } 107 | }; 108 | } 109 | 110 | fn main() { 111 | let cli = Cli::parse_from(wild::args_os()); 112 | 113 | let config = if cli.ignore_config { 114 | Config::with_none() 115 | } else if let Some(path) = &cli.config_file { 116 | Config::from_file(path).expect("Provided file path is invalid") 117 | } else { 118 | Config::default() 119 | }; 120 | let flags = Flags::configure_from(&cli, &config).unwrap_or_else(|err| err.exit()); 121 | let core = Core::new(flags); 122 | 123 | let exit_code = core.run(cli.inputs); 124 | std::process::exit(exit_code as i32); 125 | } 126 | -------------------------------------------------------------------------------- /src/meta/access_control.rs: -------------------------------------------------------------------------------- 1 | use crate::color::{ColoredString, Colors, Elem}; 2 | use std::path::Path; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct AccessControl { 6 | has_acl: bool, 7 | selinux_context: String, 8 | smack_context: String, 9 | } 10 | 11 | impl AccessControl { 12 | #[cfg(not(unix))] 13 | pub fn for_path(_: &Path) -> Self { 14 | Self::from_data(false, &[], &[]) 15 | } 16 | 17 | #[cfg(unix)] 18 | pub fn for_path(path: &Path) -> Self { 19 | let has_acl = !xattr::get(path, Method::Acl.name()) 20 | .unwrap_or_default() 21 | .unwrap_or_default() 22 | .is_empty(); 23 | let selinux_context = xattr::get(path, Method::Selinux.name()) 24 | .unwrap_or_default() 25 | .unwrap_or_default(); 26 | let smack_context = xattr::get(path, Method::Smack.name()) 27 | .unwrap_or_default() 28 | .unwrap_or_default(); 29 | 30 | Self::from_data(has_acl, &selinux_context, &smack_context) 31 | } 32 | 33 | fn from_data(has_acl: bool, selinux_context: &[u8], smack_context: &[u8]) -> Self { 34 | let selinux_context = String::from_utf8_lossy(selinux_context).to_string(); 35 | let smack_context = String::from_utf8_lossy(smack_context).to_string(); 36 | Self { 37 | has_acl, 38 | selinux_context, 39 | smack_context, 40 | } 41 | } 42 | 43 | pub fn render_method(&self, colors: &Colors) -> ColoredString { 44 | if self.has_acl { 45 | colors.colorize('+', &Elem::Acl) 46 | } else if !self.selinux_context.is_empty() || !self.smack_context.is_empty() { 47 | colors.colorize('.', &Elem::Context) 48 | } else { 49 | colors.colorize("", &Elem::Acl) 50 | } 51 | } 52 | 53 | pub fn render_context(&self, colors: &Colors) -> ColoredString { 54 | let mut context = self.selinux_context.clone(); 55 | if !self.smack_context.is_empty() { 56 | if !context.is_empty() { 57 | context += "+"; 58 | } 59 | context += &self.smack_context; 60 | } 61 | if context.is_empty() { 62 | context += "?"; 63 | } 64 | colors.colorize(context, &Elem::Context) 65 | } 66 | } 67 | 68 | #[cfg(unix)] 69 | enum Method { 70 | Acl, 71 | Selinux, 72 | Smack, 73 | } 74 | 75 | #[cfg(unix)] 76 | impl Method { 77 | fn name(&self) -> &'static str { 78 | match self { 79 | Method::Acl => "system.posix_acl_access", 80 | Method::Selinux => "security.selinux", 81 | Method::Smack => "security.SMACK64", 82 | } 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod test { 88 | use super::AccessControl; 89 | use crate::color::{Colors, ThemeOption}; 90 | use crossterm::style::{Color, Stylize}; 91 | 92 | #[test] 93 | fn test_acl_only_indicator() { 94 | // actual file would collide with proper AC data, no permission to scrub those 95 | let access_control = AccessControl::from_data(true, &[], &[]); 96 | 97 | assert_eq!( 98 | String::from("+").with(Color::DarkCyan), 99 | access_control.render_method(&Colors::new(ThemeOption::Default)) 100 | ); 101 | } 102 | 103 | #[test] 104 | fn test_smack_only_indicator() { 105 | let access_control = AccessControl::from_data(false, &[], &[b'a']); 106 | 107 | assert_eq!( 108 | String::from(".").with(Color::Cyan), 109 | access_control.render_method(&Colors::new(ThemeOption::Default)) 110 | ); 111 | } 112 | 113 | #[test] 114 | fn test_acl_and_selinux_indicator() { 115 | let access_control = AccessControl::from_data(true, &[b'a'], &[]); 116 | 117 | assert_eq!( 118 | String::from("+").with(Color::DarkCyan), 119 | access_control.render_method(&Colors::new(ThemeOption::Default)) 120 | ); 121 | } 122 | 123 | #[test] 124 | fn test_selinux_context() { 125 | let access_control = AccessControl::from_data(false, &[b'a'], &[]); 126 | 127 | assert_eq!( 128 | String::from("a").with(Color::Cyan), 129 | access_control.render_context(&Colors::new(ThemeOption::Default)) 130 | ); 131 | } 132 | 133 | #[test] 134 | fn test_selinux_and_smack_context() { 135 | let access_control = AccessControl::from_data(false, &[b'a'], &[b'b']); 136 | 137 | assert_eq!( 138 | String::from("a+b").with(Color::Cyan), 139 | access_control.render_context(&Colors::new(ThemeOption::Default)) 140 | ); 141 | } 142 | 143 | #[test] 144 | fn test_no_context() { 145 | let access_control = AccessControl::from_data(false, &[], &[]); 146 | 147 | assert_eq!( 148 | String::from("?").with(Color::Cyan), 149 | access_control.render_context(&Colors::new(ThemeOption::Default)) 150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/meta/filetype.rs: -------------------------------------------------------------------------------- 1 | use crate::color::{ColoredString, Colors, Elem}; 2 | use std::fs::Metadata; 3 | 4 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 5 | #[cfg_attr(windows, allow(dead_code))] 6 | pub enum FileType { 7 | BlockDevice, 8 | CharDevice, 9 | Directory { uid: bool }, 10 | File { uid: bool, exec: bool }, 11 | SymLink { is_dir: bool }, 12 | Pipe, 13 | Socket, 14 | Special, 15 | } 16 | 17 | impl FileType { 18 | #[cfg(windows)] 19 | const EXECUTABLE_EXTENSIONS: &'static [&'static str] = &["exe", "msi", "bat", "ps1"]; 20 | 21 | #[cfg(unix)] 22 | pub fn new( 23 | meta: &Metadata, 24 | symlink_meta: Option<&Metadata>, 25 | permissions: &crate::meta::Permissions, 26 | ) -> Self { 27 | use std::os::unix::fs::FileTypeExt; 28 | 29 | let file_type = meta.file_type(); 30 | 31 | if file_type.is_file() { 32 | FileType::File { 33 | exec: permissions.is_executable(), 34 | uid: permissions.setuid, 35 | } 36 | } else if file_type.is_dir() { 37 | FileType::Directory { 38 | uid: permissions.setuid, 39 | } 40 | } else if file_type.is_fifo() { 41 | FileType::Pipe 42 | } else if file_type.is_symlink() { 43 | FileType::SymLink { 44 | // if broken, defaults to false 45 | is_dir: symlink_meta.map(|m| m.is_dir()).unwrap_or_default(), 46 | } 47 | } else if file_type.is_char_device() { 48 | FileType::CharDevice 49 | } else if file_type.is_block_device() { 50 | FileType::BlockDevice 51 | } else if file_type.is_socket() { 52 | FileType::Socket 53 | } else { 54 | FileType::Special 55 | } 56 | } 57 | 58 | #[cfg(windows)] 59 | pub fn new(meta: &Metadata, symlink_meta: Option<&Metadata>, path: &std::path::Path) -> Self { 60 | let file_type = meta.file_type(); 61 | 62 | if file_type.is_file() { 63 | let exec = path 64 | .extension() 65 | .map(|ext| { 66 | Self::EXECUTABLE_EXTENSIONS 67 | .iter() 68 | .map(std::ffi::OsStr::new) 69 | .any(|exec_ext| ext == exec_ext) 70 | }) 71 | .unwrap_or(false); 72 | FileType::File { exec, uid: false } 73 | } else if file_type.is_dir() { 74 | FileType::Directory { uid: false } 75 | } else if file_type.is_symlink() { 76 | FileType::SymLink { 77 | // if broken, defaults to false 78 | is_dir: symlink_meta.map(|m| m.is_dir()).unwrap_or_default(), 79 | } 80 | } else { 81 | FileType::Special 82 | } 83 | } 84 | 85 | pub fn is_dirlike(self) -> bool { 86 | matches!( 87 | self, 88 | FileType::Directory { .. } | FileType::SymLink { is_dir: true } 89 | ) 90 | } 91 | } 92 | 93 | impl FileType { 94 | pub fn render(self, colors: &Colors) -> ColoredString { 95 | match self { 96 | FileType::File { exec, .. } => colors.colorize('.', &Elem::File { exec, uid: false }), 97 | FileType::Directory { .. } => colors.colorize('d', &Elem::Dir { uid: false }), 98 | FileType::Pipe => colors.colorize('|', &Elem::Pipe), 99 | FileType::SymLink { .. } => colors.colorize('l', &Elem::SymLink), 100 | FileType::BlockDevice => colors.colorize('b', &Elem::BlockDevice), 101 | FileType::CharDevice => colors.colorize('c', &Elem::CharDevice), 102 | FileType::Socket => colors.colorize('s', &Elem::Socket), 103 | FileType::Special => colors.colorize('?', &Elem::Special), 104 | } 105 | } 106 | } 107 | 108 | #[cfg(test)] 109 | mod test { 110 | use super::FileType; 111 | use crate::color::{Colors, ThemeOption}; 112 | #[cfg(unix)] 113 | use crate::flags::PermissionFlag; 114 | #[cfg(unix)] 115 | use crate::meta::permissions_or_attributes::PermissionsOrAttributes; 116 | #[cfg(unix)] 117 | use crate::meta::Permissions; 118 | use crossterm::style::{Color, Stylize}; 119 | use std::fs::File; 120 | #[cfg(unix)] 121 | use std::os::unix::fs::symlink; 122 | #[cfg(unix)] 123 | use std::os::unix::net::UnixListener; 124 | #[cfg(unix)] 125 | use std::process::Command; 126 | use tempfile::tempdir; 127 | 128 | #[test] 129 | #[cfg(unix)] // Windows uses different default permissions 130 | fn test_file_type() { 131 | let tmp_dir = tempdir().expect("failed to create temp dir"); 132 | 133 | // Create the file; 134 | let file_path = tmp_dir.path().join("file.txt"); 135 | File::create(&file_path).expect("failed to create file"); 136 | let meta = file_path.metadata().expect("failed to get metas"); 137 | 138 | let colors = Colors::new(ThemeOption::NoLscolors); 139 | let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); 140 | 141 | assert_eq!( 142 | ".".to_string().with(Color::AnsiValue(184)), 143 | file_type.render(&colors) 144 | ); 145 | } 146 | 147 | #[test] 148 | fn test_dir_type() { 149 | let tmp_dir = tempdir().expect("failed to create temp dir"); 150 | #[cfg(not(windows))] 151 | let meta = crate::meta::Meta::from_path(tmp_dir.path(), false, PermissionFlag::Rwx) 152 | .expect("failed to get tempdir path"); 153 | let metadata = tmp_dir.path().metadata().expect("failed to get metas"); 154 | 155 | let colors = Colors::new(ThemeOption::NoLscolors); 156 | 157 | #[cfg(not(windows))] 158 | let file_type = match meta.permissions_or_attributes { 159 | Some(PermissionsOrAttributes::Permissions(permissions)) => { 160 | FileType::new(&metadata, None, &permissions) 161 | } 162 | _ => panic!("unexpected"), 163 | }; 164 | #[cfg(windows)] 165 | let file_type = FileType::new(&metadata, None, tmp_dir.path()); 166 | 167 | assert_eq!( 168 | "d".to_string().with(Color::AnsiValue(33)), 169 | file_type.render(&colors) 170 | ); 171 | } 172 | 173 | #[test] 174 | #[cfg(unix)] // Symlink support is *hard* on Windows 175 | fn test_symlink_type_file() { 176 | let tmp_dir = tempdir().expect("failed to create temp dir"); 177 | 178 | // Create the file; 179 | let file_path = tmp_dir.path().join("file.tmp"); 180 | File::create(&file_path).expect("failed to create file"); 181 | 182 | // Create the symlink 183 | let symlink_path = tmp_dir.path().join("target.tmp"); 184 | symlink(&file_path, &symlink_path).expect("failed to create symlink"); 185 | let meta = symlink_path 186 | .symlink_metadata() 187 | .expect("failed to get metas"); 188 | 189 | let colors = Colors::new(ThemeOption::NoLscolors); 190 | let file_type = FileType::new(&meta, Some(&meta), &Permissions::from(&meta)); 191 | 192 | assert_eq!( 193 | "l".to_string().with(Color::AnsiValue(44)), 194 | file_type.render(&colors) 195 | ); 196 | } 197 | 198 | #[test] 199 | #[cfg(unix)] 200 | fn test_symlink_type_dir() { 201 | let tmp_dir = tempdir().expect("failed to create temp dir"); 202 | 203 | // Create directory 204 | let dir_path = tmp_dir.path().join("dir.d"); 205 | std::fs::create_dir(&dir_path).expect("failed to create dir"); 206 | 207 | // Create symlink 208 | let symlink_path = tmp_dir.path().join("target.d"); 209 | symlink(&dir_path, &symlink_path).expect("failed to create symlink"); 210 | let meta = symlink_path 211 | .symlink_metadata() 212 | .expect("failed to get metas"); 213 | 214 | let colors = Colors::new(ThemeOption::NoLscolors); 215 | let file_type = FileType::new(&meta, Some(&meta), &Permissions::from(&meta)); 216 | 217 | assert_eq!( 218 | "l".to_string().with(Color::AnsiValue(44)), 219 | file_type.render(&colors) 220 | ); 221 | } 222 | 223 | #[test] 224 | #[cfg(unix)] // Windows pipes aren't like Unix pipes 225 | fn test_pipe_type() { 226 | let tmp_dir = tempdir().expect("failed to create temp dir"); 227 | 228 | // Create the pipe; 229 | let pipe_path = tmp_dir.path().join("pipe.tmp"); 230 | let success = Command::new("mkfifo") 231 | .arg(&pipe_path) 232 | .status() 233 | .expect("failed to exec mkfifo") 234 | .success(); 235 | assert!(success, "failed to exec mkfifo"); 236 | let meta = pipe_path.metadata().expect("failed to get metas"); 237 | 238 | let colors = Colors::new(ThemeOption::NoLscolors); 239 | let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); 240 | 241 | assert_eq!( 242 | "|".to_string().with(Color::AnsiValue(44)), 243 | file_type.render(&colors) 244 | ); 245 | } 246 | 247 | #[test] 248 | #[cfg(feature = "sudo")] 249 | fn test_char_device_type() { 250 | let tmp_dir = tempdir().expect("failed to create temp dir"); 251 | 252 | // Create the char device; 253 | let char_device_path = tmp_dir.path().join("char-device.tmp"); 254 | let success = Command::new("sudo") 255 | .arg("mknod") 256 | .arg(&char_device_path) 257 | .arg("c") 258 | .arg("89") 259 | .arg("1") 260 | .status() 261 | .expect("failed to exec mknod") 262 | .success(); 263 | assert!(success, "failed to exec mknod"); 264 | let meta = char_device_path.metadata().expect("failed to get metas"); 265 | 266 | let colors = Colors::new(ThemeOption::NoLscolors); 267 | let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); 268 | 269 | assert_eq!( 270 | "c".to_string().with(Color::AnsiValue(44)), 271 | file_type.render(&colors) 272 | ); 273 | } 274 | 275 | #[test] 276 | #[cfg(unix)] // Sockets don't work the same way on Windows 277 | fn test_socket_type() { 278 | let tmp_dir = tempdir().expect("failed to create temp dir"); 279 | 280 | // Create the socket; 281 | let socket_path = tmp_dir.path().join("socket.tmp"); 282 | UnixListener::bind(&socket_path).expect("failed to create the socket"); 283 | let meta = socket_path.metadata().expect("failed to get metas"); 284 | 285 | let colors = Colors::new(ThemeOption::NoLscolors); 286 | let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); 287 | 288 | assert_eq!( 289 | "s".to_string().with(Color::AnsiValue(44)), 290 | file_type.render(&colors) 291 | ); 292 | } 293 | 294 | #[cfg(windows)] 295 | #[test] 296 | fn test_file_executable() { 297 | let tmp_dir = tempdir().expect("failed to create temp dir"); 298 | for ext in FileType::EXECUTABLE_EXTENSIONS { 299 | // Create the file; 300 | let file_path = tmp_dir.path().join(format!("file.{ext}")); 301 | File::create(&file_path).expect("failed to create file"); 302 | let meta = file_path.metadata().expect("failed to get metas"); 303 | 304 | let colors = Colors::new(ThemeOption::NoLscolors); 305 | let file_type = FileType::new(&meta, None, &file_path); 306 | 307 | assert_eq!( 308 | ".".to_string().with(Color::AnsiValue(40)), 309 | file_type.render(&colors) 310 | ); 311 | } 312 | } 313 | 314 | #[cfg(windows)] 315 | #[test] 316 | fn test_file_not_executable() { 317 | let tmp_dir = tempdir().expect("failed to create temp dir"); 318 | // Create the file; 319 | let file_path = tmp_dir.path().join("file.txt"); 320 | File::create(&file_path).expect("failed to create file"); 321 | let meta = file_path.metadata().expect("failed to get metas"); 322 | 323 | let colors = Colors::new(ThemeOption::NoLscolors); 324 | let file_type = FileType::new(&meta, None, &file_path); 325 | 326 | assert_eq!( 327 | ".".to_string().with(Color::AnsiValue(184)), 328 | file_type.render(&colors) 329 | ); 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /src/meta/git_file_status.rs: -------------------------------------------------------------------------------- 1 | use crate::color::{self, ColoredString, Colors}; 2 | use crate::git::GitStatus; 3 | use crate::git_theme::GitTheme; 4 | 5 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] 6 | pub struct GitFileStatus { 7 | pub index: GitStatus, 8 | pub workdir: GitStatus, 9 | } 10 | 11 | impl Default for GitFileStatus { 12 | fn default() -> Self { 13 | Self { 14 | index: GitStatus::Default, 15 | workdir: GitStatus::Default, 16 | } 17 | } 18 | } 19 | 20 | impl GitFileStatus { 21 | #[cfg(not(feature = "no-git"))] 22 | pub fn new(status: git2::Status) -> Self { 23 | Self { 24 | index: match status { 25 | s if s.contains(git2::Status::INDEX_NEW) => GitStatus::NewInIndex, 26 | s if s.contains(git2::Status::INDEX_DELETED) => GitStatus::Deleted, 27 | s if s.contains(git2::Status::INDEX_MODIFIED) => GitStatus::Modified, 28 | s if s.contains(git2::Status::INDEX_RENAMED) => GitStatus::Renamed, 29 | s if s.contains(git2::Status::INDEX_TYPECHANGE) => GitStatus::Typechange, 30 | _ => GitStatus::Unmodified, 31 | }, 32 | 33 | workdir: match status { 34 | s if s.contains(git2::Status::WT_NEW) => GitStatus::NewInWorkdir, 35 | s if s.contains(git2::Status::WT_DELETED) => GitStatus::Deleted, 36 | s if s.contains(git2::Status::WT_MODIFIED) => GitStatus::Modified, 37 | s if s.contains(git2::Status::WT_RENAMED) => GitStatus::Renamed, 38 | s if s.contains(git2::Status::IGNORED) => GitStatus::Ignored, 39 | s if s.contains(git2::Status::WT_TYPECHANGE) => GitStatus::Typechange, 40 | s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflicted, 41 | _ => GitStatus::Unmodified, 42 | }, 43 | } 44 | } 45 | 46 | pub fn render(&self, colors: &Colors, git_theme: &GitTheme) -> ColoredString { 47 | let res = [ 48 | colors.colorize( 49 | git_theme.get_symbol(&self.index), 50 | &color::Elem::GitStatus { status: self.index }, 51 | ), 52 | colors.colorize( 53 | git_theme.get_symbol(&self.workdir), 54 | &color::Elem::GitStatus { 55 | status: self.workdir, 56 | }, 57 | ), 58 | ] 59 | .into_iter() 60 | // From the experiment, the maximum string size is 153 bytes 61 | .fold(String::with_capacity(160), |mut acc, x| { 62 | acc.push_str(&x.to_string()); 63 | acc 64 | }); 65 | ColoredString::new(Colors::default_style(), res) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/meta/indicator.rs: -------------------------------------------------------------------------------- 1 | use crate::color::{ColoredString, Colors}; 2 | use crate::flags::Flags; 3 | use crate::meta::FileType; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct Indicator(&'static str); 7 | 8 | impl From for Indicator { 9 | fn from(file_type: FileType) -> Self { 10 | let res = match file_type { 11 | FileType::Directory { .. } => "/", 12 | FileType::File { exec: true, .. } => "*", 13 | FileType::Pipe => "|", 14 | FileType::Socket => "=", 15 | FileType::SymLink { .. } => "@", 16 | _ => "", 17 | }; 18 | 19 | Indicator(res) 20 | } 21 | } 22 | 23 | impl Indicator { 24 | pub fn render(&self, flags: &Flags) -> ColoredString { 25 | if flags.display_indicators.0 { 26 | ColoredString::new(Colors::default_style(), self.0.to_string()) 27 | } else { 28 | ColoredString::new(Colors::default_style(), "".into()) 29 | } 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod test { 35 | use super::Indicator; 36 | use crate::flags::{Flags, Indicators}; 37 | use crate::meta::FileType; 38 | 39 | #[test] 40 | fn test_directory_indicator() { 41 | let flags = Flags { 42 | display_indicators: Indicators(true), 43 | ..Default::default() 44 | }; 45 | 46 | let file_type = Indicator::from(FileType::Directory { uid: false }); 47 | 48 | assert_eq!("/", file_type.render(&flags).to_string()); 49 | } 50 | 51 | #[test] 52 | fn test_executable_file_indicator() { 53 | let flags = Flags { 54 | display_indicators: Indicators(true), 55 | ..Default::default() 56 | }; 57 | 58 | let file_type = Indicator::from(FileType::File { 59 | uid: false, 60 | exec: true, 61 | }); 62 | 63 | assert_eq!("*", file_type.render(&flags).to_string()); 64 | } 65 | 66 | #[test] 67 | fn test_socket_indicator() { 68 | let flags = Flags { 69 | display_indicators: Indicators(true), 70 | ..Default::default() 71 | }; 72 | 73 | let file_type = Indicator::from(FileType::Socket); 74 | 75 | assert_eq!("=", file_type.render(&flags).to_string()); 76 | } 77 | 78 | #[test] 79 | fn test_symlink_indicator() { 80 | let flags = Flags { 81 | display_indicators: Indicators(true), 82 | ..Default::default() 83 | }; 84 | 85 | let file_type = Indicator::from(FileType::SymLink { is_dir: false }); 86 | assert_eq!("@", file_type.render(&flags).to_string()); 87 | 88 | let file_type = Indicator::from(FileType::SymLink { is_dir: true }); 89 | assert_eq!("@", file_type.render(&flags).to_string()); 90 | } 91 | 92 | #[test] 93 | fn test_not_represented_indicator() { 94 | let flags = Flags { 95 | display_indicators: Indicators(true), 96 | ..Default::default() 97 | }; 98 | 99 | // The File type doesn't have any indicator 100 | let file_type = Indicator::from(FileType::File { 101 | exec: false, 102 | uid: false, 103 | }); 104 | 105 | assert_eq!("", file_type.render(&flags).to_string()); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/meta/inode.rs: -------------------------------------------------------------------------------- 1 | use crate::color::{ColoredString, Colors, Elem}; 2 | use std::fs::Metadata; 3 | 4 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 5 | pub struct INode { 6 | index: Option, 7 | } 8 | 9 | impl From<&Metadata> for INode { 10 | #[cfg(unix)] 11 | fn from(meta: &Metadata) -> Self { 12 | use std::os::unix::fs::MetadataExt; 13 | 14 | let index = meta.ino(); 15 | 16 | Self { index: Some(index) } 17 | } 18 | 19 | #[cfg(windows)] 20 | fn from(_: &Metadata) -> Self { 21 | Self { index: None } 22 | } 23 | } 24 | 25 | impl INode { 26 | pub fn render(&self, colors: &Colors) -> ColoredString { 27 | match self.index { 28 | Some(i) => colors.colorize(i.to_string(), &Elem::INode { valid: true }), 29 | None => colors.colorize('-', &Elem::INode { valid: false }), 30 | } 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | #[cfg(unix)] 36 | mod tests { 37 | use super::INode; 38 | use std::env; 39 | use std::io; 40 | use std::path::Path; 41 | use std::process::{Command, ExitStatus}; 42 | 43 | fn cross_platform_touch(path: &Path) -> io::Result { 44 | Command::new("touch").arg(path).status() 45 | } 46 | 47 | #[test] 48 | fn test_inode_no_zero() { 49 | let mut file_path = env::temp_dir(); 50 | file_path.push("inode.tmp"); 51 | 52 | let success = cross_platform_touch(&file_path).unwrap().success(); 53 | assert!(success, "failed to exec touch"); 54 | 55 | let inode = INode::from(&file_path.metadata().unwrap()); 56 | 57 | #[cfg(unix)] 58 | assert!(inode.index.is_some()); 59 | #[cfg(windows)] 60 | assert!(inode.index.is_none()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/meta/links.rs: -------------------------------------------------------------------------------- 1 | use crate::color::{ColoredString, Colors, Elem}; 2 | use std::fs::Metadata; 3 | 4 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 5 | pub struct Links { 6 | nlink: Option, 7 | } 8 | 9 | impl From<&Metadata> for Links { 10 | #[cfg(unix)] 11 | fn from(meta: &Metadata) -> Self { 12 | use std::os::unix::fs::MetadataExt; 13 | 14 | let nlink = meta.nlink(); 15 | 16 | Self { nlink: Some(nlink) } 17 | } 18 | 19 | #[cfg(windows)] 20 | fn from(_: &Metadata) -> Self { 21 | Self { nlink: None } 22 | } 23 | } 24 | 25 | impl Links { 26 | pub fn render(&self, colors: &Colors) -> ColoredString { 27 | match self.nlink { 28 | Some(i) => colors.colorize(i.to_string(), &Elem::Links { valid: true }), 29 | None => colors.colorize('-', &Elem::Links { valid: false }), 30 | } 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | #[cfg(unix)] 36 | mod tests { 37 | use super::Links; 38 | use std::env; 39 | use std::io; 40 | use std::path::Path; 41 | use std::process::{Command, ExitStatus}; 42 | 43 | fn cross_platform_touch(path: &Path) -> io::Result { 44 | Command::new("touch").arg(path).status() 45 | } 46 | 47 | #[test] 48 | fn test_hardlinks_no_zero() { 49 | let mut file_path = env::temp_dir(); 50 | file_path.push("inode.tmp"); 51 | 52 | let success = cross_platform_touch(&file_path).unwrap().success(); 53 | assert!(success, "failed to exec touch"); 54 | 55 | let links = Links::from(&file_path.metadata().unwrap()); 56 | 57 | #[cfg(unix)] 58 | assert!(links.nlink.is_some()); 59 | #[cfg(windows)] 60 | assert!(links.nlink.is_none()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/meta/locale.rs: -------------------------------------------------------------------------------- 1 | use chrono::Locale; 2 | use once_cell::sync::OnceCell; 3 | use sys_locale::get_locale; 4 | 5 | fn locale_str() -> String { 6 | get_locale().unwrap_or_default().replace('-', "_") 7 | } 8 | 9 | /// Finds current locale 10 | pub fn current_locale() -> Locale { 11 | const DEFAULT: Locale = Locale::en_US; 12 | static CACHE: OnceCell = OnceCell::new(); 13 | 14 | *CACHE.get_or_init(|| Locale::try_from(locale_str().as_str()).unwrap_or(DEFAULT)) 15 | } 16 | -------------------------------------------------------------------------------- /src/meta/owner.rs: -------------------------------------------------------------------------------- 1 | use crate::color::{ColoredString, Colors, Elem}; 2 | use crate::Flags; 3 | #[cfg(unix)] 4 | use std::fs::Metadata; 5 | #[cfg(unix)] 6 | use users::{Groups, Users, UsersCache}; 7 | 8 | #[derive(Default)] 9 | pub struct Cache { 10 | #[cfg(unix)] 11 | users: UsersCache, 12 | #[cfg(unix)] 13 | groups: UsersCache, 14 | } 15 | 16 | #[cfg(unix)] 17 | #[derive(Clone, Debug, Default)] 18 | pub struct Owner { 19 | user: u32, 20 | group: u32, 21 | } 22 | 23 | #[cfg(windows)] 24 | #[derive(Clone, Debug, Default)] 25 | pub struct Owner { 26 | user: String, 27 | group: String, 28 | } 29 | 30 | impl Owner { 31 | #[cfg(windows)] 32 | pub fn new(user: String, group: String) -> Self { 33 | Self { user, group } 34 | } 35 | } 36 | 37 | #[cfg(unix)] 38 | impl From<&Metadata> for Owner { 39 | fn from(meta: &Metadata) -> Self { 40 | use std::os::unix::fs::MetadataExt; 41 | 42 | Self { 43 | user: meta.uid(), 44 | group: meta.gid(), 45 | } 46 | } 47 | } 48 | 49 | fn truncate(input: &str, after: Option, marker: Option) -> String { 50 | let mut output = input.to_string(); 51 | 52 | if let Some(after) = after { 53 | if output.len() > after { 54 | output.truncate(after); 55 | 56 | if let Some(marker) = marker { 57 | output.push_str(&marker); 58 | } 59 | } 60 | } 61 | 62 | output 63 | } 64 | 65 | impl Owner { 66 | // allow unused variables because cache is used in unix, maybe we can cache for windows in the future 67 | #[allow(unused_variables)] 68 | pub fn render_user(&self, colors: &Colors, cache: &Cache, flags: &Flags) -> ColoredString { 69 | #[cfg(unix)] 70 | let user = &match cache.users.get_user_by_uid(self.user) { 71 | Some(user) => user.name().to_string_lossy().to_string(), 72 | None => self.user.to_string(), 73 | }; 74 | #[cfg(windows)] 75 | let user = &self.user; 76 | 77 | colors.colorize( 78 | truncate( 79 | user, 80 | flags.truncate_owner.after, 81 | flags.truncate_owner.marker.clone(), 82 | ), 83 | &Elem::User, 84 | ) 85 | } 86 | 87 | // allow unused variables because cache is used in unix, maybe we can cache for windows in the future 88 | #[allow(unused_variables)] 89 | pub fn render_group(&self, colors: &Colors, cache: &Cache, flags: &Flags) -> ColoredString { 90 | #[cfg(unix)] 91 | let group = &match cache.groups.get_group_by_gid(self.group) { 92 | Some(group) => group.name().to_string_lossy().to_string(), 93 | None => self.group.to_string(), 94 | }; 95 | #[cfg(windows)] 96 | let group = &self.group; 97 | 98 | colors.colorize( 99 | truncate( 100 | group, 101 | flags.truncate_owner.after, 102 | flags.truncate_owner.marker.clone(), 103 | ), 104 | &Elem::Group, 105 | ) 106 | } 107 | } 108 | 109 | #[cfg(test)] 110 | mod test_truncate { 111 | use crate::meta::owner::truncate; 112 | 113 | #[test] 114 | fn test_none() { 115 | assert_eq!("a", truncate("a", None, None)); 116 | } 117 | 118 | #[test] 119 | fn test_unchanged_without_marker() { 120 | assert_eq!("a", truncate("a", Some(1), None)); 121 | } 122 | 123 | #[test] 124 | fn test_unchanged_with_marker() { 125 | assert_eq!("a", truncate("a", Some(1), Some("…".to_string()))); 126 | } 127 | 128 | #[test] 129 | fn test_truncated_without_marker() { 130 | assert_eq!("a", truncate("ab", Some(1), None)); 131 | } 132 | 133 | #[test] 134 | fn test_truncated_with_marker() { 135 | assert_eq!("a…", truncate("ab", Some(1), Some("…".to_string()))); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/meta/permissions_or_attributes.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | use super::windows_attributes::WindowsAttributes; 3 | use crate::{ 4 | color::{ColoredString, Colors}, 5 | flags::Flags, 6 | }; 7 | 8 | use super::Permissions; 9 | 10 | #[derive(Clone, Debug)] 11 | pub enum PermissionsOrAttributes { 12 | Permissions(Permissions), 13 | #[cfg(windows)] 14 | WindowsAttributes(WindowsAttributes), 15 | } 16 | 17 | impl PermissionsOrAttributes { 18 | pub fn render(&self, colors: &Colors, flags: &Flags) -> ColoredString { 19 | match self { 20 | PermissionsOrAttributes::Permissions(permissions) => permissions.render(colors, flags), 21 | #[cfg(windows)] 22 | PermissionsOrAttributes::WindowsAttributes(attributes) => { 23 | attributes.render(colors, flags) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/meta/size.rs: -------------------------------------------------------------------------------- 1 | use crate::color::{ColoredString, Colors, Elem}; 2 | use crate::flags::{Flags, SizeFlag}; 3 | use std::fs::Metadata; 4 | 5 | const KB: u64 = 1024; 6 | const MB: u64 = 1024_u64.pow(2); 7 | const GB: u64 = 1024_u64.pow(3); 8 | const TB: u64 = 1024_u64.pow(4); 9 | 10 | #[derive(Clone, Debug, PartialEq, Eq)] 11 | pub enum Unit { 12 | Byte, 13 | Kilo, 14 | Mega, 15 | Giga, 16 | Tera, 17 | } 18 | 19 | #[derive(Clone, Debug, PartialEq, Eq)] 20 | pub struct Size { 21 | bytes: u64, 22 | } 23 | 24 | impl From<&Metadata> for Size { 25 | fn from(meta: &Metadata) -> Self { 26 | Self { bytes: meta.len() } 27 | } 28 | } 29 | 30 | impl Size { 31 | pub fn new(bytes: u64) -> Self { 32 | Self { bytes } 33 | } 34 | 35 | pub fn get_bytes(&self) -> u64 { 36 | self.bytes 37 | } 38 | 39 | fn format_size(&self, number: f64) -> String { 40 | format!("{0:.1$}", number, if number < 10.0 { 1 } else { 0 }) 41 | } 42 | 43 | fn get_unit(&self, flags: &Flags) -> Unit { 44 | if flags.size == SizeFlag::Bytes { 45 | return Unit::Byte; 46 | } 47 | 48 | match self.bytes { 49 | b if b < KB => Unit::Byte, 50 | b if b < MB => Unit::Kilo, 51 | b if b < GB => Unit::Mega, 52 | b if b < TB => Unit::Giga, 53 | _ => Unit::Tera, 54 | } 55 | } 56 | 57 | pub fn render( 58 | &self, 59 | colors: &Colors, 60 | flags: &Flags, 61 | val_alignment: Option, 62 | ) -> ColoredString { 63 | let val_content = self.render_value(colors, flags); 64 | let unit_content = self.render_unit(colors, flags); 65 | 66 | let left_pad = if let Some(align) = val_alignment { 67 | " ".repeat(align - val_content.content().len()) 68 | } else { 69 | "".to_string() 70 | }; 71 | 72 | let mut strings: Vec = vec![ 73 | ColoredString::new(Colors::default_style(), left_pad), 74 | val_content, 75 | ]; 76 | if flags.size != SizeFlag::Short { 77 | strings.push(ColoredString::new(Colors::default_style(), " ".into())); 78 | } 79 | strings.push(unit_content); 80 | 81 | let res = strings 82 | .into_iter() 83 | .map(|s| s.to_string()) 84 | .collect::>() 85 | .join(""); 86 | ColoredString::new(Colors::default_style(), res) 87 | } 88 | 89 | fn paint(&self, colors: &Colors, content: String) -> ColoredString { 90 | let bytes = self.get_bytes(); 91 | 92 | let elem = if bytes >= GB { 93 | &Elem::FileLarge 94 | } else if bytes >= MB { 95 | &Elem::FileMedium 96 | } else { 97 | &Elem::FileSmall 98 | }; 99 | 100 | colors.colorize(content, elem) 101 | } 102 | 103 | pub fn render_value(&self, colors: &Colors, flags: &Flags) -> ColoredString { 104 | let content = self.value_string(flags); 105 | 106 | self.paint(colors, content) 107 | } 108 | 109 | pub fn value_string(&self, flags: &Flags) -> String { 110 | let unit = self.get_unit(flags); 111 | 112 | match unit { 113 | Unit::Byte => self.bytes.to_string(), 114 | Unit::Kilo => self.format_size(((self.bytes as f64 / KB as f64) * 10.0).round() / 10.0), 115 | Unit::Mega => self.format_size(((self.bytes as f64 / MB as f64) * 10.0).round() / 10.0), 116 | Unit::Giga => self.format_size(((self.bytes as f64 / GB as f64) * 10.0).round() / 10.0), 117 | Unit::Tera => self.format_size(((self.bytes as f64 / TB as f64) * 10.0).round() / 10.0), 118 | } 119 | } 120 | 121 | pub fn render_unit(&self, colors: &Colors, flags: &Flags) -> ColoredString { 122 | let content = self.unit_string(flags); 123 | 124 | self.paint(colors, content) 125 | } 126 | 127 | pub fn unit_string(&self, flags: &Flags) -> String { 128 | let unit = self.get_unit(flags); 129 | 130 | match flags.size { 131 | SizeFlag::Default => match unit { 132 | Unit::Byte => String::from('B'), 133 | Unit::Kilo => String::from("KB"), 134 | Unit::Mega => String::from("MB"), 135 | Unit::Giga => String::from("GB"), 136 | Unit::Tera => String::from("TB"), 137 | }, 138 | SizeFlag::Short => match unit { 139 | Unit::Byte => String::from('B'), 140 | Unit::Kilo => String::from('K'), 141 | Unit::Mega => String::from('M'), 142 | Unit::Giga => String::from('G'), 143 | Unit::Tera => String::from('T'), 144 | }, 145 | SizeFlag::Bytes => String::from(""), 146 | } 147 | } 148 | } 149 | 150 | #[cfg(test)] 151 | mod test { 152 | use super::{Size, GB, KB, MB, TB}; 153 | use crate::color::{Colors, ThemeOption}; 154 | use crate::flags::{Flags, SizeFlag}; 155 | 156 | #[test] 157 | fn render_byte() { 158 | let size = Size::new(42); // == 42 bytes 159 | let mut flags = Flags::default(); 160 | 161 | assert_eq!(size.value_string(&flags), "42"); 162 | 163 | assert_eq!(size.unit_string(&flags), "B"); 164 | flags.size = SizeFlag::Short; 165 | assert_eq!(size.unit_string(&flags), "B"); 166 | flags.size = SizeFlag::Bytes; 167 | assert_eq!(size.unit_string(&flags), ""); 168 | } 169 | 170 | #[test] 171 | fn render_10_minus_kilobyte() { 172 | let size = Size::new(4 * KB); // 4 kilobytes 173 | let mut flags = Flags::default(); 174 | 175 | assert_eq!(size.value_string(&flags), "4.0"); 176 | assert_eq!(size.unit_string(&flags), "KB"); 177 | flags.size = SizeFlag::Short; 178 | assert_eq!(size.unit_string(&flags), "K"); 179 | } 180 | 181 | #[test] 182 | fn render_kilobyte() { 183 | let size = Size::new(42 * KB); // 42 kilobytes 184 | let mut flags = Flags::default(); 185 | 186 | assert_eq!(size.value_string(&flags), "42"); 187 | assert_eq!(size.unit_string(&flags), "KB"); 188 | flags.size = SizeFlag::Short; 189 | assert_eq!(size.unit_string(&flags), "K"); 190 | } 191 | 192 | #[test] 193 | fn render_100_plus_kilobyte() { 194 | let size = Size::new(420 * KB + 420); // 420.4 kilobytes 195 | let mut flags = Flags::default(); 196 | 197 | assert_eq!(size.value_string(&flags), "420"); 198 | assert_eq!(size.unit_string(&flags), "KB"); 199 | flags.size = SizeFlag::Short; 200 | assert_eq!(size.unit_string(&flags), "K"); 201 | } 202 | 203 | #[test] 204 | fn render_10_minus_megabyte() { 205 | let size = Size::new(4 * MB); // 4 megabytes 206 | let mut flags = Flags::default(); 207 | 208 | assert_eq!(size.value_string(&flags), "4.0"); 209 | assert_eq!(size.unit_string(&flags), "MB"); 210 | flags.size = SizeFlag::Short; 211 | assert_eq!(size.unit_string(&flags), "M"); 212 | } 213 | 214 | #[test] 215 | fn render_megabyte() { 216 | let size = Size::new(42 * MB); // 42 megabytes 217 | let mut flags = Flags::default(); 218 | 219 | assert_eq!(size.value_string(&flags), "42"); 220 | assert_eq!(size.unit_string(&flags), "MB"); 221 | flags.size = SizeFlag::Short; 222 | assert_eq!(size.unit_string(&flags), "M"); 223 | } 224 | 225 | #[test] 226 | fn render_100_plus_megabyte() { 227 | let size = Size::new(420 * MB + 420 * KB); // 420.4 megabytes 228 | let mut flags = Flags::default(); 229 | 230 | assert_eq!(size.value_string(&flags), "420"); 231 | assert_eq!(size.unit_string(&flags), "MB"); 232 | flags.size = SizeFlag::Short; 233 | assert_eq!(size.unit_string(&flags), "M"); 234 | } 235 | 236 | #[test] 237 | fn render_10_minus_gigabyte() { 238 | let size = Size::new(4 * GB); // 4 gigabytes 239 | let mut flags = Flags::default(); 240 | 241 | assert_eq!(size.value_string(&flags), "4.0"); 242 | assert_eq!(size.unit_string(&flags), "GB"); 243 | flags.size = SizeFlag::Short; 244 | assert_eq!(size.unit_string(&flags), "G"); 245 | } 246 | 247 | #[test] 248 | fn render_gigabyte() { 249 | let size = Size::new(42 * GB); // 42 gigabytes 250 | let mut flags = Flags::default(); 251 | 252 | assert_eq!(size.value_string(&flags), "42"); 253 | assert_eq!(size.unit_string(&flags), "GB"); 254 | flags.size = SizeFlag::Short; 255 | assert_eq!(size.unit_string(&flags), "G"); 256 | } 257 | 258 | #[test] 259 | fn render_100_plus_gigabyte() { 260 | let size = Size::new(420 * GB + 420 * MB); // 420.4 gigabytes 261 | let mut flags = Flags::default(); 262 | 263 | assert_eq!(size.value_string(&flags), "420"); 264 | assert_eq!(size.unit_string(&flags), "GB"); 265 | flags.size = SizeFlag::Short; 266 | assert_eq!(size.unit_string(&flags), "G"); 267 | } 268 | 269 | #[test] 270 | fn render_10_minus_terabyte() { 271 | let size = Size::new(4 * TB); // 4 terabytes 272 | let mut flags = Flags::default(); 273 | 274 | assert_eq!(size.value_string(&flags), "4.0"); 275 | assert_eq!(size.unit_string(&flags), "TB"); 276 | flags.size = SizeFlag::Short; 277 | assert_eq!(size.unit_string(&flags), "T"); 278 | } 279 | 280 | #[test] 281 | fn render_terabyte() { 282 | let size = Size::new(42 * TB); // 42 terabytes 283 | let mut flags = Flags::default(); 284 | 285 | assert_eq!(size.value_string(&flags), "42"); 286 | assert_eq!(size.unit_string(&flags), "TB"); 287 | flags.size = SizeFlag::Short; 288 | assert_eq!(size.unit_string(&flags), "T"); 289 | } 290 | 291 | #[test] 292 | fn render_100_plus_terabyte() { 293 | let size = Size::new(420 * TB + 420 * GB); // 420.4 terabytes 294 | let mut flags = Flags::default(); 295 | 296 | assert_eq!(size.value_string(&flags), "420"); 297 | assert_eq!(size.unit_string(&flags), "TB"); 298 | flags.size = SizeFlag::Short; 299 | assert_eq!(size.unit_string(&flags), "T"); 300 | } 301 | 302 | #[test] 303 | fn render_with_a_fraction() { 304 | let size = Size::new(42 * KB + 103); // 42.1 kilobytes 305 | let flags = Flags::default(); 306 | 307 | assert_eq!(size.value_string(&flags), "42"); 308 | assert_eq!(size.unit_string(&flags), "KB"); 309 | } 310 | 311 | #[test] 312 | fn render_with_a_truncated_fraction() { 313 | let size = Size::new(42 * KB + 1); // 42.001 kilobytes == 42 kilobytes 314 | let flags = Flags::default(); 315 | 316 | assert_eq!(size.value_string(&flags), "42"); 317 | assert_eq!(size.unit_string(&flags), "KB"); 318 | } 319 | 320 | #[test] 321 | fn render_short_nospaces() { 322 | let size = Size::new(42 * KB); // 42 kilobytes 323 | let flags = Flags { 324 | size: SizeFlag::Short, 325 | ..Default::default() 326 | }; 327 | let colors = Colors::new(ThemeOption::NoColor); 328 | 329 | assert_eq!(size.render(&colors, &flags, Some(2)).to_string(), "42K"); 330 | assert_eq!(size.render(&colors, &flags, Some(3)).to_string(), " 42K"); 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/meta/symlink.rs: -------------------------------------------------------------------------------- 1 | use crate::color::{ColoredString, Colors, Elem}; 2 | use crate::flags::Flags; 3 | use std::fs::read_link; 4 | use std::path::Path; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct SymLink { 8 | target: Option, 9 | valid: bool, 10 | } 11 | 12 | impl From<&Path> for SymLink { 13 | fn from(path: &Path) -> Self { 14 | if let Ok(target) = read_link(path) { 15 | if target.is_absolute() || path.parent().is_none() { 16 | return Self { 17 | valid: target.exists(), 18 | target: Some( 19 | target 20 | .to_str() 21 | .expect("failed to convert symlink to str") 22 | .to_string(), 23 | ), 24 | }; 25 | } 26 | 27 | return Self { 28 | target: Some( 29 | target 30 | .to_str() 31 | .expect("failed to convert symlink to str") 32 | .to_string(), 33 | ), 34 | valid: path.parent().unwrap().join(target).exists(), 35 | }; 36 | } 37 | 38 | Self { 39 | target: None, 40 | valid: false, 41 | } 42 | } 43 | } 44 | 45 | impl SymLink { 46 | pub fn symlink_string(&self) -> Option { 47 | self.target.as_ref().map(|target| target.to_string()) 48 | } 49 | 50 | pub fn render(&self, colors: &Colors, flag: &Flags) -> ColoredString { 51 | if let Some(target_string) = self.symlink_string() { 52 | let elem = if self.valid { 53 | &Elem::SymLink 54 | } else { 55 | &Elem::MissingSymLinkTarget 56 | }; 57 | 58 | let strings: &[ColoredString] = &[ 59 | ColoredString::new(Colors::default_style(), format!(" {} ", flag.symlink_arrow)), // ⇒ \u{21d2} 60 | colors.colorize(target_string, elem), 61 | ]; 62 | 63 | let res = strings 64 | .iter() 65 | .map(|s| s.to_string()) 66 | .collect::>() 67 | .join(""); 68 | ColoredString::new(Colors::default_style(), res) 69 | } else { 70 | ColoredString::new(Colors::default_style(), "".into()) 71 | } 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use clap::Parser; 78 | 79 | use super::SymLink; 80 | 81 | use crate::app::Cli; 82 | use crate::color::{Colors, ThemeOption}; 83 | use crate::config_file::Config; 84 | use crate::flags::Flags; 85 | 86 | #[test] 87 | fn test_symlink_render_default_valid_target_nocolor() { 88 | let link = SymLink { 89 | target: Some("/target".to_string()), 90 | valid: true, 91 | }; 92 | let argv = ["lsd"]; 93 | let cli = Cli::try_parse_from(argv).unwrap(); 94 | assert_eq!( 95 | format!("{}", " ⇒ /target"), 96 | link.render( 97 | &Colors::new(ThemeOption::NoColor), 98 | &Flags::configure_from(&cli, &Config::with_none()).unwrap() 99 | ) 100 | .to_string() 101 | ); 102 | } 103 | 104 | #[test] 105 | fn test_symlink_render_default_invalid_target_nocolor() { 106 | let link = SymLink { 107 | target: Some("/target".to_string()), 108 | valid: false, 109 | }; 110 | let argv = ["lsd"]; 111 | let cli = Cli::try_parse_from(argv).unwrap(); 112 | assert_eq!( 113 | format!("{}", " ⇒ /target"), 114 | link.render( 115 | &Colors::new(ThemeOption::NoColor), 116 | &Flags::configure_from(&cli, &Config::with_none()).unwrap() 117 | ) 118 | .to_string() 119 | ); 120 | } 121 | 122 | #[test] 123 | fn test_symlink_render_default_invalid_target_withcolor() { 124 | let link = SymLink { 125 | target: Some("/target".to_string()), 126 | valid: false, 127 | }; 128 | let argv = ["lsd"]; 129 | let cli = Cli::try_parse_from(argv).unwrap(); 130 | assert_eq!( 131 | format!("{}", " ⇒ \u{1b}[38;5;124m/target\u{1b}[39m"), 132 | link.render( 133 | &Colors::new(ThemeOption::NoLscolors), 134 | &Flags::configure_from(&cli, &Config::with_none()).unwrap() 135 | ) 136 | .to_string() 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/meta/windows_attributes.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | color::{ColoredString, Colors, Elem}, 3 | flags::Flags, 4 | }; 5 | 6 | use std::os::windows::fs::MetadataExt; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct WindowsAttributes { 10 | pub archive: bool, 11 | pub readonly: bool, 12 | pub hidden: bool, 13 | pub system: bool, 14 | } 15 | 16 | pub fn get_attributes(metadata: &std::fs::Metadata) -> WindowsAttributes { 17 | use windows::Win32::Storage::FileSystem::{ 18 | FILE_ATTRIBUTE_ARCHIVE, FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_READONLY, 19 | FILE_ATTRIBUTE_SYSTEM, FILE_FLAGS_AND_ATTRIBUTES, 20 | }; 21 | 22 | let bits = metadata.file_attributes(); 23 | let has_bit = |bit: FILE_FLAGS_AND_ATTRIBUTES| bits & bit.0 == bit.0; 24 | 25 | // https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants 26 | WindowsAttributes { 27 | archive: has_bit(FILE_ATTRIBUTE_ARCHIVE), 28 | readonly: has_bit(FILE_ATTRIBUTE_READONLY), 29 | hidden: has_bit(FILE_ATTRIBUTE_HIDDEN), 30 | system: has_bit(FILE_ATTRIBUTE_SYSTEM), 31 | } 32 | } 33 | 34 | impl WindowsAttributes { 35 | pub fn render(&self, colors: &Colors, _flags: &Flags) -> ColoredString { 36 | let res = [ 37 | match self.archive { 38 | true => colors.colorize("a", &Elem::Archive), 39 | false => colors.colorize('-', &Elem::NoAccess), 40 | }, 41 | match self.readonly { 42 | true => colors.colorize("r", &Elem::AttributeRead), 43 | false => colors.colorize('-', &Elem::NoAccess), 44 | }, 45 | match self.hidden { 46 | true => colors.colorize("h", &Elem::Hidden), 47 | false => colors.colorize('-', &Elem::NoAccess), 48 | }, 49 | match self.system { 50 | true => colors.colorize("s", &Elem::System), 51 | false => colors.colorize('-', &Elem::NoAccess), 52 | }, 53 | ] 54 | .into_iter() 55 | .fold(String::with_capacity(4), |mut acc, x| { 56 | acc.push_str(&x.to_string()); 57 | acc 58 | }); 59 | ColoredString::new(Colors::default_style(), res) 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod test { 65 | use std::fs; 66 | use std::io::Write; 67 | use std::process::Command; 68 | 69 | use crate::{ 70 | color::{Colors, ThemeOption}, 71 | flags::Flags, 72 | }; 73 | 74 | use super::get_attributes; 75 | use tempfile::tempdir; 76 | 77 | #[test] 78 | pub fn archived_file() { 79 | let attribute_string = create_and_process_file_with_attributes("archived_file.txt", "+A"); 80 | assert_eq!("a---", attribute_string); 81 | } 82 | 83 | #[test] 84 | pub fn readonly_file() { 85 | let attribute_string = create_and_process_file_with_attributes("readonly_file.txt", "+R"); 86 | assert_eq!("ar--", attribute_string); 87 | } 88 | 89 | #[test] 90 | pub fn hidden_file() { 91 | let attribute_string = create_and_process_file_with_attributes("hidden_file.txt", "+H"); 92 | assert_eq!("a-h-", attribute_string); 93 | } 94 | 95 | #[test] 96 | pub fn system_file() { 97 | let attribute_string = create_and_process_file_with_attributes("system_file.txt", "+S"); 98 | assert_eq!("a--s", attribute_string); 99 | } 100 | 101 | fn create_and_process_file_with_attributes(name: &str, attrs: &str) -> String { 102 | let tmp_dir = tempdir().expect("failed to create temp dir"); 103 | let path = tmp_dir.path().join(name); 104 | let mut file = fs::File::create(path.clone()).unwrap(); 105 | writeln!(file, "Test content").unwrap(); 106 | Command::new("attrib") 107 | .arg(attrs) 108 | .arg(&path) 109 | .output() 110 | .expect("able to set attributes"); 111 | let metadata = file.metadata().expect("able to get metadata"); 112 | 113 | let colors = Colors::new(ThemeOption::NoColor); 114 | 115 | let attributes = get_attributes(&metadata); 116 | attributes 117 | .render(&colors, &Flags::default()) 118 | .content() 119 | .to_string() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/theme.rs: -------------------------------------------------------------------------------- 1 | pub mod color; 2 | pub mod git; 3 | pub mod icon; 4 | 5 | use std::path::Path; 6 | use std::{fs, io}; 7 | 8 | use serde::{de::DeserializeOwned, Deserialize}; 9 | use thiserror::Error; 10 | 11 | use crate::config_file; 12 | use crate::print_error; 13 | 14 | use color::ColorTheme; 15 | use git::GitThemeSymbols; 16 | use icon::IconTheme; 17 | 18 | #[derive(Debug, Deserialize, Default, PartialEq, Eq)] 19 | #[serde(rename_all = "kebab-case")] 20 | #[serde(deny_unknown_fields)] 21 | #[serde(default)] 22 | pub struct Theme { 23 | pub color: ColorTheme, 24 | pub icon: IconTheme, 25 | pub git_theme: GitThemeSymbols, 26 | } 27 | 28 | #[derive(Error, Debug)] 29 | pub enum Error { 30 | #[error("Can not read the theme file")] 31 | ReadFailed(#[from] io::Error), 32 | #[error("Theme file format invalid")] 33 | InvalidFormat(#[from] serde_yaml::Error), 34 | #[error("Theme file path invalid {0}")] 35 | InvalidPath(String), 36 | } 37 | 38 | impl Theme { 39 | /// Read theme from a file path 40 | /// use the file path as-is if it is absolute 41 | /// search the config paths folders for it if not 42 | pub fn from_path(file: &str) -> Result 43 | where 44 | D: DeserializeOwned + Default, 45 | { 46 | let real = if let Some(path) = config_file::expand_home(file) { 47 | path 48 | } else { 49 | print_error!("Not a valid theme file path: {}.", &file); 50 | return Err(Error::InvalidPath(file.to_string())); 51 | }; 52 | 53 | let mut paths = if Path::new(&real).is_absolute() { 54 | vec![real].into_iter() 55 | } else { 56 | config_file::Config::config_paths() 57 | .map(|p| p.join(real.clone())) 58 | .collect::>() 59 | .into_iter() 60 | }; 61 | 62 | let Some(valid) = paths.find_map(|p| { 63 | let yaml = p.with_extension("yaml"); 64 | let yml = p.with_extension("yml"); 65 | if yaml.is_file() { 66 | Some(yaml) 67 | } else if yml.is_file() { 68 | Some(yml) 69 | } else { 70 | None 71 | } 72 | }) else { 73 | return Err(Error::InvalidPath("No valid theme file found".to_string())); 74 | }; 75 | 76 | match fs::read_to_string(valid) { 77 | Ok(yaml) => match Self::with_yaml(&yaml) { 78 | Ok(t) => Ok(t), 79 | Err(e) => Err(Error::InvalidFormat(e)), 80 | }, 81 | Err(e) => Err(Error::ReadFailed(e)), 82 | } 83 | } 84 | 85 | /// This constructs a Theme struct with a passed [Yaml] str. 86 | fn with_yaml(yaml: &str) -> Result 87 | where 88 | D: DeserializeOwned + Default, 89 | { 90 | if yaml.trim() == "" { 91 | return Ok(D::default()); 92 | } 93 | serde_yaml::from_str::(yaml) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/theme/git.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize, PartialEq, Eq)] 4 | #[serde(rename_all = "kebab-case")] 5 | #[serde(deny_unknown_fields)] 6 | #[serde(default)] 7 | pub struct GitThemeSymbols { 8 | pub default: String, 9 | pub unmodified: String, 10 | pub new_in_index: String, 11 | pub new_in_workdir: String, 12 | pub deleted: String, 13 | pub modified: String, 14 | pub renamed: String, 15 | pub ignored: String, 16 | pub typechange: String, 17 | pub conflicted: String, 18 | } 19 | 20 | impl Default for GitThemeSymbols { 21 | fn default() -> GitThemeSymbols { 22 | GitThemeSymbols { 23 | default: "-".into(), 24 | unmodified: ".".into(), 25 | new_in_index: "N".into(), 26 | new_in_workdir: "?".into(), 27 | deleted: "D".into(), 28 | modified: "M".into(), 29 | renamed: "R".into(), 30 | ignored: "I".into(), 31 | typechange: "T".into(), 32 | conflicted: "C".into(), 33 | } 34 | } 35 | } 36 | --------------------------------------------------------------------------------