Feat:Moved to Rust

swadhinbiswas committed 8 months ago
a330027
1 parent 353426d
commit a330027
15 files changed
+3773 -497
▼.gitignore
+17-0
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
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
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
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
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
32
16
33
clean:
17
34
rm -rf dist
35
+ cargo clean
18
36
▼README.md
+39-238
1
-# ✨ ls-f — A Beautiful `ls` with Icons
1
+# ls-f
2
2
3
-![Tests](https://github.com/swadhinbiswas/ls-f/workflows/Tests%20and%20Coverage/badge.svg)
4
-[![codecov](https://codecov.io/gh/swadhinbiswas/ls-f/branch/main/graph/badge.svg)](https://codecov.io/gh/swadhinbiswas/ls-f)
5
-[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
![Example output screenshot](docs/example.png)
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
-# 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
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
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
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
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
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
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
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
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