├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── cliff.toml
├── clippy.toml
├── config
├── config.toml.advanced
├── config.toml.default
└── config.toml.tiny
├── demo
├── file-explorer.gif
├── populate
├── preview-archive.gif
└── record
├── release
└── src
├── cli.rs
├── config.rs
├── handler.rs
└── main.rs
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | on: [push, pull_request]
2 |
3 | env:
4 | CARGO_TERM_COLOR: always
5 |
6 | jobs:
7 | build:
8 | strategy:
9 | matrix:
10 | os: [ubuntu-latest, macos-latest]
11 | runs-on: ${{ matrix.os }}
12 | steps:
13 | - uses: actions/checkout@v2
14 | - run: cargo build --verbose
15 |
16 | test:
17 | strategy:
18 | matrix:
19 | os: [ubuntu-latest, macos-latest]
20 | runs-on: ${{ matrix.os }}
21 | steps:
22 | - uses: actions/checkout@v2
23 | - run: cargo test --verbose
24 |
25 | clippy:
26 | strategy:
27 | matrix:
28 | os: [ubuntu-latest, macos-latest]
29 | runs-on: ${{ matrix.os }}
30 | steps:
31 | - uses: actions/checkout@v2
32 | - uses: actions-rs/toolchain@v1
33 | with:
34 | profile: minimal
35 | toolchain: stable
36 | override: true
37 | - run: rustup component add clippy
38 | - uses: actions-rs/cargo@v1
39 | with:
40 | command: clippy
41 | args: -- -D warnings
42 |
43 | fmt:
44 | runs-on: ubuntu-latest
45 | steps:
46 | - uses: actions/checkout@v2
47 | - uses: actions-rs/toolchain@v1
48 | with:
49 | profile: minimal
50 | toolchain: stable
51 | override: true
52 | - run: rustup component add rustfmt
53 | - uses: actions-rs/cargo@v1
54 | with:
55 | command: fmt
56 | args: --all -- --check
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /demo/sample.*
3 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # https://pre-commit.com
2 | repos:
3 | - repo: https://github.com/pre-commit/pre-commit-hooks
4 | rev: v5.0.0
5 | hooks:
6 | - id: check-added-large-files
7 | - id: check-case-conflict
8 | - id: check-executables-have-shebangs
9 | - id: check-json
10 | - id: check-merge-conflict
11 | - id: check-symlinks
12 | - id: check-toml
13 | - id: check-vcs-permalinks
14 | - id: check-xml
15 | - id: check-yaml
16 | - id: end-of-file-fixer
17 | - id: fix-byte-order-marker
18 | - id: mixed-line-ending
19 | args:
20 | - --fix=no
21 | - id: trailing-whitespace
22 | args:
23 | - --markdown-linebreak-ext=md
24 |
25 | - repo: https://github.com/doublify/pre-commit-rust
26 | rev: v1.0
27 | hooks:
28 | - id: cargo-check
29 | - id: clippy
30 | - id: fmt
31 |
32 | - repo: https://github.com/shellcheck-py/shellcheck-py
33 | rev: v0.10.0.1
34 | hooks:
35 | - id: shellcheck
36 |
37 | - repo: https://github.com/pre-commit/mirrors-prettier
38 | rev: v2.4.0
39 | hooks:
40 | - id: prettier
41 | args:
42 | - --print-width=120
43 | - --write
44 | stages: [pre-commit]
45 |
46 | - repo: https://github.com/compilerla/conventional-pre-commit
47 | rev: v3.6.0
48 | hooks:
49 | - id: conventional-pre-commit
50 | stages: [commit-msg]
51 | args: [chore, config, doc, refactor, test]
52 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## 1.4.2 - 2024-06-07
6 |
7 | ### Bug Fixes
8 |
9 | - Replace buggy/abandoned term size crate
10 | - Shlex deprecation warning
11 |
12 | ### Documentation
13 |
14 | - Rename AUR package
15 |
16 | ### Miscellaneous Tasks
17 |
18 | - Version 1.4.2
19 |
20 | ### Refactor
21 |
22 | - Remove dedicated splice code path
23 |
24 | ## 1.4.1 - 2024-01-19
25 |
26 | ### Bug Fixes
27 |
28 | - Mime iteration for piped data
29 | - Try alternate mode handlers when piped too
30 |
31 | ### Features
32 |
33 | - Update advanced config
34 |
35 | ### Miscellaneous Tasks
36 |
37 | - Version 1.4.1
38 |
39 | ## 1.4.0 - 2023-11-20
40 |
41 | ### Features
42 |
43 | - More general support for MIME prefix match
44 | - Update/improve advanced config
45 |
46 | ### Miscellaneous Tasks
47 |
48 | - Move from structopt to clap
49 | - Version 1.4.0
50 |
51 | ## 1.3.1 - 2023-10-10
52 |
53 | ### Bug Fixes
54 |
55 | - %m substitution sometimes skipped
56 |
57 | ### Miscellaneous Tasks
58 |
59 | - Lint
60 | - Version 1.3.1
61 |
62 | ## 1.3.0 - 2023-04-23
63 |
64 | ### Bug Fixes
65 |
66 | - Build on MacOS
67 |
68 | ### Documentation
69 |
70 | - Fix ranger scope.sh instructions
71 |
72 | ### Features
73 |
74 | - Support edit action
75 |
76 | ### Miscellaneous Tasks
77 |
78 | - Lint
79 | - Lint
80 | - Lint
81 | - Version 1.3.0
82 |
83 | ### Testing
84 |
85 | - Add macos-latest machine for ci test (#4)
86 |
87 | ## 1.2.2 - 2022-10-31
88 |
89 | ### Bug Fixes
90 |
91 | - Archive open handler in advanced config example
92 | - Disable default nix features
93 |
94 | ### Features
95 |
96 | - Update advanced config
97 | - Improve error handling in worker threads
98 | - Update advanced config
99 | - Build with full LTO + strip
100 | - Add RSOP_INPUT_IS_STDIN_COPY·env·var.
101 |
102 | ### Miscellaneous Tasks
103 |
104 | - Lint
105 | - Update dependencies
106 | - Rename release script
107 | - Version 1.2.2
108 |
109 | ## 1.2.1 - 2022-03-30
110 |
111 | ### Bug Fixes
112 |
113 | - Run check/tests in release script
114 |
115 | ### Features
116 |
117 | - Improve reporting of rsi errors
118 | - Update advanced config
119 | - Update advanced config
120 | - Add check for invalid config with no_pipe=false and multiple input patterns
121 |
122 | ### Miscellaneous Tasks
123 |
124 | - Lint
125 | - Version 1.2.1
126 |
127 | ## 1.2.0 - 2022-01-08
128 |
129 | ### Features
130 |
131 | - Support matching by double extensions
132 | - Ensure extension matching is case insensitive
133 | - Update advanced config
134 |
135 | ### Miscellaneous Tasks
136 |
137 | - Version 1.2.0
138 |
139 | ## 1.1.2 - 2021-12-29
140 |
141 | ### Documentation
142 |
143 | - Fix README typo
144 | - Add AUR package link in README
145 |
146 | ### Features
147 |
148 | - Improve error display for common errors
149 | - Improve error display for common errors, take 2
150 |
151 | ### Miscellaneous Tasks
152 |
153 | - Version 1.1.2
154 |
155 | ### Refactor
156 |
157 | - Remove better-panic
158 |
159 | ## 1.1.1 - 2021-12-05
160 |
161 | ### Bug Fixes
162 |
163 | - Mode detection with absolute path
164 |
165 | ### Miscellaneous Tasks
166 |
167 | - Version 1.1.1
168 |
169 | ## 1.1.0 - 2021-09-27
170 |
171 | ### Bug Fixes
172 |
173 | - Add config check for handlers with both 'no_pipe = true' and 'wait = false'
174 | - Incompatible flags in advanced config
175 |
176 | ### Configuration
177 |
178 | - Add application/x-cpio MIME in advanced config + reformat long lists
179 | - Fix some handlers in advanced config when piped
180 | - Add application/x-archive MIME in advanced config
181 | - Fix some more handlers in advanced config when piped
182 | - Add openscad preview in advanced config
183 | - Fix one more handler in advanced config when piped
184 | - Fix remaining handlers in advanced config when piped + remove redundant flags
185 |
186 | ### Documentation
187 |
188 | - Use git-cliff to generate changelog
189 |
190 | ### Features
191 |
192 | - Support file:// url prefix
193 | - Dynamically compute pipe peek size from system page size
194 | - Support %m substitution in command for MIME type
195 | - Add no_pipe option to use temp file if handler does not support reading from stdin
196 | - URL handlers for xdg-open compatibility
197 |
198 | ### Miscellaneous Tasks
199 |
200 | - Version 1.1.0
201 |
202 | ### Refactor
203 |
204 | - Remove duplicate/hardcoded strings in mode handling
205 | - Factorize pattern substitution code
206 |
207 | ### Testing
208 |
209 | - Add tests for default and advanced config
210 | - Test for smallest possible config
211 |
212 |
213 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "addr2line"
7 | version = "0.24.2"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
10 | dependencies = [
11 | "gimli",
12 | ]
13 |
14 | [[package]]
15 | name = "adler2"
16 | version = "2.0.0"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
19 |
20 | [[package]]
21 | name = "aho-corasick"
22 | version = "1.1.3"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
25 | dependencies = [
26 | "memchr",
27 | ]
28 |
29 | [[package]]
30 | name = "anstream"
31 | version = "0.6.18"
32 | source = "registry+https://github.com/rust-lang/crates.io-index"
33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
34 | dependencies = [
35 | "anstyle",
36 | "anstyle-parse",
37 | "anstyle-query",
38 | "anstyle-wincon",
39 | "colorchoice",
40 | "is_terminal_polyfill",
41 | "utf8parse",
42 | ]
43 |
44 | [[package]]
45 | name = "anstyle"
46 | version = "1.0.10"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
49 |
50 | [[package]]
51 | name = "anstyle-parse"
52 | version = "0.2.6"
53 | source = "registry+https://github.com/rust-lang/crates.io-index"
54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
55 | dependencies = [
56 | "utf8parse",
57 | ]
58 |
59 | [[package]]
60 | name = "anstyle-query"
61 | version = "1.1.2"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
64 | dependencies = [
65 | "windows-sys 0.59.0",
66 | ]
67 |
68 | [[package]]
69 | name = "anstyle-wincon"
70 | version = "3.0.6"
71 | source = "registry+https://github.com/rust-lang/crates.io-index"
72 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
73 | dependencies = [
74 | "anstyle",
75 | "windows-sys 0.59.0",
76 | ]
77 |
78 | [[package]]
79 | name = "anyhow"
80 | version = "1.0.93"
81 | source = "registry+https://github.com/rust-lang/crates.io-index"
82 | checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
83 | dependencies = [
84 | "backtrace",
85 | ]
86 |
87 | [[package]]
88 | name = "backtrace"
89 | version = "0.3.74"
90 | source = "registry+https://github.com/rust-lang/crates.io-index"
91 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
92 | dependencies = [
93 | "addr2line",
94 | "cfg-if",
95 | "libc",
96 | "miniz_oxide",
97 | "object",
98 | "rustc-demangle",
99 | "windows-targets 0.52.6",
100 | ]
101 |
102 | [[package]]
103 | name = "bitflags"
104 | version = "2.6.0"
105 | source = "registry+https://github.com/rust-lang/crates.io-index"
106 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
107 |
108 | [[package]]
109 | name = "cfg-if"
110 | version = "1.0.0"
111 | source = "registry+https://github.com/rust-lang/crates.io-index"
112 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
113 |
114 | [[package]]
115 | name = "clap"
116 | version = "4.5.20"
117 | source = "registry+https://github.com/rust-lang/crates.io-index"
118 | checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
119 | dependencies = [
120 | "clap_builder",
121 | "clap_derive",
122 | ]
123 |
124 | [[package]]
125 | name = "clap_builder"
126 | version = "4.5.20"
127 | source = "registry+https://github.com/rust-lang/crates.io-index"
128 | checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
129 | dependencies = [
130 | "anstream",
131 | "anstyle",
132 | "clap_lex",
133 | "strsim",
134 | ]
135 |
136 | [[package]]
137 | name = "clap_derive"
138 | version = "4.5.18"
139 | source = "registry+https://github.com/rust-lang/crates.io-index"
140 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
141 | dependencies = [
142 | "heck",
143 | "proc-macro2",
144 | "quote",
145 | "syn",
146 | ]
147 |
148 | [[package]]
149 | name = "clap_lex"
150 | version = "0.7.2"
151 | source = "registry+https://github.com/rust-lang/crates.io-index"
152 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
153 |
154 | [[package]]
155 | name = "colorchoice"
156 | version = "1.0.3"
157 | source = "registry+https://github.com/rust-lang/crates.io-index"
158 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
159 |
160 | [[package]]
161 | name = "colored"
162 | version = "2.1.0"
163 | source = "registry+https://github.com/rust-lang/crates.io-index"
164 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
165 | dependencies = [
166 | "lazy_static",
167 | "windows-sys 0.48.0",
168 | ]
169 |
170 | [[package]]
171 | name = "const_format"
172 | version = "0.2.33"
173 | source = "registry+https://github.com/rust-lang/crates.io-index"
174 | checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b"
175 | dependencies = [
176 | "const_format_proc_macros",
177 | ]
178 |
179 | [[package]]
180 | name = "const_format_proc_macros"
181 | version = "0.2.33"
182 | source = "registry+https://github.com/rust-lang/crates.io-index"
183 | checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1"
184 | dependencies = [
185 | "proc-macro2",
186 | "quote",
187 | "unicode-xid",
188 | ]
189 |
190 | [[package]]
191 | name = "crossbeam-utils"
192 | version = "0.8.20"
193 | source = "registry+https://github.com/rust-lang/crates.io-index"
194 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
195 |
196 | [[package]]
197 | name = "displaydoc"
198 | version = "0.2.5"
199 | source = "registry+https://github.com/rust-lang/crates.io-index"
200 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
201 | dependencies = [
202 | "proc-macro2",
203 | "quote",
204 | "syn",
205 | ]
206 |
207 | [[package]]
208 | name = "equivalent"
209 | version = "1.0.1"
210 | source = "registry+https://github.com/rust-lang/crates.io-index"
211 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
212 |
213 | [[package]]
214 | name = "errno"
215 | version = "0.3.9"
216 | source = "registry+https://github.com/rust-lang/crates.io-index"
217 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
218 | dependencies = [
219 | "libc",
220 | "windows-sys 0.52.0",
221 | ]
222 |
223 | [[package]]
224 | name = "fastrand"
225 | version = "2.2.0"
226 | source = "registry+https://github.com/rust-lang/crates.io-index"
227 | checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
228 |
229 | [[package]]
230 | name = "fixedbitset"
231 | version = "0.4.2"
232 | source = "registry+https://github.com/rust-lang/crates.io-index"
233 | checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
234 |
235 | [[package]]
236 | name = "fnv"
237 | version = "1.0.7"
238 | source = "registry+https://github.com/rust-lang/crates.io-index"
239 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
240 |
241 | [[package]]
242 | name = "form_urlencoded"
243 | version = "1.2.1"
244 | source = "registry+https://github.com/rust-lang/crates.io-index"
245 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
246 | dependencies = [
247 | "percent-encoding",
248 | ]
249 |
250 | [[package]]
251 | name = "gimli"
252 | version = "0.31.1"
253 | source = "registry+https://github.com/rust-lang/crates.io-index"
254 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
255 |
256 | [[package]]
257 | name = "hashbrown"
258 | version = "0.15.1"
259 | source = "registry+https://github.com/rust-lang/crates.io-index"
260 | checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
261 |
262 | [[package]]
263 | name = "heck"
264 | version = "0.5.0"
265 | source = "registry+https://github.com/rust-lang/crates.io-index"
266 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
267 |
268 | [[package]]
269 | name = "icu_collections"
270 | version = "1.5.0"
271 | source = "registry+https://github.com/rust-lang/crates.io-index"
272 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
273 | dependencies = [
274 | "displaydoc",
275 | "yoke",
276 | "zerofrom",
277 | "zerovec",
278 | ]
279 |
280 | [[package]]
281 | name = "icu_locid"
282 | version = "1.5.0"
283 | source = "registry+https://github.com/rust-lang/crates.io-index"
284 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
285 | dependencies = [
286 | "displaydoc",
287 | "litemap",
288 | "tinystr",
289 | "writeable",
290 | "zerovec",
291 | ]
292 |
293 | [[package]]
294 | name = "icu_locid_transform"
295 | version = "1.5.0"
296 | source = "registry+https://github.com/rust-lang/crates.io-index"
297 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
298 | dependencies = [
299 | "displaydoc",
300 | "icu_locid",
301 | "icu_locid_transform_data",
302 | "icu_provider",
303 | "tinystr",
304 | "zerovec",
305 | ]
306 |
307 | [[package]]
308 | name = "icu_locid_transform_data"
309 | version = "1.5.0"
310 | source = "registry+https://github.com/rust-lang/crates.io-index"
311 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
312 |
313 | [[package]]
314 | name = "icu_normalizer"
315 | version = "1.5.0"
316 | source = "registry+https://github.com/rust-lang/crates.io-index"
317 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
318 | dependencies = [
319 | "displaydoc",
320 | "icu_collections",
321 | "icu_normalizer_data",
322 | "icu_properties",
323 | "icu_provider",
324 | "smallvec",
325 | "utf16_iter",
326 | "utf8_iter",
327 | "write16",
328 | "zerovec",
329 | ]
330 |
331 | [[package]]
332 | name = "icu_normalizer_data"
333 | version = "1.5.0"
334 | source = "registry+https://github.com/rust-lang/crates.io-index"
335 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
336 |
337 | [[package]]
338 | name = "icu_properties"
339 | version = "1.5.1"
340 | source = "registry+https://github.com/rust-lang/crates.io-index"
341 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
342 | dependencies = [
343 | "displaydoc",
344 | "icu_collections",
345 | "icu_locid_transform",
346 | "icu_properties_data",
347 | "icu_provider",
348 | "tinystr",
349 | "zerovec",
350 | ]
351 |
352 | [[package]]
353 | name = "icu_properties_data"
354 | version = "1.5.0"
355 | source = "registry+https://github.com/rust-lang/crates.io-index"
356 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
357 |
358 | [[package]]
359 | name = "icu_provider"
360 | version = "1.5.0"
361 | source = "registry+https://github.com/rust-lang/crates.io-index"
362 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
363 | dependencies = [
364 | "displaydoc",
365 | "icu_locid",
366 | "icu_provider_macros",
367 | "stable_deref_trait",
368 | "tinystr",
369 | "writeable",
370 | "yoke",
371 | "zerofrom",
372 | "zerovec",
373 | ]
374 |
375 | [[package]]
376 | name = "icu_provider_macros"
377 | version = "1.5.0"
378 | source = "registry+https://github.com/rust-lang/crates.io-index"
379 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
380 | dependencies = [
381 | "proc-macro2",
382 | "quote",
383 | "syn",
384 | ]
385 |
386 | [[package]]
387 | name = "idna"
388 | version = "1.0.3"
389 | source = "registry+https://github.com/rust-lang/crates.io-index"
390 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
391 | dependencies = [
392 | "idna_adapter",
393 | "smallvec",
394 | "utf8_iter",
395 | ]
396 |
397 | [[package]]
398 | name = "idna_adapter"
399 | version = "1.2.0"
400 | source = "registry+https://github.com/rust-lang/crates.io-index"
401 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
402 | dependencies = [
403 | "icu_normalizer",
404 | "icu_properties",
405 | ]
406 |
407 | [[package]]
408 | name = "indexmap"
409 | version = "2.6.0"
410 | source = "registry+https://github.com/rust-lang/crates.io-index"
411 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
412 | dependencies = [
413 | "equivalent",
414 | "hashbrown",
415 | ]
416 |
417 | [[package]]
418 | name = "is_terminal_polyfill"
419 | version = "1.70.1"
420 | source = "registry+https://github.com/rust-lang/crates.io-index"
421 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
422 |
423 | [[package]]
424 | name = "lazy_static"
425 | version = "1.5.0"
426 | source = "registry+https://github.com/rust-lang/crates.io-index"
427 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
428 |
429 | [[package]]
430 | name = "libc"
431 | version = "0.2.162"
432 | source = "registry+https://github.com/rust-lang/crates.io-index"
433 | checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
434 |
435 | [[package]]
436 | name = "libredox"
437 | version = "0.1.3"
438 | source = "registry+https://github.com/rust-lang/crates.io-index"
439 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
440 | dependencies = [
441 | "bitflags",
442 | "libc",
443 | "redox_syscall",
444 | ]
445 |
446 | [[package]]
447 | name = "linux-raw-sys"
448 | version = "0.4.14"
449 | source = "registry+https://github.com/rust-lang/crates.io-index"
450 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
451 |
452 | [[package]]
453 | name = "litemap"
454 | version = "0.7.3"
455 | source = "registry+https://github.com/rust-lang/crates.io-index"
456 | checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
457 |
458 | [[package]]
459 | name = "log"
460 | version = "0.4.22"
461 | source = "registry+https://github.com/rust-lang/crates.io-index"
462 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
463 |
464 | [[package]]
465 | name = "memchr"
466 | version = "2.7.4"
467 | source = "registry+https://github.com/rust-lang/crates.io-index"
468 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
469 |
470 | [[package]]
471 | name = "minimal-lexical"
472 | version = "0.2.1"
473 | source = "registry+https://github.com/rust-lang/crates.io-index"
474 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
475 |
476 | [[package]]
477 | name = "miniz_oxide"
478 | version = "0.8.0"
479 | source = "registry+https://github.com/rust-lang/crates.io-index"
480 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
481 | dependencies = [
482 | "adler2",
483 | ]
484 |
485 | [[package]]
486 | name = "nom"
487 | version = "7.1.3"
488 | source = "registry+https://github.com/rust-lang/crates.io-index"
489 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
490 | dependencies = [
491 | "memchr",
492 | "minimal-lexical",
493 | ]
494 |
495 | [[package]]
496 | name = "numtoa"
497 | version = "0.2.4"
498 | source = "registry+https://github.com/rust-lang/crates.io-index"
499 | checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f"
500 |
501 | [[package]]
502 | name = "object"
503 | version = "0.36.5"
504 | source = "registry+https://github.com/rust-lang/crates.io-index"
505 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
506 | dependencies = [
507 | "memchr",
508 | ]
509 |
510 | [[package]]
511 | name = "once_cell"
512 | version = "1.20.2"
513 | source = "registry+https://github.com/rust-lang/crates.io-index"
514 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
515 |
516 | [[package]]
517 | name = "percent-encoding"
518 | version = "2.3.1"
519 | source = "registry+https://github.com/rust-lang/crates.io-index"
520 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
521 |
522 | [[package]]
523 | name = "petgraph"
524 | version = "0.6.5"
525 | source = "registry+https://github.com/rust-lang/crates.io-index"
526 | checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
527 | dependencies = [
528 | "fixedbitset",
529 | "indexmap",
530 | ]
531 |
532 | [[package]]
533 | name = "proc-macro2"
534 | version = "1.0.89"
535 | source = "registry+https://github.com/rust-lang/crates.io-index"
536 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
537 | dependencies = [
538 | "unicode-ident",
539 | ]
540 |
541 | [[package]]
542 | name = "quote"
543 | version = "1.0.37"
544 | source = "registry+https://github.com/rust-lang/crates.io-index"
545 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
546 | dependencies = [
547 | "proc-macro2",
548 | ]
549 |
550 | [[package]]
551 | name = "redox_syscall"
552 | version = "0.5.7"
553 | source = "registry+https://github.com/rust-lang/crates.io-index"
554 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
555 | dependencies = [
556 | "bitflags",
557 | ]
558 |
559 | [[package]]
560 | name = "redox_termios"
561 | version = "0.1.3"
562 | source = "registry+https://github.com/rust-lang/crates.io-index"
563 | checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb"
564 |
565 | [[package]]
566 | name = "regex"
567 | version = "1.11.1"
568 | source = "registry+https://github.com/rust-lang/crates.io-index"
569 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
570 | dependencies = [
571 | "aho-corasick",
572 | "memchr",
573 | "regex-automata",
574 | "regex-syntax",
575 | ]
576 |
577 | [[package]]
578 | name = "regex-automata"
579 | version = "0.4.8"
580 | source = "registry+https://github.com/rust-lang/crates.io-index"
581 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
582 | dependencies = [
583 | "aho-corasick",
584 | "memchr",
585 | "regex-syntax",
586 | ]
587 |
588 | [[package]]
589 | name = "regex-syntax"
590 | version = "0.8.5"
591 | source = "registry+https://github.com/rust-lang/crates.io-index"
592 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
593 |
594 | [[package]]
595 | name = "rsop"
596 | version = "1.4.2"
597 | dependencies = [
598 | "anyhow",
599 | "clap",
600 | "const_format",
601 | "crossbeam-utils",
602 | "log",
603 | "regex",
604 | "serde",
605 | "shlex",
606 | "simple_logger",
607 | "strum",
608 | "tempfile",
609 | "termion",
610 | "thiserror",
611 | "toml",
612 | "tree_magic_mini",
613 | "url",
614 | "xdg",
615 | ]
616 |
617 | [[package]]
618 | name = "rustc-demangle"
619 | version = "0.1.24"
620 | source = "registry+https://github.com/rust-lang/crates.io-index"
621 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
622 |
623 | [[package]]
624 | name = "rustix"
625 | version = "0.38.39"
626 | source = "registry+https://github.com/rust-lang/crates.io-index"
627 | checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee"
628 | dependencies = [
629 | "bitflags",
630 | "errno",
631 | "libc",
632 | "linux-raw-sys",
633 | "windows-sys 0.52.0",
634 | ]
635 |
636 | [[package]]
637 | name = "rustversion"
638 | version = "1.0.18"
639 | source = "registry+https://github.com/rust-lang/crates.io-index"
640 | checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
641 |
642 | [[package]]
643 | name = "serde"
644 | version = "1.0.214"
645 | source = "registry+https://github.com/rust-lang/crates.io-index"
646 | checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
647 | dependencies = [
648 | "serde_derive",
649 | ]
650 |
651 | [[package]]
652 | name = "serde_derive"
653 | version = "1.0.214"
654 | source = "registry+https://github.com/rust-lang/crates.io-index"
655 | checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
656 | dependencies = [
657 | "proc-macro2",
658 | "quote",
659 | "syn",
660 | ]
661 |
662 | [[package]]
663 | name = "serde_spanned"
664 | version = "0.6.8"
665 | source = "registry+https://github.com/rust-lang/crates.io-index"
666 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
667 | dependencies = [
668 | "serde",
669 | ]
670 |
671 | [[package]]
672 | name = "shlex"
673 | version = "1.3.0"
674 | source = "registry+https://github.com/rust-lang/crates.io-index"
675 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
676 |
677 | [[package]]
678 | name = "simple_logger"
679 | version = "5.0.0"
680 | source = "registry+https://github.com/rust-lang/crates.io-index"
681 | checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb"
682 | dependencies = [
683 | "colored",
684 | "log",
685 | "windows-sys 0.48.0",
686 | ]
687 |
688 | [[package]]
689 | name = "smallvec"
690 | version = "1.13.2"
691 | source = "registry+https://github.com/rust-lang/crates.io-index"
692 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
693 |
694 | [[package]]
695 | name = "stable_deref_trait"
696 | version = "1.2.0"
697 | source = "registry+https://github.com/rust-lang/crates.io-index"
698 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
699 |
700 | [[package]]
701 | name = "strsim"
702 | version = "0.11.1"
703 | source = "registry+https://github.com/rust-lang/crates.io-index"
704 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
705 |
706 | [[package]]
707 | name = "strum"
708 | version = "0.26.3"
709 | source = "registry+https://github.com/rust-lang/crates.io-index"
710 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
711 | dependencies = [
712 | "strum_macros",
713 | ]
714 |
715 | [[package]]
716 | name = "strum_macros"
717 | version = "0.26.4"
718 | source = "registry+https://github.com/rust-lang/crates.io-index"
719 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
720 | dependencies = [
721 | "heck",
722 | "proc-macro2",
723 | "quote",
724 | "rustversion",
725 | "syn",
726 | ]
727 |
728 | [[package]]
729 | name = "syn"
730 | version = "2.0.87"
731 | source = "registry+https://github.com/rust-lang/crates.io-index"
732 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
733 | dependencies = [
734 | "proc-macro2",
735 | "quote",
736 | "unicode-ident",
737 | ]
738 |
739 | [[package]]
740 | name = "synstructure"
741 | version = "0.13.1"
742 | source = "registry+https://github.com/rust-lang/crates.io-index"
743 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
744 | dependencies = [
745 | "proc-macro2",
746 | "quote",
747 | "syn",
748 | ]
749 |
750 | [[package]]
751 | name = "tempfile"
752 | version = "3.14.0"
753 | source = "registry+https://github.com/rust-lang/crates.io-index"
754 | checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
755 | dependencies = [
756 | "cfg-if",
757 | "fastrand",
758 | "once_cell",
759 | "rustix",
760 | "windows-sys 0.59.0",
761 | ]
762 |
763 | [[package]]
764 | name = "termion"
765 | version = "4.0.3"
766 | source = "registry+https://github.com/rust-lang/crates.io-index"
767 | checksum = "7eaa98560e51a2cf4f0bb884d8b2098a9ea11ecf3b7078e9c68242c74cc923a7"
768 | dependencies = [
769 | "libc",
770 | "libredox",
771 | "numtoa",
772 | "redox_termios",
773 | ]
774 |
775 | [[package]]
776 | name = "thiserror"
777 | version = "2.0.1"
778 | source = "registry+https://github.com/rust-lang/crates.io-index"
779 | checksum = "07c1e40dd48a282ae8edc36c732cbc219144b87fb6a4c7316d611c6b1f06ec0c"
780 | dependencies = [
781 | "thiserror-impl",
782 | ]
783 |
784 | [[package]]
785 | name = "thiserror-impl"
786 | version = "2.0.1"
787 | source = "registry+https://github.com/rust-lang/crates.io-index"
788 | checksum = "874aa7e446f1da8d9c3a5c95b1c5eb41d800045252121dc7f8e0ba370cee55f5"
789 | dependencies = [
790 | "proc-macro2",
791 | "quote",
792 | "syn",
793 | ]
794 |
795 | [[package]]
796 | name = "tinystr"
797 | version = "0.7.6"
798 | source = "registry+https://github.com/rust-lang/crates.io-index"
799 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
800 | dependencies = [
801 | "displaydoc",
802 | "zerovec",
803 | ]
804 |
805 | [[package]]
806 | name = "toml"
807 | version = "0.8.19"
808 | source = "registry+https://github.com/rust-lang/crates.io-index"
809 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
810 | dependencies = [
811 | "serde",
812 | "serde_spanned",
813 | "toml_datetime",
814 | "toml_edit",
815 | ]
816 |
817 | [[package]]
818 | name = "toml_datetime"
819 | version = "0.6.8"
820 | source = "registry+https://github.com/rust-lang/crates.io-index"
821 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
822 | dependencies = [
823 | "serde",
824 | ]
825 |
826 | [[package]]
827 | name = "toml_edit"
828 | version = "0.22.22"
829 | source = "registry+https://github.com/rust-lang/crates.io-index"
830 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
831 | dependencies = [
832 | "indexmap",
833 | "serde",
834 | "serde_spanned",
835 | "toml_datetime",
836 | "winnow",
837 | ]
838 |
839 | [[package]]
840 | name = "tree_magic_mini"
841 | version = "3.1.6"
842 | source = "registry+https://github.com/rust-lang/crates.io-index"
843 | checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63"
844 | dependencies = [
845 | "fnv",
846 | "memchr",
847 | "nom",
848 | "once_cell",
849 | "petgraph",
850 | ]
851 |
852 | [[package]]
853 | name = "unicode-ident"
854 | version = "1.0.13"
855 | source = "registry+https://github.com/rust-lang/crates.io-index"
856 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
857 |
858 | [[package]]
859 | name = "unicode-xid"
860 | version = "0.2.6"
861 | source = "registry+https://github.com/rust-lang/crates.io-index"
862 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
863 |
864 | [[package]]
865 | name = "url"
866 | version = "2.5.3"
867 | source = "registry+https://github.com/rust-lang/crates.io-index"
868 | checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
869 | dependencies = [
870 | "form_urlencoded",
871 | "idna",
872 | "percent-encoding",
873 | ]
874 |
875 | [[package]]
876 | name = "utf16_iter"
877 | version = "1.0.5"
878 | source = "registry+https://github.com/rust-lang/crates.io-index"
879 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
880 |
881 | [[package]]
882 | name = "utf8_iter"
883 | version = "1.0.4"
884 | source = "registry+https://github.com/rust-lang/crates.io-index"
885 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
886 |
887 | [[package]]
888 | name = "utf8parse"
889 | version = "0.2.2"
890 | source = "registry+https://github.com/rust-lang/crates.io-index"
891 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
892 |
893 | [[package]]
894 | name = "windows-sys"
895 | version = "0.48.0"
896 | source = "registry+https://github.com/rust-lang/crates.io-index"
897 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
898 | dependencies = [
899 | "windows-targets 0.48.5",
900 | ]
901 |
902 | [[package]]
903 | name = "windows-sys"
904 | version = "0.52.0"
905 | source = "registry+https://github.com/rust-lang/crates.io-index"
906 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
907 | dependencies = [
908 | "windows-targets 0.52.6",
909 | ]
910 |
911 | [[package]]
912 | name = "windows-sys"
913 | version = "0.59.0"
914 | source = "registry+https://github.com/rust-lang/crates.io-index"
915 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
916 | dependencies = [
917 | "windows-targets 0.52.6",
918 | ]
919 |
920 | [[package]]
921 | name = "windows-targets"
922 | version = "0.48.5"
923 | source = "registry+https://github.com/rust-lang/crates.io-index"
924 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
925 | dependencies = [
926 | "windows_aarch64_gnullvm 0.48.5",
927 | "windows_aarch64_msvc 0.48.5",
928 | "windows_i686_gnu 0.48.5",
929 | "windows_i686_msvc 0.48.5",
930 | "windows_x86_64_gnu 0.48.5",
931 | "windows_x86_64_gnullvm 0.48.5",
932 | "windows_x86_64_msvc 0.48.5",
933 | ]
934 |
935 | [[package]]
936 | name = "windows-targets"
937 | version = "0.52.6"
938 | source = "registry+https://github.com/rust-lang/crates.io-index"
939 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
940 | dependencies = [
941 | "windows_aarch64_gnullvm 0.52.6",
942 | "windows_aarch64_msvc 0.52.6",
943 | "windows_i686_gnu 0.52.6",
944 | "windows_i686_gnullvm",
945 | "windows_i686_msvc 0.52.6",
946 | "windows_x86_64_gnu 0.52.6",
947 | "windows_x86_64_gnullvm 0.52.6",
948 | "windows_x86_64_msvc 0.52.6",
949 | ]
950 |
951 | [[package]]
952 | name = "windows_aarch64_gnullvm"
953 | version = "0.48.5"
954 | source = "registry+https://github.com/rust-lang/crates.io-index"
955 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
956 |
957 | [[package]]
958 | name = "windows_aarch64_gnullvm"
959 | version = "0.52.6"
960 | source = "registry+https://github.com/rust-lang/crates.io-index"
961 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
962 |
963 | [[package]]
964 | name = "windows_aarch64_msvc"
965 | version = "0.48.5"
966 | source = "registry+https://github.com/rust-lang/crates.io-index"
967 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
968 |
969 | [[package]]
970 | name = "windows_aarch64_msvc"
971 | version = "0.52.6"
972 | source = "registry+https://github.com/rust-lang/crates.io-index"
973 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
974 |
975 | [[package]]
976 | name = "windows_i686_gnu"
977 | version = "0.48.5"
978 | source = "registry+https://github.com/rust-lang/crates.io-index"
979 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
980 |
981 | [[package]]
982 | name = "windows_i686_gnu"
983 | version = "0.52.6"
984 | source = "registry+https://github.com/rust-lang/crates.io-index"
985 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
986 |
987 | [[package]]
988 | name = "windows_i686_gnullvm"
989 | version = "0.52.6"
990 | source = "registry+https://github.com/rust-lang/crates.io-index"
991 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
992 |
993 | [[package]]
994 | name = "windows_i686_msvc"
995 | version = "0.48.5"
996 | source = "registry+https://github.com/rust-lang/crates.io-index"
997 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
998 |
999 | [[package]]
1000 | name = "windows_i686_msvc"
1001 | version = "0.52.6"
1002 | source = "registry+https://github.com/rust-lang/crates.io-index"
1003 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
1004 |
1005 | [[package]]
1006 | name = "windows_x86_64_gnu"
1007 | version = "0.48.5"
1008 | source = "registry+https://github.com/rust-lang/crates.io-index"
1009 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
1010 |
1011 | [[package]]
1012 | name = "windows_x86_64_gnu"
1013 | version = "0.52.6"
1014 | source = "registry+https://github.com/rust-lang/crates.io-index"
1015 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
1016 |
1017 | [[package]]
1018 | name = "windows_x86_64_gnullvm"
1019 | version = "0.48.5"
1020 | source = "registry+https://github.com/rust-lang/crates.io-index"
1021 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
1022 |
1023 | [[package]]
1024 | name = "windows_x86_64_gnullvm"
1025 | version = "0.52.6"
1026 | source = "registry+https://github.com/rust-lang/crates.io-index"
1027 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
1028 |
1029 | [[package]]
1030 | name = "windows_x86_64_msvc"
1031 | version = "0.48.5"
1032 | source = "registry+https://github.com/rust-lang/crates.io-index"
1033 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
1034 |
1035 | [[package]]
1036 | name = "windows_x86_64_msvc"
1037 | version = "0.52.6"
1038 | source = "registry+https://github.com/rust-lang/crates.io-index"
1039 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1040 |
1041 | [[package]]
1042 | name = "winnow"
1043 | version = "0.6.20"
1044 | source = "registry+https://github.com/rust-lang/crates.io-index"
1045 | checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
1046 | dependencies = [
1047 | "memchr",
1048 | ]
1049 |
1050 | [[package]]
1051 | name = "write16"
1052 | version = "1.0.0"
1053 | source = "registry+https://github.com/rust-lang/crates.io-index"
1054 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
1055 |
1056 | [[package]]
1057 | name = "writeable"
1058 | version = "0.5.5"
1059 | source = "registry+https://github.com/rust-lang/crates.io-index"
1060 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
1061 |
1062 | [[package]]
1063 | name = "xdg"
1064 | version = "2.5.2"
1065 | source = "registry+https://github.com/rust-lang/crates.io-index"
1066 | checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
1067 |
1068 | [[package]]
1069 | name = "yoke"
1070 | version = "0.7.4"
1071 | source = "registry+https://github.com/rust-lang/crates.io-index"
1072 | checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
1073 | dependencies = [
1074 | "serde",
1075 | "stable_deref_trait",
1076 | "yoke-derive",
1077 | "zerofrom",
1078 | ]
1079 |
1080 | [[package]]
1081 | name = "yoke-derive"
1082 | version = "0.7.4"
1083 | source = "registry+https://github.com/rust-lang/crates.io-index"
1084 | checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
1085 | dependencies = [
1086 | "proc-macro2",
1087 | "quote",
1088 | "syn",
1089 | "synstructure",
1090 | ]
1091 |
1092 | [[package]]
1093 | name = "zerofrom"
1094 | version = "0.1.4"
1095 | source = "registry+https://github.com/rust-lang/crates.io-index"
1096 | checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
1097 | dependencies = [
1098 | "zerofrom-derive",
1099 | ]
1100 |
1101 | [[package]]
1102 | name = "zerofrom-derive"
1103 | version = "0.1.4"
1104 | source = "registry+https://github.com/rust-lang/crates.io-index"
1105 | checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
1106 | dependencies = [
1107 | "proc-macro2",
1108 | "quote",
1109 | "syn",
1110 | "synstructure",
1111 | ]
1112 |
1113 | [[package]]
1114 | name = "zerovec"
1115 | version = "0.10.4"
1116 | source = "registry+https://github.com/rust-lang/crates.io-index"
1117 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
1118 | dependencies = [
1119 | "yoke",
1120 | "zerofrom",
1121 | "zerovec-derive",
1122 | ]
1123 |
1124 | [[package]]
1125 | name = "zerovec-derive"
1126 | version = "0.10.3"
1127 | source = "registry+https://github.com/rust-lang/crates.io-index"
1128 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
1129 | dependencies = [
1130 | "proc-macro2",
1131 | "quote",
1132 | "syn",
1133 | ]
1134 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "rsop"
3 | version = "1.4.2"
4 | edition = "2018"
5 |
6 | [profile.release]
7 | lto = true
8 | codegen-units = 1
9 | strip = true
10 |
11 | [dependencies]
12 | anyhow = { version = "1.0.93", default-features = false, features = ["backtrace", "std"] }
13 | clap = { version = "4.5.20", default-features = false, features = ["std", "color", "help", "usage", "error-context", "suggestions", "derive"] }
14 | const_format = { version = "0.2.33", default-features = false, features = ["const_generics"] }
15 | crossbeam-utils = { version = "0.8.20", default-features = false, features = ["std"] }
16 | log = { version = "0.4.22", default-features = false, features = ["max_level_trace", "release_max_level_info"] }
17 | regex = { version = "1.11.1", default-features = false, features = ["std"] }
18 | serde = { version = "1.0.214", default-features = false, features = ["derive", "std"] }
19 | shlex = { version = "1.3.0", default-features = false, features = ["std"] }
20 | simple_logger = { version = "5.0.0", default-features = false, features = ["colors", "stderr"] }
21 | strum = { version = "0.26.3", default-features = false, features = ["derive", "std"] }
22 | tempfile = { version = "3.14.0", default-features = false }
23 | termion = { version = "4.0.3", default-features = false }
24 | thiserror = { version = "2.0.1", default-features = false }
25 | toml = { version = "0.8.19", default-features = false, features = ["parse"] }
26 | tree_magic_mini = { version = "3.1.6", default-features = false }
27 | url = { version = "2.5.3", default-features = false }
28 | xdg = { version = "2.5.2", default-features = false }
29 |
30 | [lints.rust]
31 | # https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html
32 | explicit_outlives_requirements = "warn"
33 | missing_docs = "warn"
34 | non_ascii_idents = "deny"
35 | redundant-lifetimes = "warn"
36 | single-use-lifetimes = "warn"
37 | unit-bindings = "warn"
38 | unreachable_pub = "warn"
39 | unused_crate_dependencies = "warn"
40 | unused-lifetimes = "warn"
41 | unused-qualifications = "warn"
42 |
43 | [lints.clippy]
44 | pedantic = { level = "warn", priority = -1 }
45 | # below lints are from clippy::restriction, and assume clippy >= 1.82
46 | # https://rust-lang.github.io/rust-clippy/master/index.html#/?levels=allow&groups=restriction
47 | allow_attributes = "warn"
48 | clone_on_ref_ptr = "warn"
49 | dbg_macro = "warn"
50 | empty_enum_variants_with_brackets = "warn"
51 | expect_used = "warn"
52 | field_scoped_visibility_modifiers = "warn"
53 | fn_to_numeric_cast_any = "warn"
54 | format_push_string = "warn"
55 | if_then_some_else_none = "warn"
56 | impl_trait_in_params = "warn"
57 | infinite_loop = "warn"
58 | lossy_float_literal = "warn"
59 | mixed_read_write_in_expression = "warn"
60 | multiple_inherent_impl = "warn"
61 | needless_raw_strings = "warn"
62 | panic = "warn"
63 | pathbuf_init_then_push = "warn"
64 | pub_without_shorthand = "warn"
65 | redundant_type_annotations = "warn"
66 | ref_patterns = "warn"
67 | renamed_function_params = "warn"
68 | rest_pat_in_fully_bound_structs = "warn"
69 | same_name_method = "warn"
70 | semicolon_inside_block = "warn"
71 | shadow_unrelated = "warn"
72 | str_to_string = "warn"
73 | string_slice = "warn"
74 | string_to_string = "warn"
75 | tests_outside_test_module = "warn"
76 | try_err = "warn"
77 | undocumented_unsafe_blocks = "warn"
78 | unnecessary_safety_comment = "warn"
79 | unnecessary_safety_doc = "warn"
80 | unneeded_field_pattern = "warn"
81 | unseparated_literal_suffix = "warn"
82 | unused_result_ok = "warn"
83 | unwrap_used = "warn"
84 | verbose_file_reads = "warn"
85 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 desbma
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rsop
2 |
3 | [](https://github.com/desbma/rsop/actions)
4 | [](https://aur.archlinux.org/packages/rsop-open/)
5 | [](https://github.com/desbma/rsop/blob/master/LICENSE)
6 |
7 | Simple, fast & configurable tool to open and preview files.
8 |
9 | Ideal for use with terminal file managers (ranger, lf, nnn, yazi, etc.). Can also be used as a general alternative to `xdg-open` and its various clones.
10 |
11 | If you spend most time in a terminal, and are unsatisfied by current solutions to associate file types with handler programs, this tool may be for you.
12 |
13 | ## Features
14 |
15 | - Start program to view/edit file according to extension or MIME type
16 | - Provides 4 commands (all symlinks to a single `rsop` binary):
17 | - `rso`: open file
18 | - `rse`: edit file (similar to `rso` in most cases, except when open to view or edit have a different handler)
19 | - `rsp`: preview files in terminal, to be used for example in terminal file managers or [`fzf`](https://github.com/junegunn/fzf) preview panel
20 | - `rsi`: to identify MIME type
21 | - Supports opening and previewing from data piped on stdin (very handy for advanced shell scripting, see [below](#show-me-some-cool-stuff-rsop-can-do))
22 | - Supports chainable filters to preprocess data (for example to transparently handle `.log.xz` files)
23 | - Simple config file (no regex or funky conditionals) to describe file formats, handlers, and associate both
24 | - [`xdg-open`](https://linux.die.net/man/1/xdg-open) compatibility mode
25 |
26 | Compared to other `xdg-open` alternatives:
27 |
28 | - `rsop` is consistent and accurate, unlike say [ranger](https://github.com/ranger/ranger/issues/1804)
29 | - `rsop` does not rely on `.desktop` files (see section [Why no .desktop support](#why-no-desktop-support))
30 | - `rsop` does opening and previewing with a single self contained tool and config file
31 | - `rsop` is not tied to a file manager or a runtime environment, you only need the `rsop` binary and your config file and can use it in interactive terminal sessions, file managers, `fzf` invocations...
32 | - `rsop` is taylored for terminal users (especially the preview feature)
33 | - `rsop` is very fast (see [performance](#performance) section)
34 |
35 | ## Installation
36 |
37 | ### From source
38 |
39 | You need a Rust build environment for example from [rustup](https://rustup.rs/).
40 |
41 | ```
42 | cargo build --release
43 | install -Dm 755 -t /usr/local/bin target/release/rsop
44 | ln -rsv /usr/local/bin/rs{op,p}
45 | ln -rsv /usr/local/bin/rs{op,o}
46 | ln -rsv /usr/local/bin/rs{op,e}
47 | ln -rsv /usr/local/bin/rs{op,i}
48 | # to replace system xdg-open:
49 | ln -rsv /usr/local/bin/{rsop,xdg-open}
50 | ```
51 |
52 | ### From the AUR
53 |
54 | Arch Linux users can install the [rsop-open AUR package](https://aur.archlinux.org/packages/rsop-open/).
55 |
56 | ## Configuration
57 |
58 | When first started, `rsop` will create a minimal configuration file usually in `~/.config/rsop/config.toml`.
59 |
60 | See comments and example in that file to set up file types and handlers for your needs.
61 |
62 | A more advanced example configuration file is also available [here](./config/config.toml.advanced).
63 |
64 | ### Usage with [ranger](https://github.com/ranger/ranger)
65 |
66 | **Warning: because ranger is built on Python's old ncurses version, the preview panel only supports 8bit colors (see https://github.com/ranger/ranger/issues/690#issuecomment-255590479), so if the output seems wrong you may need to tweak handlers to generate 8bit colors instead of 24.**
67 |
68 | In `rifle.conf`:
69 |
70 | = rso "$@"
71 |
72 | In `scope.sh`:
73 |
74 | #!/bin/sh
75 | COLUMNS="$2" LINES="$3" exec rsp "$1"
76 |
77 | Dont forget to make it executable with `chmod +x ~/.config/ranger/scope.sh`.
78 |
79 | ### Usage with [lf](https://github.com/gokcehan/lf)
80 |
81 | Add in `lfrc`:
82 |
83 | set filesep "\n"
84 | set ifs "\n"
85 | set previewer ~/.config/lf/preview
86 | cmd open ${{
87 | for f in ${fx[@]}; do rso "${f}"; done
88 | lf -remote "send $id redraw"
89 | }}
90 |
91 | And create `~/.config/lf/preview` with:
92 |
93 | #!/bin/sh
94 | COLUMNS="$2" LINES="$3" exec rsp "$1"
95 |
96 | ## Usage with [yazi](https://github.com/sxyazi/yazi)
97 |
98 | Yazi has a complex LUA plugin system. Some built in previewers are superior to what `rsp` can provide (integrated image preview, seeking...), however in most cases `rsop` is more powerful and flexible, so this configuration mixes both built-in previewers and calls to `rsp`. Keep in mind the Yazi plugin API is not yet stable so this can break and requires changing frequently.
99 |
100 |
101 | ~/.config/yazi/yazi.toml
102 |
103 | ```yaml
104 | [plugin]
105 | previewers = [
106 | { name = "*/", run = "folder" },
107 | { mime = "image/{avif,hei?,jxl,svg+xml}", run = "magick" },
108 | { mime = "image/*", run = "image" },
109 | { mime = "font/*", run = "font" },
110 | { mime = "application/vnd.ms-opentype", run = "font" },
111 | { mime = "application/pdf", run = "pdf" },
112 | { mime = "video/*", run = "video" },
113 | { mime = "inode/empty", run = "empty" },
114 | { name = "*", run = "rsp" },
115 | ]
116 |
117 | [opener]
118 | open = [
119 | { run = 'rso "$1"', desc = "Open", block = true },
120 | ]
121 | edit = [
122 | { run = 'rse "$1"', desc = "Edit" },
123 | ]
124 | edit_block = [
125 | { run = 'rse "$1"', desc = "Edit (blocking)", block = true },
126 | ]
127 |
128 | [open]
129 | rules = [
130 | { mime = "application/{,g}zip", use = [ "open", "extract" ] },
131 | { mime = "application/{tar,bzip*,7z*,xz,rar}", use = [ "open", "extract" ] },
132 | { mime = "text/*", use = [ "open", "edit_block" ] },
133 | { name = "*", use = [ "open", "edit", "edit_block" ] },
134 | ]
135 | ```
136 |
137 |
138 |
139 |
140 | ~/.config/yazi/plugins/rsop/main.lua
141 |
142 | ```lua
143 | local M = {}
144 |
145 | function M:peek(job)
146 | local child = Command("rsp")
147 | :args({
148 | tostring(job.file.url),
149 | })
150 | :env("COLUMNS", tostring(job.area.w))
151 | :env("LINES", tostring(job.area.h))
152 | :stdout(Command.PIPED)
153 | :stderr(Command.PIPED)
154 | :spawn()
155 |
156 | if not child then
157 | return require("code").peek(job)
158 | end
159 |
160 | local limit = job.area.h
161 | local i, lines = 0, ""
162 | repeat
163 | local next, event = child:read_line()
164 | if event == 1 then
165 | return require("code").peek(job)
166 | elseif event ~= 0 then
167 | break
168 | end
169 |
170 | i = i + 1
171 | if i > job.skip then
172 | lines = lines .. next
173 | end
174 | until i >= job.skip + limit
175 |
176 | child:start_kill()
177 | if job.skip > 0 and i < job.skip + limit then
178 | ya.emit("peek", { math.max(0, i - limit), only_if = job.file.url, upper_bound = true })
179 | else
180 | lines = lines:gsub("\t", string.rep(" ", rt.preview.tab_size))
181 | ya.preview_widgets(job, {
182 | ui.Text.parse(lines):area(job.area):wrap(rt.preview.wrap == "yes" and ui.Text.WRAP or ui.Text.WRAP_NO),
183 | })
184 | end
185 | end
186 |
187 | function M:seek(job) require("code"):seek(job) end
188 |
189 | return M
190 | ```
191 |
192 |
193 |
194 | ## Show me some cool stuff `rsop` can do
195 |
196 | - Simple file explorer with fuzzy searching, using [fd](https://github.com/sharkdp/fd) and [fzf](https://github.com/junegunn/fzf), using `rso` to preview files and `rsp` to open them:
197 |
198 | ```
199 | fd . | fzf --preview='rsp {}' | xargs -r rso
200 | ```
201 |
202 | [](https://raw.githubusercontent.com/desbma/rsop/master/demo/file-explorer.gif)
203 |
204 | - Preview files inside an archive, **without decompressing it entirely**, select one and open it (uses [`bsdtar`](https://www.libarchive.org/), [`fzf`](https://github.com/junegunn/fzf) and `rso`/`rsp`):
205 |
206 | ```
207 | # preview archive (.tar, .tar.gz, .zip, .7z, etc.)
208 | # usage: pa
209 | pa() {
210 | local -r archive="${1:?}"
211 | bsdtar -tf "${archive}" |
212 | grep -v '/$' |
213 | fzf --preview="bsdtar -xOf \"${archive}\" {} | rsp" |
214 | xargs -r bsdtar -xOf "${archive}" |
215 | rso
216 | }
217 | ```
218 |
219 | [](https://raw.githubusercontent.com/desbma/rsop/master/demo/preview-archive.gif)
220 |
221 | **This is now integrated in the [advanced config example](./config/config.toml.advanced), so you can just run `rso ` and get the same result.**
222 |
223 | ## Performance
224 |
225 | `rsop` is quite fast. In practice it rarely matters because choosing with which program to open or preview files is usually so quick it is not perceptible. However performance can matter if for example you are decompressing a huge `tar.gz` archive to preview its content.
226 | To help with that, `rsop` uses the [`splice` system call](https://man7.org/linux/man-pages/man2/splice.2.html) if available on your platform. In the `.tar.gz` example this allows decompressing data with `gzip` or `pigz` and passing it to tar (or whatever you have configured to handle `application/x-tar` MIME type), **without wasting time to copy data in user space** between the two programs. This was previously done using a custom code path, but is now done [transparently](https://github.com/rust-lang/rust/pull/75272) by the standard library.
227 |
228 | Other stuff `rsop` does to remain quick:
229 |
230 | - it is written in Rust (setting aside the RiiR memes, this avoid the 20-50ms startup time of for example Python interpreters)
231 | - it uses hashtables to search for handlers from MIME types or extensions in constant time
232 | - it uses the great [tree_magic_mini crate](https://crates.io/crates/tree_magic_mini) for fast MIME identification
233 |
234 | ## FAQ
235 |
236 | ### Why no [`.desktop`](https://specifications.freedesktop.org/desktop-entry-spec/latest/) support?
237 |
238 | - `.desktop` do not provide a _preview_ action separate from _open_.
239 | - One may need to pipe several programs to get to desired behavior, `.desktop` files does not help with this.
240 | - Many programs do not ship one, especially command line tools, so this would be incomplete anyway.
241 | - On a philosophical level, with `.desktop` files, the program's author (or packager) decides which MIME types to support, and which arguments to pass to the program. This is a wrong paradidm, as this is fundamentally a user's decision.
242 |
243 | ### What does `rsop` stands for?
244 |
245 | "**R**eally **S**imple **O**pener/**P**reviewer" or "**R**eliable **S**imple **O**pener/**P**reviewer" or "**R**u**S**t **O**pener/**P**reviewer"
246 |
247 | I haven't really decided yet...
248 |
249 | ### What is the difference between the open, edit and preview actions?
250 |
251 | Each action has customizable handlers, so they only do what you set them to do.
252 |
253 | However the philosophy is the following :
254 |
255 | - preview is **non interactive**, typically with only terminal UI, and a maximum number of lines in the output
256 | - open can be interactive or not, and can open graphical windows or not
257 | - edit is **interactive**, defaults to the open handler if no edit handler is set, and only makes sense if you need an action separate from open, for example to edit images with GIMP versus to view them with just an image viewer
258 |
259 | ## License
260 |
261 | [MIT](./LICENSE)
262 |
--------------------------------------------------------------------------------
/cliff.toml:
--------------------------------------------------------------------------------
1 | # configuration file for git-cliff (0.1.0)
2 |
3 | [changelog]
4 | # changelog header
5 | header = """
6 | # Changelog
7 |
8 | All notable changes to this project will be documented in this file.
9 |
10 | """
11 | # template for the changelog body
12 | # https://tera.netlify.app/docs/#introduction
13 | body = """
14 | {% if version %}\
15 | ## {{ version | replace(from="v", to="") }} - {{ timestamp | date(format="%Y-%m-%d") }}
16 | {% else %}\
17 | ## _unreleased_
18 | {% endif %}\
19 | {% for group, commits in commits | group_by(attribute="group") %}
20 | ### {{ group | upper_first }}
21 | {% for commit in commits %}
22 | - {{ commit.message | upper_first }}\
23 | {% endfor %}
24 | {% endfor %}\n
25 | """
26 | # remove the leading and trailing whitespaces from the template
27 | trim = true
28 | # changelog footer
29 | footer = """
30 |
31 | """
32 |
33 | [git]
34 | # allow only conventional commits
35 | # https://www.conventionalcommits.org
36 | conventional_commits = true
37 | # regex for parsing and grouping commits
38 | commit_parsers = [
39 | { message = "^feat*", group = "Features"},
40 | { message = "^fix*", group = "Bug Fixes"},
41 | { message = "^doc*", group = "Documentation"},
42 | { message = "^perf*", group = "Performance"},
43 | { message = "^refactor*", group = "Refactor"},
44 | { message = "^style*", group = "Styling"},
45 | { message = "^test*", group = "Testing"},
46 | { message = "^chore\\(release\\): prepare for*", skip = true},
47 | { message = "^chore*", group = "Miscellaneous Tasks"},
48 | { body = ".*security", group = "Security"},
49 | { message = "^config*", group = "Configuration"},
50 | ]
51 | # filter out the commits that are not matched by commit parsers
52 | filter_commits = false
53 | # glob pattern for matching git tags
54 | tag_pattern = "[0-9.]*"
55 | # regex for skipping tags
56 | #skip_tags = ""
57 |
--------------------------------------------------------------------------------
/clippy.toml:
--------------------------------------------------------------------------------
1 | allow-expect-in-tests = true
2 | allow-panic-in-tests = true
3 | allow-unwrap-in-tests = true
4 | avoid-breaking-exported-api = false
5 |
--------------------------------------------------------------------------------
/config/config.toml.advanced:
--------------------------------------------------------------------------------
1 | #
2 | # This is an example of config file for rsop, showing advanced usage.
3 | #
4 | # It makes use of many external programs you may need to install from your distribution packages or from source:
5 | # - atril https://github.com/mate-desktop/atril
6 | # - bat https://github.com/sharkdp/bat
7 | # - bsdtar https://www.libarchive.org/
8 | # - chafa https://hpjansson.org/chafa/
9 | # - delta https://github.com/dandavison/delta
10 | # - dpkg https://wiki.debian.org/Teams/Dpkg
11 | # - ffmpegthumbnailer https://github.com/dirkvdb/ffmpegthumbnailer
12 | # - ffprobe https://ffmpeg.org/
13 | # - firefox https://firefox.com/
14 | # - hexyl https://github.com/sharkdp/hexyl
15 | # - imv https://github.com/eXeC64/imv
16 | # - libreoffice https://www.libreoffice.org/
17 | # - lsd https://github.com/Peltoche/lsd
18 | # - mdcat https://github.com/lunaryorn/mdcat
19 | # - mediainfo https://mediaarea.net/en/MediaInfo
20 | # - moreutils https://joeyh.name/code/moreutils/
21 | # - mpv https://mpv.io/
22 | # - odt2txt https://github.com/dstosberg/odt2txt/
23 | # - openscad https://openscad.org/
24 | # - openssl https://www.openssl.org/
25 | # - pandoc https://pandoc.org/
26 | # - pbzip2 http://compression.ca/pbzip2/
27 | # - pigz https://www.zlib.net/pigz/
28 | # - pdftoppm https://poppler.freedesktop.org/
29 | # - retext https://github.com/retext-project/retext
30 | # - sqlite3 https://www.sqlite.org/index.html
31 | # - ss https://git.kernel.org/pub/scm/network/iproute2/iproute2.git
32 | # - transmission-remote-gtk https://github.com/transmission-remote-gtk/transmission-remote-gtk
33 | # - transmission-show https://transmissionbt.com/
34 | # - tree http://mama.indstate.edu/users/ice/tree/
35 | # - tshark https://www.wireshark.org/
36 | # - tuxguitar http://www.tuxguitar.com.ar/
37 | # - w3m https://salsa.debian.org/debian/w3m
38 | # - wireshark https://www.wireshark.org/
39 | # - xz https://tukaani.org/xz/
40 | # - zstd https://facebook.github.io/zstd/
41 | #
42 | # You most likely want to edit this file to fit your needs.
43 | #
44 |
45 | #
46 | # File types, identified by extension or MIME type
47 | #
48 | # - extensions
49 | # List of extensions, always checked before MIME type. Double extensions (ie. 'tar.gz') are supported, although it usually
50 | # makes more sense to use a filter instead.
51 | #
52 | # - mimes
53 | # List of MIME types, a prefix (part before the '+', '.' or '/') can be used to match several subtypes.
54 | # Compared to identification by extension this has the advantage of also working with data piped from stdin.
55 | #
56 |
57 | [filetype.archive]
58 | mimes = [
59 | "application/java-archive",
60 | "application/vnd.rar",
61 | "application/x-7z-compressed",
62 | "application/x-archive",
63 | "application/x-cpio",
64 | "application/x-rar",
65 | "application/x-rpm",
66 | "application/x-tar",
67 | "application/zip"
68 | ]
69 | # bsdtar can decompress transparently
70 | extensions = ["iso", "tar.bz2", "tar.gz", "tar.xz", "tar.zst"]
71 |
72 | [filetype.audio]
73 | mimes = ["audio", "video/ogg"]
74 | extensions = ["m4a", "ogg"]
75 |
76 | [filetype.binary]
77 | mimes = ["application/octet-stream"]
78 |
79 | [filetype.bzip2]
80 | mimes = ["application/x-bzip2"]
81 |
82 | [filetype.certificate]
83 | mimes = ["application/pkix-cert"]
84 |
85 | [filetype.deb]
86 | extensions = ["deb"]
87 | mimes = ["application/vnd.debian.binary-package"]
88 |
89 | [filetype.directory]
90 | mimes = ["inode/directory"]
91 |
92 | [filetype.dot]
93 | extensions = ["dot"]
94 | mimes = ["text/vnd.graphviz"]
95 |
96 | [filetype.drawio]
97 | extensions = ["drawio"]
98 |
99 | [filetype.epub]
100 | mimes = ["application/epub"]
101 |
102 | [filetype.gif]
103 | extensions = ["gif"]
104 | mimes = ["image/gif"]
105 |
106 | [filetype.graph]
107 | extensions = ["graph"]
108 |
109 | [filetype.guitar_tab]
110 | extensions = ["gp3", "gp4", "gp5", "ptb"]
111 |
112 | [filetype.gzip]
113 | mimes = ["application/gzip"]
114 |
115 | [filetype.html]
116 | extensions = ["htm", "html", "xhtml"]
117 | mimes = ["text/html"]
118 |
119 | [filetype.image]
120 | mimes = ["image"]
121 |
122 | [filetype.jpeg]
123 | mimes = ["image/jpeg"]
124 |
125 | [filetype.markdown]
126 | extensions = ["md"]
127 |
128 | [filetype.mobi]
129 | extensions = ["mobi"]
130 |
131 | [filetype.motion_jpeg]
132 | extensions = ["mp.jpg"]
133 |
134 | [filetype.msdocument]
135 | extensions = ["doc", "docx", "pptx", "rtf", "xlsx"]
136 | mimes = ["application/vnd.openxmlformats-officedocument", "text/rtf"]
137 |
138 | [filetype.opendocument]
139 | extensions = ["odg", "odp", "ods", "odt"]
140 | mimes = ["application/vnd.oasis.opendocument"]
141 |
142 | [filetype.patch]
143 | mimes = ["text/x-patch"]
144 | extensions = ["patch"]
145 |
146 | [filetype.pcap]
147 | mimes = ["application/vnd.tcpdump.pcap", "application/x-pcapng"]
148 |
149 | [filetype.pdf]
150 | mimes = ["application/pdf"]
151 |
152 | [filetype.scad]
153 | extensions = ["scad"]
154 |
155 | [filetype.socket]
156 | mimes = ["inode/socket"]
157 |
158 | [filetype.sqlite]
159 | mimes = ["application/vnd.sqlite3"]
160 |
161 | [filetype.svg]
162 | mimes = ["image/svg"]
163 | extensions = ["svg"]
164 |
165 | [filetype.text]
166 | mimes = [
167 | "text",
168 | "application/mbox",
169 | "application/pkcs8+pem",
170 | "application/x-desktop",
171 | "application/x-perl",
172 | "application/x-php",
173 | "application/x-shellscript",
174 | "application/x-subrip",
175 | "application/xml"
176 | ]
177 |
178 | [filetype.torrent]
179 | mimes = ["application/x-bittorrent"]
180 |
181 | [filetype.video]
182 | mimes = ["video", "application/vnd.ms-asf", "application/x-matroska", "application/x-riff"]
183 | extensions = ["3gp", "avi", "mp4", "ogv"]
184 |
185 | [filetype.xsv]
186 | extensions = ["csv", "tsv"]
187 |
188 | [filetype.xz]
189 | mimes = ["application/x-xz"]
190 |
191 | [filetype.zstandard]
192 | mimes = ["application/zstd"]
193 |
194 |
195 | #
196 | # File handlers
197 | #
198 | # - command
199 | # The command to run to open or preview file.
200 | # Substitution is done for the following expressions:
201 | # %c: terminal column count
202 | # %i: input path
203 | # %l: terminal line count
204 | # %m: input MIME type
205 | # Use '%%' if you need to pass a literal '%' char.
206 | #
207 | # - shell
208 | # If true, runs the command in a shell, use this if you use pipes. Defaults to false.
209 | #
210 | # - wait
211 | # If true, waits for the handler to exit. Defaults to true.
212 | #
213 | # - no_pipe
214 | # If true, disable piping data to handler's stdin, and use a slower temporary file instead if data is piped to rsop.
215 | # Incompatible with 'wait = false'. Defaults to false.
216 | #
217 | # - stdin_arg
218 | # When previewing or opening data from stdin, with what string to substitute '%i'. Defaults to "-", some programs require "".
219 | #
220 |
221 | [default_handler_preview]
222 | command = "echo '🔍 MIME: %m'; hexyl --border none %i | head -n $((%l - 1))"
223 | shell = true
224 | stdin_arg = ""
225 |
226 | [default_handler_open]
227 | command = "hexyl %i | less -R"
228 | shell = true
229 | stdin_arg = ""
230 |
231 | [handler_preview.archive]
232 | command = "echo '🔍 MIME: %m'; bsdtar -tf %i | grep -v '/$' | tree -C --noreport --fromfile . | tail -n +2 | sed 's@^....@@' | head -n $((%l - 3))"
233 | shell = true
234 |
235 | [handler_open.archive]
236 | command = "bsdtar -tf %i | grep -v /$ | fzf -m --preview=\"bsdtar -xOf %i {} | rsp\" --print0 | xargs -0r bsdtar -xOf %i | ifne rso"
237 | shell = true
238 | no_pipe = true
239 |
240 | [handler_preview.audio]
241 | command = "mediainfo %i | sed 's@ \\+: @: @' | column -s ':' -t -l 2 | sed 's@ *$@@'"
242 | shell = true
243 |
244 | [handler_open.audio]
245 | command = "mpv %i"
246 | wait = false
247 |
248 | [handler_preview.binary]
249 | command = "hexyl --border none %i | head -n %l"
250 | shell = true
251 | stdin_arg = ""
252 |
253 | [handler_preview.certificate]
254 | command = "openssl x509 -in %i -text"
255 |
256 | [handler_open.certificate]
257 | command = "openssl x509 -in %i -text | less -R"
258 | shell = true
259 |
260 | [handler_preview.deb]
261 | command = "dpkg -c %i | head -n %l"
262 | shell = true
263 |
264 | [handler_preview.directory]
265 | command = "lsd -alFh --tree --color=always --icon=always %i | head -n %l"
266 | shell = true
267 |
268 | [handler_open.directory]
269 | command = "lsd -alFh --tree --color=always --icon=always %i | less -R"
270 | shell = true
271 |
272 | [handler_preview.dot]
273 | command = "dot -Tdot %i | graph-easy --as=boxart 2> /dev/null"
274 | shell = true
275 | stdin_arg = ""
276 |
277 | [handler_open.dot]
278 | command = "dot -Tsvg %i | rso"
279 | shell = true
280 | stdin_arg = ""
281 |
282 | [handler_edit.drawio]
283 | command = "drawio %i"
284 | no_pipe = true
285 |
286 | [handler_open.epub]
287 | command = "atril %i"
288 | no_pipe = true
289 |
290 | [handler_open.gif]
291 | command = "mpv --loop %i"
292 | wait = false
293 |
294 | [handler_preview.graph]
295 | command = "graph-easy --as=boxart %i"
296 | stdin_arg = ""
297 |
298 | [handler_open.graph]
299 | command = "graph-easy --as=dot %i | dot -Tsvg | rso"
300 | stdin_arg = ""
301 | shell = true
302 |
303 | [handler_open.guitar_tab]
304 | command = "tuxguitar %i"
305 | no_pipe = true
306 |
307 | [handler_preview.html]
308 | command = "w3m -dump %i"
309 | no_pipe = true
310 |
311 | [handler_open.html]
312 | command = "firefox %i"
313 | no_pipe = true
314 |
315 | [handler_preview.image]
316 | command = "chafa -s %cx%l %i"
317 |
318 | [handler_open.image]
319 | command = "imv %i"
320 | wait = false
321 |
322 | [handler_edit.image]
323 | command = "gimp %i"
324 | no_pipe = true
325 |
326 | [handler_preview.jpeg]
327 | command = "exiftran -a -o /dev/stdout %i | chafa -s %cx%l"
328 | shell = true
329 | stdin_arg = "/dev/stdin"
330 |
331 | [handler_preview.markdown]
332 | command = "mdcat %i"
333 |
334 | [handler_edit.markdown]
335 | command = "retext %i"
336 | wait = false
337 |
338 | [handler_open.mobi]
339 | command = "FBReader %i"
340 | no_pipe = true
341 |
342 | [handler_preview.msdocument]
343 | command = "pandoc -s -t markdown -- %i | mdcat"
344 | shell = true
345 |
346 | [handler_edit.msdocument]
347 | command = "libreoffice %i"
348 | no_pipe = true
349 |
350 | [handler_preview.opendocument]
351 | command = "odt2txt %i"
352 | no_pipe = true
353 |
354 | [handler_edit.opendocument]
355 | command = "libreoffice %i"
356 | no_pipe = true
357 |
358 | [handler_preview.patch]
359 | command = "cat %i | delta"
360 | shell = true
361 |
362 | [handler_preview.pcap]
363 | command = "tshark -t a -r %i | head -n %l"
364 | shell = true
365 |
366 | [handler_open.pcap]
367 | command = "wireshark %i"
368 | no_pipe = true
369 |
370 | [handler_preview.pdf]
371 | command = "t=$(mktemp); pdftoppm -f 1 -l 1 -scale-to-x 800 -scale-to-y -1 -singlefile -jpeg -jpegopt quality=60 -tiffcompression jpeg -- %i \"${t}\" && chafa -s %cx%l \"${t}.jpg\"; rm \"${t}.jpg\""
372 | shell = true
373 |
374 | [handler_open.pdf]
375 | command = "atril %i"
376 | no_pipe = true
377 |
378 | [handler_preview.scad]
379 | command = "openscad -q --render --colorscheme=Solarized --imgsize=800,600 --export-format png -o - %i | chafa -s %cx%l -"
380 | shell = true
381 | no_pipe = true
382 |
383 | [handler_edit.scad]
384 | command = "openscad %i"
385 | no_pipe = true
386 |
387 | [handler_preview.socket]
388 | command = "ss -alxp src unix:'%i'"
389 |
390 | [handler_preview.sqlite]
391 | command = "sqlite3 %i .dump | bat -P --color=always -n --terminal-width %c -r :%l -l sql"
392 | shell = true
393 | no_pipe = true
394 |
395 | [handler_open.sqlite]
396 | command = "sqlite3 %i .dump | bat --paging always --color=always -n --terminal-width %c -l sql"
397 | shell = true
398 | no_pipe = true
399 |
400 | [handler_preview.svg]
401 | command = "chafa -s %cx%l %i"
402 |
403 | [handler_open.svg]
404 | command = "firefox %i"
405 | no_pipe = true
406 |
407 | [handler_preview.text]
408 | command = "bat -P --color=always -n --terminal-width %c -r :%l %i"
409 |
410 | [handler_open.text]
411 | command = "bat --paging always --color=always -n --terminal-width %c %i"
412 |
413 | [handler_preview.torrent]
414 | command = "transmission-show -- %i"
415 | no_pipe = true
416 |
417 | [handler_open.torrent]
418 | command = "transmission-remote-gtk -- %i"
419 | no_pipe = true
420 |
421 | [handler_preview.video]
422 | command = "s=$(ffprobe -hide_banner %i 2>&1 | grep -E '^ (Stream|Duration)' | sed 's@^ Stream [^ ]*:@@' | sed 's@ *@@'); l=$(echo \"$s\" | wc -l); ffmpegthumbnailer -s 800 -c jpg -q 6 -i %i -o - 2> /dev/null | chafa -s %cx$((%l - l)) -; echo \"$s\""
423 | shell = true
424 | no_pipe = true
425 |
426 | [handler_open.video]
427 | command = "mpv --loop %i"
428 | no_pipe = true
429 |
430 | [handler_preview.xsv]
431 | command = "bat -P --color=always -n --terminal-width %c -r :%l -l csv %i"
432 |
433 | [handler_open.xsv]
434 | command = "libreoffice %i"
435 | no_pipe = true
436 |
437 |
438 | #
439 | # Filters
440 | #
441 | # Filters are special handlers that process their input and send their output either to another filter or to a final handler.
442 | # They are typically useful to transparently decompress files like .log.xz, .pcapng.gz, tar.gz, etc.
443 | # but you can also use it for more specific needs like converting some document formats to markdown and then using your usual handler
444 | # for markdown files to preview or open it.
445 | # Filter configuration parameters are similar to handler, except wait that is implied as true.
446 | #
447 |
448 | [filter.bzip2]
449 | command = "pbzip2 -dc %i"
450 |
451 | [filter.gzip]
452 | command = "pigz -dc %i"
453 |
454 | [filter.motion_jpeg]
455 | # https://linuxreviews.org/Google_Pixel_%22Motion_Photo%22
456 | command = "tail -c +$(( $(grep -EUboa '(ftypisom|ftypmp42)' %i | cut -d ':' -f 1) - 3)) %i 2> /dev/null || cat %i"
457 | shell = true
458 | no_pipe = true
459 |
460 | [filter.xz]
461 | command = "xz -dc %i"
462 |
463 | [filter.zstandard]
464 | command = "zstd -dc %i"
465 |
466 |
467 | #
468 | # Scheme handlers
469 | #
470 | # Handlers for use in 'xdg-open' mode, with URLs instead of paths. URLs prefixed with 'file://' are handled by file handlers.
471 | # Configuration is similar to file handlers, but only 'command' and 'shell' parameters are supported.
472 | #
473 |
474 | [handler_scheme.http]
475 | command = "firefox %i"
476 |
477 | [handler_scheme.https]
478 | command = "firefox %i"
479 |
--------------------------------------------------------------------------------
/config/config.toml.default:
--------------------------------------------------------------------------------
1 | #
2 | # This is a default minimal config file for rsop.
3 | # For a more advanced config example, see https://raw.githubusercontent.com/desbma/rsop/master/config/config.toml.advanced
4 | # You most likely want to edit this file to fit your needs.
5 | #
6 |
7 | #
8 | # File types, identified by extension or MIME type
9 | #
10 | # - extensions
11 | # List of extensions, always checked before MIME type. Double extensions (ie. 'tar.gz') are supported, although it usually
12 | # makes more sense to use a filter instead.
13 | #
14 | # - mimes
15 | # List of MIME types, a prefix (part before the '+', '.' or '/') can be used to match several subtypes.
16 | # Compared to identification by extension this has the advantage of also working with data piped from stdin.
17 | #
18 |
19 | [filetype.gzip]
20 | mimes = ["application/gzip"]
21 |
22 | [filetype.text]
23 | mimes = ["text"]
24 |
25 |
26 | #
27 | # File handlers
28 | #
29 | # - command
30 | # The command to run to open or preview file.
31 | # Substitution is done for the following expressions:
32 | # %c: terminal column count
33 | # %i: input path
34 | # %l: terminal line count
35 | # %m: input MIME type
36 | # Use '%%' if you need to pass a literal '%' char.
37 | #
38 | # - shell
39 | # If true, runs the command in a shell, use this if you use pipes. Defaults to false.
40 | #
41 | # - wait
42 | # If true, waits for the handler to exit. Defaults to true.
43 | #
44 | # - no_pipe
45 | # If true, disable piping data to handler's stdin, and use a slower temporary file instead if data is piped to rsop.
46 | # Incompatible with 'wait = false'. Defaults to false.
47 | #
48 | # - stdin_arg
49 | # When previewing or opening data from stdin, with what string to substitute '%i'. Defaults to "-", some programs require "".
50 | #
51 |
52 | [default_handler_preview]
53 | command = "file %i"
54 |
55 | [default_handler_open]
56 | command = "cat -A %i"
57 |
58 | [handler_preview.text]
59 | command = "head -n %l %i"
60 |
61 | [handler_open.text]
62 | command = "less %i"
63 |
64 |
65 | #
66 | # Filters
67 | #
68 | # Filters are special handlers that process their input and send their output either to another filter or to a final handler.
69 | # They are typically useful to transparently decompress files like .log.xz, .pcapng.gz, tar.gz, etc.
70 | # but you can also use it for more specific needs like converting some document formats to markdown and then using your usual handler
71 | # for markdown files to preview or open it.
72 | # Filter configuration parameters are similar to handler, except wait that is implied as true.
73 | #
74 |
75 | [filter.gzip]
76 | command = "gzip -dc %i"
77 |
78 |
79 | #
80 | # Scheme handlers
81 | #
82 | # Handlers for use in 'xdg-open' mode, with URLs instead of paths. URLs prefixed with 'file://' are handled by file handlers.
83 | # Configuration is similar to file handlers, but only 'command' and 'shell' parameters are supported.
84 | #
85 |
86 | [handler_scheme.http]
87 | command = "firefox %i"
88 |
89 | [handler_scheme.https]
90 | command = "firefox %i"
91 |
--------------------------------------------------------------------------------
/config/config.toml.tiny:
--------------------------------------------------------------------------------
1 | #
2 | # This is a the smallest config accepted by rsop.
3 | # Only useful for automated tests.
4 | #
5 |
6 | [default_handler_preview]
7 | command = "file %i"
8 |
9 | [default_handler_open]
10 | command = "cat -A %i"
11 |
--------------------------------------------------------------------------------
/demo/file-explorer.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/desbma/rsop/36f38a8cc6035992f38dfa02f3da36b75a66deac/demo/file-explorer.gif
--------------------------------------------------------------------------------
/demo/populate:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | set -o pipefail
4 |
5 | ensure_sample() {
6 | local -r url="${1:?}"
7 | local -r ext="${2:-${url##*.}}"
8 | local -t output="sample.${ext}"
9 |
10 | if [ ! -f "${output}" ]
11 | then
12 | curl "${url}" -o "${output}"
13 | fi
14 | }
15 |
16 | cd "$(dirname -- "$0")"
17 |
18 | if [ "${1:-}" = 'clean' ]
19 | then
20 | rm -f sample.*
21 |
22 | else
23 | # https://commons.wikimedia.org/wiki/Category:Commons_sample_files
24 | ensure_sample 'https://upload.wikimedia.org/wikipedia/commons/4/4d/GridRPC_paradigm.pdf'
25 | ensure_sample 'https://upload.wikimedia.org/wikipedia/commons/5/5a/Test-kdenlive-title.webm'
26 | ensure_sample 'https://upload.wikimedia.org/wikipedia/commons/8/8d/Qsicon_inArbeit_%28jha%29.svg'
27 | cp ../src/handler.rs sample.rs
28 | cp ../README.md sample.md
29 | fi
30 |
--------------------------------------------------------------------------------
/demo/preview-archive.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/desbma/rsop/36f38a8cc6035992f38dfa02f3da36b75a66deac/demo/preview-archive.gif
--------------------------------------------------------------------------------
/demo/record:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | readonly OUTPUT="${1:?}"
4 |
5 | reset
6 |
7 | t-rec -q -d none -n -s 300ms -e 800ms "$SHELL"
8 |
9 | mv t-rec.gif "${OUTPUT}"
10 |
--------------------------------------------------------------------------------
/release:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | set -o pipefail
4 |
5 | readonly VERSION="${1:?}"
6 |
7 | cd "$(git rev-parse --show-toplevel)"
8 |
9 | cargo set-version "${VERSION}"
10 |
11 | cargo upgrade
12 | cargo update
13 |
14 | cargo check
15 | cargo test
16 |
17 | git add Cargo.{toml,lock}
18 |
19 | git commit -m "chore: version ${VERSION}"
20 | git tag -m "Version ${VERSION}" "${VERSION}"
21 |
22 | git cliff 1.0.0..HEAD > CHANGELOG.md
23 | git add CHANGELOG.md
24 | git commit --amend --no-edit
25 |
26 | git tag -f -m "Version ${VERSION}" "${VERSION}"
27 |
--------------------------------------------------------------------------------
/src/cli.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use clap::Parser;
4 |
5 | #[derive(Debug, Parser)]
6 | #[structopt(version=env!("CARGO_PKG_VERSION"), about="Open or preview files.")]
7 | pub(crate) struct CommandLineOpts {
8 | pub path: Option,
9 | }
10 |
--------------------------------------------------------------------------------
/src/config.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | collections::HashMap,
3 | fs::File,
4 | io::Write,
5 | path::{Path, PathBuf},
6 | };
7 |
8 | #[derive(Debug, serde::Deserialize)]
9 | pub(crate) struct Filetype {
10 | #[serde(default)]
11 | pub extensions: Vec,
12 |
13 | #[serde(default)]
14 | pub mimes: Vec,
15 | }
16 |
17 | #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
18 | pub(crate) struct FileHandler {
19 | pub command: String,
20 | #[serde(default = "default_file_handler_wait")]
21 | pub wait: bool,
22 | #[serde(default)]
23 | pub shell: bool,
24 | #[serde(default)]
25 | pub no_pipe: bool,
26 | pub stdin_arg: Option,
27 | }
28 |
29 | const fn default_file_handler_wait() -> bool {
30 | true
31 | }
32 |
33 | #[derive(Clone, Debug, serde::Deserialize)]
34 | pub(crate) struct FileFilter {
35 | pub command: String,
36 | #[serde(default)]
37 | pub shell: bool,
38 | #[serde(default)]
39 | pub no_pipe: bool,
40 | pub stdin_arg: Option,
41 | }
42 |
43 | #[derive(Clone, Debug, serde::Deserialize)]
44 | pub(crate) struct SchemeHandler {
45 | pub command: String,
46 | #[serde(default)]
47 | pub shell: bool,
48 | }
49 |
50 | #[derive(Debug, serde::Deserialize)]
51 | pub(crate) struct Config {
52 | #[serde(default)]
53 | pub filetype: HashMap,
54 |
55 | #[serde(default)]
56 | pub handler_preview: HashMap,
57 | pub default_handler_preview: FileHandler,
58 |
59 | #[serde(default)]
60 | pub handler_open: HashMap,
61 | pub default_handler_open: FileHandler,
62 |
63 | #[serde(default)]
64 | pub handler_edit: HashMap,
65 |
66 | #[serde(default)]
67 | pub filter: HashMap,
68 |
69 | #[serde(default)]
70 | pub handler_scheme: HashMap,
71 | }
72 |
73 | pub(crate) fn parse_config() -> anyhow::Result {
74 | parse_config_path(&get_config_path()?)
75 | }
76 |
77 | fn get_config_path() -> anyhow::Result {
78 | const CONFIG_FILENAME: &str = "config.toml";
79 | const DEFAULT_CONFIG_STR: &str = include_str!("../config/config.toml.default");
80 | let binary_name = env!("CARGO_PKG_NAME");
81 | let xdg_dirs = xdg::BaseDirectories::with_prefix(binary_name)?;
82 | let config_filepath = if let Some(p) = xdg_dirs.find_config_file(CONFIG_FILENAME) {
83 | p
84 | } else {
85 | let path = xdg_dirs.place_config_file(CONFIG_FILENAME)?;
86 | log::warn!("No config file found, creating a default one in {:?}", path);
87 | let mut file = File::create(&path)?;
88 | file.write_all(DEFAULT_CONFIG_STR.as_bytes())?;
89 | path
90 | };
91 |
92 | log::debug!("Config filepath: {:?}", config_filepath);
93 |
94 | Ok(config_filepath)
95 | }
96 |
97 | fn parse_config_path(path: &Path) -> anyhow::Result {
98 | let toml_data = std::fs::read_to_string(path)?;
99 | log::trace!("Config data: {:?}", toml_data);
100 |
101 | let mut config: Config = toml::from_str(&toml_data)?;
102 | // Normalize extensions to lower case
103 | for filetype in config.filetype.values_mut() {
104 | filetype.extensions = filetype
105 | .extensions
106 | .iter()
107 | .map(|e| e.to_lowercase())
108 | .collect();
109 | }
110 | log::trace!("Config: {:?}", config);
111 |
112 | Ok(config)
113 | }
114 |
115 | #[cfg(test)]
116 | mod tests {
117 | use super::*;
118 |
119 | #[test]
120 | fn test_tiny_config() {
121 | const TINY_CONFIG_STR: &str = include_str!("../config/config.toml.tiny");
122 | let mut config_file = tempfile::NamedTempFile::new().unwrap();
123 | config_file.write_all(TINY_CONFIG_STR.as_bytes()).unwrap();
124 |
125 | let res = parse_config_path(config_file.path());
126 | assert!(res.is_ok());
127 | let config = res.unwrap();
128 |
129 | assert_eq!(config.filetype.len(), 0);
130 | assert_eq!(config.handler_preview.len(), 0);
131 | assert_eq!(
132 | config.default_handler_preview,
133 | FileHandler {
134 | command: "file %i".to_owned(),
135 | wait: true,
136 | shell: false,
137 | no_pipe: false,
138 | stdin_arg: None
139 | }
140 | );
141 | assert_eq!(config.handler_open.len(), 0);
142 | assert_eq!(
143 | config.default_handler_open,
144 | FileHandler {
145 | command: "cat -A %i".to_owned(),
146 | wait: true,
147 | shell: false,
148 | no_pipe: false,
149 | stdin_arg: None
150 | }
151 | );
152 | assert_eq!(config.filter.len(), 0);
153 | }
154 |
155 | #[test]
156 | fn test_default_config() {
157 | const DEFAULT_CONFIG_STR: &str = include_str!("../config/config.toml.default");
158 | let mut config_file = tempfile::NamedTempFile::new().unwrap();
159 | config_file
160 | .write_all(DEFAULT_CONFIG_STR.as_bytes())
161 | .unwrap();
162 |
163 | let res = parse_config_path(config_file.path());
164 | assert!(res.is_ok());
165 | let config = res.unwrap();
166 |
167 | assert_eq!(config.filetype.len(), 2);
168 | assert_eq!(config.handler_preview.len(), 1);
169 | assert_eq!(
170 | config.default_handler_preview,
171 | FileHandler {
172 | command: "file %i".to_owned(),
173 | wait: true,
174 | shell: false,
175 | no_pipe: false,
176 | stdin_arg: None
177 | }
178 | );
179 | assert_eq!(config.handler_open.len(), 1);
180 | assert_eq!(
181 | config.default_handler_open,
182 | FileHandler {
183 | command: "cat -A %i".to_owned(),
184 | wait: true,
185 | shell: false,
186 | no_pipe: false,
187 | stdin_arg: None
188 | }
189 | );
190 | assert_eq!(config.filter.len(), 1);
191 | }
192 |
193 | #[test]
194 | fn test_advanced_config() {
195 | const ADVANCED_CONFIG_STR: &str = include_str!("../config/config.toml.advanced");
196 | let mut config_file = tempfile::NamedTempFile::new().unwrap();
197 | config_file
198 | .write_all(ADVANCED_CONFIG_STR.as_bytes())
199 | .unwrap();
200 |
201 | let res = parse_config_path(config_file.path());
202 | assert!(res.is_ok());
203 | let config = res.unwrap();
204 |
205 | assert_eq!(config.filetype.len(), 35);
206 | assert_eq!(config.handler_preview.len(), 25);
207 | assert_eq!(
208 | config.default_handler_preview,
209 | FileHandler {
210 | command: "echo '🔍 MIME: %m'; hexyl --border none %i | head -n $((%l - 1))"
211 | .to_owned(),
212 | wait: true,
213 | shell: true,
214 | no_pipe: false,
215 | stdin_arg: Some(String::new())
216 | }
217 | );
218 | assert_eq!(config.handler_open.len(), 20);
219 | assert_eq!(
220 | config.default_handler_open,
221 | FileHandler {
222 | command: "hexyl %i | less -R".to_owned(),
223 | wait: true,
224 | shell: true,
225 | no_pipe: false,
226 | stdin_arg: Some(String::new())
227 | }
228 | );
229 | assert_eq!(config.filter.len(), 5);
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/src/handler.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | collections::HashMap,
3 | env,
4 | fs::File,
5 | io::{self, copy, stdin, Read, Write},
6 | iter,
7 | os::unix::{
8 | fs::FileTypeExt,
9 | io::{AsRawFd, FromRawFd},
10 | },
11 | path::{Path, PathBuf},
12 | process::{Child, Command, Stdio},
13 | rc::Rc,
14 | };
15 |
16 | use anyhow::Context as _;
17 |
18 | use crate::{
19 | config,
20 | config::{FileFilter, FileHandler, SchemeHandler},
21 | RsopMode,
22 | };
23 |
24 | #[derive(Debug)]
25 | enum FileProcessor {
26 | Filter(FileFilter),
27 | Handler(FileHandler),
28 | }
29 |
30 | enum PipeOrTmpFile {
31 | Pipe(T),
32 | TmpFile(tempfile::NamedTempFile),
33 | }
34 |
35 | impl FileProcessor {
36 | /// Return true if command string contains a given % prefixed pattern
37 | fn has_pattern(&self, pattern: char) -> bool {
38 | let re_str = format!("[^%]%{pattern}");
39 | #[expect(clippy::unwrap_used)]
40 | let re = regex::Regex::new(&re_str).unwrap();
41 | let command = match self {
42 | FileProcessor::Filter(f) => &f.command,
43 | FileProcessor::Handler(h) => &h.command,
44 | };
45 | re.is_match(command)
46 | }
47 | }
48 |
49 | #[derive(Debug)]
50 | struct FileHandlers {
51 | extensions: HashMap>,
52 | mimes: HashMap>,
53 | default: FileHandler,
54 | }
55 |
56 | impl FileHandlers {
57 | pub(crate) fn new(default: &FileHandler) -> FileHandlers {
58 | FileHandlers {
59 | extensions: HashMap::new(),
60 | mimes: HashMap::new(),
61 | default: default.clone(),
62 | }
63 | }
64 |
65 | pub(crate) fn add(&mut self, processor: &Rc, filetype: &config::Filetype) {
66 | for extension in &filetype.extensions {
67 | self.extensions
68 | .insert(extension.clone(), Rc::clone(processor));
69 | }
70 | for mime in &filetype.mimes {
71 | self.mimes.insert(mime.clone(), Rc::clone(processor));
72 | }
73 | }
74 | }
75 |
76 | #[derive(Debug)]
77 | struct SchemeHandlers {
78 | schemes: HashMap,
79 | }
80 |
81 | impl SchemeHandlers {
82 | pub(crate) fn new() -> SchemeHandlers {
83 | SchemeHandlers {
84 | schemes: HashMap::new(),
85 | }
86 | }
87 |
88 | pub(crate) fn add(&mut self, handler: &SchemeHandler, scheme: &str) {
89 | self.schemes.insert(scheme.to_owned(), handler.clone());
90 | }
91 | }
92 |
93 | #[derive(Debug)]
94 | pub(crate) struct HandlerMapping {
95 | preview: FileHandlers,
96 | open: FileHandlers,
97 | edit: FileHandlers,
98 | scheme: SchemeHandlers,
99 | }
100 |
101 | #[derive(thiserror::Error, Debug)]
102 | pub(crate) enum HandlerError {
103 | #[error("Failed to run handler command {:?}: {err}", .cmd.connect(" "))]
104 | Start { err: io::Error, cmd: Vec },
105 | #[error("Failed to read input file {path:?}: {err}")]
106 | Input { err: io::Error, path: PathBuf },
107 | #[error(transparent)]
108 | Io(#[from] io::Error),
109 | #[error(transparent)]
110 | Other(#[from] anyhow::Error),
111 | }
112 |
113 | /// How many bytes to read from pipe to guess MIME type, use a full memory page
114 | const PIPE_INITIAL_READ_LENGTH: usize = 4096;
115 |
116 | impl HandlerMapping {
117 | #[expect(clippy::similar_names)]
118 | pub(crate) fn new(cfg: &config::Config) -> anyhow::Result {
119 | let mut handlers_open = FileHandlers::new(&cfg.default_handler_open);
120 | let mut handlers_edit = FileHandlers::new(&cfg.default_handler_open);
121 | let mut handlers_preview = FileHandlers::new(&cfg.default_handler_preview);
122 | for (name, filetype) in &cfg.filetype {
123 | let handler_open = cfg.handler_open.get(name).cloned();
124 | let handler_edit = cfg.handler_edit.get(name).cloned();
125 | let handler_preview = cfg.handler_preview.get(name).cloned();
126 | let filter = cfg.filter.get(name).cloned();
127 | anyhow::ensure!(
128 | handler_open.is_some()
129 | || handler_edit.is_some()
130 | || handler_preview.is_some()
131 | || filter.is_some(),
132 | "Filetype {} is not bound to any handler or filter",
133 | name
134 | );
135 | if let Some(handler_open) = handler_open {
136 | Self::validate_handler(&handler_open)?;
137 | handlers_open.add(&Rc::new(FileProcessor::Handler(handler_open)), filetype);
138 | }
139 | if let Some(handler_edit) = handler_edit {
140 | Self::validate_handler(&handler_edit)?;
141 | handlers_edit.add(&Rc::new(FileProcessor::Handler(handler_edit)), filetype);
142 | }
143 | if let Some(handler_preview) = handler_preview {
144 | Self::validate_handler(&handler_preview)?;
145 | handlers_preview.add(&Rc::new(FileProcessor::Handler(handler_preview)), filetype);
146 | }
147 | if let Some(filter) = filter {
148 | anyhow::ensure!(
149 | filter.no_pipe || (Self::count_pattern(&filter.command, 'i') <= 1),
150 | "Filter {:?} can not have both 'no_pipe = false' and multiple %i in command",
151 | filter
152 | );
153 | let proc_filter = Rc::new(FileProcessor::Filter(filter));
154 | handlers_open.add(&Rc::clone(&proc_filter), filetype);
155 | handlers_edit.add(&Rc::clone(&proc_filter), filetype);
156 | handlers_preview.add(&Rc::clone(&proc_filter), filetype);
157 | }
158 | }
159 |
160 | let mut handlers_scheme = SchemeHandlers::new();
161 | for (schemes, handler) in &cfg.handler_scheme {
162 | handlers_scheme.add(handler, schemes);
163 | }
164 |
165 | Ok(HandlerMapping {
166 | preview: handlers_preview,
167 | open: handlers_open,
168 | edit: handlers_edit,
169 | scheme: handlers_scheme,
170 | })
171 | }
172 |
173 | fn validate_handler(handler: &FileHandler) -> anyhow::Result<()> {
174 | anyhow::ensure!(
175 | !handler.no_pipe || handler.wait,
176 | "Handler {:?} can not have both 'no_pipe = true' and 'wait = false'",
177 | handler
178 | );
179 | anyhow::ensure!(
180 | handler.no_pipe || (Self::count_pattern(&handler.command, 'i') <= 1),
181 | "Handler {:?} can not have both 'no_pipe = false' and multiple %i in command",
182 | handler
183 | );
184 | Ok(())
185 | }
186 |
187 | /// Count number of a given % prefixed pattern in command string
188 | fn count_pattern(command: &str, pattern: char) -> usize {
189 | let re_str = format!("[^%]%{pattern}");
190 | #[expect(clippy::unwrap_used)]
191 | let re = regex::Regex::new(&re_str).unwrap();
192 | re.find_iter(command).count()
193 | }
194 |
195 | pub(crate) fn handle_path(&self, mode: &RsopMode, path: &Path) -> Result<(), HandlerError> {
196 | if let (RsopMode::XdgOpen, Ok(url)) = (
197 | &mode,
198 | url::Url::parse(
199 | path.to_str()
200 | .ok_or_else(|| anyhow::anyhow!("Unable to decode path {:?}", path))?,
201 | ),
202 | ) {
203 | if url.scheme() == "file" {
204 | let url_path = &url[url::Position::BeforeUsername..];
205 | let parsed_path = PathBuf::from(url_path);
206 | log::trace!("url={}, parsed_path={:?}", url, parsed_path);
207 | self.dispatch_path(&parsed_path, mode)
208 | } else {
209 | self.dispatch_url(&url)
210 | }
211 | } else {
212 | self.dispatch_path(path, mode)
213 | }
214 | }
215 |
216 | pub(crate) fn handle_pipe(&self, mode: &RsopMode) -> Result<(), HandlerError> {
217 | let stdin = Self::stdin_reader();
218 | self.dispatch_pipe(stdin, mode)
219 | }
220 |
221 | fn path_mime(path: &Path) -> Result