Feat:Moved to Rust
swadhinbiswas committed 8 months ago
a330027
1 parent 353426d
commit a330027
15 files changed
+3773 -497
Showing 15 changed files+3773-493
▼.gitignore
+17-0
| @@ -0,0 +1,17 @@ | ||
1 | +# Generated by Gemini CLI | |
2 | +/target/ | |
3 | + | |
4 | +# IDEs | |
5 | +.vscode/ | |
6 | +.idea/ | |
7 | +*.swp | |
8 | +*.swo | |
9 | + | |
10 | +# OS | |
11 | +.DS_Store | |
12 | + | |
13 | +# Remove log files if they are generated | |
14 | +*.log | |
15 | + | |
16 | +# Backup files | |
17 | +**/*.rs.bk | |
0 | 18 | |
▼BENCHMARKS.md
+194-0
| @@ -0,0 +1,194 @@ | ||
1 | +# Benchmarks: lsf vs GNU ls | |
2 | + | |
3 | +**lsf v5.0.0** (Rust rewrite) vs **GNU coreutils ls 9.10** | |
4 | + | |
5 | +Benchmarked with [hyperfine](https://github.com/sharkdp/hyperfine) using `--shell=none` (`-N`) for accurate sub-millisecond measurement. All tests run with `--color=always` to enable full ANSI color output on both tools. | |
6 | + | |
7 | +**System:** Linux x86-64 | |
8 | + | |
9 | +--- | |
10 | + | |
11 | +## Results Summary | |
12 | + | |
13 | +| Scenario | GNU ls | lsf | Ratio | Winner | | |
14 | +|---|---|---|---|---| | |
15 | +| Small dir, grid (7 files) | 0.75 ms | 0.65 ms | **0.87x** | lsf | | |
16 | +| Large dir, grid (7300 files) | 10.2 ms | 12.9 ms | **1.27x** | GNU ls | | |
17 | +| Long format, small dir | 1.7 ms | 1.8 ms | **1.06x** | ~tied | | |
18 | +| Long format, large dir (7300 files) | 18.4 ms | 18.3 ms | **1.00x** | ~tied | | |
19 | +| Single file (`-l Cargo.toml`) | 1.8 ms | 1.7 ms | **0.94x** | lsf | | |
20 | +| Recursive (`-R /usr/share/doc/`) | 17.1 ms | 27.1 ms | **1.59x** | GNU ls | | |
21 | + | |
22 | +> **lsf adds Nerd Font icons and colored icons to every entry** -- GNU ls only adds filename colors. Despite this extra work, lsf matches or beats GNU ls in most scenarios. | |
23 | + | |
24 | +--- | |
25 | + | |
26 | +## Detailed Results | |
27 | + | |
28 | +### 1. Small Directory - Grid View | |
29 | + | |
30 | +Listing `src/` (7 Rust source files): | |
31 | + | |
32 | +``` | |
33 | +Benchmark 1: /sbin/ls --color=always src/ | |
34 | + Time (mean +/- s): 745.8 us +/- 465.4 us | |
35 | + Range (min ... max): 351.0 us ... 3375.1 us | |
36 | + | |
37 | +Benchmark 2: ./target/release/lsf --color=always src/ | |
38 | + Time (mean +/- s): 649.2 us +/- 329.1 us | |
39 | + Range (min ... max): 399.0 us ... 2437.5 us | |
40 | + | |
41 | +Summary: lsf ran 1.15x faster than GNU ls | |
42 | +``` | |
43 | + | |
44 | +### 2. Large Directory - Grid View | |
45 | + | |
46 | +Listing `/tmp/lsf-bench/` (7300 files: 5000 .txt, 500 dirs, 1800 multi-extension): | |
47 | + | |
48 | +``` | |
49 | +Benchmark 1: /sbin/ls --color=always /tmp/lsf-bench/ | |
50 | + Time (mean +/- s): 10.2 ms +/- 3.6 ms | |
51 | + Range (min ... max): 5.7 ms ... 22.8 ms | |
52 | + | |
53 | +Benchmark 2: ./target/release/lsf --color=always /tmp/lsf-bench/ | |
54 | + Time (mean +/- s): 12.9 ms +/- 3.2 ms | |
55 | + Range (min ... max): 8.5 ms ... 21.5 ms | |
56 | + | |
57 | +Summary: GNU ls ran 1.27x faster than lsf | |
58 | +``` | |
59 | + | |
60 | +### 3. Long Format - Small Directory | |
61 | + | |
62 | +`-lah` on `src/` (7 files): | |
63 | + | |
64 | +``` | |
65 | +Benchmark 1: /sbin/ls -lah --color=always src/ | |
66 | + Time (mean +/- s): 1.7 ms +/- 1.0 ms | |
67 | + | |
68 | +Benchmark 2: ./target/release/lsf -lah --color=always src/ | |
69 | + Time (mean +/- s): 1.8 ms +/- 1.1 ms | |
70 | + | |
71 | +Summary: ~tied (1.06x) | |
72 | +``` | |
73 | + | |
74 | +### 4. Long Format - Large Directory | |
75 | + | |
76 | +`-lah` on 7300 files: | |
77 | + | |
78 | +``` | |
79 | +Benchmark 1: /sbin/ls -lah --color=always /tmp/lsf-bench/ | |
80 | + Time (mean +/- s): 18.4 ms +/- 3.2 ms | |
81 | + | |
82 | +Benchmark 2: ./target/release/lsf -lah --color=always /tmp/lsf-bench/ | |
83 | + Time (mean +/- s): 18.3 ms +/- 3.6 ms | |
84 | + | |
85 | +Summary: lsf ran 1.01x faster (effectively tied) | |
86 | +``` | |
87 | + | |
88 | +### 5. Single File | |
89 | + | |
90 | +`-l Cargo.toml`: | |
91 | + | |
92 | +``` | |
93 | +Benchmark 1: /sbin/ls -l --color=always Cargo.toml | |
94 | + Time (mean +/- s): 1.8 ms +/- 1.0 ms | |
95 | + | |
96 | +Benchmark 2: ./target/release/lsf -l --color=always Cargo.toml | |
97 | + Time (mean +/- s): 1.7 ms +/- 1.0 ms | |
98 | + | |
99 | +Summary: lsf ran 1.09x faster | |
100 | +``` | |
101 | + | |
102 | +### 6. Recursive Listing | |
103 | + | |
104 | +`-R /usr/share/doc/`: | |
105 | + | |
106 | +``` | |
107 | +Benchmark 1: /sbin/ls -R --color=always /usr/share/doc/ | |
108 | + Time (mean +/- s): 17.1 ms +/- 3.1 ms | |
109 | + | |
110 | +Benchmark 2: ./target/release/lsf -R --color=always /usr/share/doc/ | |
111 | + Time (mean +/- s): 27.1 ms +/- 2.1 ms | |
112 | + | |
113 | +Summary: GNU ls ran 1.59x faster | |
114 | +``` | |
115 | + | |
116 | +--- | |
117 | + | |
118 | +## lsf-Only Features | |
119 | + | |
120 | +### 7. Tree View | |
121 | + | |
122 | +`--tree --depth 3` on `/usr/share/doc/`: | |
123 | + | |
124 | +``` | |
125 | +Benchmark: ./target/release/lsf --tree --depth 3 --color=always /usr/share/doc/ | |
126 | + Time (mean +/- s): 17.2 ms +/- 3.0 ms | |
127 | +``` | |
128 | + | |
129 | +> Tree view has no GNU ls equivalent. lsf renders a full tree in ~17ms. | |
130 | + | |
131 | +### 8. Icon Overhead | |
132 | + | |
133 | +lsf with icons vs without on 7300 files: | |
134 | + | |
135 | +``` | |
136 | +With icons: 13.2 ms +/- 3.2 ms | |
137 | +Without icons: 13.3 ms +/- 3.4 ms | |
138 | + | |
139 | +Summary: icon lookup adds ~0% overhead (effectively zero) | |
140 | +``` | |
141 | + | |
142 | +> The icon lookup system is so fast that enabling/disabling icons makes no measurable difference. | |
143 | + | |
144 | +--- | |
145 | + | |
146 | +## Binary Size | |
147 | + | |
148 | +| | Size | | |
149 | +|---|---| | |
150 | +| lsf binary (stripped, LTO) | 935 KB | | |
151 | +| GNU ls binary | ~140 KB | | |
152 | + | |
153 | +lsf is a single static binary with zero runtime dependencies (no Bash, no Python, no GNU coreutils needed). | |
154 | + | |
155 | +--- | |
156 | + | |
157 | +## How to Reproduce | |
158 | + | |
159 | +```bash | |
160 | +# Install hyperfine | |
161 | +cargo install hyperfine | |
162 | + | |
163 | +# Create test directory (7300 files) | |
164 | +mkdir -p /tmp/lsf-bench | |
165 | +for i in $(seq 1 5000); do touch "/tmp/lsf-bench/file_$i.txt"; done | |
166 | +for i in $(seq 1 500); do mkdir -p "/tmp/lsf-bench/dir_$i"; done | |
167 | +for ext in rs py js ts go c cpp h hpp java rb php css html json yaml toml xml md sh; do | |
168 | + for i in $(seq 1 100); do touch "/tmp/lsf-bench/sample_${i}.${ext}"; done | |
169 | +done | |
170 | + | |
171 | +# Build release binary | |
172 | +cargo build --release | |
173 | + | |
174 | +# Run benchmarks | |
175 | +hyperfine --warmup 10 --min-runs 1000 -N \ | |
176 | + '/sbin/ls --color=always src/' \ | |
177 | + './target/release/lsf --color=always src/' | |
178 | + | |
179 | +hyperfine --warmup 5 --min-runs 100 -N \ | |
180 | + '/sbin/ls --color=always /tmp/lsf-bench/' \ | |
181 | + './target/release/lsf --color=always /tmp/lsf-bench/' | |
182 | + | |
183 | +hyperfine --warmup 10 --min-runs 1000 -N \ | |
184 | + '/sbin/ls -lah --color=always src/' \ | |
185 | + './target/release/lsf -lah --color=always src/' | |
186 | + | |
187 | +hyperfine --warmup 5 --min-runs 100 -N \ | |
188 | + '/sbin/ls -lah --color=always /tmp/lsf-bench/' \ | |
189 | + './target/release/lsf -lah --color=always /tmp/lsf-bench/' | |
190 | + | |
191 | +hyperfine --warmup 2 --min-runs 10 -N \ | |
192 | + '/sbin/ls -R --color=always /usr/share/doc/' \ | |
193 | + './target/release/lsf -R --color=always /usr/share/doc/' | |
194 | +``` | |
0 | 195 | |
▼Cargo.lock
+764-0
| @@ -0,0 +1,764 @@ | ||
1 | +# This file is automatically @generated by Cargo. | |
2 | +# It is not intended for manual editing. | |
3 | +version = 4 | |
4 | + | |
5 | +[[package]] | |
6 | +name = "android_system_properties" | |
7 | +version = "0.1.5" | |
8 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
9 | +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" | |
10 | +dependencies = [ | |
11 | + "libc", | |
12 | +] | |
13 | + | |
14 | +[[package]] | |
15 | +name = "anstream" | |
16 | +version = "0.6.21" | |
17 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
18 | +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" | |
19 | +dependencies = [ | |
20 | + "anstyle", | |
21 | + "anstyle-parse", | |
22 | + "anstyle-query", | |
23 | + "anstyle-wincon", | |
24 | + "colorchoice", | |
25 | + "is_terminal_polyfill", | |
26 | + "utf8parse", | |
27 | +] | |
28 | + | |
29 | +[[package]] | |
30 | +name = "anstyle" | |
31 | +version = "1.0.13" | |
32 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
33 | +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" | |
34 | + | |
35 | +[[package]] | |
36 | +name = "anstyle-parse" | |
37 | +version = "0.2.7" | |
38 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
39 | +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" | |
40 | +dependencies = [ | |
41 | + "utf8parse", | |
42 | +] | |
43 | + | |
44 | +[[package]] | |
45 | +name = "anstyle-query" | |
46 | +version = "1.1.5" | |
47 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
48 | +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" | |
49 | +dependencies = [ | |
50 | + "windows-sys 0.61.2", | |
51 | +] | |
52 | + | |
53 | +[[package]] | |
54 | +name = "anstyle-wincon" | |
55 | +version = "3.0.11" | |
56 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
57 | +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" | |
58 | +dependencies = [ | |
59 | + "anstyle", | |
60 | + "once_cell_polyfill", | |
61 | + "windows-sys 0.61.2", | |
62 | +] | |
63 | + | |
64 | +[[package]] | |
65 | +name = "autocfg" | |
66 | +version = "1.5.0" | |
67 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
68 | +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" | |
69 | + | |
70 | +[[package]] | |
71 | +name = "bitflags" | |
72 | +version = "2.10.0" | |
73 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
74 | +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" | |
75 | + | |
76 | +[[package]] | |
77 | +name = "bumpalo" | |
78 | +version = "3.19.1" | |
79 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
80 | +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" | |
81 | + | |
82 | +[[package]] | |
83 | +name = "cc" | |
84 | +version = "1.2.55" | |
85 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
86 | +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" | |
87 | +dependencies = [ | |
88 | + "find-msvc-tools", | |
89 | + "shlex", | |
90 | +] | |
91 | + | |
92 | +[[package]] | |
93 | +name = "cfg-if" | |
94 | +version = "1.0.4" | |
95 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
96 | +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" | |
97 | + | |
98 | +[[package]] | |
99 | +name = "chrono" | |
100 | +version = "0.4.43" | |
101 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
102 | +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" | |
103 | +dependencies = [ | |
104 | + "iana-time-zone", | |
105 | + "js-sys", | |
106 | + "num-traits", | |
107 | + "wasm-bindgen", | |
108 | + "windows-link", | |
109 | +] | |
110 | + | |
111 | +[[package]] | |
112 | +name = "clap" | |
113 | +version = "4.5.58" | |
114 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
115 | +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" | |
116 | +dependencies = [ | |
117 | + "clap_builder", | |
118 | + "clap_derive", | |
119 | +] | |
120 | + | |
121 | +[[package]] | |
122 | +name = "clap_builder" | |
123 | +version = "4.5.58" | |
124 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
125 | +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" | |
126 | +dependencies = [ | |
127 | + "anstream", | |
128 | + "anstyle", | |
129 | + "clap_lex", | |
130 | + "strsim", | |
131 | +] | |
132 | + | |
133 | +[[package]] | |
134 | +name = "clap_derive" | |
135 | +version = "4.5.55" | |
136 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
137 | +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" | |
138 | +dependencies = [ | |
139 | + "heck", | |
140 | + "proc-macro2", | |
141 | + "quote", | |
142 | + "syn", | |
143 | +] | |
144 | + | |
145 | +[[package]] | |
146 | +name = "clap_lex" | |
147 | +version = "1.0.0" | |
148 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
149 | +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" | |
150 | + | |
151 | +[[package]] | |
152 | +name = "colorchoice" | |
153 | +version = "1.0.4" | |
154 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
155 | +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" | |
156 | + | |
157 | +[[package]] | |
158 | +name = "core-foundation-sys" | |
159 | +version = "0.8.7" | |
160 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
161 | +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" | |
162 | + | |
163 | +[[package]] | |
164 | +name = "dirs" | |
165 | +version = "6.0.0" | |
166 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
167 | +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" | |
168 | +dependencies = [ | |
169 | + "dirs-sys", | |
170 | +] | |
171 | + | |
172 | +[[package]] | |
173 | +name = "dirs-sys" | |
174 | +version = "0.5.0" | |
175 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
176 | +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" | |
177 | +dependencies = [ | |
178 | + "libc", | |
179 | + "option-ext", | |
180 | + "redox_users", | |
181 | + "windows-sys 0.61.2", | |
182 | +] | |
183 | + | |
184 | +[[package]] | |
185 | +name = "equivalent" | |
186 | +version = "1.0.2" | |
187 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
188 | +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" | |
189 | + | |
190 | +[[package]] | |
191 | +name = "errno" | |
192 | +version = "0.3.14" | |
193 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
194 | +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | |
195 | +dependencies = [ | |
196 | + "libc", | |
197 | + "windows-sys 0.61.2", | |
198 | +] | |
199 | + | |
200 | +[[package]] | |
201 | +name = "find-msvc-tools" | |
202 | +version = "0.1.9" | |
203 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
204 | +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" | |
205 | + | |
206 | +[[package]] | |
207 | +name = "getrandom" | |
208 | +version = "0.2.17" | |
209 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
210 | +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" | |
211 | +dependencies = [ | |
212 | + "cfg-if", | |
213 | + "libc", | |
214 | + "wasi", | |
215 | +] | |
216 | + | |
217 | +[[package]] | |
218 | +name = "hashbrown" | |
219 | +version = "0.16.1" | |
220 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
221 | +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" | |
222 | + | |
223 | +[[package]] | |
224 | +name = "heck" | |
225 | +version = "0.5.0" | |
226 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
227 | +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | |
228 | + | |
229 | +[[package]] | |
230 | +name = "iana-time-zone" | |
231 | +version = "0.1.65" | |
232 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
233 | +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" | |
234 | +dependencies = [ | |
235 | + "android_system_properties", | |
236 | + "core-foundation-sys", | |
237 | + "iana-time-zone-haiku", | |
238 | + "js-sys", | |
239 | + "log", | |
240 | + "wasm-bindgen", | |
241 | + "windows-core", | |
242 | +] | |
243 | + | |
244 | +[[package]] | |
245 | +name = "iana-time-zone-haiku" | |
246 | +version = "0.1.2" | |
247 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
248 | +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" | |
249 | +dependencies = [ | |
250 | + "cc", | |
251 | +] | |
252 | + | |
253 | +[[package]] | |
254 | +name = "indexmap" | |
255 | +version = "2.13.0" | |
256 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
257 | +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" | |
258 | +dependencies = [ | |
259 | + "equivalent", | |
260 | + "hashbrown", | |
261 | +] | |
262 | + | |
263 | +[[package]] | |
264 | +name = "is_terminal_polyfill" | |
265 | +version = "1.70.2" | |
266 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
267 | +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" | |
268 | + | |
269 | +[[package]] | |
270 | +name = "js-sys" | |
271 | +version = "0.3.85" | |
272 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
273 | +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" | |
274 | +dependencies = [ | |
275 | + "once_cell", | |
276 | + "wasm-bindgen", | |
277 | +] | |
278 | + | |
279 | +[[package]] | |
280 | +name = "libc" | |
281 | +version = "0.2.181" | |
282 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
283 | +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" | |
284 | + | |
285 | +[[package]] | |
286 | +name = "libredox" | |
287 | +version = "0.1.12" | |
288 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
289 | +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" | |
290 | +dependencies = [ | |
291 | + "bitflags", | |
292 | + "libc", | |
293 | +] | |
294 | + | |
295 | +[[package]] | |
296 | +name = "linux-raw-sys" | |
297 | +version = "0.11.0" | |
298 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
299 | +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" | |
300 | + | |
301 | +[[package]] | |
302 | +name = "log" | |
303 | +version = "0.4.29" | |
304 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
305 | +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" | |
306 | + | |
307 | +[[package]] | |
308 | +name = "lsf" | |
309 | +version = "5.0.0" | |
310 | +dependencies = [ | |
311 | + "chrono", | |
312 | + "clap", | |
313 | + "dirs", | |
314 | + "libc", | |
315 | + "serde", | |
316 | + "terminal_size", | |
317 | + "toml", | |
318 | + "unicode-width", | |
319 | + "users", | |
320 | +] | |
321 | + | |
322 | +[[package]] | |
323 | +name = "memchr" | |
324 | +version = "2.8.0" | |
325 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
326 | +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" | |
327 | + | |
328 | +[[package]] | |
329 | +name = "num-traits" | |
330 | +version = "0.2.19" | |
331 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
332 | +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" | |
333 | +dependencies = [ | |
334 | + "autocfg", | |
335 | +] | |
336 | + | |
337 | +[[package]] | |
338 | +name = "once_cell" | |
339 | +version = "1.21.3" | |
340 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
341 | +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" | |
342 | + | |
343 | +[[package]] | |
344 | +name = "once_cell_polyfill" | |
345 | +version = "1.70.2" | |
346 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
347 | +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" | |
348 | + | |
349 | +[[package]] | |
350 | +name = "option-ext" | |
351 | +version = "0.2.0" | |
352 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
353 | +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" | |
354 | + | |
355 | +[[package]] | |
356 | +name = "proc-macro2" | |
357 | +version = "1.0.106" | |
358 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
359 | +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" | |
360 | +dependencies = [ | |
361 | + "unicode-ident", | |
362 | +] | |
363 | + | |
364 | +[[package]] | |
365 | +name = "quote" | |
366 | +version = "1.0.44" | |
367 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
368 | +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" | |
369 | +dependencies = [ | |
370 | + "proc-macro2", | |
371 | +] | |
372 | + | |
373 | +[[package]] | |
374 | +name = "redox_users" | |
375 | +version = "0.5.2" | |
376 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
377 | +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" | |
378 | +dependencies = [ | |
379 | + "getrandom", | |
380 | + "libredox", | |
381 | + "thiserror", | |
382 | +] | |
383 | + | |
384 | +[[package]] | |
385 | +name = "rustix" | |
386 | +version = "1.1.3" | |
387 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
388 | +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" | |
389 | +dependencies = [ | |
390 | + "bitflags", | |
391 | + "errno", | |
392 | + "libc", | |
393 | + "linux-raw-sys", | |
394 | + "windows-sys 0.61.2", | |
395 | +] | |
396 | + | |
397 | +[[package]] | |
398 | +name = "rustversion" | |
399 | +version = "1.0.22" | |
400 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
401 | +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" | |
402 | + | |
403 | +[[package]] | |
404 | +name = "serde" | |
405 | +version = "1.0.228" | |
406 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
407 | +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" | |
408 | +dependencies = [ | |
409 | + "serde_core", | |
410 | + "serde_derive", | |
411 | +] | |
412 | + | |
413 | +[[package]] | |
414 | +name = "serde_core" | |
415 | +version = "1.0.228" | |
416 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
417 | +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" | |
418 | +dependencies = [ | |
419 | + "serde_derive", | |
420 | +] | |
421 | + | |
422 | +[[package]] | |
423 | +name = "serde_derive" | |
424 | +version = "1.0.228" | |
425 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
426 | +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" | |
427 | +dependencies = [ | |
428 | + "proc-macro2", | |
429 | + "quote", | |
430 | + "syn", | |
431 | +] | |
432 | + | |
433 | +[[package]] | |
434 | +name = "serde_spanned" | |
435 | +version = "0.6.9" | |
436 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
437 | +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" | |
438 | +dependencies = [ | |
439 | + "serde", | |
440 | +] | |
441 | + | |
442 | +[[package]] | |
443 | +name = "shlex" | |
444 | +version = "1.3.0" | |
445 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
446 | +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" | |
447 | + | |
448 | +[[package]] | |
449 | +name = "strsim" | |
450 | +version = "0.11.1" | |
451 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
452 | +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" | |
453 | + | |
454 | +[[package]] | |
455 | +name = "syn" | |
456 | +version = "2.0.115" | |
457 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
458 | +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" | |
459 | +dependencies = [ | |
460 | + "proc-macro2", | |
461 | + "quote", | |
462 | + "unicode-ident", | |
463 | +] | |
464 | + | |
465 | +[[package]] | |
466 | +name = "terminal_size" | |
467 | +version = "0.4.3" | |
468 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
469 | +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" | |
470 | +dependencies = [ | |
471 | + "rustix", | |
472 | + "windows-sys 0.60.2", | |
473 | +] | |
474 | + | |
475 | +[[package]] | |
476 | +name = "thiserror" | |
477 | +version = "2.0.18" | |
478 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
479 | +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" | |
480 | +dependencies = [ | |
481 | + "thiserror-impl", | |
482 | +] | |
483 | + | |
484 | +[[package]] | |
485 | +name = "thiserror-impl" | |
486 | +version = "2.0.18" | |
487 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
488 | +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" | |
489 | +dependencies = [ | |
490 | + "proc-macro2", | |
491 | + "quote", | |
492 | + "syn", | |
493 | +] | |
494 | + | |
495 | +[[package]] | |
496 | +name = "toml" | |
497 | +version = "0.8.23" | |
498 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
499 | +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" | |
500 | +dependencies = [ | |
501 | + "serde", | |
502 | + "serde_spanned", | |
503 | + "toml_datetime", | |
504 | + "toml_edit", | |
505 | +] | |
506 | + | |
507 | +[[package]] | |
508 | +name = "toml_datetime" | |
509 | +version = "0.6.11" | |
510 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
511 | +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" | |
512 | +dependencies = [ | |
513 | + "serde", | |
514 | +] | |
515 | + | |
516 | +[[package]] | |
517 | +name = "toml_edit" | |
518 | +version = "0.22.27" | |
519 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
520 | +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" | |
521 | +dependencies = [ | |
522 | + "indexmap", | |
523 | + "serde", | |
524 | + "serde_spanned", | |
525 | + "toml_datetime", | |
526 | + "toml_write", | |
527 | + "winnow", | |
528 | +] | |
529 | + | |
530 | +[[package]] | |
531 | +name = "toml_write" | |
532 | +version = "0.1.2" | |
533 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
534 | +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" | |
535 | + | |
536 | +[[package]] | |
537 | +name = "unicode-ident" | |
538 | +version = "1.0.23" | |
539 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
540 | +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" | |
541 | + | |
542 | +[[package]] | |
543 | +name = "unicode-width" | |
544 | +version = "0.2.2" | |
545 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
546 | +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" | |
547 | + | |
548 | +[[package]] | |
549 | +name = "users" | |
550 | +version = "0.11.0" | |
551 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
552 | +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" | |
553 | +dependencies = [ | |
554 | + "libc", | |
555 | + "log", | |
556 | +] | |
557 | + | |
558 | +[[package]] | |
559 | +name = "utf8parse" | |
560 | +version = "0.2.2" | |
561 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
562 | +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" | |
563 | + | |
564 | +[[package]] | |
565 | +name = "wasi" | |
566 | +version = "0.11.1+wasi-snapshot-preview1" | |
567 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
568 | +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" | |
569 | + | |
570 | +[[package]] | |
571 | +name = "wasm-bindgen" | |
572 | +version = "0.2.108" | |
573 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
574 | +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" | |
575 | +dependencies = [ | |
576 | + "cfg-if", | |
577 | + "once_cell", | |
578 | + "rustversion", | |
579 | + "wasm-bindgen-macro", | |
580 | + "wasm-bindgen-shared", | |
581 | +] | |
582 | + | |
583 | +[[package]] | |
584 | +name = "wasm-bindgen-macro" | |
585 | +version = "0.2.108" | |
586 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
587 | +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" | |
588 | +dependencies = [ | |
589 | + "quote", | |
590 | + "wasm-bindgen-macro-support", | |
591 | +] | |
592 | + | |
593 | +[[package]] | |
594 | +name = "wasm-bindgen-macro-support" | |
595 | +version = "0.2.108" | |
596 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
597 | +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" | |
598 | +dependencies = [ | |
599 | + "bumpalo", | |
600 | + "proc-macro2", | |
601 | + "quote", | |
602 | + "syn", | |
603 | + "wasm-bindgen-shared", | |
604 | +] | |
605 | + | |
606 | +[[package]] | |
607 | +name = "wasm-bindgen-shared" | |
608 | +version = "0.2.108" | |
609 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
610 | +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" | |
611 | +dependencies = [ | |
612 | + "unicode-ident", | |
613 | +] | |
614 | + | |
615 | +[[package]] | |
616 | +name = "windows-core" | |
617 | +version = "0.62.2" | |
618 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
619 | +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" | |
620 | +dependencies = [ | |
621 | + "windows-implement", | |
622 | + "windows-interface", | |
623 | + "windows-link", | |
624 | + "windows-result", | |
625 | + "windows-strings", | |
626 | +] | |
627 | + | |
628 | +[[package]] | |
629 | +name = "windows-implement" | |
630 | +version = "0.60.2" | |
631 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
632 | +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" | |
633 | +dependencies = [ | |
634 | + "proc-macro2", | |
635 | + "quote", | |
636 | + "syn", | |
637 | +] | |
638 | + | |
639 | +[[package]] | |
640 | +name = "windows-interface" | |
641 | +version = "0.59.3" | |
642 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
643 | +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" | |
644 | +dependencies = [ | |
645 | + "proc-macro2", | |
646 | + "quote", | |
647 | + "syn", | |
648 | +] | |
649 | + | |
650 | +[[package]] | |
651 | +name = "windows-link" | |
652 | +version = "0.2.1" | |
653 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
654 | +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" | |
655 | + | |
656 | +[[package]] | |
657 | +name = "windows-result" | |
658 | +version = "0.4.1" | |
659 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
660 | +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" | |
661 | +dependencies = [ | |
662 | + "windows-link", | |
663 | +] | |
664 | + | |
665 | +[[package]] | |
666 | +name = "windows-strings" | |
667 | +version = "0.5.1" | |
668 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
669 | +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" | |
670 | +dependencies = [ | |
671 | + "windows-link", | |
672 | +] | |
673 | + | |
674 | +[[package]] | |
675 | +name = "windows-sys" | |
676 | +version = "0.60.2" | |
677 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
678 | +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" | |
679 | +dependencies = [ | |
680 | + "windows-targets", | |
681 | +] | |
682 | + | |
683 | +[[package]] | |
684 | +name = "windows-sys" | |
685 | +version = "0.61.2" | |
686 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
687 | +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" | |
688 | +dependencies = [ | |
689 | + "windows-link", | |
690 | +] | |
691 | + | |
692 | +[[package]] | |
693 | +name = "windows-targets" | |
694 | +version = "0.53.5" | |
695 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
696 | +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" | |
697 | +dependencies = [ | |
698 | + "windows-link", | |
699 | + "windows_aarch64_gnullvm", | |
700 | + "windows_aarch64_msvc", | |
701 | + "windows_i686_gnu", | |
702 | + "windows_i686_gnullvm", | |
703 | + "windows_i686_msvc", | |
704 | + "windows_x86_64_gnu", | |
705 | + "windows_x86_64_gnullvm", | |
706 | + "windows_x86_64_msvc", | |
707 | +] | |
708 | + | |
709 | +[[package]] | |
710 | +name = "windows_aarch64_gnullvm" | |
711 | +version = "0.53.1" | |
712 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
713 | +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" | |
714 | + | |
715 | +[[package]] | |
716 | +name = "windows_aarch64_msvc" | |
717 | +version = "0.53.1" | |
718 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
719 | +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" | |
720 | + | |
721 | +[[package]] | |
722 | +name = "windows_i686_gnu" | |
723 | +version = "0.53.1" | |
724 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
725 | +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" | |
726 | + | |
727 | +[[package]] | |
728 | +name = "windows_i686_gnullvm" | |
729 | +version = "0.53.1" | |
730 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
731 | +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" | |
732 | + | |
733 | +[[package]] | |
734 | +name = "windows_i686_msvc" | |
735 | +version = "0.53.1" | |
736 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
737 | +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" | |
738 | + | |
739 | +[[package]] | |
740 | +name = "windows_x86_64_gnu" | |
741 | +version = "0.53.1" | |
742 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
743 | +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" | |
744 | + | |
745 | +[[package]] | |
746 | +name = "windows_x86_64_gnullvm" | |
747 | +version = "0.53.1" | |
748 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
749 | +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" | |
750 | + | |
751 | +[[package]] | |
752 | +name = "windows_x86_64_msvc" | |
753 | +version = "0.53.1" | |
754 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
755 | +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" | |
756 | + | |
757 | +[[package]] | |
758 | +name = "winnow" | |
759 | +version = "0.7.14" | |
760 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
761 | +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" | |
762 | +dependencies = [ | |
763 | + "memchr", | |
764 | +] | |
0 | 765 | |
▼Cargo.toml
+33-0
| @@ -0,0 +1,33 @@ | ||
1 | +[package] | |
2 | +name = "lsf" | |
3 | +version = "5.0.0" | |
4 | +edition = "2021" | |
5 | +authors = ["swadhinbiswas"] | |
6 | +description = "A beautiful ls replacement with Nerd Font icons and colors" | |
7 | +license = "MIT" | |
8 | +repository = "https://github.com/swadhinbiswas/ls-f" | |
9 | +readme = "README.md" | |
10 | +keywords = ["ls", "cli", "icons", "nerd-font", "terminal"] | |
11 | +categories = ["command-line-utilities"] | |
12 | + | |
13 | +[[bin]] | |
14 | +name = "lsf" | |
15 | +path = "src/main.rs" | |
16 | + | |
17 | +[dependencies] | |
18 | +clap = { version = "4", features = ["derive"] } | |
19 | +terminal_size = "0.4" | |
20 | +chrono = "0.4" | |
21 | +users = "0.11" | |
22 | +libc = "0.2" | |
23 | +unicode-width = "0.2" | |
24 | +toml = "0.8" | |
25 | +serde = { version = "1", features = ["derive"] } | |
26 | +dirs = "6" | |
27 | + | |
28 | +[profile.release] | |
29 | +opt-level = 3 | |
30 | +lto = true | |
31 | +codegen-units = 1 | |
32 | +strip = true | |
33 | +panic = "abort" | |
0 | 34 | |
▼Makefile
+20-2
| @@ -1,9 +1,26 @@ | ||
1 | 1 | SHELL := /usr/bin/env bash |
2 | +PREFIX ?= /usr/local | |
3 | +BINDIR := $(PREFIX)/bin | |
2 | 4 | |
3 | -.PHONY: help release clean | |
5 | +.PHONY: help release clean install uninstall remove | |
4 | 6 | |
5 | 7 | help: |
6 | - @echo "Available targets:\n release VERSION=x.y.z Build release artifacts into dist/\n clean Remove dist/" | |
8 | + @echo "Available targets:" | |
9 | + @echo " install Build and install lsf to $(BINDIR)" | |
10 | + @echo " uninstall/remove Remove lsf from $(BINDIR)" | |
11 | + @echo " release VERSION=x.y.z Build release artifacts into dist/" | |
12 | + @echo " clean Remove dist/ and target/" | |
13 | + | |
14 | +install: | |
15 | + cargo build --release | |
16 | + install -Dm755 target/release/lsf "$(DESTDIR)$(BINDIR)/lsf" | |
17 | + @echo "lsf installed to $(BINDIR)/lsf" | |
18 | + | |
19 | +uninstall: | |
20 | + rm -f "$(DESTDIR)$(BINDIR)/lsf" | |
21 | + @echo "lsf removed from $(BINDIR)/lsf" | |
22 | + | |
23 | +remove: uninstall | |
7 | 24 | |
8 | 25 | release: |
9 | 26 | @if [[ -z "$$VERSION" ]]; then \ |
| @@ -15,3 +32,4 @@ release: | ||
15 | 32 | |
16 | 33 | clean: |
17 | 34 | rm -rf dist |
35 | + cargo clean | |
18 | 36 | |
▼README.md
+39-238
| @@ -1,262 +1,63 @@ | ||
1 | -# ✨ ls-f — A Beautiful `ls` with Icons | |
1 | +# ls-f | |
2 | 2 | |
3 | - | |
4 | -[](https://codecov.io/gh/swadhinbiswas/ls-f) | |
5 | -[](https://opensource.org/licenses/MIT) | |
3 | +A simple wrapper for `ls` that adds icons. It works just like the `ls` you already know, but looks better. | |
6 | 4 | |
7 | -`ls-f` is a friendly drop-in wrapper around GNU `ls` that adds Nerd Font icons, color hints, and a few quality-of-life improvements while still letting your familiar flags pass straight through. Use it as a standalone command or alias it to `ls` for a richer directory view. | |
8 | - | |
9 | -## Example Output | |
10 | 5 |  |
11 | 6 | |
12 | -```bash | |
13 | -$ ./lsf -x | |
14 | - DOCUMENTATION_SUMMARY.md IMPLEMENTATION_COMPLETE.md | |
15 | - INSTALL.md LICENSE | |
16 | - QUICK_REFERENCE.md README.md | |
17 | - column.py config | |
18 | - docs index.html | |
19 | - install.sh lsf | |
20 | - test_flags.sh | |
21 | -``` | |
7 | +## Why use this? | |
8 | +- **Familiar:** It uses your system's `ls` under the hood. All your favorite flags (`-la`, `-R`, `-h`) just work. | |
9 | +- **Icons:** Automatically adds Nerd Font icons based on file extensions. | |
10 | +- **Simple:** No complex configuration needed. It's just a bash script and a tiny python helper. | |
22 | 11 | |
23 | -## Quick Start | |
12 | +## Prerequisites | |
13 | +- **A Nerd Font:** You need a font that supports icons (like FiraCode Nerd Font, JetBrainsMono, etc.) set in your terminal. | |
14 | +- **Python 3:** Used for the pretty grid layout. | |
24 | 15 | |
25 | -### Prerequisites | |
26 | -- A Nerd Font (or any font with the necessary glyphs) installed and selected in your terminal | |
27 | -- Bash 4.0+ (icons rely on associative arrays) | |
28 | -- Python 3 (recommended for column formatting; falls back gracefully otherwise) | |
16 | +## Installation | |
17 | + | |
18 | +The easiest way is to use the one-liner: | |
29 | 19 | |
30 | -### Install | |
31 | 20 | ```bash |
32 | -# One-liner (downloads both lsf and column.py automatically) | |
33 | 21 | curl -fsSL https://raw.githubusercontent.com/swadhinbiswas/ls-f/main/install-standalone.sh | bash |
34 | - | |
35 | -# Or clone and install | |
36 | -git clone https://github.com/swadhinbiswas/ls-f.git | |
37 | -cd ls-f | |
38 | -./install.sh --user # or: sudo ./install.sh --system | |
39 | 22 | ``` |
40 | 23 | |
41 | -Note: lsf uses a small Python helper (`column.py`) for grid formatting. The one-liner installer downloads both files automatically. The repository installer also installs both `lsf` and `column.py`. If installing manually, make sure both are present in your PATH. | |
24 | +This will download `lsf` and its helper to your machine and make them ready to use. | |
42 | 25 | |
43 | -### Optional: Alias `ls` | |
44 | -```bash | |
45 | -echo "alias ls='lsf'" >> ~/.zshrc # or ~/.bashrc, ~/.config/fish/config.fish, ... | |
46 | -``` | |
47 | -Reload your shell configuration afterwards (e.g. `source ~/.zshrc`). | |
26 | +### Manual Install | |
27 | +If you prefer doing it yourself: | |
28 | +1. Clone the repo: `git clone https://github.com/swadhinbiswas/ls-f.git` | |
29 | +2. Enter the folder: `cd ls-f` | |
30 | +3. Run the install script: `./install.sh --user` | |
48 | 31 | |
49 | -## Testing | |
50 | - | |
51 | -ls-f includes automated tests for core functionality: | |
32 | +## Setting up the Alias | |
33 | +To make `ls` show icons automatically, add an alias to your shell config file. | |
52 | 34 | |
35 | +**For Bash (`~/.bashrc`):** | |
53 | 36 | ```bash |
54 | -# Run the test suite | |
55 | -python3 test_lsf.py | |
56 | - | |
57 | -# Run with coverage | |
58 | -pip install coverage | |
59 | -coverage run test_lsf.py | |
60 | -coverage report | |
37 | +echo "alias ls='lsf'" >> ~/.bashrc | |
38 | +source ~/.bashrc | |
61 | 39 | ``` |
62 | 40 | |
63 | -Tests cover: | |
64 | -- Column formatter (column.py) with different layouts and inputs | |
65 | -- Basic lsf functionality (--version, --help, --no-icons) | |
66 | -- Flag parsing and error handling | |
67 | -- Unicode icon handling | |
68 | - | |
69 | -Continuous integration runs tests across multiple Python (3.7-3.11) and Bash versions. | |
70 | - | |
71 | -## Feature Highlights | |
72 | -- Icons & colors that automatically match hundreds of common file types | |
73 | -- Works out-of-the-box with long listings (`-l`), recursion (`-R`), and hidden files (`-A`, `-a`) | |
74 | -- Escape-aware output (`-b`) so icon lookup still succeeds with quirky filenames | |
75 | -- Horizontal layout toggle (`-x`) and extension-based sorting (`-X`) | |
76 | -- Tab-size control (`-T`, `--tabsize=COLS`) that reaches into the bundled formatter | |
77 | -- Security-context display (`-Z`, `--context`) without losing icons | |
78 | -- `--zero` mode for null-terminated output when scripting | |
79 | -- `--no-icons` to fall back to the exact `ls` output | |
80 | - | |
81 | -## Supported Flags (Native Integration) | |
82 | -| Flag(s) | Behavior | | |
83 | -|---------|----------| | |
84 | -| `-l` | Long format with icons preserved in the first column | | |
85 | -| `-R` | Recursive listings with colored directory headers | | |
86 | -| `-A`, `--almost-all` | Show hidden files while skipping `.` and `..` | | |
87 | -| `-b`, `--escape` | Treat escape sequences literally in icon lookups | | |
88 | -| `-x` | Row-major layout (matches `ls -x`) | | |
89 | -| `-X` | Sorts by extension while keeping icon formatting | | |
90 | -| `-T`, `--tabsize=COLS` | Custom tab stop width for the grid formatter | | |
91 | -| `-Z`, `--context` | Emit SELinux/AppArmor security contexts with icons | | |
92 | -| `--zero` | Terminate every entry with `\0` for safe piping | | |
93 | -| `--no-icons` | Bypass icon rendering and defer to raw `ls` output | | |
94 | -| `--help` | Display the built-in usage summary | | |
95 | -| `--version` | Print the current `ls-f` version | | |
96 | - | |
97 | -> ℹ️ Most other GNU `ls` flags are forwarded untouched to the underlying command. Sorting, grouping, and metadata switches generally work today, but some formats still need first-class, icon-aware handling—see the roadmap below. | |
98 | - | |
99 | -## Roadmap: GNU `ls` Compatibility | |
100 | -The table below inventories the remaining GNU `ls` flags and notes their current status inside `ls-f`. Entries marked "Pass-through" already work by delegating to `ls`, but we plan to make their output fully icon-aware. "Planned" items need additional logic before they behave identically to `ls`. | |
101 | - | |
102 | -| Option(s) | Description | Status | | |
103 | -|-----------|-------------|--------| | |
104 | -| *(baseline)* | List information about files, sorting alphabetically unless another sort flag is given. | ✅ Native | | |
105 | -| *(arguments)* | Long-option arguments should match their short-option counterparts. | 🛠️ Planned (documentation & validation) | | |
106 | -| `-a`, `--all` | Do not ignore entries starting with `.`. | 🟡 Pass-through (native polish planned) | | |
107 | -| `-A`, `--almost-all` | Do not list implied `.` and `..`. | ✅ Native | | |
108 | -| `--author` | With `-l`, show the author column. | 🟡 Pass-through | | |
109 | -| `-b`, `--escape` | Print C-style escapes for nongraphic characters. | ✅ Native | | |
110 | -| `--block-size=SIZE` | Scale sizes by `SIZE` when printing with `-l`. | 🟡 Pass-through | | |
111 | -| `-B`, `--ignore-backups` | Skip files ending with `~`. | 🟡 Pass-through | | |
112 | -| `-c` | Control ctime display and sorting. | 🟡 Pass-through | | |
113 | -| `-C` | Column-major layout. | 🟡 Pass-through | | |
114 | -| `--color[=WHEN]` | Honour color directives from `ls`. | 🛠️ Planned (currently disabled to avoid double coloring) | | |
115 | -| `-d`, `--directory` | List directories themselves, not their contents. | 🟡 Pass-through | | |
116 | -| `-D`, `--dired` | Emit GNU Emacs `dired` hints. | 🟡 Pass-through (needs validation) | | |
117 | -| `-f` | Same as `-a -U`. | 🟡 Pass-through | | |
118 | -| `-F`, `--classify[=WHEN]` | Append type indicators. | 🟡 Pass-through | | |
119 | -| `--file-type` | Append indicators, excluding `*`. | 🟡 Pass-through | | |
120 | -| `--format=WORD` | Choose layout: across, commas, long, etc. | 🛠️ Planned (current grid reformatting overrides `WORD`) | | |
121 | -| `--full-time` | Show full ISO timestamps. | 🟡 Pass-through | | |
122 | -| `-g` | Like `-l`, omit owner. | 🟡 Pass-through | | |
123 | -| `--group-directories-first` | Group directories before files. | 🟡 Pass-through | | |
124 | -| `-G`, `--no-group` | Suppress group column in long format. | 🟡 Pass-through | | |
125 | -| `-h`, `--human-readable` | Human-readable sizes. | 🟡 Pass-through | | |
126 | -| `--si` | Human-readable sizes base 1000. | 🟡 Pass-through | | |
127 | -| `-H`, `--dereference-command-line` | Follow command-line symlinks. | 🟡 Pass-through | | |
128 | -| `--dereference-command-line-symlink-to-dir` | Follow command-line symlinks to dirs. | 🟡 Pass-through | | |
129 | -| `--hide=PATTERN` | Hide entries matching `PATTERN`. | 🟡 Pass-through | | |
130 | -| `--hyperlink[=WHEN]` | Emit OSC 8 hyperlinks. | 🛠️ Planned (needs escape-safe icon handling) | | |
131 | -| `--indicator-style=WORD` | Control appended indicators. | 🟡 Pass-through | | |
132 | -| `-i`, `--inode` | Print inode numbers. | 🟡 Pass-through | | |
133 | -| `-I`, `--ignore=PATTERN` | Ignore entries matching `PATTERN`. | 🟡 Pass-through | | |
134 | -| `-k`, `--kibibytes` | Use 1024-byte blocks for sizes. | 🟡 Pass-through | | |
135 | -| `-l` | Long listing format. | ✅ Native | | |
136 | -| `-L`, `--dereference` | Follow symlinks when showing info. | 🟡 Pass-through | | |
137 | -| `-m` | Comma-separated output. | 🛠️ Planned (icons currently attach to the first entry only) | | |
138 | -| `-n`, `--numeric-uid-gid` | Show numeric IDs. | 🟡 Pass-through | | |
139 | -| `-N`, `--literal` | Output names without quoting. | 🟡 Pass-through | | |
140 | -| `-o` | Like `-l`, omit group. | 🟡 Pass-through | | |
141 | -| `-p`, `--indicator-style=slash` | Append `/` to directories. | 🟡 Pass-through | | |
142 | -| `-q`, `--hide-control-chars` | Replace nongraphic chars with `?`. | 🟡 Pass-through | | |
143 | -| `--show-control-chars` | Display nongraphic characters as-is. | 🟡 Pass-through | | |
144 | -| `-Q`, `--quote-name` | Quote entry names. | 🛠️ Planned (quotes break icon detection) | | |
145 | -| `--quoting-style=WORD` | Choose quoting style. | 🛠️ Planned (needs decode-aware lookup) | | |
146 | -| `-r`, `--reverse` | Reverse sort order. | 🟡 Pass-through | | |
147 | -| `-R`, `--recursive` | List subdirectories recursively. | ✅ Native | | |
148 | -| `-s`, `--size` | Show allocated blocks. | 🟡 Pass-through | | |
149 | -| `-S` | Sort by file size. | 🟡 Pass-through | | |
150 | -| `--sort=WORD` | Choose ordering (none, size, time, version, extension, name, width). | 🟡 Pass-through | | |
151 | -| `--time=WORD` | Select which timestamp to display/sort. | 🟡 Pass-through | | |
152 | -| `--time-style=TIME_STYLE` | Customize time/date formatting. | 🟡 Pass-through | | |
153 | -| `-t` | Sort by modification time. | 🟡 Pass-through | | |
154 | -| `-T`, `--tabsize=COLS` | Set tab stops. | ✅ Native | | |
155 | -| `-u` | Sort/show access time. | 🟡 Pass-through | | |
156 | -| `-U` | Do not sort directory entries. | 🟡 Pass-through | | |
157 | -| `-v` | Natural sort of numbers. | 🟡 Pass-through | | |
158 | -| `-w`, `--width=COLS` | Set output width. | 🛠️ Planned (formatter currently ignores this setting) | | |
159 | -| `-x` | Row-major layout. | ✅ Native | | |
160 | -| `-X` | Sort by extension. | ✅ Native | | |
161 | -| `-Z`, `--context` | Show security context. | ✅ Native | | |
162 | -| `--zero` | Delimit entries with `NUL`. | ✅ Native | | |
163 | -| `-1` | One entry per line. | 🛠️ Planned (column formatter reflows output) | | |
164 | -| `--help` | Show help and exit. | ✅ Native | | |
165 | -| `--version` | Output version information and exit. | ✅ Native | | |
166 | - | |
167 | -Legend: `✅ Native` = icon-aware handling built into `ls-f`; `🟡 Pass-through` = delegated to `ls` today but slated for native polish; `🛠️ Planned` = not yet behaving like GNU `ls` when icons are enabled. | |
168 | - | |
169 | -## Next Steps: Development Roadmap | |
170 | - | |
171 | -### v4.6.0 - Enhanced Formatting & Display | |
172 | -**Priority: High** | **Target: Q1 2025** | |
173 | - | |
174 | -- **`-1` (one per line)**: Override column formatter to respect single-column output | |
175 | -- **`-w`, `--width=COLS`**: Pass terminal width to column.py for proper wrapping | |
176 | -- **`-m` (comma-separated)**: Icon-aware comma formatting for compact output | |
177 | -- **`--color` integration**: Smart color passthrough without conflicting with icons | |
178 | -- **Directory coloring**: Enhance folder icon colors based on permissions/type | |
179 | - | |
180 | -### v4.7.0 - Advanced GNU ls Parity | |
181 | -**Priority: Medium** | **Target: Q2 2025** | |
182 | - | |
183 | -- **`-Q`, `--quote-name`**: Quote-aware icon detection and placement | |
184 | -- **`--quoting-style=WORD`**: Support all GNU ls quoting styles with icons | |
185 | -- **`-F`, `--classify` native**: Icon + indicator combinations (e.g., 🐍script.py*) | |
186 | -- **`--hyperlink`**: OSC 8 hyperlink support with icon preservation | |
187 | -- **`-g`, `-o`, `-G`**: Native long format variants with proper icon alignment | |
188 | - | |
189 | -### v4.8.0 - Performance & Efficiency | |
190 | -**Priority: Medium** | **Target: Q3 2025** | |
191 | - | |
192 | -- **Caching layer**: Icon lookup cache for large directories | |
193 | -- **Async processing**: Parallel icon resolution for recursive listings | |
194 | -- **Memory optimization**: Reduce overhead for very large directory trees | |
195 | -- **Config preprocessing**: Compile icon rules for faster matching | |
196 | - | |
197 | -### v5.0.0 - Extended Features | |
198 | -**Priority: Low** | **Target: Q4 2025** | |
199 | - | |
200 | -- **Custom icon themes**: Theme system (material, nerd, emoji, ascii) | |
201 | -- **Plugin architecture**: External icon providers and formatters | |
202 | -- **Terminal capability detection**: Auto-disable features on limited terminals | |
203 | -- **Network file support**: Icons for remote/mounted filesystems | |
204 | -- **Integration helpers**: Git status indicators, file age colors, size categories | |
205 | - | |
206 | -### Beyond v5.0 - Future Vision | |
207 | - | |
208 | -- **Shell integration**: Native completions for bash/zsh/fish | |
209 | -- **GUI file manager**: GTK/Qt frontend using ls-f as backend | |
210 | -- **Cross-platform**: Windows PowerShell module, macOS Finder integration | |
211 | -- **Language bindings**: Python/Node.js APIs for programmatic use | |
212 | -- **Cloud support**: S3, Google Drive, Dropbox icon providers | |
213 | - | |
214 | -### Contributing Priorities | |
215 | - | |
216 | -**🔥 Hot Areas** (easy wins, high impact): | |
217 | -1. Fix `-1` and `-w` flag handling in column.py | |
218 | -2. Add more file type icons (current: ~100, target: 500+) | |
219 | -3. Improve error messages and help text | |
220 | -4. Add regression test coverage for new flags | |
221 | - | |
222 | -**💡 Medium Effort**: | |
223 | -1. Quote-style and classifier integration | |
224 | -2. Color scheme management and themes | |
225 | -3. Performance profiling and optimization | |
226 | -4. Windows/macOS platform testing | |
227 | - | |
228 | -**🚀 Ambitious Projects**: | |
229 | -1. Plugin system design and implementation | |
230 | -2. GUI frontend development | |
231 | -3. Shell completion generators | |
232 | -4. Language binding APIs | |
233 | - | |
234 | -Want to contribute? See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and [the issues page](https://github.com/swadhinbiswas/ls-f/issues) for specific tasks! | |
235 | - | |
236 | -## Customization | |
237 | -Override any icon or color in `~/.config/lsf`: | |
41 | +**For Zsh (`~/.zshrc`):** | |
238 | 42 | ```bash |
239 | -# nano lsf or nvim lsf | |
240 | -ICONS["dir"]="" | |
241 | -COLORS["dir"]="34" | |
242 | -ICONS[".py"]="🐍" | |
243 | -COLORS[".py"]="33" | |
43 | +echo "alias ls='lsf'" >> ~/.zshrc | |
44 | +source ~/.zshrc | |
244 | 45 | ``` |
245 | -Reload your shell (or re-run `lsf`) to apply changes. The configuration file is ordinary shell syntax, so you can use conditionals, helper functions, and any POSIX-compatible features. | |
246 | 46 | |
247 | -## Documentation Hub | |
248 | -- `README.md` — you are here (feature overview & roadmap) | |
249 | -- `docs/INSTALLATION.md` — deep-dive installation instructions | |
250 | -- `docs/CONFIGURATION.md` — icon and color customization reference | |
251 | -- `docs/TROUBLESHOOTING.md` — diagnostics and common fixes | |
252 | -- `index.html` — shareable one-page site with highlights and quick commands | |
47 | +**For Fish (`~/.config/fish/config.fish`):** | |
48 | +```bash | |
49 | +echo "alias ls='lsf'" >> ~/.config/fish/config.fish | |
50 | +``` | |
253 | 51 | |
254 | -## Contributing | |
255 | -Issues and pull requests are welcome! Please document new flags in the tables above and add regression tests or example commands when possible. | |
52 | +## Usage | |
53 | +Just use it like `ls`: | |
54 | +- `ls` (shows icons in a grid) | |
55 | +- `ls -la` (long format with icons) | |
56 | +- `ls -R` (recursive with icons) | |
57 | +- `ls --no-icons` (if you want the plain old boring output) | |
256 | 58 | |
257 | -- See [CONTRIBUTING.md](CONTRIBUTING.md) for how to set up your environment, commit style, and PR guidelines. | |
258 | -- Please follow our [Code of Conduct](CODE_OF_CONDUCT.md). | |
259 | -- Security issues? Read our [Security Policy](SECURITY.md) and report privately. | |
59 | +## Customizing Icons | |
60 | +If you want to change an icon, you can edit the `ICONS` mapping inside the `lsf` script or create a config at `~/.config/lsf`. | |
260 | 61 | |
261 | 62 | ## License |
262 | -Licensed under the MIT License. See [LICENSE](LICENSE) for details. | |
63 | +MIT | |
263 | 64 | |
▼docs/INSTALLATION.md
+265-253
| @@ -1,410 +1,418 @@ | ||
1 | -# Installation Guide for ls-f | |
1 | +# Installation Guide for lsf | |
2 | 2 | |
3 | -This guide covers all installation methods for **ls-f** on Linux, macOS, and other Unix-like systems. | |
3 | +**lsf v5.0.0** is a native Rust binary. No runtime dependencies -- no Bash 4+, no Python, no GNU coreutils required. Just a single binary. | |
4 | 4 | |
5 | 5 | --- |
6 | 6 | |
7 | -## 📋 Prerequisites | |
8 | - | |
9 | -Before installing `ls-f`, verify you have: | |
7 | +## Prerequisites | |
10 | 8 | |
11 | 9 | ### Required |
12 | -- **Bash 4+** or any POSIX shell (Bash, Zsh, Fish, etc.) | |
13 | -- **Python 3.x** (for column formatting) | |
14 | - | |
15 | -### Optional (Recommended) | |
16 | -- **Nerd Fonts** installed (for proper icon display) | |
10 | +- A **Nerd Font** installed and set in your terminal for icon display | |
17 | 11 | |
18 | -### Check Prerequisites | |
12 | +### Check for Nerd Fonts | |
19 | 13 | |
20 | 14 | ```bash |
21 | -# Check Bash version (should be >= 4) | |
22 | -echo $BASH_VERSION | |
23 | - | |
24 | -# Check Python availability | |
25 | -python3 --version | |
26 | - | |
27 | -# Check for Nerd Fonts | |
15 | +# Linux | |
28 | 16 | fc-list | grep -i "Nerd Font" |
17 | + | |
18 | +# macOS | |
19 | +system_profiler SPFontsDataType | grep -i "Nerd" | |
29 | 20 | ``` |
30 | 21 | |
31 | 22 | If you don't have Nerd Fonts, download from [nerdfonts.com](https://www.nerdfonts.com/). |
32 | 23 | |
24 | +Popular choices: **JetBrainsMono Nerd Font**, **FiraCode Nerd Font**, **Hack Nerd Font**. | |
33 | 25 | |
34 | -## 🚀 Installation Methods | |
35 | -git clone https://github.com/swadhinbiswas/ls-f.git | |
36 | -### Method 1: One-Liner Install (Recommended) | |
37 | - | |
38 | -Quickest method - downloads both files and installs automatically: | |
39 | - | |
40 | -```bash | |
41 | -# User-local installation (no sudo needed) | |
42 | -curl -fsSL https://raw.githubusercontent.com/swadhinbiswas/ls-f/main/install-standalone.sh | bash | |
43 | - | |
44 | -# OR system-wide installation | |
45 | -curl -fsSL https://raw.githubusercontent.com/swadhinbiswas/ls-f/main/install-standalone.sh | bash -s system | |
26 | +--- | |
46 | 27 | |
47 | -# Verify installation | |
48 | -lsf --version | |
49 | -lsf -x | |
50 | -``` | |
28 | +## Installation Methods | |
51 | 29 | |
52 | -### Method 2: Git Clone | |
30 | +### Method 1: Build from Source (Recommended) | |
53 | 31 | |
54 | -For development or customization: | |
32 | +Requires [Rust toolchain](https://rustup.rs/) (1.70+): | |
55 | 33 | |
56 | 34 | ```bash |
57 | -# Clone the repository | |
58 | 35 | git clone https://github.com/swadhinbiswas/ls-f.git |
59 | 36 | cd ls-f |
37 | +cargo build --release | |
60 | 38 | |
61 | -# User-local installation (no sudo needed, default for non-root) | |
62 | -./install.sh --user | |
39 | +# The binary is at ./target/release/lsf | |
40 | +# Copy it to your PATH: | |
41 | +cp target/release/lsf ~/.local/bin/ | |
42 | +# OR system-wide: | |
43 | +sudo cp target/release/lsf /usr/local/bin/ | |
44 | +``` | |
63 | 45 | |
64 | -# OR system-wide installation (requires sudo) | |
65 | -sudo ./install.sh --system | |
46 | +### Method 2: Cargo Install (from crates.io) | |
66 | 47 | |
67 | -# Verify installation | |
68 | -lsf --version | |
69 | -lsf -x | |
70 | -``` | |
71 | -sudo ./install.sh --system | |
72 | -### Method 2: Direct Download | |
48 | +```bash | |
49 | +cargo install lsf | |
73 | 50 | |
74 | -# Verify installation | |
75 | -lsf --version | |
76 | - Download both scripts directly from GitHub: | |
51 | +# Binary is installed to ~/.cargo/bin/lsf | |
52 | +# Make sure ~/.cargo/bin is in your PATH | |
77 | 53 | ``` |
78 | 54 | |
79 | -### Method 2: Direct Download | |
80 | -sudo curl -fL https://raw.githubusercontent.com/swadhinbiswas/ls-f/main/lsf \ | |
81 | -Download both scripts directly from GitHub: | |
55 | +### Method 3: Download Prebuilt Binary | |
82 | 56 | |
83 | 57 | ```bash |
84 | -sudo curl -fL https://raw.githubusercontent.com/swadhinbiswas/ls-f/main/column.py \ | |
85 | -sudo curl -fL https://raw.githubusercontent.com/swadhin-biswas/ls-f/main/lsf \ | |
86 | - -o /usr/local/bin/lsf | |
87 | - | |
88 | -# Download column.py helper | |
89 | -sudo curl -fL https://raw.githubusercontent.com/swadhin-biswas/ls-f/main/column.py \ | |
90 | - -o /usr/local/bin/column.py | |
91 | - | |
92 | -# Make both executable | |
93 | -sudo chmod +x /usr/local/bin/lsf /usr/local/bin/column.py | |
94 | -git clone https://github.com/swadhinbiswas/ls-f.git && cd ls-f && ./install.sh --user | |
95 | -# Create config directory | |
96 | -mkdir -p ~/.config/lsf | |
97 | -git clone https://github.com/swadhinbiswas/ls-f.git && cd ls-f && sudo ./install.sh --system | |
98 | -# Verify installation | |
99 | -lsf --version | |
100 | -lsf -x | |
58 | +# Download the latest release binary for your platform from GitHub Releases | |
59 | +# https://github.com/swadhinbiswas/ls-f/releases | |
60 | + | |
61 | +# Example for Linux x86-64: | |
62 | +curl -fsSL https://github.com/swadhinbiswas/ls-f/releases/latest/download/lsf-linux-x86_64 -o lsf | |
63 | +chmod +x lsf | |
64 | +sudo mv lsf /usr/local/bin/ | |
101 | 65 | ``` |
102 | 66 | |
103 | -### Method 3: One-Liner Installation | |
67 | +### Verify Installation | |
104 | 68 | |
105 | -Clone and install in a single command: | |
106 | -git clone https://github.com/swadhinbiswas/ls-f.git | |
107 | 69 | ```bash |
108 | -# User installation (no sudo needed) | |
109 | -git clone https://github.com/swadhin-biswas/ls-f.git && cd ls-f && ./install.sh --user | |
110 | -# System-wide installation | |
111 | -git clone https://github.com/swadhin-biswas/ls-f.git && cd ls-f && sudo ./install.sh --system | |
70 | +lsf --version # Should print: lsf 5.0.0 | |
71 | +lsf --help # Show all available flags | |
72 | +lsf # List current directory with icons | |
73 | +lsf -la # Long format with all files | |
112 | 74 | ``` |
113 | 75 | |
114 | -> **Note:** The installer requires both `lsf` and `column.py` from the repository, so piping directly to bash is not recommended. | |
76 | +--- | |
115 | 77 | |
116 | -### Method 4: Manual Installation | |
78 | +## Shell Alias Setup | |
117 | 79 | |
118 | -For complete control: | |
80 | +You can use `lsf` as a drop-in replacement for `ls` by setting up aliases. Below are instructions for **every major shell**, showing how to alias both `lsf` (as a shortcut) and `ls` (as a full replacement). | |
119 | 81 | |
120 | -```bash | |
121 | -# Clone and enter directory | |
122 | -git clone https://github.com/swadhinbiswas/ls-f.git | |
123 | -cd ls-f | |
82 | +### Bash | |
124 | 83 | |
125 | -# Create install directory (user or system) | |
126 | -mkdir -p ~/.local/bin # User install | |
127 | -# OR | |
128 | -sudo mkdir -p /usr/local/bin # System install | |
84 | +Edit `~/.bashrc` (or `~/.bash_profile` on macOS): | |
129 | 85 | |
130 | -# Copy files | |
131 | -cp lsf column.py ~/.local/bin/ # User install | |
132 | -# OR | |
133 | -sudo cp lsf column.py /usr/local/bin/ # System install | |
86 | +```bash | |
87 | +# Replace ls with lsf | |
88 | +alias ls='lsf' | |
134 | 89 | |
135 | -# Make executable | |
136 | -chmod +x ~/.local/bin/{lsf,column.py} | |
137 | -# OR | |
138 | -sudo chmod +x /usr/local/bin/{lsf,column.py} | |
90 | +# Optional: common ls shortcuts using lsf | |
91 | +alias ll='lsf -lah' | |
92 | +alias la='lsf -A' | |
93 | +alias l='lsf -1' | |
94 | +alias lt='lsf --tree --depth 2' | |
95 | +``` | |
96 | + | |
97 | +Apply changes: | |
139 | 98 | |
140 | -# Ensure user bin is in PATH (for user install) | |
141 | -echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc | |
99 | +```bash | |
142 | 100 | source ~/.bashrc |
143 | 101 | ``` |
144 | 102 | |
103 | +### Zsh | |
145 | 104 | |
146 | -## 🔧 Installation Options | |
105 | +Edit `~/.zshrc`: | |
147 | 106 | |
148 | -The `install.sh` script supports several options for customization: | |
107 | +```zsh | |
108 | +# Replace ls with lsf | |
109 | +alias ls='lsf' | |
149 | 110 | |
150 | -```bash | |
151 | -./install.sh [--user | --system] [--replace-ls] [--force] [--no-font-check] [-h|--help] | |
111 | +# Optional: common shortcuts | |
112 | +alias ll='lsf -lah' | |
113 | +alias la='lsf -A' | |
114 | +alias l='lsf -1' | |
115 | +alias lt='lsf --tree --depth 2' | |
152 | 116 | ``` |
153 | 117 | |
154 | -### Options | |
118 | +Apply changes: | |
155 | 119 | |
156 | -| Option | Description | | |
157 | -|--------|-------------| | |
158 | -| `--user` | Install to `~/.local/bin` (default for non-root users) | | |
159 | -| `--system` | Install to `/usr/local/bin` (requires sudo) | | |
160 | -| `--replace-ls` | Replace system `ls` with wrapper (system-only; creates backup) | | |
161 | -| `--force` | Overwrite existing files without prompting | | |
162 | -| `--no-font-check` | Skip Nerd Font detection | | |
163 | -| `-h, --help` | Show help message | | |
120 | +```bash | |
121 | +source ~/.zshrc | |
122 | +``` | |
164 | 123 | |
165 | -### Examples | |
124 | +**Oh My Zsh users:** Oh My Zsh defines its own `ls` aliases in the `lib/directories.zsh` plugin. To override them, put your aliases **after** the `source $ZSH/oh-my-zsh.sh` line in `~/.zshrc`, or add them to a custom plugin: | |
166 | 125 | |
167 | 126 | ```bash |
168 | -# User-local installation | |
169 | -./install.sh --user | |
127 | +# Create custom plugin | |
128 | +mkdir -p ~/.oh-my-zsh/custom/plugins/lsf | |
129 | +cat > ~/.oh-my-zsh/custom/plugins/lsf/lsf.plugin.zsh << 'EOF' | |
130 | +alias ls='lsf' | |
131 | +alias ll='lsf -lah' | |
132 | +alias la='lsf -A' | |
133 | +alias l='lsf -1' | |
134 | +alias lt='lsf --tree --depth 2' | |
135 | +EOF | |
136 | +``` | |
170 | 137 | |
171 | -# System-wide with Nerd Font check skipped | |
172 | -sudo ./install.sh --system --no-font-check | |
138 | +Then add `lsf` to your plugins list in `~/.zshrc`: | |
173 | 139 | |
174 | -# Replace system ls (backup created) | |
175 | -sudo ./install.sh --system --replace-ls | |
140 | +```zsh | |
141 | +plugins=(git lsf) | |
142 | +``` | |
176 | 143 | |
177 | -# Force install without prompts | |
178 | -./install.sh --force | |
144 | +### Fish | |
179 | 145 | |
180 | -# Help | |
181 | -./install.sh --help | |
182 | -``` | |
146 | +Edit `~/.config/fish/config.fish`: | |
183 | 147 | |
148 | +```fish | |
149 | +# Replace ls with lsf | |
150 | +alias ls 'lsf' | |
184 | 151 | |
185 | -## 🔗 Post-Installation Setup | |
152 | +# Optional: common shortcuts | |
153 | +alias ll 'lsf -lah' | |
154 | +alias la 'lsf -A' | |
155 | +alias l 'lsf -1' | |
156 | +alias lt 'lsf --tree --depth 2' | |
157 | +``` | |
186 | 158 | |
187 | -### Verify Installation | |
159 | +Or use Fish's `abbr` for expandable abbreviations (shows the expanded command): | |
188 | 160 | |
189 | -```bash | |
190 | -which lsf | |
161 | +```fish | |
162 | +abbr --add ls lsf | |
163 | +abbr --add ll 'lsf -lah' | |
164 | +abbr --add la 'lsf -A' | |
165 | +abbr --add lt 'lsf --tree --depth 2' | |
166 | +``` | |
191 | 167 | |
192 | -# Test basic functionality | |
193 | -lsf --version | |
194 | -lsf --help | |
168 | +Apply changes: | |
195 | 169 | |
196 | -# Try listing files with icons | |
197 | -lsf | |
198 | -lsf -x | |
199 | -lsf -l | |
170 | +```bash | |
171 | +source ~/.config/fish/config.fish | |
200 | 172 | ``` |
201 | 173 | |
202 | -### Set Up Alias (Optional) | |
174 | +### Tcsh / Csh | |
203 | 175 | |
204 | -To use `lsf` as your default `ls`: | |
176 | +Edit `~/.cshrc` (or `~/.tcshrc` for tcsh): | |
205 | 177 | |
206 | -#### Bash | |
178 | +```csh | |
179 | +# Replace ls with lsf | |
180 | +alias ls 'lsf' | |
207 | 181 | |
208 | -Add to `~/.bashrc`: | |
209 | -```bash | |
210 | -alias ls='lsf' | |
182 | +# Optional shortcuts | |
183 | +alias ll 'lsf -lah' | |
184 | +alias la 'lsf -A' | |
185 | +alias l 'lsf -1' | |
186 | +alias lt 'lsf --tree --depth 2' | |
211 | 187 | ``` |
212 | 188 | |
213 | -Then reload: | |
189 | +Apply changes: | |
190 | + | |
214 | 191 | ```bash |
215 | -source ~/.bashrc | |
192 | +source ~/.cshrc | |
216 | 193 | ``` |
217 | 194 | |
218 | -#### Zsh | |
195 | +### Ksh (KornShell) | |
219 | 196 | |
220 | -Add to `~/.zshrc`: | |
221 | -```bash | |
197 | +Edit `~/.kshrc` (make sure `ENV=~/.kshrc` is set in `~/.profile`): | |
198 | + | |
199 | +```ksh | |
200 | +# Replace ls with lsf | |
222 | 201 | alias ls='lsf' |
202 | + | |
203 | +# Optional shortcuts | |
204 | +alias ll='lsf -lah' | |
205 | +alias la='lsf -A' | |
206 | +alias l='lsf -1' | |
207 | +alias lt='lsf --tree --depth 2' | |
223 | 208 | ``` |
224 | 209 | |
225 | -Then reload: | |
210 | +Apply changes: | |
211 | + | |
226 | 212 | ```bash |
227 | -source ~/.zshrc | |
213 | +source ~/.kshrc | |
228 | 214 | ``` |
229 | 215 | |
230 | -#### Fish | |
216 | +### Dash / POSIX sh | |
231 | 217 | |
232 | -Add to `~/.config/fish/config.fish`: | |
233 | -```fish | |
218 | +Edit `~/.profile` (or `~/.shrc` if your system supports `ENV`): | |
219 | + | |
220 | +```sh | |
221 | +# Replace ls with lsf | |
234 | 222 | alias ls='lsf' |
223 | +alias ll='lsf -lah' | |
224 | +alias la='lsf -A' | |
235 | 225 | ``` |
236 | 226 | |
237 | -Then reload: | |
227 | +Apply changes: | |
228 | + | |
238 | 229 | ```bash |
239 | -source ~/.config/fish/config.fish | |
230 | +. ~/.profile | |
240 | 231 | ``` |
241 | 232 | |
242 | -#### Tcsh/Csh | |
233 | +### PowerShell (pwsh) | |
243 | 234 | |
244 | -Add to `~/.cshrc`: | |
245 | -```csh | |
246 | -alias ls 'lsf' | |
247 | -``` | |
235 | +Edit your PowerShell profile (`$PROFILE`): | |
248 | 236 | |
249 | -Then reload: | |
250 | -```bash | |
251 | -source ~/.cshrc | |
252 | -``` | |
237 | +```powershell | |
238 | +# Find your profile path | |
239 | +# $PROFILE | |
253 | 240 | |
254 | -### Automatic Alias Setup | |
241 | +# Replace ls with lsf | |
242 | +Set-Alias -Name ls -Value lsf -Option AllScope | |
243 | +Set-Alias -Name lsf -Value lsf -Option AllScope | |
255 | 244 | |
256 | -Run the included setup script (if available): | |
245 | +# Optional: functions for shortcuts with arguments | |
246 | +function ll { lsf -lah @args } | |
247 | +function la { lsf -A @args } | |
248 | +function lt { lsf --tree --depth 2 @args } | |
249 | +``` | |
257 | 250 | |
258 | -```bash | |
259 | -# From the ls-f repository directory | |
260 | -source config/lsf_alias_setup.sh | |
251 | +Apply changes: | |
252 | + | |
253 | +```powershell | |
254 | +. $PROFILE | |
261 | 255 | ``` |
262 | 256 | |
263 | -> **Note:** This requires you to be in the cloned `ls-f` directory. | |
257 | +### Nushell | |
264 | 258 | |
259 | +Edit `~/.config/nushell/config.nu`: | |
265 | 260 | |
266 | -## 📍 Troubleshooting Installation | |
261 | +```nu | |
262 | +# Replace ls with lsf (as external command) | |
263 | +alias ls = ^lsf | |
264 | +alias ll = ^lsf -lah | |
265 | +alias la = ^lsf -A | |
266 | +alias lt = ^lsf --tree --depth 2 | |
267 | +``` | |
267 | 268 | |
268 | -### "Permission Denied" Error | |
269 | +### Elvish | |
269 | 270 | |
270 | -**Problem:** Can't write to installation directory. | |
271 | +Edit `~/.config/elvish/rc.elv`: | |
271 | 272 | |
272 | -**Solution:** Use sudo for system install: | |
273 | -sudo ./install.sh --system | |
273 | +```elvish | |
274 | +# Replace ls with lsf | |
275 | +fn ls {|@args| e:lsf $@args } | |
276 | +fn ll {|@args| e:lsf -lah $@args } | |
277 | +fn la {|@args| e:lsf -A $@args } | |
278 | +fn lt {|@args| e:lsf --tree --depth 2 $@args } | |
274 | 279 | ``` |
275 | 280 | |
276 | -Or use user install: | |
277 | -```bash | |
278 | -./install.sh --user | |
281 | +### Xonsh | |
282 | + | |
283 | +Edit `~/.xonshrc`: | |
284 | + | |
285 | +```python | |
286 | +# Replace ls with lsf | |
287 | +aliases['ls'] = 'lsf' | |
288 | +aliases['ll'] = 'lsf -lah' | |
289 | +aliases['la'] = 'lsf -A' | |
290 | +aliases['lt'] = 'lsf --tree --depth 2' | |
279 | 291 | ``` |
280 | 292 | |
281 | -### "Command Not Found" After Installation | |
293 | +--- | |
282 | 294 | |
283 | -**Problem:** `lsf` is installed but not found in PATH. | |
295 | +## Quick One-Liner Setup | |
284 | 296 | |
285 | -**Solution (User Install):** | |
297 | +Copy-paste the right command for your shell: | |
286 | 298 | |
287 | -Add `~/.local/bin` to your PATH: | |
288 | 299 | ```bash |
289 | -# For Bash - add to ~/.bashrc | |
290 | -echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc | |
291 | -source ~/.bashrc | |
300 | +# Bash | |
301 | +echo 'alias ls="lsf"' >> ~/.bashrc && source ~/.bashrc | |
292 | 302 | |
293 | -# For Zsh - add to ~/.zshrc | |
294 | -echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc | |
295 | -source ~/.zshrc | |
296 | -``` | |
303 | +# Zsh | |
304 | +echo 'alias ls="lsf"' >> ~/.zshrc && source ~/.zshrc | |
297 | 305 | |
298 | -**Solution (System Install):** | |
299 | -```bash | |
300 | -# Restart your terminal or reload shell config | |
301 | -exec $SHELL | |
306 | +# Fish | |
307 | +echo 'alias ls lsf' >> ~/.config/fish/config.fish && source ~/.config/fish/config.fish | |
308 | + | |
309 | +# Tcsh | |
310 | +echo 'alias ls lsf' >> ~/.cshrc && source ~/.cshrc | |
311 | + | |
312 | +# Ksh | |
313 | +echo 'alias ls="lsf"' >> ~/.kshrc && source ~/.kshrc | |
302 | 314 | ``` |
303 | 315 | |
304 | -### Python or column.py Not Found | |
316 | +--- | |
305 | 317 | |
306 | -**Problem:** Error about missing `column.py` or Python. | |
318 | +## Accessing Real ls | |
307 | 319 | |
308 | -**Solution 1:** Ensure both files were installed: | |
309 | -```bash | |
310 | -which lsf column.py | |
311 | -``` | |
320 | +After aliasing, you may occasionally need the original `ls`. Use any of these methods: | |
312 | 321 | |
313 | -**Solution 2:** Reinstall from git to get both files: | |
314 | 322 | ```bash |
315 | -git clone https://github.com/swadhin-biswas/ls-f.git | |
316 | -cd ls-f | |
317 | -./install.sh --user --force | |
318 | -``` | |
323 | +# Method 1: Backslash (bypasses aliases in bash/zsh/ksh) | |
324 | +\ls | |
319 | 325 | |
320 | -**Solution 3:** Install Python 3 if missing: | |
321 | -```bash | |
322 | -# Ubuntu/Debian | |
323 | -sudo apt install python3 | |
326 | +# Method 2: Full path | |
327 | +/usr/bin/ls | |
328 | +# or | |
329 | +/sbin/ls | |
324 | 330 | |
325 | -# Fedora/RHEL | |
331 | +# Method 3: command builtin (bypasses aliases and functions) | |
332 | +command ls | |
326 | 333 | |
327 | -# macOS | |
328 | -brew install python3 | |
334 | +# Method 4: env (runs from PATH, bypasses shell builtins) | |
335 | +env ls | |
336 | +``` | |
329 | 337 | |
330 | -**Problem:** Icons show as boxes or question marks. | |
338 | +--- | |
339 | + | |
340 | +## PATH Setup | |
331 | 341 | |
332 | -**Solution:** Install Nerd Fonts: | |
333 | -1. Download from [nerdfonts.com](https://www.nerdfonts.com/) | |
334 | -2. Install to `~/.local/share/fonts/` | |
335 | -3. Set your terminal font to a Nerd Font | |
336 | -4. Use `fc-cache -fv` to refresh font cache | |
342 | +If `lsf` is not found after installation, ensure its location is in your PATH: | |
337 | 343 | |
338 | -Or skip icon checks during install: | |
339 | 344 | ```bash |
340 | -./install.sh --no-font-check | |
345 | +# For ~/.local/bin (user install) | |
346 | +# Bash/Zsh/Ksh: | |
347 | +echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc | |
348 | +source ~/.bashrc | |
349 | + | |
350 | +# Fish: | |
351 | +fish_add_path ~/.local/bin | |
352 | + | |
353 | +# For ~/.cargo/bin (cargo install) | |
354 | +# Bash/Zsh/Ksh: | |
355 | +echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc | |
356 | +source ~/.bashrc | |
357 | + | |
358 | +# Fish: | |
359 | +fish_add_path ~/.cargo/bin | |
341 | 360 | ``` |
342 | 361 | |
343 | 362 | --- |
344 | 363 | |
345 | -## 🔄 Updating | |
346 | - | |
347 | -To update `lsf` to the latest version: | |
364 | +## Uninstallation | |
348 | 365 | |
349 | 366 | ```bash |
350 | -# If cloned from git | |
351 | -cd ls-f | |
352 | -git pull origin main | |
353 | -./install.sh --user --force # or sudo ./install.sh --system --force | |
367 | +# Remove binary | |
368 | +rm ~/.local/bin/lsf # user install | |
369 | +# OR | |
370 | +sudo rm /usr/local/bin/lsf # system install | |
371 | +# OR | |
372 | +cargo uninstall lsf # cargo install | |
354 | 373 | |
355 | -# Verify the update | |
356 | -lsf --version | |
374 | +# Remove aliases from your shell config | |
375 | +# Edit ~/.bashrc, ~/.zshrc, etc. and remove the alias lines | |
357 | 376 | ``` |
358 | 377 | |
359 | 378 | --- |
360 | 379 | |
361 | -## 🗑️ Uninstallation | |
380 | +## Troubleshooting | |
362 | 381 | |
363 | -### Remove Binary | |
382 | +### Icons show as boxes or question marks | |
364 | 383 | |
365 | -```bash | |
366 | -# User install | |
367 | -rm ~/.local/bin/lsf ~/.local/bin/column.py | |
384 | +Install a [Nerd Font](https://www.nerdfonts.com/) and set it as your terminal font. | |
368 | 385 | |
369 | -# System install | |
370 | -sudo rm /usr/local/bin/lsf /usr/local/bin/column.py | |
371 | -``` | |
386 | +### "command not found" after install | |
372 | 387 | |
373 | -### Remove Configuration | |
388 | +Check that the binary location is in your PATH: | |
374 | 389 | |
375 | 390 | ```bash |
376 | -rm -rf ~/.config/lsf/ | |
391 | +which lsf | |
392 | +echo $PATH | |
377 | 393 | ``` |
378 | 394 | |
379 | -### Remove Alias | |
395 | +### Alias not working | |
380 | 396 | |
381 | -Remove or comment out the alias from your shell config: | |
382 | -```bash | |
383 | -# Edit ~/.bashrc, ~/.zshrc, etc. | |
384 | -# Remove or comment out: alias ls='lsf' | |
385 | -``` | |
397 | +1. Make sure you edited the correct config file for your shell | |
398 | +2. Make sure you reloaded the config (`source ~/.bashrc`, etc.) | |
399 | +3. Check for conflicting aliases: `alias | grep ls` | |
400 | +4. For Oh My Zsh, place aliases **after** the `source $ZSH/oh-my-zsh.sh` line | |
386 | 401 | |
387 | -### Restore Original `ls` | |
402 | +### Colors not showing | |
388 | 403 | |
389 | -If you used `--replace-ls`: | |
390 | 404 | ```bash |
391 | -# Remove wrapper | |
392 | -sudo rm /usr/local/bin/ls | |
405 | +# Force color output | |
406 | +lsf --color=always | |
393 | 407 | |
394 | -# Restore from backup | |
395 | -sudo mv /usr/local/bin/ls.bak.* /usr/local/bin/ls | |
408 | +# Check terminal color support | |
409 | +echo -e "\e[31mRed\e[0m \e[32mGreen\e[0m \e[34mBlue\e[0m" | |
396 | 410 | ``` |
397 | 411 | |
398 | 412 | --- |
399 | 413 | |
400 | -## 🆘 Getting Help | |
401 | - | |
402 | -- **Issues:** https://github.com/swadhinbiswas/ls-f/issues | |
403 | -- **Documentation:** See [README.md](../README.md) | |
404 | -- **Configuration:** See [CONFIGURATION.md](CONFIGURATION.md) | |
414 | +## More Information | |
405 | 415 | |
406 | -Happy listing! 🚀 | |
416 | +- [BENCHMARKS.md](../BENCHMARKS.md) -- Performance comparison with GNU ls | |
417 | +- [README.md](../README.md) -- Feature overview | |
418 | +- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) -- Detailed diagnostics | |
407 | 419 | |
▼src/cli.rs
+241-0
| @@ -0,0 +1,241 @@ | ||
1 | +use clap::Parser; | |
2 | + | |
3 | +#[derive(Parser, Debug, Clone)] | |
4 | +#[command( | |
5 | + name = "lsf", | |
6 | + about = "A beautiful ls replacement with Nerd Font icons and colors", | |
7 | + version, | |
8 | + disable_help_flag = true, | |
9 | + disable_version_flag = true, | |
10 | + after_help = "lsf is a fast, native ls replacement written in Rust.\nRequires a Nerd Font installed in your terminal for icons." | |
11 | +)] | |
12 | +pub struct Args { | |
13 | + /// Files or directories to list | |
14 | + #[arg(default_value = ".")] | |
15 | + pub paths: Vec<String>, | |
16 | + | |
17 | + /// Use a long listing format | |
18 | + #[arg(short = 'l', long = "long")] | |
19 | + pub long: bool, | |
20 | + | |
21 | + /// List all entries including . and .. | |
22 | + #[arg(short = 'a', long = "all")] | |
23 | + pub all: bool, | |
24 | + | |
25 | + /// Do not list implied . and .. | |
26 | + #[arg(short = 'A', long = "almost-all")] | |
27 | + pub almost_all: bool, | |
28 | + | |
29 | + /// List subdirectories recursively | |
30 | + #[arg(short = 'R', long = "recursive")] | |
31 | + pub recursive: bool, | |
32 | + | |
33 | + /// Disable icons | |
34 | + #[arg(long = "no-icons")] | |
35 | + pub no_icons: bool, | |
36 | + | |
37 | + /// Sort by file size, largest first | |
38 | + #[arg(short = 'S')] | |
39 | + pub sort_size: bool, | |
40 | + | |
41 | + /// Sort by time, newest first | |
42 | + #[arg(short = 't')] | |
43 | + pub sort_time: bool, | |
44 | + | |
45 | + /// Sort alphabetically by extension | |
46 | + #[arg(short = 'X')] | |
47 | + pub sort_extension: bool, | |
48 | + | |
49 | + /// Do not sort; list entries in directory order | |
50 | + #[arg(short = 'U')] | |
51 | + pub unsorted: bool, | |
52 | + | |
53 | + /// Natural sort of (version) numbers within text | |
54 | + #[arg(short = 'v')] | |
55 | + pub version_sort: bool, | |
56 | + | |
57 | + /// Reverse order while sorting | |
58 | + #[arg(short = 'r', long = "reverse")] | |
59 | + pub reverse: bool, | |
60 | + | |
61 | + /// List entries by lines instead of by columns | |
62 | + #[arg(short = 'x')] | |
63 | + pub across: bool, | |
64 | + | |
65 | + /// List one file per line | |
66 | + #[arg(short = '1')] | |
67 | + pub one_per_line: bool, | |
68 | + | |
69 | + /// Fill width with a comma separated list of entries | |
70 | + #[arg(short = 'm')] | |
71 | + pub comma: bool, | |
72 | + | |
73 | + /// List directories themselves, not their contents | |
74 | + #[arg(short = 'd', long = "directory")] | |
75 | + pub directory: bool, | |
76 | + | |
77 | + /// Append indicator (one of */=>@|) to entries | |
78 | + #[arg(short = 'F', long = "classify")] | |
79 | + pub classify: bool, | |
80 | + | |
81 | + /// Append / indicator to directories | |
82 | + #[arg(short = 'p')] | |
83 | + pub slash_dirs: bool, | |
84 | + | |
85 | + /// Print the allocated number of blocks for each file | |
86 | + #[arg(short = 's', long = "size")] | |
87 | + pub show_size: bool, | |
88 | + | |
89 | + /// Print the inode number of each file | |
90 | + #[arg(short = 'i', long = "inode")] | |
91 | + pub inode: bool, | |
92 | + | |
93 | + /// With -l: show numeric user and group IDs | |
94 | + #[arg(short = 'n', long = "numeric-uid-gid")] | |
95 | + pub numeric_uid_gid: bool, | |
96 | + | |
97 | + /// Like -l, but list owner | |
98 | + #[arg(short = 'o')] | |
99 | + pub long_no_group: bool, | |
100 | + | |
101 | + /// Like -l, but do not list owner | |
102 | + #[arg(short = 'g')] | |
103 | + pub long_no_owner: bool, | |
104 | + | |
105 | + /// In a long listing, don't print group names | |
106 | + #[arg(short = 'G', long = "no-group")] | |
107 | + pub no_group: bool, | |
108 | + | |
109 | + /// Print sizes in human readable format (e.g., 1K 234M 2G) | |
110 | + #[arg(short = 'h', long = "human-readable")] | |
111 | + pub human_readable: bool, | |
112 | + | |
113 | + /// Like -h but use powers of 1000 | |
114 | + #[arg(long = "si")] | |
115 | + pub si: bool, | |
116 | + | |
117 | + /// Follow symbolic links listed on the command line | |
118 | + #[arg(short = 'H', long = "dereference-command-line")] | |
119 | + pub dereference_command_line: bool, | |
120 | + | |
121 | + /// Follow all symbolic links | |
122 | + #[arg(short = 'L', long = "dereference")] | |
123 | + pub dereference: bool, | |
124 | + | |
125 | + /// Group directories before files | |
126 | + #[arg(long = "group-directories-first")] | |
127 | + pub group_directories_first: bool, | |
128 | + | |
129 | + /// End each output line with NUL, not newline | |
130 | + #[arg(long = "zero")] | |
131 | + pub zero: bool, | |
132 | + | |
133 | + /// Assume tab stops at each COLS instead of 8 | |
134 | + #[arg(short = 'T', long = "tabsize", default_value = "8")] | |
135 | + pub tabsize: usize, | |
136 | + | |
137 | + /// Assume the terminal is COLS columns wide | |
138 | + #[arg(short = 'w', long = "width")] | |
139 | + pub term_width: Option<usize>, | |
140 | + | |
141 | + /// Do not list entries matching shell PATTERN | |
142 | + #[arg(short = 'I', long = "ignore")] | |
143 | + pub ignore_pattern: Option<String>, | |
144 | + | |
145 | + /// Do not list entries ending with ~ | |
146 | + #[arg(short = 'B', long = "ignore-backups")] | |
147 | + pub ignore_backups: bool, | |
148 | + | |
149 | + /// With -l, print the author of each file | |
150 | + #[arg(long = "author")] | |
151 | + pub author: bool, | |
152 | + | |
153 | + /// Colorize the output [auto, always, never] | |
154 | + #[arg(long = "color", default_value = "auto", hide_default_value = true)] | |
155 | + pub color: String, | |
156 | + | |
157 | + /// Do not colorize the output (same as --color=never) | |
158 | + #[arg(long = "no-color")] | |
159 | + pub no_color: bool, | |
160 | + | |
161 | + /// Print tree view | |
162 | + #[arg(long = "tree")] | |
163 | + pub tree: bool, | |
164 | + | |
165 | + /// Set max depth for tree/recursive (0 = unlimited) | |
166 | + #[arg(long = "depth")] | |
167 | + pub max_depth: Option<usize>, | |
168 | + | |
169 | + /// Show git status for files (requires git) | |
170 | + #[arg(long = "git")] | |
171 | + pub git: bool, | |
172 | + | |
173 | + /// Print help information | |
174 | + #[arg(long = "help")] | |
175 | + pub help: bool, | |
176 | + | |
177 | + /// Print version information | |
178 | + #[arg(short = 'V', long = "version")] | |
179 | + pub print_version: bool, | |
180 | + | |
181 | + /// Print C-style escapes for nongraphic characters | |
182 | + #[arg(short = 'b', long = "escape")] | |
183 | + pub escape: bool, | |
184 | + | |
185 | + /// Do not sort; list entries in directory order (same as -U) | |
186 | + #[arg(short = 'f')] | |
187 | + pub no_sort_all: bool, | |
188 | + | |
189 | + /// Emit SELinux / security context | |
190 | + #[arg(short = 'Z', long = "context")] | |
191 | + pub context: bool, | |
192 | + | |
193 | + /// Show allocated blocks in -s with this scale | |
194 | + #[arg(long = "block-size")] | |
195 | + pub block_size: Option<String>, | |
196 | + | |
197 | + /// Hide entries matching pattern (overridden by -a or -A) | |
198 | + #[arg(long = "hide")] | |
199 | + pub hide_pattern: Option<String>, | |
200 | +} | |
201 | + | |
202 | +impl Args { | |
203 | + /// Whether to show hidden files | |
204 | + pub fn show_hidden(&self) -> bool { | |
205 | + self.all || self.almost_all || self.no_sort_all | |
206 | + } | |
207 | + | |
208 | + /// Whether to show . and .. | |
209 | + pub fn show_dot_dirs(&self) -> bool { | |
210 | + self.all || self.no_sort_all | |
211 | + } | |
212 | + | |
213 | + /// Whether any long-format flag is set | |
214 | + pub fn is_long(&self) -> bool { | |
215 | + self.long || self.long_no_group || self.long_no_owner || self.numeric_uid_gid | |
216 | + } | |
217 | + | |
218 | + /// Whether to use color output | |
219 | + pub fn use_color(&self) -> bool { | |
220 | + if self.no_color { | |
221 | + return false; | |
222 | + } | |
223 | + match self.color.as_str() { | |
224 | + "always" | "yes" | "force" => true, | |
225 | + "never" | "no" | "none" => false, | |
226 | + _ => { | |
227 | + // "auto" - check if stdout is a tty | |
228 | + atty_check() | |
229 | + } | |
230 | + } | |
231 | + } | |
232 | + | |
233 | + /// Whether sorting is disabled | |
234 | + pub fn no_sort(&self) -> bool { | |
235 | + self.unsorted || self.no_sort_all | |
236 | + } | |
237 | +} | |
238 | + | |
239 | +fn atty_check() -> bool { | |
240 | + unsafe { libc::isatty(libc::STDOUT_FILENO) != 0 } | |
241 | +} | |
0 | 242 | |
▼src/entry.rs
+395-0
| @@ -0,0 +1,395 @@ | ||
1 | +use std::collections::HashMap; | |
2 | +use std::fs; | |
3 | +use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; | |
4 | +use std::path::{Path, PathBuf}; | |
5 | +use std::time::SystemTime; | |
6 | + | |
7 | +use crate::cli::Args; | |
8 | + | |
9 | +/// Represents a single file/directory entry with all metadata needed for display. | |
10 | +#[derive(Debug, Clone)] | |
11 | +#[allow(dead_code)] | |
12 | +pub struct FileEntry { | |
13 | + pub name: String, | |
14 | + pub path: PathBuf, | |
15 | + pub is_dir: bool, | |
16 | + pub is_symlink: bool, | |
17 | + pub is_executable: bool, | |
18 | + pub is_hidden: bool, | |
19 | + pub is_pipe: bool, | |
20 | + pub is_socket: bool, | |
21 | + pub is_block_device: bool, | |
22 | + pub is_char_device: bool, | |
23 | + pub size: u64, | |
24 | + pub modified: Option<SystemTime>, | |
25 | + pub accessed: Option<SystemTime>, | |
26 | + pub created: Option<SystemTime>, | |
27 | + pub mode: u32, | |
28 | + pub nlink: u64, | |
29 | + pub uid: u32, | |
30 | + pub gid: u32, | |
31 | + pub blocks: u64, | |
32 | + pub inode: u64, | |
33 | + pub symlink_target: Option<String>, | |
34 | + pub extension: String, | |
35 | +} | |
36 | + | |
37 | +impl FileEntry { | |
38 | + /// Create a FileEntry from a path, reading metadata. | |
39 | + pub fn from_path(path: &Path, follow_symlinks: bool) -> std::io::Result<Self> { | |
40 | + let symlink_meta = fs::symlink_metadata(path)?; | |
41 | + let is_symlink = symlink_meta.file_type().is_symlink(); | |
42 | + | |
43 | + let meta = if follow_symlinks && is_symlink { | |
44 | + fs::metadata(path).unwrap_or_else(|_| symlink_meta.clone()) | |
45 | + } else { | |
46 | + symlink_meta.clone() | |
47 | + }; | |
48 | + | |
49 | + let file_type = meta.file_type(); | |
50 | + let name = path | |
51 | + .file_name() | |
52 | + .map(|n| n.to_string_lossy().into_owned()) | |
53 | + .unwrap_or_else(|| path.to_string_lossy().into_owned()); | |
54 | + | |
55 | + let is_dir = file_type.is_dir(); | |
56 | + let is_executable = !is_dir && (meta.permissions().mode() & 0o111 != 0); | |
57 | + let is_hidden = name.starts_with('.'); | |
58 | + let is_pipe = file_type.is_fifo(); | |
59 | + let is_socket = file_type.is_socket(); | |
60 | + let is_block_device = file_type.is_block_device(); | |
61 | + let is_char_device = file_type.is_char_device(); | |
62 | + | |
63 | + let symlink_target = if is_symlink { | |
64 | + fs::read_link(path) | |
65 | + .ok() | |
66 | + .map(|t| t.to_string_lossy().into_owned()) | |
67 | + } else { | |
68 | + None | |
69 | + }; | |
70 | + | |
71 | + let extension = path | |
72 | + .extension() | |
73 | + .map(|e| e.to_string_lossy().into_owned()) | |
74 | + .unwrap_or_default(); | |
75 | + | |
76 | + Ok(FileEntry { | |
77 | + name, | |
78 | + path: path.to_path_buf(), | |
79 | + is_dir, | |
80 | + is_symlink, | |
81 | + is_executable, | |
82 | + is_hidden, | |
83 | + is_pipe, | |
84 | + is_socket, | |
85 | + is_block_device, | |
86 | + is_char_device, | |
87 | + size: meta.len(), | |
88 | + modified: meta.modified().ok(), | |
89 | + accessed: meta.accessed().ok(), | |
90 | + created: meta.created().ok(), | |
91 | + mode: meta.mode(), | |
92 | + nlink: meta.nlink(), | |
93 | + uid: meta.uid(), | |
94 | + gid: meta.gid(), | |
95 | + blocks: meta.blocks(), | |
96 | + inode: meta.ino(), | |
97 | + symlink_target, | |
98 | + extension, | |
99 | + }) | |
100 | + } | |
101 | + | |
102 | + /// Create synthetic . and .. entries for a directory | |
103 | + pub fn dot_entries(dir: &Path) -> Vec<FileEntry> { | |
104 | + let mut entries = Vec::with_capacity(2); | |
105 | + | |
106 | + // "." | |
107 | + if let Ok(mut entry) = FileEntry::from_path(dir, false) { | |
108 | + entry.name = ".".to_string(); | |
109 | + entries.push(entry); | |
110 | + } | |
111 | + | |
112 | + // ".." | |
113 | + let parent = dir.parent().unwrap_or(dir); | |
114 | + if let Ok(mut entry) = FileEntry::from_path(parent, false) { | |
115 | + entry.name = "..".to_string(); | |
116 | + entries.push(entry); | |
117 | + } | |
118 | + | |
119 | + entries | |
120 | + } | |
121 | +} | |
122 | + | |
123 | +/// Read directory entries, filtered according to the arguments. | |
124 | +pub fn read_directory(dir: &Path, args: &Args) -> std::io::Result<Vec<FileEntry>> { | |
125 | + let mut entries = Vec::new(); | |
126 | + | |
127 | + // Add . and .. if requested | |
128 | + if args.show_dot_dirs() { | |
129 | + entries.extend(FileEntry::dot_entries(dir)); | |
130 | + } | |
131 | + | |
132 | + let follow_symlinks = args.dereference; | |
133 | + | |
134 | + let rd = fs::read_dir(dir)?; | |
135 | + for entry_result in rd { | |
136 | + let entry = match entry_result { | |
137 | + Ok(e) => e, | |
138 | + Err(_) => continue, | |
139 | + }; | |
140 | + | |
141 | + let name = entry.file_name().to_string_lossy().into_owned(); | |
142 | + | |
143 | + // Skip hidden files unless requested | |
144 | + if name.starts_with('.') && !args.show_hidden() { | |
145 | + continue; | |
146 | + } | |
147 | + | |
148 | + // Skip backup files if requested | |
149 | + if args.ignore_backups && name.ends_with('~') { | |
150 | + continue; | |
151 | + } | |
152 | + | |
153 | + // Skip files matching ignore pattern | |
154 | + if let Some(ref pattern) = args.ignore_pattern { | |
155 | + if matches_glob_simple(&name, pattern) { | |
156 | + continue; | |
157 | + } | |
158 | + } | |
159 | + | |
160 | + // Skip files matching hide pattern (unless -a or -A) | |
161 | + if !args.show_hidden() { | |
162 | + if let Some(ref pattern) = args.hide_pattern { | |
163 | + if matches_glob_simple(&name, pattern) { | |
164 | + continue; | |
165 | + } | |
166 | + } | |
167 | + } | |
168 | + | |
169 | + let path = entry.path(); | |
170 | + match FileEntry::from_path(&path, follow_symlinks) { | |
171 | + Ok(fe) => entries.push(fe), | |
172 | + Err(_) => { | |
173 | + // If we can't read metadata, create a minimal entry | |
174 | + entries.push(FileEntry { | |
175 | + name, | |
176 | + path, | |
177 | + is_dir: false, | |
178 | + is_symlink: false, | |
179 | + is_executable: false, | |
180 | + is_hidden: true, | |
181 | + is_pipe: false, | |
182 | + is_socket: false, | |
183 | + is_block_device: false, | |
184 | + is_char_device: false, | |
185 | + size: 0, | |
186 | + modified: None, | |
187 | + accessed: None, | |
188 | + created: None, | |
189 | + mode: 0, | |
190 | + nlink: 0, | |
191 | + uid: 0, | |
192 | + gid: 0, | |
193 | + blocks: 0, | |
194 | + inode: 0, | |
195 | + symlink_target: None, | |
196 | + extension: String::new(), | |
197 | + }); | |
198 | + } | |
199 | + } | |
200 | + } | |
201 | + | |
202 | + Ok(entries) | |
203 | +} | |
204 | + | |
205 | +/// Sort entries according to the arguments. | |
206 | +pub fn sort_entries(entries: &mut Vec<FileEntry>, args: &Args) { | |
207 | + if args.no_sort() { | |
208 | + return; | |
209 | + } | |
210 | + | |
211 | + // Determine the primary sort | |
212 | + if args.sort_size { | |
213 | + entries.sort_by(|a, b| b.size.cmp(&a.size)); | |
214 | + } else if args.sort_time { | |
215 | + entries.sort_by(|a, b| { | |
216 | + let ta = a.modified.unwrap_or(SystemTime::UNIX_EPOCH); | |
217 | + let tb = b.modified.unwrap_or(SystemTime::UNIX_EPOCH); | |
218 | + tb.cmp(&ta) | |
219 | + }); | |
220 | + } else if args.sort_extension { | |
221 | + entries.sort_by(|a, b| { | |
222 | + let ea = a.extension.to_lowercase(); | |
223 | + let eb = b.extension.to_lowercase(); | |
224 | + ea.cmp(&eb) | |
225 | + .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())) | |
226 | + }); | |
227 | + } else if args.version_sort { | |
228 | + entries.sort_by(|a, b| version_compare(&a.name, &b.name)); | |
229 | + } else { | |
230 | + // Default: alphabetical (case-insensitive) | |
231 | + entries.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); | |
232 | + } | |
233 | + | |
234 | + // Group directories first if requested | |
235 | + if args.group_directories_first { | |
236 | + entries.sort_by(|a, b| b.is_dir.cmp(&a.is_dir)); | |
237 | + } | |
238 | + | |
239 | + // Reverse if requested | |
240 | + if args.reverse { | |
241 | + entries.reverse(); | |
242 | + } | |
243 | +} | |
244 | + | |
245 | +/// Simple glob matching (supports * and ? wildcards) | |
246 | +fn matches_glob_simple(name: &str, pattern: &str) -> bool { | |
247 | + let mut name_chars = name.chars().peekable(); | |
248 | + let mut pat_chars = pattern.chars().peekable(); | |
249 | + | |
250 | + fn matches_inner( | |
251 | + name: &mut std::iter::Peekable<std::str::Chars>, | |
252 | + pat: &mut std::iter::Peekable<std::str::Chars>, | |
253 | + ) -> bool { | |
254 | + loop { | |
255 | + match (pat.peek().copied(), name.peek().copied()) { | |
256 | + (None, None) => return true, | |
257 | + (None, Some(_)) => return false, | |
258 | + (Some('*'), _) => { | |
259 | + pat.next(); | |
260 | + if pat.peek().is_none() { | |
261 | + return true; | |
262 | + } | |
263 | + // Try matching * against zero or more chars | |
264 | + loop { | |
265 | + let mut pat_clone = pat.clone(); | |
266 | + let mut name_clone = name.clone(); | |
267 | + if matches_inner(&mut name_clone, &mut pat_clone) { | |
268 | + return true; | |
269 | + } | |
270 | + if name.next().is_none() { | |
271 | + return false; | |
272 | + } | |
273 | + } | |
274 | + } | |
275 | + (Some('?'), Some(_)) => { | |
276 | + pat.next(); | |
277 | + name.next(); | |
278 | + } | |
279 | + (Some(p), Some(n)) if p == n => { | |
280 | + pat.next(); | |
281 | + name.next(); | |
282 | + } | |
283 | + _ => return false, | |
284 | + } | |
285 | + } | |
286 | + } | |
287 | + | |
288 | + matches_inner(&mut name_chars, &mut pat_chars) | |
289 | +} | |
290 | + | |
291 | +/// Natural/version sort comparison | |
292 | +fn version_compare(a: &str, b: &str) -> std::cmp::Ordering { | |
293 | + let mut ai = a.chars().peekable(); | |
294 | + let mut bi = b.chars().peekable(); | |
295 | + | |
296 | + loop { | |
297 | + match (ai.peek(), bi.peek()) { | |
298 | + (None, None) => return std::cmp::Ordering::Equal, | |
299 | + (None, Some(_)) => return std::cmp::Ordering::Less, | |
300 | + (Some(_), None) => return std::cmp::Ordering::Greater, | |
301 | + (Some(&ac), Some(&bc)) => { | |
302 | + if ac.is_ascii_digit() && bc.is_ascii_digit() { | |
303 | + // Compare numeric segments | |
304 | + let mut an = String::new(); | |
305 | + let mut bn = String::new(); | |
306 | + while let Some(&c) = ai.peek() { | |
307 | + if c.is_ascii_digit() { | |
308 | + an.push(c); | |
309 | + ai.next(); | |
310 | + } else { | |
311 | + break; | |
312 | + } | |
313 | + } | |
314 | + while let Some(&c) = bi.peek() { | |
315 | + if c.is_ascii_digit() { | |
316 | + bn.push(c); | |
317 | + bi.next(); | |
318 | + } else { | |
319 | + break; | |
320 | + } | |
321 | + } | |
322 | + let na: u64 = an.parse().unwrap_or(0); | |
323 | + let nb: u64 = bn.parse().unwrap_or(0); | |
324 | + match na.cmp(&nb) { | |
325 | + std::cmp::Ordering::Equal => continue, | |
326 | + other => return other, | |
327 | + } | |
328 | + } else { | |
329 | + let al = ac.to_lowercase().next().unwrap_or(ac); | |
330 | + let bl = bc.to_lowercase().next().unwrap_or(bc); | |
331 | + match al.cmp(&bl) { | |
332 | + std::cmp::Ordering::Equal => { | |
333 | + // If lowercase-equal, compare original case | |
334 | + match ac.cmp(&bc) { | |
335 | + std::cmp::Ordering::Equal => { | |
336 | + ai.next(); | |
337 | + bi.next(); | |
338 | + } | |
339 | + other => { | |
340 | + ai.next(); | |
341 | + bi.next(); | |
342 | + return other; | |
343 | + } | |
344 | + } | |
345 | + } | |
346 | + other => return other, | |
347 | + } | |
348 | + } | |
349 | + } | |
350 | + } | |
351 | + } | |
352 | +} | |
353 | + | |
354 | +/// Gather entries for a single path argument. | |
355 | +/// If `-d` is set, lists the directory itself rather than its contents. | |
356 | +#[allow(dead_code)] | |
357 | +pub fn gather_entries(path: &Path, args: &Args) -> std::io::Result<Vec<FileEntry>> { | |
358 | + let follow = args.dereference || args.dereference_command_line; | |
359 | + | |
360 | + if args.directory { | |
361 | + let entry = FileEntry::from_path(path, follow)?; | |
362 | + return Ok(vec![entry]); | |
363 | + } | |
364 | + | |
365 | + if path.is_dir() { | |
366 | + read_directory(path, args) | |
367 | + } else { | |
368 | + let entry = FileEntry::from_path(path, follow)?; | |
369 | + Ok(vec![entry]) | |
370 | + } | |
371 | +} | |
372 | + | |
373 | +/// Map of user IDs to user names (cached). | |
374 | +pub fn get_username(uid: u32, cache: &mut HashMap<u32, String>) -> String { | |
375 | + if let Some(name) = cache.get(&uid) { | |
376 | + return name.clone(); | |
377 | + } | |
378 | + let name = users::get_user_by_uid(uid) | |
379 | + .map(|u| u.name().to_string_lossy().into_owned()) | |
380 | + .unwrap_or_else(|| uid.to_string()); | |
381 | + cache.insert(uid, name.clone()); | |
382 | + name | |
383 | +} | |
384 | + | |
385 | +/// Map of group IDs to group names (cached). | |
386 | +pub fn get_groupname(gid: u32, cache: &mut HashMap<u32, String>) -> String { | |
387 | + if let Some(name) = cache.get(&gid) { | |
388 | + return name.clone(); | |
389 | + } | |
390 | + let name = users::get_group_by_gid(gid) | |
391 | + .map(|g| g.name().to_string_lossy().into_owned()) | |
392 | + .unwrap_or_else(|| gid.to_string()); | |
393 | + cache.insert(gid, name.clone()); | |
394 | + name | |
395 | +} | |
0 | 396 | |
▼src/format.rs
+339-0
| @@ -0,0 +1,339 @@ | ||
1 | +use std::collections::HashMap; | |
2 | +use std::time::SystemTime; | |
3 | + | |
4 | +use chrono::{DateTime, Local}; | |
5 | + | |
6 | +use crate::entry::FileEntry; | |
7 | + | |
8 | +/// Format the permission mode as an `ls -l` style string (e.g., drwxr-xr-x). | |
9 | +pub fn format_permissions(mode: u32, is_dir: bool, is_symlink: bool) -> String { | |
10 | + let mut s = String::with_capacity(10); | |
11 | + | |
12 | + // File type character | |
13 | + if is_symlink { | |
14 | + s.push('l'); | |
15 | + } else if is_dir { | |
16 | + s.push('d'); | |
17 | + } else if mode & 0o170000 == 0o010000 { | |
18 | + s.push('p'); // FIFO | |
19 | + } else if mode & 0o170000 == 0o020000 { | |
20 | + s.push('c'); // char device | |
21 | + } else if mode & 0o170000 == 0o060000 { | |
22 | + s.push('b'); // block device | |
23 | + } else if mode & 0o170000 == 0o140000 { | |
24 | + s.push('s'); // socket | |
25 | + } else { | |
26 | + s.push('-'); | |
27 | + } | |
28 | + | |
29 | + // Owner | |
30 | + s.push(if mode & 0o400 != 0 { 'r' } else { '-' }); | |
31 | + s.push(if mode & 0o200 != 0 { 'w' } else { '-' }); | |
32 | + s.push(if mode & 0o4000 != 0 { | |
33 | + if mode & 0o100 != 0 { | |
34 | + 's' | |
35 | + } else { | |
36 | + 'S' | |
37 | + } | |
38 | + } else { | |
39 | + if mode & 0o100 != 0 { | |
40 | + 'x' | |
41 | + } else { | |
42 | + '-' | |
43 | + } | |
44 | + }); | |
45 | + | |
46 | + // Group | |
47 | + s.push(if mode & 0o040 != 0 { 'r' } else { '-' }); | |
48 | + s.push(if mode & 0o020 != 0 { 'w' } else { '-' }); | |
49 | + s.push(if mode & 0o2000 != 0 { | |
50 | + if mode & 0o010 != 0 { | |
51 | + 's' | |
52 | + } else { | |
53 | + 'S' | |
54 | + } | |
55 | + } else { | |
56 | + if mode & 0o010 != 0 { | |
57 | + 'x' | |
58 | + } else { | |
59 | + '-' | |
60 | + } | |
61 | + }); | |
62 | + | |
63 | + // Others | |
64 | + s.push(if mode & 0o004 != 0 { 'r' } else { '-' }); | |
65 | + s.push(if mode & 0o002 != 0 { 'w' } else { '-' }); | |
66 | + s.push(if mode & 0o1000 != 0 { | |
67 | + if mode & 0o001 != 0 { | |
68 | + 't' | |
69 | + } else { | |
70 | + 'T' | |
71 | + } | |
72 | + } else { | |
73 | + if mode & 0o001 != 0 { | |
74 | + 'x' | |
75 | + } else { | |
76 | + '-' | |
77 | + } | |
78 | + }); | |
79 | + | |
80 | + s | |
81 | +} | |
82 | + | |
83 | +/// Format a file size for display. | |
84 | +pub fn format_size(size: u64, human: bool, si: bool) -> String { | |
85 | + if !human { | |
86 | + return size.to_string(); | |
87 | + } | |
88 | + | |
89 | + let base: f64 = if si { 1000.0 } else { 1024.0 }; | |
90 | + let units = if si { | |
91 | + &["B", "kB", "MB", "GB", "TB", "PB"][..] | |
92 | + } else { | |
93 | + &["", "K", "M", "G", "T", "P"][..] | |
94 | + }; | |
95 | + | |
96 | + if size == 0 { | |
97 | + return if si { | |
98 | + "0B".to_string() | |
99 | + } else { | |
100 | + "0".to_string() | |
101 | + }; | |
102 | + } | |
103 | + | |
104 | + let mut val = size as f64; | |
105 | + let mut unit_idx = 0; | |
106 | + | |
107 | + while val >= base && unit_idx < units.len() - 1 { | |
108 | + val /= base; | |
109 | + unit_idx += 1; | |
110 | + } | |
111 | + | |
112 | + if unit_idx == 0 { | |
113 | + format!("{}{}", size, units[0]) | |
114 | + } else if val < 10.0 { | |
115 | + format!("{:.1}{}", val, units[unit_idx]) | |
116 | + } else { | |
117 | + format!("{:.0}{}", val, units[unit_idx]) | |
118 | + } | |
119 | +} | |
120 | + | |
121 | +/// Format a timestamp for display (like ls). | |
122 | +pub fn format_time(time: Option<SystemTime>) -> String { | |
123 | + let time = match time { | |
124 | + Some(t) => t, | |
125 | + None => return " ".to_string(), // 12 chars | |
126 | + }; | |
127 | + | |
128 | + let datetime: DateTime<Local> = time.into(); | |
129 | + let now = Local::now(); | |
130 | + let six_months_ago = now - chrono::Duration::days(182); | |
131 | + | |
132 | + if datetime < six_months_ago || datetime > now + chrono::Duration::hours(1) { | |
133 | + // Show year for old/future files | |
134 | + datetime.format("%b %e %Y").to_string() | |
135 | + } else { | |
136 | + // Show time for recent files | |
137 | + datetime.format("%b %e %H:%M").to_string() | |
138 | + } | |
139 | +} | |
140 | + | |
141 | +/// Format a long-listing line for a single entry. | |
142 | +pub fn format_long_entry( | |
143 | + entry: &FileEntry, | |
144 | + icon_str: &str, | |
145 | + show_icons: bool, | |
146 | + show_owner: bool, | |
147 | + show_group: bool, | |
148 | + show_inode: bool, | |
149 | + show_blocks: bool, | |
150 | + show_author: bool, | |
151 | + human_readable: bool, | |
152 | + si: bool, | |
153 | + numeric_ids: bool, | |
154 | + use_color: bool, | |
155 | + user_cache: &mut HashMap<u32, String>, | |
156 | + group_cache: &mut HashMap<u32, String>, | |
157 | + // Width hints for alignment | |
158 | + nlink_width: usize, | |
159 | + owner_width: usize, | |
160 | + group_width: usize, | |
161 | + size_width: usize, | |
162 | + inode_width: usize, | |
163 | + blocks_width: usize, | |
164 | +) -> String { | |
165 | + let mut line = String::with_capacity(128); | |
166 | + | |
167 | + // Inode | |
168 | + if show_inode { | |
169 | + line.push_str(&format!("{:>width$} ", entry.inode, width = inode_width)); | |
170 | + } | |
171 | + | |
172 | + // Blocks | |
173 | + if show_blocks { | |
174 | + let blocks = entry.blocks / 2; // Convert 512-byte blocks to 1K blocks | |
175 | + line.push_str(&format!("{:>width$} ", blocks, width = blocks_width)); | |
176 | + } | |
177 | + | |
178 | + // Icon | |
179 | + if show_icons { | |
180 | + line.push_str(icon_str); | |
181 | + line.push(' '); | |
182 | + } | |
183 | + | |
184 | + // Permissions | |
185 | + let perms = format_permissions(entry.mode, entry.is_dir, entry.is_symlink); | |
186 | + if use_color { | |
187 | + line.push_str(&colorize_permissions(&perms)); | |
188 | + } else { | |
189 | + line.push_str(&perms); | |
190 | + } | |
191 | + | |
192 | + // Hard link count | |
193 | + line.push_str(&format!(" {:>width$}", entry.nlink, width = nlink_width)); | |
194 | + | |
195 | + // Owner | |
196 | + if show_owner { | |
197 | + let owner = if numeric_ids { | |
198 | + entry.uid.to_string() | |
199 | + } else { | |
200 | + crate::entry::get_username(entry.uid, user_cache) | |
201 | + }; | |
202 | + line.push_str(&format!(" {:<width$}", owner, width = owner_width)); | |
203 | + } | |
204 | + | |
205 | + // Group | |
206 | + if show_group { | |
207 | + let group = if numeric_ids { | |
208 | + entry.gid.to_string() | |
209 | + } else { | |
210 | + crate::entry::get_groupname(entry.gid, group_cache) | |
211 | + }; | |
212 | + line.push_str(&format!(" {:<width$}", group, width = group_width)); | |
213 | + } | |
214 | + | |
215 | + // Author (same as owner for now) | |
216 | + if show_author { | |
217 | + let author = if numeric_ids { | |
218 | + entry.uid.to_string() | |
219 | + } else { | |
220 | + crate::entry::get_username(entry.uid, user_cache) | |
221 | + }; | |
222 | + line.push_str(&format!(" {:<width$}", author, width = owner_width)); | |
223 | + } | |
224 | + | |
225 | + // Size | |
226 | + let size_str = format_size(entry.size, human_readable, si); | |
227 | + line.push_str(&format!(" {:>width$}", size_str, width = size_width)); | |
228 | + | |
229 | + // Timestamp | |
230 | + let time_str = format_time(entry.modified); | |
231 | + line.push_str(&format!(" {}", time_str)); | |
232 | + | |
233 | + // Filename (with color if applicable) | |
234 | + line.push(' '); | |
235 | + line.push_str(&colorize_filename(entry, use_color)); | |
236 | + | |
237 | + // Symlink target | |
238 | + if entry.is_symlink { | |
239 | + if let Some(ref target) = entry.symlink_target { | |
240 | + line.push_str(" -> "); | |
241 | + if use_color { | |
242 | + line.push_str(&format!("\x1b[36m{}\x1b[0m", target)); | |
243 | + } else { | |
244 | + line.push_str(target); | |
245 | + } | |
246 | + } | |
247 | + } | |
248 | + | |
249 | + // Classify indicator | |
250 | + // (handled in the caller) | |
251 | + | |
252 | + line | |
253 | +} | |
254 | + | |
255 | +/// Colorize a filename based on its type. | |
256 | +pub fn colorize_filename(entry: &FileEntry, use_color: bool) -> String { | |
257 | + if !use_color { | |
258 | + return entry.name.clone(); | |
259 | + } | |
260 | + | |
261 | + if entry.is_dir { | |
262 | + format!("\x1b[1;34m{}\x1b[0m", entry.name) | |
263 | + } else if entry.is_symlink { | |
264 | + format!("\x1b[36m{}\x1b[0m", entry.name) | |
265 | + } else if entry.is_executable { | |
266 | + format!("\x1b[1;32m{}\x1b[0m", entry.name) | |
267 | + } else if entry.is_pipe { | |
268 | + format!("\x1b[33m{}\x1b[0m", entry.name) | |
269 | + } else if entry.is_socket { | |
270 | + format!("\x1b[1;35m{}\x1b[0m", entry.name) | |
271 | + } else if entry.is_block_device || entry.is_char_device { | |
272 | + format!("\x1b[1;33m{}\x1b[0m", entry.name) | |
273 | + } else { | |
274 | + // Check extension for known media/archive types | |
275 | + let ext = entry.extension.to_lowercase(); | |
276 | + match ext.as_str() { | |
277 | + // Archives | |
278 | + "zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar" | "tgz" | "tbz2" | "txz" | "zst" | |
279 | + | "deb" | "rpm" | "iso" | "dmg" => { | |
280 | + format!("\x1b[1;31m{}\x1b[0m", entry.name) | |
281 | + } | |
282 | + // Images | |
283 | + "png" | "jpg" | "jpeg" | "gif" | "svg" | "webp" | "bmp" | "ico" | "tiff" | "tif" | |
284 | + | "psd" | "heic" | "avif" => { | |
285 | + format!("\x1b[35m{}\x1b[0m", entry.name) | |
286 | + } | |
287 | + // Audio | |
288 | + "mp3" | "wav" | "flac" | "aac" | "ogg" | "m4a" | "wma" | "opus" => { | |
289 | + format!("\x1b[36m{}\x1b[0m", entry.name) | |
290 | + } | |
291 | + // Video | |
292 | + "mp4" | "mov" | "avi" | "mkv" | "webm" | "flv" | "wmv" | "m4v" | "mpg" | "mpeg" => { | |
293 | + format!("\x1b[1;35m{}\x1b[0m", entry.name) | |
294 | + } | |
295 | + _ => entry.name.clone(), | |
296 | + } | |
297 | + } | |
298 | +} | |
299 | + | |
300 | +/// Colorize permission string. | |
301 | +fn colorize_permissions(perms: &str) -> String { | |
302 | + let mut result = String::with_capacity(perms.len() * 5); | |
303 | + for c in perms.chars() { | |
304 | + match c { | |
305 | + 'd' => result.push_str(&format!("\x1b[1;34m{}\x1b[0m", c)), | |
306 | + 'l' => result.push_str(&format!("\x1b[1;36m{}\x1b[0m", c)), | |
307 | + 'r' => result.push_str(&format!("\x1b[33m{}\x1b[0m", c)), | |
308 | + 'w' => result.push_str(&format!("\x1b[31m{}\x1b[0m", c)), | |
309 | + 'x' | 's' | 't' => result.push_str(&format!("\x1b[1;32m{}\x1b[0m", c)), | |
310 | + 'S' | 'T' => result.push_str(&format!("\x1b[32m{}\x1b[0m", c)), | |
311 | + '-' => result.push_str(&format!("\x1b[90m{}\x1b[0m", c)), | |
312 | + _ => result.push(c), | |
313 | + } | |
314 | + // Don't add separator | |
315 | + } | |
316 | + result | |
317 | +} | |
318 | + | |
319 | +/// Get the classify indicator for a file. | |
320 | +pub fn classify_indicator(entry: &FileEntry, full: bool) -> &'static str { | |
321 | + if entry.is_dir { | |
322 | + "/" | |
323 | + } else if entry.is_symlink { | |
324 | + "@" | |
325 | + } else if entry.is_pipe { | |
326 | + "|" | |
327 | + } else if entry.is_socket { | |
328 | + "=" | |
329 | + } else if full && entry.is_executable { | |
330 | + "*" | |
331 | + } else { | |
332 | + "" | |
333 | + } | |
334 | +} | |
335 | + | |
336 | +/// Calculate the total blocks for a list of entries (for long-format "total" line). | |
337 | +pub fn total_blocks(entries: &[FileEntry]) -> u64 { | |
338 | + entries.iter().map(|e| e.blocks).sum::<u64>() / 2 // 512-byte -> 1K blocks | |
339 | +} | |
0 | 340 | |
▼src/grid.rs
+156-0
| @@ -0,0 +1,156 @@ | ||
1 | +use unicode_width::UnicodeWidthStr; | |
2 | + | |
3 | +/// Calculate the display width of a string, ignoring ANSI escape sequences. | |
4 | +pub fn display_width(s: &str) -> usize { | |
5 | + let stripped = strip_ansi(s); | |
6 | + UnicodeWidthStr::width(stripped.as_str()) | |
7 | +} | |
8 | + | |
9 | +/// Strip ANSI escape sequences from a string. | |
10 | +pub fn strip_ansi(s: &str) -> String { | |
11 | + let mut result = String::with_capacity(s.len()); | |
12 | + let mut in_escape = false; | |
13 | + let mut chars = s.chars(); | |
14 | + | |
15 | + while let Some(c) = chars.next() { | |
16 | + if c == '\x1b' { | |
17 | + in_escape = true; | |
18 | + continue; | |
19 | + } | |
20 | + if in_escape { | |
21 | + if c == 'm' { | |
22 | + in_escape = false; | |
23 | + } | |
24 | + continue; | |
25 | + } | |
26 | + result.push(c); | |
27 | + } | |
28 | + | |
29 | + result | |
30 | +} | |
31 | + | |
32 | +/// Format entries into a multi-column grid. | |
33 | +/// `items` - pre-formatted strings (with ANSI colors). | |
34 | +/// `term_width` - terminal width in columns. | |
35 | +/// `tabsize` - tab stop width. | |
36 | +/// `by_rows` - if true, fill rows first (like `ls -x`); if false, fill columns first. | |
37 | +pub fn format_grid(items: &[String], term_width: usize, tabsize: usize, by_rows: bool) -> String { | |
38 | + if items.is_empty() { | |
39 | + return String::new(); | |
40 | + } | |
41 | + | |
42 | + let tabsize = if tabsize == 0 { 8 } else { tabsize }; | |
43 | + | |
44 | + // Find the maximum display width | |
45 | + let max_len = items.iter().map(|s| display_width(s)).max().unwrap_or(0); | |
46 | + | |
47 | + // Calculate column width (align to tab stops) | |
48 | + let col_width = if max_len == 0 { | |
49 | + tabsize | |
50 | + } else { | |
51 | + let w = ((max_len + tabsize - 1) / tabsize) * tabsize; | |
52 | + if w <= max_len { | |
53 | + max_len + tabsize | |
54 | + } else { | |
55 | + w | |
56 | + } | |
57 | + }; | |
58 | + | |
59 | + let cols = (term_width / col_width).max(1); | |
60 | + let rows = (items.len() + cols - 1) / cols; | |
61 | + | |
62 | + let mut output = String::with_capacity(items.len() * (col_width + 2)); | |
63 | + | |
64 | + for i in 0..rows { | |
65 | + for j in 0..cols { | |
66 | + let index = if by_rows { i * cols + j } else { i + j * rows }; | |
67 | + | |
68 | + if index >= items.len() { | |
69 | + continue; | |
70 | + } | |
71 | + | |
72 | + let item = &items[index]; | |
73 | + let d_len = display_width(item); | |
74 | + | |
75 | + // Determine if there's a next item in this row | |
76 | + let has_next = if by_rows { | |
77 | + j != cols - 1 && (index + 1) < items.len() | |
78 | + } else { | |
79 | + j != cols - 1 && (index + rows) < items.len() | |
80 | + }; | |
81 | + | |
82 | + output.push_str(item); | |
83 | + | |
84 | + if has_next { | |
85 | + let offset = d_len % col_width; | |
86 | + let padding = if offset == 0 { | |
87 | + tabsize | |
88 | + } else { | |
89 | + let p = col_width - offset; | |
90 | + if p < 1 { | |
91 | + tabsize | |
92 | + } else { | |
93 | + p | |
94 | + } | |
95 | + }; | |
96 | + for _ in 0..padding { | |
97 | + output.push(' '); | |
98 | + } | |
99 | + } | |
100 | + } | |
101 | + output.push('\n'); | |
102 | + } | |
103 | + | |
104 | + output | |
105 | +} | |
106 | + | |
107 | +/// Format entries as one-per-line. | |
108 | +pub fn format_single_column(items: &[String], terminator: char) -> String { | |
109 | + let mut output = String::with_capacity(items.len() * 40); | |
110 | + for item in items { | |
111 | + output.push_str(item); | |
112 | + output.push(terminator); | |
113 | + } | |
114 | + output | |
115 | +} | |
116 | + | |
117 | +/// Format entries as comma-separated. | |
118 | +pub fn format_comma(items: &[String], term_width: usize) -> String { | |
119 | + if items.is_empty() { | |
120 | + return String::new(); | |
121 | + } | |
122 | + | |
123 | + let mut output = String::with_capacity(items.len() * 20); | |
124 | + let mut line_len: usize = 0; | |
125 | + | |
126 | + for (i, item) in items.iter().enumerate() { | |
127 | + let d_len = display_width(item); | |
128 | + let separator = if i < items.len() - 1 { ", " } else { "" }; | |
129 | + let needed = d_len + separator.len(); | |
130 | + | |
131 | + if line_len > 0 && line_len + needed > term_width { | |
132 | + output.push('\n'); | |
133 | + line_len = 0; | |
134 | + } | |
135 | + | |
136 | + output.push_str(item); | |
137 | + output.push_str(separator); | |
138 | + line_len += needed; | |
139 | + } | |
140 | + | |
141 | + if !output.ends_with('\n') { | |
142 | + output.push('\n'); | |
143 | + } | |
144 | + | |
145 | + output | |
146 | +} | |
147 | + | |
148 | +/// Get terminal width, with optional override. | |
149 | +pub fn get_terminal_width(override_width: Option<usize>) -> usize { | |
150 | + if let Some(w) = override_width { | |
151 | + return w; | |
152 | + } | |
153 | + terminal_size::terminal_size() | |
154 | + .map(|(w, _)| w.0 as usize) | |
155 | + .unwrap_or(80) | |
156 | +} | |
0 | 157 | |
▼src/icons.rs
+629-0
| @@ -0,0 +1,629 @@ | ||
1 | +use std::collections::HashMap; | |
2 | + | |
3 | +/// An icon entry: the Nerd Font glyph and its ANSI color code. | |
4 | +#[derive(Debug, Clone)] | |
5 | +pub struct IconEntry { | |
6 | + pub icon: &'static str, | |
7 | + pub color: &'static str, | |
8 | +} | |
9 | + | |
10 | +impl IconEntry { | |
11 | + const fn new(icon: &'static str, color: &'static str) -> Self { | |
12 | + Self { icon, color } | |
13 | + } | |
14 | + | |
15 | + /// Return the icon wrapped in ANSI color escape codes. | |
16 | + pub fn colored(&self) -> String { | |
17 | + format!("\x1b[{}m{}\x1b[0m", self.color, self.icon) | |
18 | + } | |
19 | + | |
20 | + /// Return the icon without color. | |
21 | + pub fn plain(&self) -> String { | |
22 | + self.icon.to_string() | |
23 | + } | |
24 | +} | |
25 | + | |
26 | +// Default icon for unknown files | |
27 | +pub const DEFAULT_ICON: IconEntry = IconEntry::new("\u{f016}", "37"); // | |
28 | + | |
29 | +// Directory icons | |
30 | +pub const DIR_ICON: IconEntry = IconEntry::new("\u{f115}", "1;34"); // | |
31 | +pub const DIR_HIDDEN_ICON: IconEntry = IconEntry::new("\u{f114}", "34"); // | |
32 | + | |
33 | +// Symlink icon | |
34 | +pub const SYMLINK_ICON: IconEntry = IconEntry::new("\u{f0c1}", "36"); // | |
35 | + | |
36 | +// Executable icon | |
37 | +pub const EXEC_ICON: IconEntry = IconEntry::new("\u{f489}", "1;32"); // | |
38 | + | |
39 | +// Pipe / socket / device icons | |
40 | +pub const PIPE_ICON: IconEntry = IconEntry::new("\u{f731}", "33"); // | |
41 | +pub const SOCKET_ICON: IconEntry = IconEntry::new("\u{f6a7}", "35"); // | |
42 | +pub const BLOCK_DEV_ICON: IconEntry = IconEntry::new("\u{f0a0}", "1;33"); // | |
43 | +pub const CHAR_DEV_ICON: IconEntry = IconEntry::new("\u{e601}", "1;33"); // | |
44 | + | |
45 | +/// Build the complete extension -> IconEntry map. | |
46 | +/// Uses a function so the map is built once and reused. | |
47 | +pub fn build_icon_map() -> HashMap<&'static str, IconEntry> { | |
48 | + let mut m = HashMap::with_capacity(256); | |
49 | + | |
50 | + // ── Common Config Files (exact filename match, lowercase) ────────── | |
51 | + m.insert(".gitignore", IconEntry::new("\u{e702}", "91")); | |
52 | + m.insert(".gitconfig", IconEntry::new("\u{e702}", "91")); | |
53 | + m.insert(".gitattributes", IconEntry::new("\u{e702}", "91")); | |
54 | + m.insert(".gitmodules", IconEntry::new("\u{e702}", "91")); | |
55 | + m.insert(".gitlab-ci.yml", IconEntry::new("\u{e702}", "91")); | |
56 | + m.insert(".github", IconEntry::new("\u{e702}", "90")); | |
57 | + m.insert("dockerfile", IconEntry::new("\u{f308}", "94")); | |
58 | + m.insert(".dockerignore", IconEntry::new("\u{f308}", "94")); | |
59 | + m.insert("docker-compose.yml", IconEntry::new("\u{f308}", "94")); | |
60 | + m.insert("docker-compose.yaml", IconEntry::new("\u{f308}", "94")); | |
61 | + m.insert("license", IconEntry::new("\u{f718}", "90")); | |
62 | + m.insert("license.md", IconEntry::new("\u{f718}", "90")); | |
63 | + m.insert("license.txt", IconEntry::new("\u{f718}", "90")); | |
64 | + m.insert("readme.md", IconEntry::new("\u{f48a}", "96")); | |
65 | + m.insert("readme", IconEntry::new("\u{f48a}", "96")); | |
66 | + m.insert("readme.txt", IconEntry::new("\u{f48a}", "96")); | |
67 | + m.insert("makefile", IconEntry::new("\u{f085}", "90")); | |
68 | + m.insert("cmakelists.txt", IconEntry::new("\u{f085}", "90")); | |
69 | + m.insert(".editorconfig", IconEntry::new("\u{f085}", "90")); | |
70 | + m.insert(".eslintrc", IconEntry::new("\u{f085}", "95")); | |
71 | + m.insert(".eslintrc.js", IconEntry::new("\u{f085}", "95")); | |
72 | + m.insert(".eslintrc.json", IconEntry::new("\u{f085}", "95")); | |
73 | + m.insert(".eslintrc.yml", IconEntry::new("\u{f085}", "95")); | |
74 | + m.insert(".prettierrc", IconEntry::new("\u{f085}", "95")); | |
75 | + m.insert(".prettierrc.js", IconEntry::new("\u{f085}", "95")); | |
76 | + m.insert(".prettierrc.json", IconEntry::new("\u{f085}", "95")); | |
77 | + m.insert(".prettierrc.yml", IconEntry::new("\u{f085}", "95")); | |
78 | + m.insert(".npmrc", IconEntry::new("\u{e71e}", "91")); | |
79 | + m.insert(".nvmrc", IconEntry::new("\u{e718}", "92")); | |
80 | + m.insert(".babelrc", IconEntry::new("\u{f085}", "93")); | |
81 | + m.insert(".browserslistrc", IconEntry::new("\u{f085}", "90")); | |
82 | + m.insert("tsconfig.json", IconEntry::new("\u{e628}", "94")); | |
83 | + m.insert("jsconfig.json", IconEntry::new("\u{e781}", "93")); | |
84 | + m.insert(".env", IconEntry::new("\u{f462}", "90")); | |
85 | + m.insert(".env.local", IconEntry::new("\u{f462}", "90")); | |
86 | + m.insert(".env.development", IconEntry::new("\u{f462}", "90")); | |
87 | + m.insert(".env.production", IconEntry::new("\u{f462}", "90")); | |
88 | + m.insert(".env.test", IconEntry::new("\u{f462}", "90")); | |
89 | + m.insert("vagrantfile", IconEntry::new("\u{f085}", "94")); | |
90 | + m.insert("gruntfile.js", IconEntry::new("\u{e74c}", "93")); | |
91 | + m.insert("gulpfile.js", IconEntry::new("\u{e610}", "91")); | |
92 | + m.insert("webpack.config.js", IconEntry::new("\u{f72b}", "94")); | |
93 | + m.insert("rollup.config.js", IconEntry::new("\u{f085}", "91")); | |
94 | + m.insert("vite.config.js", IconEntry::new("\u{f085}", "95")); | |
95 | + m.insert("vite.config.ts", IconEntry::new("\u{f085}", "95")); | |
96 | + m.insert("next.config.js", IconEntry::new("\u{f085}", "37")); | |
97 | + m.insert("nuxt.config.js", IconEntry::new("\u{f085}", "92")); | |
98 | + m.insert("angular.json", IconEntry::new("\u{e753}", "91")); | |
99 | + m.insert("tailwind.config.js", IconEntry::new("\u{f085}", "36")); | |
100 | + m.insert("tailwind.config.ts", IconEntry::new("\u{f085}", "36")); | |
101 | + m.insert("postcss.config.js", IconEntry::new("\u{f085}", "91")); | |
102 | + m.insert("jest.config.js", IconEntry::new("\u{f085}", "91")); | |
103 | + m.insert("jest.config.ts", IconEntry::new("\u{f085}", "91")); | |
104 | + m.insert("vitest.config.ts", IconEntry::new("\u{f085}", "92")); | |
105 | + m.insert("cargo.toml", IconEntry::new("\u{e7a8}", "91")); | |
106 | + m.insert("cargo.lock", IconEntry::new("\u{f023}", "90")); | |
107 | + | |
108 | + // ── Package Managers & Dependencies ──────────────────────────────── | |
109 | + m.insert("package.json", IconEntry::new("\u{e71e}", "91")); | |
110 | + m.insert("package-lock.json", IconEntry::new("\u{f023}", "90")); | |
111 | + m.insert("yarn.lock", IconEntry::new("\u{f023}", "90")); | |
112 | + m.insert("pnpm-lock.yaml", IconEntry::new("\u{f023}", "90")); | |
113 | + m.insert("composer.json", IconEntry::new("\u{e608}", "90")); | |
114 | + m.insert("composer.lock", IconEntry::new("\u{f023}", "90")); | |
115 | + m.insert("gemfile", IconEntry::new("\u{e21e}", "91")); | |
116 | + m.insert("gemfile.lock", IconEntry::new("\u{f023}", "90")); | |
117 | + m.insert("requirements.txt", IconEntry::new("\u{e606}", "92")); | |
118 | + m.insert("pipfile", IconEntry::new("\u{e606}", "92")); | |
119 | + m.insert("pipfile.lock", IconEntry::new("\u{f023}", "90")); | |
120 | + m.insert("pyproject.toml", IconEntry::new("\u{e606}", "92")); | |
121 | + m.insert("setup.py", IconEntry::new("\u{e606}", "92")); | |
122 | + m.insert("setup.cfg", IconEntry::new("\u{e606}", "92")); | |
123 | + m.insert("go.mod", IconEntry::new("\u{e626}", "96")); | |
124 | + m.insert("go.sum", IconEntry::new("\u{f023}", "90")); | |
125 | + m.insert("pubspec.yaml", IconEntry::new("\u{e798}", "94")); | |
126 | + m.insert("pubspec.lock", IconEntry::new("\u{f023}", "90")); | |
127 | + m.insert("build.gradle", IconEntry::new("\u{e660}", "94")); | |
128 | + m.insert("build.gradle.kts", IconEntry::new("\u{e660}", "94")); | |
129 | + m.insert("settings.gradle", IconEntry::new("\u{e660}", "94")); | |
130 | + m.insert("pom.xml", IconEntry::new("\u{e674}", "91")); | |
131 | + m.insert("mix.exs", IconEntry::new("\u{e62d}", "95")); | |
132 | + m.insert("mix.lock", IconEntry::new("\u{f023}", "90")); | |
133 | + m.insert("flake.nix", IconEntry::new("\u{f313}", "94")); | |
134 | + m.insert("flake.lock", IconEntry::new("\u{f023}", "90")); | |
135 | + m.insert("default.nix", IconEntry::new("\u{f313}", "94")); | |
136 | + m.insert("shell.nix", IconEntry::new("\u{f313}", "94")); | |
137 | + | |
138 | + // ── CI/CD ────────────────────────────────────────────────────────── | |
139 | + m.insert(".travis.yml", IconEntry::new("\u{e77e}", "91")); | |
140 | + m.insert("jenkinsfile", IconEntry::new("\u{e767}", "91")); | |
141 | + m.insert(".circleci", IconEntry::new("\u{f085}", "92")); | |
142 | + | |
143 | + // ── Programming Languages (by extension) ─────────────────────────── | |
144 | + // Python | |
145 | + m.insert(".py", IconEntry::new("\u{e606}", "92")); | |
146 | + m.insert(".pyc", IconEntry::new("\u{e606}", "90")); | |
147 | + m.insert(".pyo", IconEntry::new("\u{e606}", "90")); | |
148 | + m.insert(".pyd", IconEntry::new("\u{e606}", "90")); | |
149 | + m.insert(".pyw", IconEntry::new("\u{e606}", "92")); | |
150 | + m.insert(".pyx", IconEntry::new("\u{e606}", "92")); | |
151 | + m.insert(".pxd", IconEntry::new("\u{e606}", "92")); | |
152 | + m.insert(".ipynb", IconEntry::new("\u{e606}", "93")); | |
153 | + | |
154 | + // JavaScript / TypeScript | |
155 | + m.insert(".js", IconEntry::new("\u{e781}", "93")); | |
156 | + m.insert(".jsx", IconEntry::new("\u{e7ba}", "96")); | |
157 | + m.insert(".ts", IconEntry::new("\u{e628}", "94")); | |
158 | + m.insert(".tsx", IconEntry::new("\u{e7ba}", "96")); | |
159 | + m.insert(".mjs", IconEntry::new("\u{e781}", "93")); | |
160 | + m.insert(".cjs", IconEntry::new("\u{e781}", "93")); | |
161 | + | |
162 | + // Java / JVM | |
163 | + m.insert(".java", IconEntry::new("\u{e738}", "93")); | |
164 | + m.insert(".class", IconEntry::new("\u{e738}", "90")); | |
165 | + m.insert(".jar", IconEntry::new("\u{e738}", "93")); | |
166 | + m.insert(".war", IconEntry::new("\u{e738}", "93")); | |
167 | + m.insert(".gradle", IconEntry::new("\u{e660}", "94")); | |
168 | + | |
169 | + // C / C++ | |
170 | + m.insert(".c", IconEntry::new("\u{e61e}", "96")); | |
171 | + m.insert(".cpp", IconEntry::new("\u{e61d}", "96")); | |
172 | + m.insert(".cc", IconEntry::new("\u{e61d}", "96")); | |
173 | + m.insert(".cxx", IconEntry::new("\u{e61d}", "96")); | |
174 | + m.insert(".c++", IconEntry::new("\u{e61d}", "96")); | |
175 | + m.insert(".h", IconEntry::new("\u{e61e}", "96")); | |
176 | + m.insert(".hpp", IconEntry::new("\u{e61d}", "96")); | |
177 | + m.insert(".hh", IconEntry::new("\u{e61d}", "96")); | |
178 | + m.insert(".hxx", IconEntry::new("\u{e61d}", "96")); | |
179 | + m.insert(".h++", IconEntry::new("\u{e61d}", "96")); | |
180 | + | |
181 | + // Rust | |
182 | + m.insert(".rs", IconEntry::new("\u{e7a8}", "91")); | |
183 | + m.insert(".rlib", IconEntry::new("\u{e7a8}", "90")); | |
184 | + | |
185 | + // Go | |
186 | + m.insert(".go", IconEntry::new("\u{e626}", "96")); | |
187 | + | |
188 | + // PHP | |
189 | + m.insert(".php", IconEntry::new("\u{e608}", "95")); | |
190 | + m.insert(".phtml", IconEntry::new("\u{e608}", "95")); | |
191 | + | |
192 | + // Ruby | |
193 | + m.insert(".rb", IconEntry::new("\u{e21e}", "91")); | |
194 | + m.insert(".erb", IconEntry::new("\u{e21e}", "91")); | |
195 | + m.insert(".rake", IconEntry::new("\u{e21e}", "91")); | |
196 | + m.insert(".gemspec", IconEntry::new("\u{e21e}", "91")); | |
197 | + | |
198 | + // Swift | |
199 | + m.insert(".swift", IconEntry::new("\u{e755}", "93")); | |
200 | + | |
201 | + // Kotlin | |
202 | + m.insert(".kt", IconEntry::new("\u{e634}", "93")); | |
203 | + m.insert(".kts", IconEntry::new("\u{e634}", "93")); | |
204 | + | |
205 | + // Lua | |
206 | + m.insert(".lua", IconEntry::new("\u{e620}", "94")); | |
207 | + | |
208 | + // Dart | |
209 | + m.insert(".dart", IconEntry::new("\u{e798}", "94")); | |
210 | + | |
211 | + // Haskell | |
212 | + m.insert(".hs", IconEntry::new("\u{e61f}", "95")); | |
213 | + m.insert(".lhs", IconEntry::new("\u{e61f}", "95")); | |
214 | + m.insert(".cabal", IconEntry::new("\u{e61f}", "95")); | |
215 | + | |
216 | + // R | |
217 | + m.insert(".r", IconEntry::new("\u{f25d}", "94")); | |
218 | + m.insert(".rmd", IconEntry::new("\u{f25d}", "94")); | |
219 | + | |
220 | + // Julia | |
221 | + m.insert(".jl", IconEntry::new("\u{e624}", "95")); | |
222 | + | |
223 | + // Perl | |
224 | + m.insert(".pl", IconEntry::new("\u{e769}", "94")); | |
225 | + m.insert(".pm", IconEntry::new("\u{e769}", "94")); | |
226 | + | |
227 | + // Shell | |
228 | + m.insert(".sh", IconEntry::new("\u{f489}", "32")); | |
229 | + m.insert(".bash", IconEntry::new("\u{f489}", "32")); | |
230 | + m.insert(".zsh", IconEntry::new("\u{f489}", "32")); | |
231 | + m.insert(".fish", IconEntry::new("\u{f489}", "32")); | |
232 | + m.insert(".ksh", IconEntry::new("\u{f489}", "32")); | |
233 | + m.insert(".csh", IconEntry::new("\u{f489}", "32")); | |
234 | + m.insert(".tcsh", IconEntry::new("\u{f489}", "32")); | |
235 | + m.insert(".ps1", IconEntry::new("\u{f489}", "94")); | |
236 | + m.insert(".psm1", IconEntry::new("\u{f489}", "94")); | |
237 | + m.insert(".psd1", IconEntry::new("\u{f489}", "94")); | |
238 | + m.insert(".bat", IconEntry::new("\u{f489}", "92")); | |
239 | + m.insert(".cmd", IconEntry::new("\u{f489}", "92")); | |
240 | + | |
241 | + // C# / .NET | |
242 | + m.insert(".cs", IconEntry::new("\u{f81a}", "94")); | |
243 | + m.insert(".csx", IconEntry::new("\u{f81a}", "94")); | |
244 | + m.insert(".csproj", IconEntry::new("\u{f81a}", "94")); | |
245 | + m.insert(".sln", IconEntry::new("\u{f81a}", "95")); | |
246 | + | |
247 | + // F# | |
248 | + m.insert(".fs", IconEntry::new("\u{e7a7}", "94")); | |
249 | + m.insert(".fsx", IconEntry::new("\u{e7a7}", "94")); | |
250 | + m.insert(".fsi", IconEntry::new("\u{e7a7}", "94")); | |
251 | + m.insert(".fsproj", IconEntry::new("\u{e7a7}", "94")); | |
252 | + | |
253 | + // VB | |
254 | + m.insert(".vb", IconEntry::new("\u{f81a}", "94")); | |
255 | + m.insert(".vbs", IconEntry::new("\u{f81a}", "94")); | |
256 | + | |
257 | + // Scala | |
258 | + m.insert(".scala", IconEntry::new("\u{e737}", "91")); | |
259 | + m.insert(".sc", IconEntry::new("\u{e737}", "91")); | |
260 | + m.insert(".sbt", IconEntry::new("\u{e737}", "91")); | |
261 | + | |
262 | + // Clojure | |
263 | + m.insert(".clj", IconEntry::new("\u{e768}", "92")); | |
264 | + m.insert(".cljs", IconEntry::new("\u{e768}", "92")); | |
265 | + m.insert(".cljc", IconEntry::new("\u{e768}", "92")); | |
266 | + m.insert(".edn", IconEntry::new("\u{e768}", "92")); | |
267 | + | |
268 | + // Erlang | |
269 | + m.insert(".erl", IconEntry::new("\u{e7b1}", "95")); | |
270 | + m.insert(".hrl", IconEntry::new("\u{e7b1}", "95")); | |
271 | + | |
272 | + // Elixir | |
273 | + m.insert(".ex", IconEntry::new("\u{e62d}", "95")); | |
274 | + m.insert(".exs", IconEntry::new("\u{e62d}", "95")); | |
275 | + m.insert(".eex", IconEntry::new("\u{e62d}", "95")); | |
276 | + m.insert(".heex", IconEntry::new("\u{e62d}", "95")); | |
277 | + m.insert(".leex", IconEntry::new("\u{e62d}", "95")); | |
278 | + | |
279 | + // Vim | |
280 | + m.insert(".vim", IconEntry::new("\u{e62b}", "92")); | |
281 | + m.insert(".vimrc", IconEntry::new("\u{e62b}", "92")); | |
282 | + | |
283 | + // Zig | |
284 | + m.insert(".zig", IconEntry::new("\u{f0e7}", "93")); | |
285 | + | |
286 | + // Nim | |
287 | + m.insert(".nim", IconEntry::new("\u{f0e7}", "93")); | |
288 | + m.insert(".nimble", IconEntry::new("\u{f0e7}", "93")); | |
289 | + | |
290 | + // Crystal | |
291 | + m.insert(".cr", IconEntry::new("\u{e62f}", "37")); | |
292 | + | |
293 | + // OCaml | |
294 | + m.insert(".ml", IconEntry::new("\u{03bb}", "93")); // λ | |
295 | + m.insert(".mli", IconEntry::new("\u{03bb}", "93")); | |
296 | + | |
297 | + // Verilog / VHDL | |
298 | + m.insert(".v", IconEntry::new("\u{f085}", "94")); | |
299 | + m.insert(".sv", IconEntry::new("\u{f085}", "94")); | |
300 | + m.insert(".vhd", IconEntry::new("\u{f085}", "94")); | |
301 | + m.insert(".vhdl", IconEntry::new("\u{f085}", "94")); | |
302 | + | |
303 | + // Assembly | |
304 | + m.insert(".asm", IconEntry::new("\u{f471}", "91")); | |
305 | + m.insert(".s", IconEntry::new("\u{f471}", "91")); | |
306 | + m.insert(".S", IconEntry::new("\u{f471}", "91")); | |
307 | + | |
308 | + // Nix | |
309 | + m.insert(".nix", IconEntry::new("\u{f313}", "94")); | |
310 | + | |
311 | + // Terraform | |
312 | + m.insert(".tf", IconEntry::new("\u{f085}", "95")); | |
313 | + m.insert(".tfvars", IconEntry::new("\u{f085}", "95")); | |
314 | + | |
315 | + // ── Web & Frontend ───────────────────────────────────────────────── | |
316 | + m.insert(".html", IconEntry::new("\u{e736}", "95")); | |
317 | + m.insert(".htm", IconEntry::new("\u{e736}", "95")); | |
318 | + m.insert(".xhtml", IconEntry::new("\u{e736}", "95")); | |
319 | + m.insert(".css", IconEntry::new("\u{e749}", "36")); | |
320 | + m.insert(".scss", IconEntry::new("\u{e749}", "36")); | |
321 | + m.insert(".sass", IconEntry::new("\u{e749}", "36")); | |
322 | + m.insert(".less", IconEntry::new("\u{e749}", "36")); | |
323 | + m.insert(".styl", IconEntry::new("\u{e749}", "36")); | |
324 | + m.insert(".vue", IconEntry::new("\u{e6a0}", "92")); | |
325 | + m.insert(".svelte", IconEntry::new("\u{e697}", "91")); | |
326 | + m.insert(".astro", IconEntry::new("\u{e697}", "91")); | |
327 | + m.insert(".wasm", IconEntry::new("\u{e6a1}", "95")); | |
328 | + | |
329 | + // ── Data & Config ────────────────────────────────────────────────── | |
330 | + m.insert(".json", IconEntry::new("\u{e60b}", "94")); | |
331 | + m.insert(".jsonc", IconEntry::new("\u{e60b}", "94")); | |
332 | + m.insert(".json5", IconEntry::new("\u{e60b}", "94")); | |
333 | + m.insert(".jsonl", IconEntry::new("\u{e60b}", "94")); | |
334 | + m.insert(".ndjson", IconEntry::new("\u{e60b}", "94")); | |
335 | + m.insert(".xml", IconEntry::new("\u{e619}", "91")); | |
336 | + m.insert(".xsl", IconEntry::new("\u{e619}", "91")); | |
337 | + m.insert(".xsd", IconEntry::new("\u{e619}", "91")); | |
338 | + m.insert(".yml", IconEntry::new("\u{e60b}", "94")); | |
339 | + m.insert(".yaml", IconEntry::new("\u{e60b}", "94")); | |
340 | + m.insert(".toml", IconEntry::new("\u{e60b}", "94")); | |
341 | + m.insert(".ini", IconEntry::new("\u{f085}", "90")); | |
342 | + m.insert(".env", IconEntry::new("\u{f462}", "90")); | |
343 | + m.insert(".conf", IconEntry::new("\u{f085}", "90")); | |
344 | + m.insert(".cfg", IconEntry::new("\u{f085}", "90")); | |
345 | + m.insert(".properties", IconEntry::new("\u{f085}", "90")); | |
346 | + m.insert(".sql", IconEntry::new("\u{f1c0}", "93")); | |
347 | + m.insert(".db", IconEntry::new("\u{f1c0}", "93")); | |
348 | + m.insert(".sqlite", IconEntry::new("\u{f1c0}", "93")); | |
349 | + m.insert(".sqlite3", IconEntry::new("\u{f1c0}", "93")); | |
350 | + m.insert(".graphql", IconEntry::new("\u{e662}", "95")); | |
351 | + m.insert(".gql", IconEntry::new("\u{e662}", "95")); | |
352 | + m.insert(".proto", IconEntry::new("\u{f085}", "94")); | |
353 | + m.insert(".csv", IconEntry::new("\u{f1c3}", "92")); | |
354 | + m.insert(".tsv", IconEntry::new("\u{f1c3}", "92")); | |
355 | + m.insert(".hcl", IconEntry::new("\u{f085}", "95")); | |
356 | + | |
357 | + // ── Documents ────────────────────────────────────────────────────── | |
358 | + m.insert(".md", IconEntry::new("\u{e73e}", "96")); | |
359 | + m.insert(".markdown", IconEntry::new("\u{e73e}", "96")); | |
360 | + m.insert(".mdx", IconEntry::new("\u{e73e}", "96")); | |
361 | + m.insert(".txt", IconEntry::new("\u{f15c}", "37")); | |
362 | + m.insert(".pdf", IconEntry::new("\u{f1c1}", "31")); | |
363 | + m.insert(".doc", IconEntry::new("\u{f1c2}", "94")); | |
364 | + m.insert(".docx", IconEntry::new("\u{f1c2}", "94")); | |
365 | + m.insert(".xls", IconEntry::new("\u{f1c3}", "92")); | |
366 | + m.insert(".xlsx", IconEntry::new("\u{f1c3}", "92")); | |
367 | + m.insert(".ppt", IconEntry::new("\u{f1c4}", "91")); | |
368 | + m.insert(".pptx", IconEntry::new("\u{f1c4}", "91")); | |
369 | + m.insert(".log", IconEntry::new("\u{f15c}", "90")); | |
370 | + m.insert(".tex", IconEntry::new("\u{f15c}", "92")); | |
371 | + m.insert(".bib", IconEntry::new("\u{f15c}", "93")); | |
372 | + m.insert(".rtf", IconEntry::new("\u{f1c2}", "94")); | |
373 | + m.insert(".odt", IconEntry::new("\u{f1c2}", "94")); | |
374 | + m.insert(".ods", IconEntry::new("\u{f1c3}", "92")); | |
375 | + m.insert(".odp", IconEntry::new("\u{f1c4}", "91")); | |
376 | + m.insert(".epub", IconEntry::new("\u{f02d}", "91")); | |
377 | + m.insert(".rst", IconEntry::new("\u{e73e}", "96")); | |
378 | + m.insert(".org", IconEntry::new("\u{e633}", "92")); | |
379 | + m.insert(".adoc", IconEntry::new("\u{e73e}", "96")); | |
380 | + m.insert(".man", IconEntry::new("\u{f15c}", "90")); | |
381 | + | |
382 | + // ── Media - Images ───────────────────────────────────────────────── | |
383 | + m.insert(".png", IconEntry::new("\u{f1c5}", "93")); | |
384 | + m.insert(".jpg", IconEntry::new("\u{f1c5}", "93")); | |
385 | + m.insert(".jpeg", IconEntry::new("\u{f1c5}", "93")); | |
386 | + m.insert(".svg", IconEntry::new("\u{f1c5}", "93")); | |
387 | + m.insert(".gif", IconEntry::new("\u{f1c5}", "93")); | |
388 | + m.insert(".webp", IconEntry::new("\u{f1c5}", "93")); | |
389 | + m.insert(".bmp", IconEntry::new("\u{f1c5}", "93")); | |
390 | + m.insert(".ico", IconEntry::new("\u{f1c5}", "93")); | |
391 | + m.insert(".tiff", IconEntry::new("\u{f1c5}", "93")); | |
392 | + m.insert(".tif", IconEntry::new("\u{f1c5}", "93")); | |
393 | + m.insert(".psd", IconEntry::new("\u{e7b8}", "94")); | |
394 | + m.insert(".ai", IconEntry::new("\u{e7b4}", "93")); | |
395 | + m.insert(".eps", IconEntry::new("\u{f1c5}", "93")); | |
396 | + m.insert(".raw", IconEntry::new("\u{f1c5}", "93")); | |
397 | + m.insert(".cr2", IconEntry::new("\u{f1c5}", "93")); | |
398 | + m.insert(".nef", IconEntry::new("\u{f1c5}", "93")); | |
399 | + m.insert(".heic", IconEntry::new("\u{f1c5}", "93")); | |
400 | + m.insert(".heif", IconEntry::new("\u{f1c5}", "93")); | |
401 | + m.insert(".avif", IconEntry::new("\u{f1c5}", "93")); | |
402 | + m.insert(".jxl", IconEntry::new("\u{f1c5}", "93")); | |
403 | + | |
404 | + // ── Media - Audio ────────────────────────────────────────────────── | |
405 | + m.insert(".mp3", IconEntry::new("\u{f1c7}", "95")); | |
406 | + m.insert(".wav", IconEntry::new("\u{f1c7}", "95")); | |
407 | + m.insert(".flac", IconEntry::new("\u{f1c7}", "95")); | |
408 | + m.insert(".aac", IconEntry::new("\u{f1c7}", "95")); | |
409 | + m.insert(".ogg", IconEntry::new("\u{f1c7}", "95")); | |
410 | + m.insert(".m4a", IconEntry::new("\u{f1c7}", "95")); | |
411 | + m.insert(".wma", IconEntry::new("\u{f1c7}", "95")); | |
412 | + m.insert(".opus", IconEntry::new("\u{f1c7}", "95")); | |
413 | + m.insert(".mid", IconEntry::new("\u{f1c7}", "95")); | |
414 | + m.insert(".midi", IconEntry::new("\u{f1c7}", "95")); | |
415 | + m.insert(".aiff", IconEntry::new("\u{f1c7}", "95")); | |
416 | + m.insert(".ape", IconEntry::new("\u{f1c7}", "95")); | |
417 | + | |
418 | + // ── Media - Video ────────────────────────────────────────────────── | |
419 | + m.insert(".mp4", IconEntry::new("\u{f1c8}", "95")); | |
420 | + m.insert(".mov", IconEntry::new("\u{f1c8}", "95")); | |
421 | + m.insert(".avi", IconEntry::new("\u{f1c8}", "95")); | |
422 | + m.insert(".mkv", IconEntry::new("\u{f1c8}", "95")); | |
423 | + m.insert(".webm", IconEntry::new("\u{f1c8}", "95")); | |
424 | + m.insert(".flv", IconEntry::new("\u{f1c8}", "95")); | |
425 | + m.insert(".wmv", IconEntry::new("\u{f1c8}", "95")); | |
426 | + m.insert(".m4v", IconEntry::new("\u{f1c8}", "95")); | |
427 | + m.insert(".mpg", IconEntry::new("\u{f1c8}", "95")); | |
428 | + m.insert(".mpeg", IconEntry::new("\u{f1c8}", "95")); | |
429 | + m.insert(".3gp", IconEntry::new("\u{f1c8}", "95")); | |
430 | + m.insert(".ts", IconEntry::new("\u{f1c8}", "95")); // NOTE: conflicts with TypeScript, extension wins last | |
431 | + | |
432 | + // ── Compressed & Archives ────────────────────────────────────────── | |
433 | + m.insert(".zip", IconEntry::new("\u{f1c6}", "95")); | |
434 | + m.insert(".tar", IconEntry::new("\u{f1c6}", "95")); | |
435 | + m.insert(".gz", IconEntry::new("\u{f1c6}", "95")); | |
436 | + m.insert(".bz2", IconEntry::new("\u{f1c6}", "95")); | |
437 | + m.insert(".xz", IconEntry::new("\u{f1c6}", "95")); | |
438 | + m.insert(".zst", IconEntry::new("\u{f1c6}", "95")); | |
439 | + m.insert(".lz", IconEntry::new("\u{f1c6}", "95")); | |
440 | + m.insert(".lz4", IconEntry::new("\u{f1c6}", "95")); | |
441 | + m.insert(".lzma", IconEntry::new("\u{f1c6}", "95")); | |
442 | + m.insert(".7z", IconEntry::new("\u{f1c6}", "95")); | |
443 | + m.insert(".rar", IconEntry::new("\u{f1c6}", "95")); | |
444 | + m.insert(".tgz", IconEntry::new("\u{f1c6}", "95")); | |
445 | + m.insert(".tbz2", IconEntry::new("\u{f1c6}", "95")); | |
446 | + m.insert(".txz", IconEntry::new("\u{f1c6}", "95")); | |
447 | + m.insert(".iso", IconEntry::new("\u{f1c6}", "95")); | |
448 | + m.insert(".dmg", IconEntry::new("\u{f1c6}", "95")); | |
449 | + m.insert(".img", IconEntry::new("\u{f1c6}", "95")); | |
450 | + m.insert(".cab", IconEntry::new("\u{f1c6}", "95")); | |
451 | + m.insert(".cpio", IconEntry::new("\u{f1c6}", "95")); | |
452 | + m.insert(".ar", IconEntry::new("\u{f1c6}", "95")); | |
453 | + m.insert(".deb", IconEntry::new("\u{f187}", "91")); | |
454 | + m.insert(".rpm", IconEntry::new("\u{f187}", "91")); | |
455 | + m.insert(".apk", IconEntry::new("\u{e70e}", "92")); | |
456 | + m.insert(".snap", IconEntry::new("\u{f187}", "92")); | |
457 | + m.insert(".flatpak", IconEntry::new("\u{f187}", "94")); | |
458 | + m.insert(".appimage", IconEntry::new("\u{f187}", "92")); | |
459 | + | |
460 | + // ── Executables & Binaries ───────────────────────────────────────── | |
461 | + m.insert(".exe", IconEntry::new("\u{f17a}", "92")); | |
462 | + m.insert(".app", IconEntry::new("\u{f179}", "92")); | |
463 | + m.insert(".dll", IconEntry::new("\u{f17a}", "90")); | |
464 | + m.insert(".so", IconEntry::new("\u{f17c}", "90")); | |
465 | + m.insert(".dylib", IconEntry::new("\u{f179}", "90")); | |
466 | + m.insert(".bin", IconEntry::new("\u{f489}", "90")); | |
467 | + m.insert(".msi", IconEntry::new("\u{f17a}", "92")); | |
468 | + m.insert(".out", IconEntry::new("\u{f489}", "90")); | |
469 | + m.insert(".elf", IconEntry::new("\u{f489}", "90")); | |
470 | + m.insert(".o", IconEntry::new("\u{f471}", "90")); | |
471 | + m.insert(".a", IconEntry::new("\u{f471}", "90")); | |
472 | + | |
473 | + // ── Fonts ────────────────────────────────────────────────────────── | |
474 | + m.insert(".ttf", IconEntry::new("\u{f031}", "37")); | |
475 | + m.insert(".otf", IconEntry::new("\u{f031}", "37")); | |
476 | + m.insert(".woff", IconEntry::new("\u{f031}", "37")); | |
477 | + m.insert(".woff2", IconEntry::new("\u{f031}", "37")); | |
478 | + m.insert(".eot", IconEntry::new("\u{f031}", "37")); | |
479 | + | |
480 | + // ── 3D & CAD ─────────────────────────────────────────────────────── | |
481 | + m.insert(".obj", IconEntry::new("\u{f1b2}", "95")); | |
482 | + m.insert(".fbx", IconEntry::new("\u{f1b2}", "95")); | |
483 | + m.insert(".stl", IconEntry::new("\u{f1b2}", "95")); | |
484 | + m.insert(".blend", IconEntry::new("\u{f1b2}", "93")); | |
485 | + m.insert(".3ds", IconEntry::new("\u{f1b2}", "95")); | |
486 | + m.insert(".dae", IconEntry::new("\u{f1b2}", "95")); | |
487 | + m.insert(".gltf", IconEntry::new("\u{f1b2}", "95")); | |
488 | + m.insert(".glb", IconEntry::new("\u{f1b2}", "95")); | |
489 | + m.insert(".usdz", IconEntry::new("\u{f1b2}", "95")); | |
490 | + | |
491 | + // ── Security / Crypto ────────────────────────────────────────────── | |
492 | + m.insert(".key", IconEntry::new("\u{f084}", "91")); | |
493 | + m.insert(".pem", IconEntry::new("\u{f084}", "91")); | |
494 | + m.insert(".crt", IconEntry::new("\u{f084}", "91")); | |
495 | + m.insert(".cert", IconEntry::new("\u{f084}", "91")); | |
496 | + m.insert(".cer", IconEntry::new("\u{f084}", "91")); | |
497 | + m.insert(".ca", IconEntry::new("\u{f084}", "91")); | |
498 | + m.insert(".pub", IconEntry::new("\u{f084}", "91")); | |
499 | + m.insert(".gpg", IconEntry::new("\u{f084}", "91")); | |
500 | + m.insert(".pgp", IconEntry::new("\u{f084}", "91")); | |
501 | + m.insert(".asc", IconEntry::new("\u{f084}", "91")); | |
502 | + m.insert(".sig", IconEntry::new("\u{f084}", "91")); | |
503 | + m.insert(".p12", IconEntry::new("\u{f084}", "91")); | |
504 | + m.insert(".pfx", IconEntry::new("\u{f084}", "91")); | |
505 | + m.insert(".csr", IconEntry::new("\u{f084}", "91")); | |
506 | + | |
507 | + // ── Other / Misc ─────────────────────────────────────────────────── | |
508 | + m.insert(".lock", IconEntry::new("\u{f023}", "90")); | |
509 | + m.insert(".bak", IconEntry::new("\u{f0e2}", "90")); | |
510 | + m.insert(".tmp", IconEntry::new("\u{f0e2}", "90")); | |
511 | + m.insert(".temp", IconEntry::new("\u{f0e2}", "90")); | |
512 | + m.insert(".swp", IconEntry::new("\u{f0e2}", "90")); | |
513 | + m.insert(".swo", IconEntry::new("\u{f0e2}", "90")); | |
514 | + m.insert(".cache", IconEntry::new("\u{f0e2}", "90")); | |
515 | + m.insert(".pid", IconEntry::new("\u{f085}", "90")); | |
516 | + m.insert(".sock", IconEntry::new("\u{f6a7}", "35")); | |
517 | + m.insert(".patch", IconEntry::new("\u{f440}", "92")); | |
518 | + m.insert(".diff", IconEntry::new("\u{f440}", "92")); | |
519 | + | |
520 | + // TypeScript should take priority over video .ts | |
521 | + // Re-insert TypeScript to override the video .ts entry | |
522 | + m.insert(".ts", IconEntry::new("\u{e628}", "94")); | |
523 | + | |
524 | + m | |
525 | +} | |
526 | + | |
527 | +/// Look up the icon for a file by name, extension, and file type. | |
528 | +/// `name` is the file name (just the filename, not the full path). | |
529 | +/// `is_dir` indicates whether the entry is a directory. | |
530 | +/// `is_hidden` indicates whether the entry starts with '.'. | |
531 | +/// `is_symlink` indicates whether the entry is a symbolic link. | |
532 | +/// `is_executable` indicates whether the entry has execute permission. | |
533 | +pub fn get_icon<'a>( | |
534 | + icon_map: &'a HashMap<&str, IconEntry>, | |
535 | + name: &str, | |
536 | + is_dir: bool, | |
537 | + is_hidden: bool, | |
538 | + is_symlink: bool, | |
539 | + is_executable: bool, | |
540 | + is_pipe: bool, | |
541 | + is_socket: bool, | |
542 | + is_block_device: bool, | |
543 | + is_char_device: bool, | |
544 | +) -> &'a IconEntry { | |
545 | + // Special file types first | |
546 | + if is_pipe { | |
547 | + return &PIPE_ICON; | |
548 | + } | |
549 | + if is_socket { | |
550 | + return &SOCKET_ICON; | |
551 | + } | |
552 | + if is_block_device { | |
553 | + return &BLOCK_DEV_ICON; | |
554 | + } | |
555 | + if is_char_device { | |
556 | + return &CHAR_DEV_ICON; | |
557 | + } | |
558 | + | |
559 | + // Directories | |
560 | + if is_dir { | |
561 | + // Check for special directory names | |
562 | + let lower = name.to_lowercase(); | |
563 | + if let Some(entry) = icon_map.get(lower.as_str()) { | |
564 | + return entry; | |
565 | + } | |
566 | + if is_hidden { | |
567 | + return &DIR_HIDDEN_ICON; | |
568 | + } | |
569 | + return &DIR_ICON; | |
570 | + } | |
571 | + | |
572 | + // Symlinks (show the symlink icon if it can't be resolved to a type) | |
573 | + if is_symlink { | |
574 | + // Still try to match by name/extension, fall back to symlink icon | |
575 | + let lower = name.to_lowercase(); | |
576 | + if let Some(entry) = icon_map.get(lower.as_str()) { | |
577 | + return entry; | |
578 | + } | |
579 | + // Try compound extensions (e.g., .tar.gz) | |
580 | + if let Some(idx) = lower.find('.') { | |
581 | + if let Some(idx2) = lower[idx + 1..].find('.') { | |
582 | + let compound = &lower[idx + 1 + idx2..]; | |
583 | + if let Some(entry) = icon_map.get(compound) { | |
584 | + return entry; | |
585 | + } | |
586 | + } | |
587 | + } | |
588 | + if let Some(dot_pos) = lower.rfind('.') { | |
589 | + let ext = &lower[dot_pos..]; | |
590 | + if let Some(entry) = icon_map.get(ext) { | |
591 | + return entry; | |
592 | + } | |
593 | + } | |
594 | + return &SYMLINK_ICON; | |
595 | + } | |
596 | + | |
597 | + // Regular files: check exact filename, then compound extension, then extension | |
598 | + let lower = name.to_lowercase(); | |
599 | + | |
600 | + // Exact filename match | |
601 | + if let Some(entry) = icon_map.get(lower.as_str()) { | |
602 | + return entry; | |
603 | + } | |
604 | + | |
605 | + // Compound extension (e.g., .tar.gz, .spec.ts) | |
606 | + if let Some(idx) = lower.find('.') { | |
607 | + if let Some(idx2) = lower[idx + 1..].find('.') { | |
608 | + let compound_ext = &lower[idx + 1 + idx2..]; | |
609 | + if let Some(entry) = icon_map.get(compound_ext) { | |
610 | + return entry; | |
611 | + } | |
612 | + } | |
613 | + } | |
614 | + | |
615 | + // Simple extension | |
616 | + if let Some(dot_pos) = lower.rfind('.') { | |
617 | + let ext = &lower[dot_pos..]; | |
618 | + if let Some(entry) = icon_map.get(ext) { | |
619 | + return entry; | |
620 | + } | |
621 | + } | |
622 | + | |
623 | + // Executable files | |
624 | + if is_executable { | |
625 | + return &EXEC_ICON; | |
626 | + } | |
627 | + | |
628 | + &DEFAULT_ICON | |
629 | +} | |
0 | 630 | |
▼src/main.rs
+75-0
| @@ -0,0 +1,75 @@ | ||
1 | +mod cli; | |
2 | +mod entry; | |
3 | +mod format; | |
4 | +mod grid; | |
5 | +mod icons; | |
6 | +mod output; | |
7 | + | |
8 | +use std::io::{self, BufWriter, Write}; | |
9 | +use std::path::Path; | |
10 | +use std::process; | |
11 | + | |
12 | +use clap::Parser; | |
13 | + | |
14 | +fn main() { | |
15 | + let args = cli::Args::parse(); | |
16 | + | |
17 | + // Handle custom help/version flags | |
18 | + if args.help { | |
19 | + use clap::CommandFactory; | |
20 | + let mut cmd = cli::Args::command(); | |
21 | + cmd.print_help().ok(); | |
22 | + println!(); | |
23 | + return; | |
24 | + } | |
25 | + if args.print_version { | |
26 | + println!("lsf {}", env!("CARGO_PKG_VERSION")); | |
27 | + return; | |
28 | + } | |
29 | + | |
30 | + // Handle tree mode separately | |
31 | + if args.tree { | |
32 | + let stdout = io::stdout(); | |
33 | + let mut out = BufWriter::new(stdout.lock()); | |
34 | + let icon_map = if args.no_icons { | |
35 | + std::collections::HashMap::new() | |
36 | + } else { | |
37 | + icons::build_icon_map() | |
38 | + }; | |
39 | + let use_color = args.use_color(); | |
40 | + let show_icons = !args.no_icons; | |
41 | + | |
42 | + for path_str in &args.paths { | |
43 | + let path = Path::new(path_str); | |
44 | + if !path.exists() { | |
45 | + eprintln!( | |
46 | + "lsf: cannot access '{}': No such file or directory", | |
47 | + path_str | |
48 | + ); | |
49 | + continue; | |
50 | + } | |
51 | + if let Err(e) = output::print_tree( | |
52 | + &mut out, path, &args, &icon_map, use_color, show_icons, "", true, 0, | |
53 | + ) { | |
54 | + eprintln!("lsf: {}: {}", path_str, e); | |
55 | + } | |
56 | + } | |
57 | + | |
58 | + if let Err(e) = out.flush() { | |
59 | + if e.kind() != io::ErrorKind::BrokenPipe { | |
60 | + eprintln!("lsf: write error: {}", e); | |
61 | + } | |
62 | + } | |
63 | + return; | |
64 | + } | |
65 | + | |
66 | + // Normal listing | |
67 | + if let Err(e) = output::run(&args) { | |
68 | + if e.kind() == io::ErrorKind::BrokenPipe { | |
69 | + // Silently exit on broken pipe (e.g., piping to `head`) | |
70 | + process::exit(0); | |
71 | + } | |
72 | + eprintln!("lsf: {}", e); | |
73 | + process::exit(1); | |
74 | + } | |
75 | +} | |
0 | 76 | |
▼src/output.rs
+577-0
| @@ -0,0 +1,577 @@ | ||
1 | +use std::collections::HashMap; | |
2 | +use std::io::{self, BufWriter, Write}; | |
3 | +use std::path::Path; | |
4 | + | |
5 | +use crate::cli::Args; | |
6 | +use crate::entry::{self, FileEntry}; | |
7 | +use crate::format; | |
8 | +use crate::grid; | |
9 | +use crate::icons::{self, IconEntry}; | |
10 | + | |
11 | +/// Main output driver. Takes parsed args and runs the listing. | |
12 | +pub fn run(args: &Args) -> io::Result<()> { | |
13 | + let stdout = io::stdout(); | |
14 | + let mut out = BufWriter::new(stdout.lock()); | |
15 | + | |
16 | + let icon_map = if args.no_icons { | |
17 | + HashMap::new() | |
18 | + } else { | |
19 | + icons::build_icon_map() | |
20 | + }; | |
21 | + | |
22 | + let use_color = args.use_color(); | |
23 | + let show_icons = !args.no_icons; | |
24 | + let term_width = grid::get_terminal_width(args.term_width); | |
25 | + let terminator = if args.zero { '\0' } else { '\n' }; | |
26 | + | |
27 | + let paths: Vec<&str> = args.paths.iter().map(|s| s.as_str()).collect(); | |
28 | + let multi = paths.len() > 1; | |
29 | + | |
30 | + // Separate files and directories | |
31 | + let mut file_entries: Vec<FileEntry> = Vec::new(); | |
32 | + let mut dir_paths: Vec<&str> = Vec::new(); | |
33 | + | |
34 | + for path_str in &paths { | |
35 | + let path = Path::new(path_str); | |
36 | + if !path.exists() && !path.is_symlink() { | |
37 | + eprintln!( | |
38 | + "lsf: cannot access '{}': No such file or directory", | |
39 | + path_str | |
40 | + ); | |
41 | + continue; | |
42 | + } | |
43 | + | |
44 | + if path.is_file() || path.is_symlink() && !path.is_dir() || args.directory { | |
45 | + let follow = args.dereference || args.dereference_command_line; | |
46 | + if let Ok(fe) = FileEntry::from_path(path, follow) { | |
47 | + file_entries.push(fe); | |
48 | + } | |
49 | + } else { | |
50 | + dir_paths.push(path_str); | |
51 | + } | |
52 | + } | |
53 | + | |
54 | + // Print file arguments first | |
55 | + if !file_entries.is_empty() { | |
56 | + entry::sort_entries(&mut file_entries, args); | |
57 | + print_entries( | |
58 | + &mut out, | |
59 | + &file_entries, | |
60 | + args, | |
61 | + &icon_map, | |
62 | + use_color, | |
63 | + show_icons, | |
64 | + term_width, | |
65 | + terminator, | |
66 | + )?; | |
67 | + if !dir_paths.is_empty() { | |
68 | + write!(out, "\n")?; | |
69 | + } | |
70 | + } | |
71 | + | |
72 | + // Print directories | |
73 | + let show_header = multi || !file_entries.is_empty() || args.recursive; | |
74 | + | |
75 | + for (idx, path_str) in dir_paths.iter().enumerate() { | |
76 | + let path = Path::new(path_str); | |
77 | + | |
78 | + if show_header { | |
79 | + if idx > 0 || !file_entries.is_empty() { | |
80 | + write!(out, "\n")?; | |
81 | + } | |
82 | + if use_color { | |
83 | + write!(out, "\x1b[1;34m{}:\x1b[0m\n", path_str)?; | |
84 | + } else { | |
85 | + write!(out, "{}:\n", path_str)?; | |
86 | + } | |
87 | + } | |
88 | + | |
89 | + match entry::read_directory(path, args) { | |
90 | + Ok(mut entries) => { | |
91 | + entry::sort_entries(&mut entries, args); | |
92 | + | |
93 | + if args.is_long() { | |
94 | + // Print total line | |
95 | + let total = format::total_blocks(&entries); | |
96 | + writeln!(out, "total {}", total)?; | |
97 | + } | |
98 | + | |
99 | + print_entries( | |
100 | + &mut out, &entries, args, &icon_map, use_color, show_icons, term_width, | |
101 | + terminator, | |
102 | + )?; | |
103 | + | |
104 | + // Recursive subdirectories | |
105 | + if args.recursive { | |
106 | + let mut subdirs: Vec<FileEntry> = entries | |
107 | + .iter() | |
108 | + .filter(|e| e.is_dir && e.name != "." && e.name != "..") | |
109 | + .cloned() | |
110 | + .collect(); | |
111 | + subdirs.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); | |
112 | + | |
113 | + if args.reverse { | |
114 | + subdirs.reverse(); | |
115 | + } | |
116 | + | |
117 | + for subdir in subdirs { | |
118 | + let subpath = subdir.path.to_string_lossy().to_string(); | |
119 | + | |
120 | + // Check depth | |
121 | + if let Some(max_depth) = args.max_depth { | |
122 | + let base = Path::new(path_str); | |
123 | + let sub = Path::new(&subpath); | |
124 | + let depth = sub | |
125 | + .strip_prefix(base) | |
126 | + .map(|p| p.components().count()) | |
127 | + .unwrap_or(0); | |
128 | + if depth > max_depth { | |
129 | + continue; | |
130 | + } | |
131 | + } | |
132 | + | |
133 | + print_recursive( | |
134 | + &mut out, &subpath, args, &icon_map, use_color, show_icons, term_width, | |
135 | + terminator, path_str, | |
136 | + )?; | |
137 | + } | |
138 | + } | |
139 | + } | |
140 | + Err(e) => { | |
141 | + eprintln!("lsf: cannot open directory '{}': {}", path_str, e); | |
142 | + } | |
143 | + } | |
144 | + } | |
145 | + | |
146 | + // If we only had a single directory without header | |
147 | + if !show_header && dir_paths.len() == 1 { | |
148 | + // Already printed above, but without header | |
149 | + } | |
150 | + | |
151 | + out.flush()?; | |
152 | + Ok(()) | |
153 | +} | |
154 | + | |
155 | +/// Print entries for a single directory (not recursive, just the entries). | |
156 | +fn print_entries( | |
157 | + out: &mut impl Write, | |
158 | + entries: &[FileEntry], | |
159 | + args: &Args, | |
160 | + icon_map: &HashMap<&str, IconEntry>, | |
161 | + use_color: bool, | |
162 | + show_icons: bool, | |
163 | + term_width: usize, | |
164 | + terminator: char, | |
165 | +) -> io::Result<()> { | |
166 | + if entries.is_empty() { | |
167 | + return Ok(()); | |
168 | + } | |
169 | + | |
170 | + if args.is_long() { | |
171 | + print_long(out, entries, args, icon_map, use_color, show_icons)?; | |
172 | + } else if args.one_per_line { | |
173 | + let items = format_items(entries, args, icon_map, use_color, show_icons); | |
174 | + let output = grid::format_single_column(&items, terminator); | |
175 | + write!(out, "{}", output)?; | |
176 | + } else if args.comma { | |
177 | + let items = format_items(entries, args, icon_map, use_color, show_icons); | |
178 | + let output = grid::format_comma(&items, term_width); | |
179 | + write!(out, "{}", output)?; | |
180 | + } else { | |
181 | + // Grid view | |
182 | + let items = format_items(entries, args, icon_map, use_color, show_icons); | |
183 | + if args.zero { | |
184 | + let output = grid::format_single_column(&items, '\0'); | |
185 | + write!(out, "{}", output)?; | |
186 | + } else { | |
187 | + let output = grid::format_grid(&items, term_width, args.tabsize, args.across); | |
188 | + write!(out, "{}", output)?; | |
189 | + } | |
190 | + } | |
191 | + | |
192 | + Ok(()) | |
193 | +} | |
194 | + | |
195 | +/// Format items for grid / one-per-line / comma views. | |
196 | +fn format_items( | |
197 | + entries: &[FileEntry], | |
198 | + args: &Args, | |
199 | + icon_map: &HashMap<&str, IconEntry>, | |
200 | + use_color: bool, | |
201 | + show_icons: bool, | |
202 | +) -> Vec<String> { | |
203 | + entries | |
204 | + .iter() | |
205 | + .map(|entry| { | |
206 | + let mut item = String::with_capacity(64); | |
207 | + | |
208 | + // Inode | |
209 | + if args.inode { | |
210 | + item.push_str(&format!("{} ", entry.inode)); | |
211 | + } | |
212 | + | |
213 | + // Blocks | |
214 | + if args.show_size { | |
215 | + let blocks = entry.blocks / 2; | |
216 | + item.push_str(&format!("{} ", blocks)); | |
217 | + } | |
218 | + | |
219 | + // Icon | |
220 | + if show_icons { | |
221 | + let icon = icons::get_icon( | |
222 | + icon_map, | |
223 | + &entry.name, | |
224 | + entry.is_dir, | |
225 | + entry.is_hidden, | |
226 | + entry.is_symlink, | |
227 | + entry.is_executable, | |
228 | + entry.is_pipe, | |
229 | + entry.is_socket, | |
230 | + entry.is_block_device, | |
231 | + entry.is_char_device, | |
232 | + ); | |
233 | + if use_color { | |
234 | + item.push_str(&icon.colored()); | |
235 | + } else { | |
236 | + item.push_str(&icon.plain()); | |
237 | + } | |
238 | + item.push(' '); | |
239 | + } | |
240 | + | |
241 | + // Filename (with color) | |
242 | + item.push_str(&format::colorize_filename(entry, use_color)); | |
243 | + | |
244 | + // Classify indicator | |
245 | + if args.classify { | |
246 | + item.push_str(format::classify_indicator(entry, true)); | |
247 | + } else if args.slash_dirs && entry.is_dir { | |
248 | + item.push('/'); | |
249 | + } | |
250 | + | |
251 | + // Symlink target in non-long modes | |
252 | + if entry.is_symlink && args.one_per_line { | |
253 | + if let Some(ref target) = entry.symlink_target { | |
254 | + item.push_str(" -> "); | |
255 | + if use_color { | |
256 | + item.push_str(&format!("\x1b[36m{}\x1b[0m", target)); | |
257 | + } else { | |
258 | + item.push_str(target); | |
259 | + } | |
260 | + } | |
261 | + } | |
262 | + | |
263 | + item | |
264 | + }) | |
265 | + .collect() | |
266 | +} | |
267 | + | |
268 | +/// Print entries in long format. | |
269 | +fn print_long( | |
270 | + out: &mut impl Write, | |
271 | + entries: &[FileEntry], | |
272 | + args: &Args, | |
273 | + icon_map: &HashMap<&str, IconEntry>, | |
274 | + use_color: bool, | |
275 | + show_icons: bool, | |
276 | +) -> io::Result<()> { | |
277 | + let show_owner = !args.long_no_owner; | |
278 | + let show_group = !args.long_no_group && !args.no_group; | |
279 | + let show_inode = args.inode; | |
280 | + let show_blocks = args.show_size; | |
281 | + let show_author = args.author; | |
282 | + let human_readable = args.human_readable; | |
283 | + let si = args.si; | |
284 | + let numeric_ids = args.numeric_uid_gid; | |
285 | + | |
286 | + let mut user_cache: HashMap<u32, String> = HashMap::new(); | |
287 | + let mut group_cache: HashMap<u32, String> = HashMap::new(); | |
288 | + | |
289 | + // Pre-calculate column widths for alignment | |
290 | + let nlink_width = entries | |
291 | + .iter() | |
292 | + .map(|e| e.nlink.to_string().len()) | |
293 | + .max() | |
294 | + .unwrap_or(1); | |
295 | + | |
296 | + let owner_width = if show_owner { | |
297 | + entries | |
298 | + .iter() | |
299 | + .map(|e| { | |
300 | + if numeric_ids { | |
301 | + e.uid.to_string().len() | |
302 | + } else { | |
303 | + entry::get_username(e.uid, &mut user_cache).len() | |
304 | + } | |
305 | + }) | |
306 | + .max() | |
307 | + .unwrap_or(1) | |
308 | + } else { | |
309 | + 0 | |
310 | + }; | |
311 | + | |
312 | + let group_width = if show_group { | |
313 | + entries | |
314 | + .iter() | |
315 | + .map(|e| { | |
316 | + if numeric_ids { | |
317 | + e.gid.to_string().len() | |
318 | + } else { | |
319 | + entry::get_groupname(e.gid, &mut group_cache).len() | |
320 | + } | |
321 | + }) | |
322 | + .max() | |
323 | + .unwrap_or(1) | |
324 | + } else { | |
325 | + 0 | |
326 | + }; | |
327 | + | |
328 | + let size_width = entries | |
329 | + .iter() | |
330 | + .map(|e| format::format_size(e.size, human_readable, si).len()) | |
331 | + .max() | |
332 | + .unwrap_or(1); | |
333 | + | |
334 | + let inode_width = if show_inode { | |
335 | + entries | |
336 | + .iter() | |
337 | + .map(|e| e.inode.to_string().len()) | |
338 | + .max() | |
339 | + .unwrap_or(1) | |
340 | + } else { | |
341 | + 0 | |
342 | + }; | |
343 | + | |
344 | + let blocks_width = if show_blocks { | |
345 | + entries | |
346 | + .iter() | |
347 | + .map(|e| (e.blocks / 2).to_string().len()) | |
348 | + .max() | |
349 | + .unwrap_or(1) | |
350 | + } else { | |
351 | + 0 | |
352 | + }; | |
353 | + | |
354 | + for entry in entries { | |
355 | + let icon_str = if show_icons { | |
356 | + let icon = icons::get_icon( | |
357 | + icon_map, | |
358 | + &entry.name, | |
359 | + entry.is_dir, | |
360 | + entry.is_hidden, | |
361 | + entry.is_symlink, | |
362 | + entry.is_executable, | |
363 | + entry.is_pipe, | |
364 | + entry.is_socket, | |
365 | + entry.is_block_device, | |
366 | + entry.is_char_device, | |
367 | + ); | |
368 | + if use_color { | |
369 | + icon.colored() | |
370 | + } else { | |
371 | + icon.plain() | |
372 | + } | |
373 | + } else { | |
374 | + String::new() | |
375 | + }; | |
376 | + | |
377 | + let mut line = format::format_long_entry( | |
378 | + entry, | |
379 | + &icon_str, | |
380 | + show_icons, | |
381 | + show_owner, | |
382 | + show_group, | |
383 | + show_inode, | |
384 | + show_blocks, | |
385 | + show_author, | |
386 | + human_readable, | |
387 | + si, | |
388 | + numeric_ids, | |
389 | + use_color, | |
390 | + &mut user_cache, | |
391 | + &mut group_cache, | |
392 | + nlink_width, | |
393 | + owner_width, | |
394 | + group_width, | |
395 | + size_width, | |
396 | + inode_width, | |
397 | + blocks_width, | |
398 | + ); | |
399 | + | |
400 | + // Classify indicator | |
401 | + if args.classify { | |
402 | + line.push_str(format::classify_indicator(entry, true)); | |
403 | + } else if args.slash_dirs && entry.is_dir { | |
404 | + line.push('/'); | |
405 | + } | |
406 | + | |
407 | + writeln!(out, "{}", line)?; | |
408 | + } | |
409 | + | |
410 | + Ok(()) | |
411 | +} | |
412 | + | |
413 | +/// Recursively print a subdirectory. | |
414 | +fn print_recursive( | |
415 | + out: &mut impl Write, | |
416 | + dir_path: &str, | |
417 | + args: &Args, | |
418 | + icon_map: &HashMap<&str, IconEntry>, | |
419 | + use_color: bool, | |
420 | + show_icons: bool, | |
421 | + term_width: usize, | |
422 | + terminator: char, | |
423 | + base_path: &str, | |
424 | +) -> io::Result<()> { | |
425 | + let path = Path::new(dir_path); | |
426 | + | |
427 | + write!(out, "\n")?; | |
428 | + if use_color { | |
429 | + write!(out, "\x1b[1;34m{}:\x1b[0m\n", dir_path)?; | |
430 | + } else { | |
431 | + write!(out, "{}:\n", dir_path)?; | |
432 | + } | |
433 | + | |
434 | + match entry::read_directory(path, args) { | |
435 | + Ok(mut entries) => { | |
436 | + entry::sort_entries(&mut entries, args); | |
437 | + | |
438 | + if args.is_long() { | |
439 | + let total = format::total_blocks(&entries); | |
440 | + writeln!(out, "total {}", total)?; | |
441 | + } | |
442 | + | |
443 | + print_entries( | |
444 | + out, &entries, args, icon_map, use_color, show_icons, term_width, terminator, | |
445 | + )?; | |
446 | + | |
447 | + // Recurse into subdirectories | |
448 | + let mut subdirs: Vec<FileEntry> = entries | |
449 | + .iter() | |
450 | + .filter(|e| e.is_dir && e.name != "." && e.name != "..") | |
451 | + .cloned() | |
452 | + .collect(); | |
453 | + subdirs.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); | |
454 | + | |
455 | + if args.reverse { | |
456 | + subdirs.reverse(); | |
457 | + } | |
458 | + | |
459 | + for subdir in subdirs { | |
460 | + let subpath = subdir.path.to_string_lossy().to_string(); | |
461 | + | |
462 | + // Check depth | |
463 | + if let Some(max_depth) = args.max_depth { | |
464 | + let base = Path::new(base_path); | |
465 | + let sub = Path::new(&subpath); | |
466 | + let depth = sub | |
467 | + .strip_prefix(base) | |
468 | + .map(|p| p.components().count()) | |
469 | + .unwrap_or(0); | |
470 | + if depth > max_depth { | |
471 | + continue; | |
472 | + } | |
473 | + } | |
474 | + | |
475 | + print_recursive( | |
476 | + out, &subpath, args, icon_map, use_color, show_icons, term_width, terminator, | |
477 | + base_path, | |
478 | + )?; | |
479 | + } | |
480 | + } | |
481 | + Err(e) => { | |
482 | + eprintln!("lsf: cannot open directory '{}': {}", dir_path, e); | |
483 | + } | |
484 | + } | |
485 | + | |
486 | + Ok(()) | |
487 | +} | |
488 | + | |
489 | +/// Print entries in tree view. | |
490 | +pub fn print_tree( | |
491 | + out: &mut impl Write, | |
492 | + path: &Path, | |
493 | + args: &Args, | |
494 | + icon_map: &HashMap<&str, IconEntry>, | |
495 | + use_color: bool, | |
496 | + show_icons: bool, | |
497 | + prefix: &str, | |
498 | + is_last: bool, | |
499 | + depth: usize, | |
500 | +) -> io::Result<()> { | |
501 | + let follow = args.dereference; | |
502 | + let entry = FileEntry::from_path(path, follow)?; | |
503 | + | |
504 | + // Print this entry | |
505 | + let connector = if depth == 0 { | |
506 | + "" | |
507 | + } else if is_last { | |
508 | + "└── " | |
509 | + } else { | |
510 | + "├── " | |
511 | + }; | |
512 | + | |
513 | + let icon_str = if show_icons { | |
514 | + let icon = icons::get_icon( | |
515 | + icon_map, | |
516 | + &entry.name, | |
517 | + entry.is_dir, | |
518 | + entry.is_hidden, | |
519 | + entry.is_symlink, | |
520 | + entry.is_executable, | |
521 | + entry.is_pipe, | |
522 | + entry.is_socket, | |
523 | + entry.is_block_device, | |
524 | + entry.is_char_device, | |
525 | + ); | |
526 | + if use_color { | |
527 | + format!("{} ", icon.colored()) | |
528 | + } else { | |
529 | + format!("{} ", icon.plain()) | |
530 | + } | |
531 | + } else { | |
532 | + String::new() | |
533 | + }; | |
534 | + | |
535 | + let colored_name = format::colorize_filename(&entry, use_color); | |
536 | + writeln!(out, "{}{}{}{}", prefix, connector, icon_str, colored_name)?; | |
537 | + | |
538 | + // Recurse into directories | |
539 | + if entry.is_dir { | |
540 | + if let Some(max_depth) = args.max_depth { | |
541 | + if depth >= max_depth { | |
542 | + return Ok(()); | |
543 | + } | |
544 | + } | |
545 | + | |
546 | + if let Ok(mut children) = entry::read_directory(path, args) { | |
547 | + entry::sort_entries(&mut children, args); | |
548 | + | |
549 | + let child_prefix = if depth == 0 { | |
550 | + String::new() | |
551 | + } else { | |
552 | + format!("{}{}", prefix, if is_last { " " } else { "│ " }) | |
553 | + }; | |
554 | + | |
555 | + let len = children.len(); | |
556 | + for (i, child) in children.iter().enumerate() { | |
557 | + if child.name == "." || child.name == ".." { | |
558 | + continue; | |
559 | + } | |
560 | + let child_is_last = i == len - 1; | |
561 | + print_tree( | |
562 | + out, | |
563 | + &child.path, | |
564 | + args, | |
565 | + icon_map, | |
566 | + use_color, | |
567 | + show_icons, | |
568 | + &child_prefix, | |
569 | + child_is_last, | |
570 | + depth + 1, | |
571 | + )?; | |
572 | + } | |
573 | + } | |
574 | + } | |
575 | + | |
576 | + Ok(()) | |
577 | +} | |
0 | 578 | |
▼tag_release.sh
+29-0
| @@ -0,0 +1,29 @@ | ||
1 | +#!/bin/bash | |
2 | + | |
3 | +# Check if a version argument is provided | |
4 | +if [ -z "$1" ]; then | |
5 | + echo "Usage: $0 <version>" | |
6 | + echo "Example: $0 1.0.0" | |
7 | + exit 1 | |
8 | +fi | |
9 | + | |
10 | +VERSION=$1 | |
11 | +TAG="v$VERSION" | |
12 | + | |
13 | +# Confirm with the user | |
14 | +read -p "This will create and push tag $TAG. Proceed? (y/n) " -n 1 -r | |
15 | +echo | |
16 | +if [[ ! $REPLY =~ ^[Yy]$ ]]; then | |
17 | + echo "Aborted." | |
18 | + exit 1 | |
19 | +fi | |
20 | + | |
21 | +# Create the tag | |
22 | +echo "Creating tag $TAG..." | |
23 | +git tag -a "$TAG" -m "Release $TAG" | |
24 | + | |
25 | +# Push the tag | |
26 | +echo "Pushing tag $TAG to origin..." | |
27 | +git push origin "$TAG" | |
28 | + | |
29 | +echo "Done! This should trigger the Release workflow on GitHub." | |
0 | 30 | |