├── .cargo
└── config.toml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── _config.yml
├── _layouts
└── default.html
├── assets
├── css
│ └── style.scss
└── icon
│ └── terminal.ico
├── examples
├── (ಠ_ಠ).txt
├── (☞゚∀゚)☞ 'щ(ಠ益ಠщ)'.sh
├── Drag 'me!'
├── bash-test.sh
└── python-test.sh
├── package-release.ps1
├── wslscript
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── build.rs
└── src
│ ├── gui
│ ├── listview.rs
│ └── mod.rs
│ └── main.rs
├── wslscript_common
├── .gitignore
├── Cargo.lock
├── Cargo.toml
└── src
│ ├── error.rs
│ ├── font.rs
│ ├── icon.rs
│ ├── lib.rs
│ ├── registry.rs
│ ├── ver.rs
│ ├── win32.rs
│ └── wsl.rs
└── wslscript_handler
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── build.rs
└── src
├── interface.rs
├── lib.rs
└── progress.rs
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [profile.release]
2 | panic = "abort"
3 | lto = "fat"
4 | codegen-units = 1
5 | opt-level = 3
6 | debug = false
7 | debug-assertions = false
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vscode/
2 | /target/
3 | /build/
4 | **/*.rs.bk
5 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "android-tzdata"
7 | version = "0.1.1"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
10 |
11 | [[package]]
12 | name = "android_system_properties"
13 | version = "0.1.5"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
16 | dependencies = [
17 | "libc",
18 | ]
19 |
20 | [[package]]
21 | name = "anyhow"
22 | version = "1.0.96"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
25 |
26 | [[package]]
27 | name = "autocfg"
28 | version = "1.4.0"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
31 |
32 | [[package]]
33 | name = "bitflags"
34 | version = "2.8.0"
35 | source = "registry+https://github.com/rust-lang/crates.io-index"
36 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
37 |
38 | [[package]]
39 | name = "bumpalo"
40 | version = "3.17.0"
41 | source = "registry+https://github.com/rust-lang/crates.io-index"
42 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
43 |
44 | [[package]]
45 | name = "cc"
46 | version = "1.2.15"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
49 | dependencies = [
50 | "shlex",
51 | ]
52 |
53 | [[package]]
54 | name = "cfg-if"
55 | version = "1.0.0"
56 | source = "registry+https://github.com/rust-lang/crates.io-index"
57 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
58 |
59 | [[package]]
60 | name = "chrono"
61 | version = "0.4.39"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
64 | dependencies = [
65 | "android-tzdata",
66 | "iana-time-zone",
67 | "js-sys",
68 | "num-traits",
69 | "wasm-bindgen",
70 | "windows-targets 0.52.6",
71 | ]
72 |
73 | [[package]]
74 | name = "comedy"
75 | version = "0.2.0"
76 | source = "registry+https://github.com/rust-lang/crates.io-index"
77 | checksum = "74428ae4f7f05f32f4448e9f42d371538196919c4834979f4f96d1fdebffcb47"
78 | dependencies = [
79 | "winapi",
80 | ]
81 |
82 | [[package]]
83 | name = "core-foundation-sys"
84 | version = "0.8.7"
85 | source = "registry+https://github.com/rust-lang/crates.io-index"
86 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
87 |
88 | [[package]]
89 | name = "equivalent"
90 | version = "1.0.2"
91 | source = "registry+https://github.com/rust-lang/crates.io-index"
92 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
93 |
94 | [[package]]
95 | name = "guid_win"
96 | version = "0.2.0"
97 | source = "registry+https://github.com/rust-lang/crates.io-index"
98 | checksum = "d87f4be87a557b98b4e4316f2009834f4448652938a950c1e8b33ae25f6f183b"
99 | dependencies = [
100 | "comedy",
101 | "winapi",
102 | ]
103 |
104 | [[package]]
105 | name = "hashbrown"
106 | version = "0.15.2"
107 | source = "registry+https://github.com/rust-lang/crates.io-index"
108 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
109 |
110 | [[package]]
111 | name = "iana-time-zone"
112 | version = "0.1.61"
113 | source = "registry+https://github.com/rust-lang/crates.io-index"
114 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
115 | dependencies = [
116 | "android_system_properties",
117 | "core-foundation-sys",
118 | "iana-time-zone-haiku",
119 | "js-sys",
120 | "wasm-bindgen",
121 | "windows-core 0.52.0",
122 | ]
123 |
124 | [[package]]
125 | name = "iana-time-zone-haiku"
126 | version = "0.1.2"
127 | source = "registry+https://github.com/rust-lang/crates.io-index"
128 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
129 | dependencies = [
130 | "cc",
131 | ]
132 |
133 | [[package]]
134 | name = "indexmap"
135 | version = "2.7.1"
136 | source = "registry+https://github.com/rust-lang/crates.io-index"
137 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
138 | dependencies = [
139 | "equivalent",
140 | "hashbrown",
141 | ]
142 |
143 | [[package]]
144 | name = "js-sys"
145 | version = "0.3.77"
146 | source = "registry+https://github.com/rust-lang/crates.io-index"
147 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
148 | dependencies = [
149 | "once_cell",
150 | "wasm-bindgen",
151 | ]
152 |
153 | [[package]]
154 | name = "lazy_static"
155 | version = "1.5.0"
156 | source = "registry+https://github.com/rust-lang/crates.io-index"
157 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
158 |
159 | [[package]]
160 | name = "libc"
161 | version = "0.2.169"
162 | source = "registry+https://github.com/rust-lang/crates.io-index"
163 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
164 |
165 | [[package]]
166 | name = "libloading"
167 | version = "0.8.6"
168 | source = "registry+https://github.com/rust-lang/crates.io-index"
169 | checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
170 | dependencies = [
171 | "cfg-if",
172 | "windows-targets 0.52.6",
173 | ]
174 |
175 | [[package]]
176 | name = "log"
177 | version = "0.4.26"
178 | source = "registry+https://github.com/rust-lang/crates.io-index"
179 | checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
180 |
181 | [[package]]
182 | name = "memchr"
183 | version = "2.7.4"
184 | source = "registry+https://github.com/rust-lang/crates.io-index"
185 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
186 |
187 | [[package]]
188 | name = "num-traits"
189 | version = "0.2.19"
190 | source = "registry+https://github.com/rust-lang/crates.io-index"
191 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
192 | dependencies = [
193 | "autocfg",
194 | ]
195 |
196 | [[package]]
197 | name = "num_enum"
198 | version = "0.7.3"
199 | source = "registry+https://github.com/rust-lang/crates.io-index"
200 | checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
201 | dependencies = [
202 | "num_enum_derive",
203 | ]
204 |
205 | [[package]]
206 | name = "num_enum_derive"
207 | version = "0.7.3"
208 | source = "registry+https://github.com/rust-lang/crates.io-index"
209 | checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
210 | dependencies = [
211 | "proc-macro-crate",
212 | "proc-macro2",
213 | "quote",
214 | "syn 2.0.98",
215 | ]
216 |
217 | [[package]]
218 | name = "once_cell"
219 | version = "1.20.3"
220 | source = "registry+https://github.com/rust-lang/crates.io-index"
221 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
222 |
223 | [[package]]
224 | name = "proc-macro-crate"
225 | version = "3.2.0"
226 | source = "registry+https://github.com/rust-lang/crates.io-index"
227 | checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
228 | dependencies = [
229 | "toml_edit",
230 | ]
231 |
232 | [[package]]
233 | name = "proc-macro2"
234 | version = "1.0.93"
235 | source = "registry+https://github.com/rust-lang/crates.io-index"
236 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
237 | dependencies = [
238 | "unicode-ident",
239 | ]
240 |
241 | [[package]]
242 | name = "quote"
243 | version = "1.0.38"
244 | source = "registry+https://github.com/rust-lang/crates.io-index"
245 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
246 | dependencies = [
247 | "proc-macro2",
248 | ]
249 |
250 | [[package]]
251 | name = "redox_syscall"
252 | version = "0.1.57"
253 | source = "registry+https://github.com/rust-lang/crates.io-index"
254 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
255 |
256 | [[package]]
257 | name = "rustversion"
258 | version = "1.0.19"
259 | source = "registry+https://github.com/rust-lang/crates.io-index"
260 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
261 |
262 | [[package]]
263 | name = "serde"
264 | version = "1.0.218"
265 | source = "registry+https://github.com/rust-lang/crates.io-index"
266 | checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
267 | dependencies = [
268 | "serde_derive",
269 | ]
270 |
271 | [[package]]
272 | name = "serde_derive"
273 | version = "1.0.218"
274 | source = "registry+https://github.com/rust-lang/crates.io-index"
275 | checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
276 | dependencies = [
277 | "proc-macro2",
278 | "quote",
279 | "syn 2.0.98",
280 | ]
281 |
282 | [[package]]
283 | name = "serde_spanned"
284 | version = "0.6.8"
285 | source = "registry+https://github.com/rust-lang/crates.io-index"
286 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
287 | dependencies = [
288 | "serde",
289 | ]
290 |
291 | [[package]]
292 | name = "shlex"
293 | version = "1.3.0"
294 | source = "registry+https://github.com/rust-lang/crates.io-index"
295 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
296 |
297 | [[package]]
298 | name = "simple-logging"
299 | version = "2.0.2"
300 | source = "registry+https://github.com/rust-lang/crates.io-index"
301 | checksum = "b00d48e85675326bb182a2286ea7c1a0b264333ae10f27a937a72be08628b542"
302 | dependencies = [
303 | "lazy_static",
304 | "log",
305 | "thread-id",
306 | ]
307 |
308 | [[package]]
309 | name = "syn"
310 | version = "1.0.109"
311 | source = "registry+https://github.com/rust-lang/crates.io-index"
312 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
313 | dependencies = [
314 | "proc-macro2",
315 | "quote",
316 | "unicode-ident",
317 | ]
318 |
319 | [[package]]
320 | name = "syn"
321 | version = "2.0.98"
322 | source = "registry+https://github.com/rust-lang/crates.io-index"
323 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
324 | dependencies = [
325 | "proc-macro2",
326 | "quote",
327 | "unicode-ident",
328 | ]
329 |
330 | [[package]]
331 | name = "thiserror"
332 | version = "2.0.11"
333 | source = "registry+https://github.com/rust-lang/crates.io-index"
334 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
335 | dependencies = [
336 | "thiserror-impl",
337 | ]
338 |
339 | [[package]]
340 | name = "thiserror-impl"
341 | version = "2.0.11"
342 | source = "registry+https://github.com/rust-lang/crates.io-index"
343 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
344 | dependencies = [
345 | "proc-macro2",
346 | "quote",
347 | "syn 2.0.98",
348 | ]
349 |
350 | [[package]]
351 | name = "thread-id"
352 | version = "3.3.0"
353 | source = "registry+https://github.com/rust-lang/crates.io-index"
354 | checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1"
355 | dependencies = [
356 | "libc",
357 | "redox_syscall",
358 | "winapi",
359 | ]
360 |
361 | [[package]]
362 | name = "toml"
363 | version = "0.5.11"
364 | source = "registry+https://github.com/rust-lang/crates.io-index"
365 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
366 | dependencies = [
367 | "serde",
368 | ]
369 |
370 | [[package]]
371 | name = "toml"
372 | version = "0.8.20"
373 | source = "registry+https://github.com/rust-lang/crates.io-index"
374 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
375 | dependencies = [
376 | "serde",
377 | "serde_spanned",
378 | "toml_datetime",
379 | "toml_edit",
380 | ]
381 |
382 | [[package]]
383 | name = "toml_datetime"
384 | version = "0.6.8"
385 | source = "registry+https://github.com/rust-lang/crates.io-index"
386 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
387 | dependencies = [
388 | "serde",
389 | ]
390 |
391 | [[package]]
392 | name = "toml_edit"
393 | version = "0.22.24"
394 | source = "registry+https://github.com/rust-lang/crates.io-index"
395 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
396 | dependencies = [
397 | "indexmap",
398 | "serde",
399 | "serde_spanned",
400 | "toml_datetime",
401 | "winnow",
402 | ]
403 |
404 | [[package]]
405 | name = "unicode-ident"
406 | version = "1.0.17"
407 | source = "registry+https://github.com/rust-lang/crates.io-index"
408 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
409 |
410 | [[package]]
411 | name = "wasm-bindgen"
412 | version = "0.2.100"
413 | source = "registry+https://github.com/rust-lang/crates.io-index"
414 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
415 | dependencies = [
416 | "cfg-if",
417 | "once_cell",
418 | "rustversion",
419 | "wasm-bindgen-macro",
420 | ]
421 |
422 | [[package]]
423 | name = "wasm-bindgen-backend"
424 | version = "0.2.100"
425 | source = "registry+https://github.com/rust-lang/crates.io-index"
426 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
427 | dependencies = [
428 | "bumpalo",
429 | "log",
430 | "proc-macro2",
431 | "quote",
432 | "syn 2.0.98",
433 | "wasm-bindgen-shared",
434 | ]
435 |
436 | [[package]]
437 | name = "wasm-bindgen-macro"
438 | version = "0.2.100"
439 | source = "registry+https://github.com/rust-lang/crates.io-index"
440 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
441 | dependencies = [
442 | "quote",
443 | "wasm-bindgen-macro-support",
444 | ]
445 |
446 | [[package]]
447 | name = "wasm-bindgen-macro-support"
448 | version = "0.2.100"
449 | source = "registry+https://github.com/rust-lang/crates.io-index"
450 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
451 | dependencies = [
452 | "proc-macro2",
453 | "quote",
454 | "syn 2.0.98",
455 | "wasm-bindgen-backend",
456 | "wasm-bindgen-shared",
457 | ]
458 |
459 | [[package]]
460 | name = "wasm-bindgen-shared"
461 | version = "0.2.100"
462 | source = "registry+https://github.com/rust-lang/crates.io-index"
463 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
464 | dependencies = [
465 | "unicode-ident",
466 | ]
467 |
468 | [[package]]
469 | name = "wchar"
470 | version = "0.11.1"
471 | source = "registry+https://github.com/rust-lang/crates.io-index"
472 | checksum = "e8be48fe4c433c0d4aa71bb8759c5f7b1da6dacb1b99998566ebe16503f6a59c"
473 | dependencies = [
474 | "wchar-impl",
475 | ]
476 |
477 | [[package]]
478 | name = "wchar-impl"
479 | version = "0.11.0"
480 | source = "registry+https://github.com/rust-lang/crates.io-index"
481 | checksum = "075c93156fed21f9dab57af5e81604d0fdb67432c919a8c1f78bb979f06a3d25"
482 | dependencies = [
483 | "proc-macro2",
484 | "quote",
485 | "syn 1.0.109",
486 | ]
487 |
488 | [[package]]
489 | name = "widestring"
490 | version = "1.1.0"
491 | source = "registry+https://github.com/rust-lang/crates.io-index"
492 | checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
493 |
494 | [[package]]
495 | name = "winapi"
496 | version = "0.3.9"
497 | source = "registry+https://github.com/rust-lang/crates.io-index"
498 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
499 | dependencies = [
500 | "winapi-i686-pc-windows-gnu",
501 | "winapi-x86_64-pc-windows-gnu",
502 | ]
503 |
504 | [[package]]
505 | name = "winapi-i686-pc-windows-gnu"
506 | version = "0.4.0"
507 | source = "registry+https://github.com/rust-lang/crates.io-index"
508 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
509 |
510 | [[package]]
511 | name = "winapi-x86_64-pc-windows-gnu"
512 | version = "0.4.0"
513 | source = "registry+https://github.com/rust-lang/crates.io-index"
514 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
515 |
516 | [[package]]
517 | name = "windows"
518 | version = "0.59.0"
519 | source = "registry+https://github.com/rust-lang/crates.io-index"
520 | checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1"
521 | dependencies = [
522 | "windows-core 0.59.0",
523 | "windows-targets 0.53.0",
524 | ]
525 |
526 | [[package]]
527 | name = "windows-core"
528 | version = "0.52.0"
529 | source = "registry+https://github.com/rust-lang/crates.io-index"
530 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
531 | dependencies = [
532 | "windows-targets 0.52.6",
533 | ]
534 |
535 | [[package]]
536 | name = "windows-core"
537 | version = "0.59.0"
538 | source = "registry+https://github.com/rust-lang/crates.io-index"
539 | checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce"
540 | dependencies = [
541 | "windows-implement",
542 | "windows-interface",
543 | "windows-result",
544 | "windows-strings",
545 | "windows-targets 0.53.0",
546 | ]
547 |
548 | [[package]]
549 | name = "windows-implement"
550 | version = "0.59.0"
551 | source = "registry+https://github.com/rust-lang/crates.io-index"
552 | checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
553 | dependencies = [
554 | "proc-macro2",
555 | "quote",
556 | "syn 2.0.98",
557 | ]
558 |
559 | [[package]]
560 | name = "windows-interface"
561 | version = "0.59.0"
562 | source = "registry+https://github.com/rust-lang/crates.io-index"
563 | checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01"
564 | dependencies = [
565 | "proc-macro2",
566 | "quote",
567 | "syn 2.0.98",
568 | ]
569 |
570 | [[package]]
571 | name = "windows-result"
572 | version = "0.3.0"
573 | source = "registry+https://github.com/rust-lang/crates.io-index"
574 | checksum = "d08106ce80268c4067c0571ca55a9b4e9516518eaa1a1fe9b37ca403ae1d1a34"
575 | dependencies = [
576 | "windows-targets 0.53.0",
577 | ]
578 |
579 | [[package]]
580 | name = "windows-strings"
581 | version = "0.3.0"
582 | source = "registry+https://github.com/rust-lang/crates.io-index"
583 | checksum = "b888f919960b42ea4e11c2f408fadb55f78a9f236d5eef084103c8ce52893491"
584 | dependencies = [
585 | "windows-targets 0.53.0",
586 | ]
587 |
588 | [[package]]
589 | name = "windows-sys"
590 | version = "0.59.0"
591 | source = "registry+https://github.com/rust-lang/crates.io-index"
592 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
593 | dependencies = [
594 | "windows-targets 0.52.6",
595 | ]
596 |
597 | [[package]]
598 | name = "windows-targets"
599 | version = "0.52.6"
600 | source = "registry+https://github.com/rust-lang/crates.io-index"
601 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
602 | dependencies = [
603 | "windows_aarch64_gnullvm 0.52.6",
604 | "windows_aarch64_msvc 0.52.6",
605 | "windows_i686_gnu 0.52.6",
606 | "windows_i686_gnullvm 0.52.6",
607 | "windows_i686_msvc 0.52.6",
608 | "windows_x86_64_gnu 0.52.6",
609 | "windows_x86_64_gnullvm 0.52.6",
610 | "windows_x86_64_msvc 0.52.6",
611 | ]
612 |
613 | [[package]]
614 | name = "windows-targets"
615 | version = "0.53.0"
616 | source = "registry+https://github.com/rust-lang/crates.io-index"
617 | checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
618 | dependencies = [
619 | "windows_aarch64_gnullvm 0.53.0",
620 | "windows_aarch64_msvc 0.53.0",
621 | "windows_i686_gnu 0.53.0",
622 | "windows_i686_gnullvm 0.53.0",
623 | "windows_i686_msvc 0.53.0",
624 | "windows_x86_64_gnu 0.53.0",
625 | "windows_x86_64_gnullvm 0.53.0",
626 | "windows_x86_64_msvc 0.53.0",
627 | ]
628 |
629 | [[package]]
630 | name = "windows_aarch64_gnullvm"
631 | version = "0.52.6"
632 | source = "registry+https://github.com/rust-lang/crates.io-index"
633 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
634 |
635 | [[package]]
636 | name = "windows_aarch64_gnullvm"
637 | version = "0.53.0"
638 | source = "registry+https://github.com/rust-lang/crates.io-index"
639 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
640 |
641 | [[package]]
642 | name = "windows_aarch64_msvc"
643 | version = "0.52.6"
644 | source = "registry+https://github.com/rust-lang/crates.io-index"
645 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
646 |
647 | [[package]]
648 | name = "windows_aarch64_msvc"
649 | version = "0.53.0"
650 | source = "registry+https://github.com/rust-lang/crates.io-index"
651 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
652 |
653 | [[package]]
654 | name = "windows_i686_gnu"
655 | version = "0.52.6"
656 | source = "registry+https://github.com/rust-lang/crates.io-index"
657 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
658 |
659 | [[package]]
660 | name = "windows_i686_gnu"
661 | version = "0.53.0"
662 | source = "registry+https://github.com/rust-lang/crates.io-index"
663 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
664 |
665 | [[package]]
666 | name = "windows_i686_gnullvm"
667 | version = "0.52.6"
668 | source = "registry+https://github.com/rust-lang/crates.io-index"
669 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
670 |
671 | [[package]]
672 | name = "windows_i686_gnullvm"
673 | version = "0.53.0"
674 | source = "registry+https://github.com/rust-lang/crates.io-index"
675 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
676 |
677 | [[package]]
678 | name = "windows_i686_msvc"
679 | version = "0.52.6"
680 | source = "registry+https://github.com/rust-lang/crates.io-index"
681 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
682 |
683 | [[package]]
684 | name = "windows_i686_msvc"
685 | version = "0.53.0"
686 | source = "registry+https://github.com/rust-lang/crates.io-index"
687 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
688 |
689 | [[package]]
690 | name = "windows_x86_64_gnu"
691 | version = "0.52.6"
692 | source = "registry+https://github.com/rust-lang/crates.io-index"
693 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
694 |
695 | [[package]]
696 | name = "windows_x86_64_gnu"
697 | version = "0.53.0"
698 | source = "registry+https://github.com/rust-lang/crates.io-index"
699 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
700 |
701 | [[package]]
702 | name = "windows_x86_64_gnullvm"
703 | version = "0.52.6"
704 | source = "registry+https://github.com/rust-lang/crates.io-index"
705 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
706 |
707 | [[package]]
708 | name = "windows_x86_64_gnullvm"
709 | version = "0.53.0"
710 | source = "registry+https://github.com/rust-lang/crates.io-index"
711 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
712 |
713 | [[package]]
714 | name = "windows_x86_64_msvc"
715 | version = "0.52.6"
716 | source = "registry+https://github.com/rust-lang/crates.io-index"
717 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
718 |
719 | [[package]]
720 | name = "windows_x86_64_msvc"
721 | version = "0.53.0"
722 | source = "registry+https://github.com/rust-lang/crates.io-index"
723 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
724 |
725 | [[package]]
726 | name = "winnow"
727 | version = "0.7.3"
728 | source = "registry+https://github.com/rust-lang/crates.io-index"
729 | checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
730 | dependencies = [
731 | "memchr",
732 | ]
733 |
734 | [[package]]
735 | name = "winreg"
736 | version = "0.55.0"
737 | source = "registry+https://github.com/rust-lang/crates.io-index"
738 | checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
739 | dependencies = [
740 | "cfg-if",
741 | "windows-sys",
742 | ]
743 |
744 | [[package]]
745 | name = "winres"
746 | version = "0.1.12"
747 | source = "registry+https://github.com/rust-lang/crates.io-index"
748 | checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
749 | dependencies = [
750 | "toml 0.5.11",
751 | ]
752 |
753 | [[package]]
754 | name = "wslscript"
755 | version = "0.7.1"
756 | dependencies = [
757 | "chrono",
758 | "log",
759 | "num_enum",
760 | "once_cell",
761 | "serde",
762 | "serde_derive",
763 | "simple-logging",
764 | "toml 0.8.20",
765 | "wchar",
766 | "widestring",
767 | "winapi",
768 | "winres",
769 | "wslscript_common",
770 | ]
771 |
772 | [[package]]
773 | name = "wslscript_common"
774 | version = "0.1.0"
775 | dependencies = [
776 | "anyhow",
777 | "guid_win",
778 | "libloading",
779 | "log",
780 | "once_cell",
781 | "simple-logging",
782 | "thiserror",
783 | "wchar",
784 | "widestring",
785 | "winapi",
786 | "winreg",
787 | ]
788 |
789 | [[package]]
790 | name = "wslscript_handler"
791 | version = "0.1.0"
792 | dependencies = [
793 | "bitflags",
794 | "chrono",
795 | "guid_win",
796 | "log",
797 | "num_enum",
798 | "once_cell",
799 | "serde",
800 | "serde_derive",
801 | "simple-logging",
802 | "toml 0.8.20",
803 | "wchar",
804 | "widestring",
805 | "winapi",
806 | "windows",
807 | "windows-core 0.59.0",
808 | "winres",
809 | "wslscript_common",
810 | ]
811 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = ["wslscript", "wslscript_handler", "wslscript_common"]
3 | resolver = "2"
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 - 2025 Joni Kollani
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [WSL Script](https://sop.github.io/wslscript/)
2 |
3 | Shell script _(.sh)_ handler for
4 | [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) _(WSL)_.
5 |
6 | Associates .sh _(or any other)_ extension to be executed in WSL.
7 | Automatically handles Windows → Unix path conversions.
8 | Files can be dragged and dropped to registered file icon in explorer
9 | to pass paths as arguments.
10 |
11 | ## Usage
12 |
13 | Copy `wslscript.exe` and `wslscript_handler.dll` to a location of your choice.
14 | These files are used to invoke WSL, so don't move them afterwards.
15 |
16 | Run `wslscript.exe` to open a setup GUI.
17 | Enter the extension and click _Register_ button to add filetype association
18 | into Windows registry.
19 |
20 | After registration, `.sh` files can be executed from explorer by double clicking.
21 | Other files can be passed as path arguments by dragging and dropping them into
22 | `.sh` file icon.
23 |
24 | Scripts are executed in the same folder where the script file is located,
25 | ie. `$PWD` is set to script's directory.
26 |
27 | ## Tips
28 |
29 | ### Change the Default User
30 |
31 | If scripts run as root, you may wish to [change the default WSL user](https://learn.microsoft.com/en-us/windows/wsl/wsl-config#user-settings).
32 |
33 | Add the following to `/etc/wsl.conf` file:
34 |
35 | ```ini
36 | [user]
37 | default = username
38 | ```
39 |
40 | ## TODO
41 |
42 | - [ ] Optionally register for all users
43 |
44 | ## License
45 |
46 | This project is licensed under the
47 | [MIT License](https://github.com/sop/wslscript/blob/master/LICENSE).
48 |
49 | Icon by [Tango Desktop Project](http://tango.freedesktop.org/Tango_Desktop_Project).
50 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-hacker
2 | title: WSL Script
3 | description: Associate shell script files with Windows Subsystem for Linux.
4 | release_url: https://github.com/sop/wslscript/releases/latest
--------------------------------------------------------------------------------
/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {% seo %}
10 |
11 |
12 |
13 |
14 |
15 |
18 |
{{ site.description | default: site.github.project_tagline }}
19 |
20 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/assets/css/style.scss:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
4 | @import "{{ site.theme }}";
5 |
6 | h1 a {
7 | color: inherit;
8 | text-shadow: none;
9 | text-decoration: none;
10 | }
11 |
12 | header h1::before {
13 | content: none;
14 | }
15 |
16 | h1 img {
17 | vertical-align: text-bottom;
18 | margin-right: 8px;
19 | }
20 |
--------------------------------------------------------------------------------
/assets/icon/terminal.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sop/wslscript/3d85b33b1c9ebf456cb224ba0e842e92cf03a4a6/assets/icon/terminal.ico
--------------------------------------------------------------------------------
/examples/(ಠ_ಠ).txt:
--------------------------------------------------------------------------------
1 | This file has unicode characters in its filename and may be used to test drag & drop handling.
--------------------------------------------------------------------------------
/examples/(☞゚∀゚)☞ 'щ(ಠ益ಠщ)'.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # This script has unicode characters, whitespace and single quotes in its filename.
3 |
4 | printf 'This script'\''s filename is "%s"\n' "$0"
5 | printf 'Canonical path is "%s"\n' "$(readlink -f "$0")"
6 | argv=("$0" "$@")
7 | argc=${#argv[@]}
8 | for ((i = 0; i < $argc; i++)); do
9 | arg="${argv[$i]}"
10 | printf 'Argument #%d: %s\n' "$i" "$arg"
11 | if [[ -e "$arg" ]]; then
12 | stat "$arg"
13 | fi
14 | done
15 | # exit with an error to leave terminal open
16 | exit 1
17 |
--------------------------------------------------------------------------------
/examples/Drag 'me!':
--------------------------------------------------------------------------------
1 | This file has single quotes in its filename and may be used to test drag & drop handling.
--------------------------------------------------------------------------------
/examples/bash-test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | printf "Current directory: %s\n" "$PWD"
4 | printf "Path to this script: %s\n" "$0"
5 | argv=("$@")
6 | argc=${#argv[@]}
7 | for ((i = 0; i < $argc; i++)); do
8 | arg="${argv[$i]}"
9 | printf 'Argument #%d: %s\n' "$(($i + 1))" "$arg"
10 | if [[ -e "$arg" ]]; then
11 | stat "$arg"
12 | fi
13 | done
14 | # exit with an error to leave terminal open
15 | exit 1
16 |
--------------------------------------------------------------------------------
/examples/python-test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import sys
4 | import io
5 |
6 | for i, arg in enumerate(sys.argv):
7 | print("Argument #{}: {}".format(i, arg))
8 | with open(arg, 'rb') as f:
9 | f.seek(0, io.SEEK_END)
10 | print("File size is {} bytes.".format(f.tell()))
11 | sys.exit(1)
12 |
--------------------------------------------------------------------------------
/package-release.ps1:
--------------------------------------------------------------------------------
1 | $version = Get-Content .\wslscript\Cargo.toml |
2 | Select-String -Pattern '^version = "([^"]+)"' |
3 | Select-Object -First 1 | ForEach-Object {
4 | $_.Matches.Groups[1].Value
5 | }
6 | $buildir = "build"
7 | New-Item -ItemType Directory -Name $buildir -ErrorAction Ignore
8 | $srcdir = "target\release"
9 | $a = @{
10 | DestinationPath = "$buildir\wslscript-$version.zip"
11 | Path = "$srcdir\wslscript.exe", "$srcdir\wslscript_handler.dll"
12 | Force = $true
13 | }
14 | Compress-Archive @a
15 |
--------------------------------------------------------------------------------
/wslscript/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 |
--------------------------------------------------------------------------------
/wslscript/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.13.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072"
10 | dependencies = [
11 | "gimli",
12 | ]
13 |
14 | [[package]]
15 | name = "adler"
16 | version = "0.2.3"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
19 |
20 | [[package]]
21 | name = "autocfg"
22 | version = "1.0.1"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
25 |
26 | [[package]]
27 | name = "backtrace"
28 | version = "0.1.8"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "150ae7828afa7afb6d474f909d64072d21de1f3365b6e8ad8029bf7b1c6350a0"
31 | dependencies = [
32 | "backtrace-sys",
33 | "cfg-if 0.1.10",
34 | "dbghelp-sys",
35 | "debug-builders",
36 | "kernel32-sys",
37 | "libc",
38 | "winapi 0.2.8",
39 | ]
40 |
41 | [[package]]
42 | name = "backtrace"
43 | version = "0.3.53"
44 | source = "registry+https://github.com/rust-lang/crates.io-index"
45 | checksum = "707b586e0e2f247cbde68cdd2c3ce69ea7b7be43e1c5b426e37c9319c4b9838e"
46 | dependencies = [
47 | "addr2line",
48 | "cfg-if 1.0.0",
49 | "libc",
50 | "miniz_oxide",
51 | "object",
52 | "rustc-demangle",
53 | ]
54 |
55 | [[package]]
56 | name = "backtrace-sys"
57 | version = "0.1.37"
58 | source = "registry+https://github.com/rust-lang/crates.io-index"
59 | checksum = "18fbebbe1c9d1f383a9cc7e8ccdb471b91c8d024ee9c2ca5b5346121fe8b4399"
60 | dependencies = [
61 | "cc",
62 | "libc",
63 | ]
64 |
65 | [[package]]
66 | name = "bitflags"
67 | version = "0.7.0"
68 | source = "registry+https://github.com/rust-lang/crates.io-index"
69 | checksum = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
70 |
71 | [[package]]
72 | name = "bitflags"
73 | version = "1.2.1"
74 | source = "registry+https://github.com/rust-lang/crates.io-index"
75 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
76 |
77 | [[package]]
78 | name = "byteorder"
79 | version = "1.3.4"
80 | source = "registry+https://github.com/rust-lang/crates.io-index"
81 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
82 |
83 | [[package]]
84 | name = "cc"
85 | version = "1.0.61"
86 | source = "registry+https://github.com/rust-lang/crates.io-index"
87 | checksum = "ed67cbde08356238e75fc4656be4749481eeffb09e19f320a25237d5221c985d"
88 |
89 | [[package]]
90 | name = "cfg-if"
91 | version = "0.1.10"
92 | source = "registry+https://github.com/rust-lang/crates.io-index"
93 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
94 |
95 | [[package]]
96 | name = "cfg-if"
97 | version = "1.0.0"
98 | source = "registry+https://github.com/rust-lang/crates.io-index"
99 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
100 |
101 | [[package]]
102 | name = "chomp"
103 | version = "0.3.1"
104 | source = "registry+https://github.com/rust-lang/crates.io-index"
105 | checksum = "9f74ad218e66339b11fd23f693fb8f1d621e80ba6ac218297be26073365d163d"
106 | dependencies = [
107 | "bitflags 0.7.0",
108 | "conv",
109 | "debugtrace",
110 | "either",
111 | ]
112 |
113 | [[package]]
114 | name = "chrono"
115 | version = "0.4.19"
116 | source = "registry+https://github.com/rust-lang/crates.io-index"
117 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
118 | dependencies = [
119 | "libc",
120 | "num-integer",
121 | "num-traits",
122 | "time",
123 | "winapi 0.3.9",
124 | ]
125 |
126 | [[package]]
127 | name = "cloudabi"
128 | version = "0.0.3"
129 | source = "registry+https://github.com/rust-lang/crates.io-index"
130 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
131 | dependencies = [
132 | "bitflags 1.2.1",
133 | ]
134 |
135 | [[package]]
136 | name = "conv"
137 | version = "0.3.3"
138 | source = "registry+https://github.com/rust-lang/crates.io-index"
139 | checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299"
140 | dependencies = [
141 | "custom_derive",
142 | ]
143 |
144 | [[package]]
145 | name = "custom_derive"
146 | version = "0.1.7"
147 | source = "registry+https://github.com/rust-lang/crates.io-index"
148 | checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9"
149 |
150 | [[package]]
151 | name = "dbghelp-sys"
152 | version = "0.2.0"
153 | source = "registry+https://github.com/rust-lang/crates.io-index"
154 | checksum = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850"
155 | dependencies = [
156 | "winapi 0.2.8",
157 | "winapi-build",
158 | ]
159 |
160 | [[package]]
161 | name = "debug-builders"
162 | version = "0.1.0"
163 | source = "registry+https://github.com/rust-lang/crates.io-index"
164 | checksum = "0f5d8e3d14cabcb2a8a59d7147289173c6ada77a0bc526f6b85078f941c0cf12"
165 |
166 | [[package]]
167 | name = "debugtrace"
168 | version = "0.1.0"
169 | source = "registry+https://github.com/rust-lang/crates.io-index"
170 | checksum = "62e432bd83c5d70317f6ebd8a50ed4afb32907c64d6e2e1e65e339b06dc553f3"
171 | dependencies = [
172 | "backtrace 0.1.8",
173 | ]
174 |
175 | [[package]]
176 | name = "either"
177 | version = "0.1.7"
178 | source = "registry+https://github.com/rust-lang/crates.io-index"
179 | checksum = "a39bffec1e2015c5d8a6773cb0cf48d0d758c842398f624c34969071f5499ea7"
180 |
181 | [[package]]
182 | name = "failure"
183 | version = "0.1.8"
184 | source = "registry+https://github.com/rust-lang/crates.io-index"
185 | checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
186 | dependencies = [
187 | "backtrace 0.3.53",
188 | "failure_derive",
189 | ]
190 |
191 | [[package]]
192 | name = "failure_derive"
193 | version = "0.1.8"
194 | source = "registry+https://github.com/rust-lang/crates.io-index"
195 | checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
196 | dependencies = [
197 | "proc-macro2 1.0.24",
198 | "quote 1.0.7",
199 | "syn 1.0.48",
200 | "synstructure",
201 | ]
202 |
203 | [[package]]
204 | name = "fuchsia-cprng"
205 | version = "0.1.1"
206 | source = "registry+https://github.com/rust-lang/crates.io-index"
207 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
208 |
209 | [[package]]
210 | name = "gimli"
211 | version = "0.22.0"
212 | source = "registry+https://github.com/rust-lang/crates.io-index"
213 | checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724"
214 |
215 | [[package]]
216 | name = "guid"
217 | version = "0.1.0"
218 | source = "registry+https://github.com/rust-lang/crates.io-index"
219 | checksum = "e691c64d9b226c7597e29aeb46be753beb8c9eeef96d8c78dfd4d306338a38da"
220 | dependencies = [
221 | "chomp",
222 | "failure",
223 | "failure_derive",
224 | "guid-macro-impl",
225 | "guid-parser",
226 | "proc-macro-hack 0.4.2",
227 | "winapi 0.2.8",
228 | ]
229 |
230 | [[package]]
231 | name = "guid-create"
232 | version = "0.1.1"
233 | source = "registry+https://github.com/rust-lang/crates.io-index"
234 | checksum = "fcea207bf7a6092166ab590f98fe5dde5a7deed1f1920d98dcac31f80814c40d"
235 | dependencies = [
236 | "byteorder",
237 | "chomp",
238 | "guid",
239 | "guid-parser",
240 | "rand",
241 | "winapi 0.3.9",
242 | ]
243 |
244 | [[package]]
245 | name = "guid-macro-impl"
246 | version = "0.1.0"
247 | source = "registry+https://github.com/rust-lang/crates.io-index"
248 | checksum = "08d50f7c496073b5a5dec0f6f1c149113a50960ce25dd2a559987a5a71190816"
249 | dependencies = [
250 | "chomp",
251 | "guid-parser",
252 | "proc-macro-hack 0.4.2",
253 | "quote 0.4.2",
254 | "syn 0.12.15",
255 | ]
256 |
257 | [[package]]
258 | name = "guid-parser"
259 | version = "0.1.0"
260 | source = "registry+https://github.com/rust-lang/crates.io-index"
261 | checksum = "abc7adb441828023999e6cff9eb1ea63156f7ec37ab5bf690005e8fc6c1148ad"
262 | dependencies = [
263 | "chomp",
264 | "winapi 0.2.8",
265 | ]
266 |
267 | [[package]]
268 | name = "kernel32-sys"
269 | version = "0.2.2"
270 | source = "registry+https://github.com/rust-lang/crates.io-index"
271 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
272 | dependencies = [
273 | "winapi 0.2.8",
274 | "winapi-build",
275 | ]
276 |
277 | [[package]]
278 | name = "lazy_static"
279 | version = "1.4.0"
280 | source = "registry+https://github.com/rust-lang/crates.io-index"
281 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
282 |
283 | [[package]]
284 | name = "libc"
285 | version = "0.2.80"
286 | source = "registry+https://github.com/rust-lang/crates.io-index"
287 | checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
288 |
289 | [[package]]
290 | name = "log"
291 | version = "0.4.14"
292 | source = "registry+https://github.com/rust-lang/crates.io-index"
293 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
294 | dependencies = [
295 | "cfg-if 1.0.0",
296 | ]
297 |
298 | [[package]]
299 | name = "miniz_oxide"
300 | version = "0.4.3"
301 | source = "registry+https://github.com/rust-lang/crates.io-index"
302 | checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d"
303 | dependencies = [
304 | "adler",
305 | "autocfg",
306 | ]
307 |
308 | [[package]]
309 | name = "num-derive"
310 | version = "0.3.2"
311 | source = "registry+https://github.com/rust-lang/crates.io-index"
312 | checksum = "6f09b9841adb6b5e1f89ef7087ea636e0fd94b2851f887c1e3eb5d5f8228fab3"
313 | dependencies = [
314 | "proc-macro2 1.0.24",
315 | "quote 1.0.7",
316 | "syn 1.0.48",
317 | ]
318 |
319 | [[package]]
320 | name = "num-integer"
321 | version = "0.1.43"
322 | source = "registry+https://github.com/rust-lang/crates.io-index"
323 | checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b"
324 | dependencies = [
325 | "autocfg",
326 | "num-traits",
327 | ]
328 |
329 | [[package]]
330 | name = "num-traits"
331 | version = "0.2.12"
332 | source = "registry+https://github.com/rust-lang/crates.io-index"
333 | checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
334 | dependencies = [
335 | "autocfg",
336 | ]
337 |
338 | [[package]]
339 | name = "object"
340 | version = "0.21.1"
341 | source = "registry+https://github.com/rust-lang/crates.io-index"
342 | checksum = "37fd5004feb2ce328a52b0b3d01dbf4ffff72583493900ed15f22d4111c51693"
343 |
344 | [[package]]
345 | name = "proc-macro-hack"
346 | version = "0.4.2"
347 | source = "registry+https://github.com/rust-lang/crates.io-index"
348 | checksum = "463bf29e7f11344e58c9e01f171470ab15c925c6822ad75028cc1c0e1d1eb63b"
349 | dependencies = [
350 | "proc-macro-hack-impl",
351 | ]
352 |
353 | [[package]]
354 | name = "proc-macro-hack"
355 | version = "0.5.18"
356 | source = "registry+https://github.com/rust-lang/crates.io-index"
357 | checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598"
358 |
359 | [[package]]
360 | name = "proc-macro-hack-impl"
361 | version = "0.4.2"
362 | source = "registry+https://github.com/rust-lang/crates.io-index"
363 | checksum = "38c47dcb1594802de8c02f3b899e2018c78291168a22c281be21ea0fb4796842"
364 |
365 | [[package]]
366 | name = "proc-macro2"
367 | version = "0.2.3"
368 | source = "registry+https://github.com/rust-lang/crates.io-index"
369 | checksum = "cd07deb3c6d1d9ff827999c7f9b04cdfd66b1b17ae508e14fe47b620f2282ae0"
370 | dependencies = [
371 | "unicode-xid 0.1.0",
372 | ]
373 |
374 | [[package]]
375 | name = "proc-macro2"
376 | version = "1.0.24"
377 | source = "registry+https://github.com/rust-lang/crates.io-index"
378 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
379 | dependencies = [
380 | "unicode-xid 0.2.1",
381 | ]
382 |
383 | [[package]]
384 | name = "quote"
385 | version = "0.4.2"
386 | source = "registry+https://github.com/rust-lang/crates.io-index"
387 | checksum = "1eca14c727ad12702eb4b6bfb5a232287dcf8385cb8ca83a3eeaf6519c44c408"
388 | dependencies = [
389 | "proc-macro2 0.2.3",
390 | ]
391 |
392 | [[package]]
393 | name = "quote"
394 | version = "1.0.7"
395 | source = "registry+https://github.com/rust-lang/crates.io-index"
396 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
397 | dependencies = [
398 | "proc-macro2 1.0.24",
399 | ]
400 |
401 | [[package]]
402 | name = "rand"
403 | version = "0.5.6"
404 | source = "registry+https://github.com/rust-lang/crates.io-index"
405 | checksum = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9"
406 | dependencies = [
407 | "cloudabi",
408 | "fuchsia-cprng",
409 | "libc",
410 | "rand_core 0.3.1",
411 | "winapi 0.3.9",
412 | ]
413 |
414 | [[package]]
415 | name = "rand_core"
416 | version = "0.3.1"
417 | source = "registry+https://github.com/rust-lang/crates.io-index"
418 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
419 | dependencies = [
420 | "rand_core 0.4.2",
421 | ]
422 |
423 | [[package]]
424 | name = "rand_core"
425 | version = "0.4.2"
426 | source = "registry+https://github.com/rust-lang/crates.io-index"
427 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
428 |
429 | [[package]]
430 | name = "redox_syscall"
431 | version = "0.1.57"
432 | source = "registry+https://github.com/rust-lang/crates.io-index"
433 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
434 |
435 | [[package]]
436 | name = "rustc-demangle"
437 | version = "0.1.18"
438 | source = "registry+https://github.com/rust-lang/crates.io-index"
439 | checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232"
440 |
441 | [[package]]
442 | name = "serde"
443 | version = "1.0.117"
444 | source = "registry+https://github.com/rust-lang/crates.io-index"
445 | checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
446 |
447 | [[package]]
448 | name = "serde_derive"
449 | version = "1.0.117"
450 | source = "registry+https://github.com/rust-lang/crates.io-index"
451 | checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
452 | dependencies = [
453 | "proc-macro2 1.0.24",
454 | "quote 1.0.7",
455 | "syn 1.0.48",
456 | ]
457 |
458 | [[package]]
459 | name = "shell32-sys"
460 | version = "0.1.2"
461 | source = "registry+https://github.com/rust-lang/crates.io-index"
462 | checksum = "9ee04b46101f57121c9da2b151988283b6beb79b34f5bb29a58ee48cb695122c"
463 | dependencies = [
464 | "winapi 0.2.8",
465 | "winapi-build",
466 | ]
467 |
468 | [[package]]
469 | name = "simple-logging"
470 | version = "2.0.2"
471 | source = "registry+https://github.com/rust-lang/crates.io-index"
472 | checksum = "b00d48e85675326bb182a2286ea7c1a0b264333ae10f27a937a72be08628b542"
473 | dependencies = [
474 | "lazy_static",
475 | "log",
476 | "thread-id",
477 | ]
478 |
479 | [[package]]
480 | name = "syn"
481 | version = "0.12.15"
482 | source = "registry+https://github.com/rust-lang/crates.io-index"
483 | checksum = "c97c05b8ebc34ddd6b967994d5c6e9852fa92f8b82b3858c39451f97346dcce5"
484 | dependencies = [
485 | "proc-macro2 0.2.3",
486 | "quote 0.4.2",
487 | "unicode-xid 0.1.0",
488 | ]
489 |
490 | [[package]]
491 | name = "syn"
492 | version = "1.0.48"
493 | source = "registry+https://github.com/rust-lang/crates.io-index"
494 | checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac"
495 | dependencies = [
496 | "proc-macro2 1.0.24",
497 | "quote 1.0.7",
498 | "unicode-xid 0.2.1",
499 | ]
500 |
501 | [[package]]
502 | name = "synstructure"
503 | version = "0.12.4"
504 | source = "registry+https://github.com/rust-lang/crates.io-index"
505 | checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701"
506 | dependencies = [
507 | "proc-macro2 1.0.24",
508 | "quote 1.0.7",
509 | "syn 1.0.48",
510 | "unicode-xid 0.2.1",
511 | ]
512 |
513 | [[package]]
514 | name = "thread-id"
515 | version = "3.3.0"
516 | source = "registry+https://github.com/rust-lang/crates.io-index"
517 | checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1"
518 | dependencies = [
519 | "libc",
520 | "redox_syscall",
521 | "winapi 0.3.9",
522 | ]
523 |
524 | [[package]]
525 | name = "time"
526 | version = "0.1.44"
527 | source = "registry+https://github.com/rust-lang/crates.io-index"
528 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
529 | dependencies = [
530 | "libc",
531 | "wasi",
532 | "winapi 0.3.9",
533 | ]
534 |
535 | [[package]]
536 | name = "toml"
537 | version = "0.5.7"
538 | source = "registry+https://github.com/rust-lang/crates.io-index"
539 | checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645"
540 | dependencies = [
541 | "serde",
542 | ]
543 |
544 | [[package]]
545 | name = "unicode-xid"
546 | version = "0.1.0"
547 | source = "registry+https://github.com/rust-lang/crates.io-index"
548 | checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
549 |
550 | [[package]]
551 | name = "unicode-xid"
552 | version = "0.2.1"
553 | source = "registry+https://github.com/rust-lang/crates.io-index"
554 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
555 |
556 | [[package]]
557 | name = "wasi"
558 | version = "0.10.0+wasi-snapshot-preview1"
559 | source = "registry+https://github.com/rust-lang/crates.io-index"
560 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
561 |
562 | [[package]]
563 | name = "wchar"
564 | version = "0.6.1"
565 | source = "registry+https://github.com/rust-lang/crates.io-index"
566 | checksum = "c74d010bf16569f942b0b7d3c777dd674f8ee539b48d809dc548b3453039c2df"
567 | dependencies = [
568 | "proc-macro-hack 0.5.18",
569 | "wchar-impl",
570 | ]
571 |
572 | [[package]]
573 | name = "wchar-impl"
574 | version = "0.6.0"
575 | source = "registry+https://github.com/rust-lang/crates.io-index"
576 | checksum = "f135922b9303f899bfa446fce1eb149f43462f1e9ac7f50e24ea6b913416dd84"
577 | dependencies = [
578 | "proc-macro-hack 0.5.18",
579 | "proc-macro2 1.0.24",
580 | "quote 1.0.7",
581 | "syn 1.0.48",
582 | ]
583 |
584 | [[package]]
585 | name = "widestring"
586 | version = "0.4.3"
587 | source = "registry+https://github.com/rust-lang/crates.io-index"
588 | checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c"
589 |
590 | [[package]]
591 | name = "winapi"
592 | version = "0.2.8"
593 | source = "registry+https://github.com/rust-lang/crates.io-index"
594 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
595 |
596 | [[package]]
597 | name = "winapi"
598 | version = "0.3.9"
599 | source = "registry+https://github.com/rust-lang/crates.io-index"
600 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
601 | dependencies = [
602 | "winapi-i686-pc-windows-gnu",
603 | "winapi-x86_64-pc-windows-gnu",
604 | ]
605 |
606 | [[package]]
607 | name = "winapi-build"
608 | version = "0.1.1"
609 | source = "registry+https://github.com/rust-lang/crates.io-index"
610 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
611 |
612 | [[package]]
613 | name = "winapi-i686-pc-windows-gnu"
614 | version = "0.4.0"
615 | source = "registry+https://github.com/rust-lang/crates.io-index"
616 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
617 |
618 | [[package]]
619 | name = "winapi-x86_64-pc-windows-gnu"
620 | version = "0.4.0"
621 | source = "registry+https://github.com/rust-lang/crates.io-index"
622 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
623 |
624 | [[package]]
625 | name = "winreg"
626 | version = "0.7.0"
627 | source = "registry+https://github.com/rust-lang/crates.io-index"
628 | checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
629 | dependencies = [
630 | "winapi 0.3.9",
631 | ]
632 |
633 | [[package]]
634 | name = "winres"
635 | version = "0.1.11"
636 | source = "registry+https://github.com/rust-lang/crates.io-index"
637 | checksum = "ff4fb510bbfe5b8992ff15f77a2e6fe6cf062878f0eda00c0f44963a807ca5dc"
638 | dependencies = [
639 | "toml",
640 | ]
641 |
642 | [[package]]
643 | name = "wslscript"
644 | version = "0.6.2"
645 | dependencies = [
646 | "chrono",
647 | "failure",
648 | "guid-create",
649 | "log",
650 | "num-derive",
651 | "num-traits",
652 | "serde",
653 | "serde_derive",
654 | "shell32-sys",
655 | "simple-logging",
656 | "toml",
657 | "wchar",
658 | "widestring",
659 | "winapi 0.3.9",
660 | "winreg",
661 | "winres",
662 | ]
663 |
--------------------------------------------------------------------------------
/wslscript/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "wslscript"
3 | description = "Shell script handler for WSL."
4 | version = "0.7.1"
5 | authors = ["Joni Kollani "]
6 | license = "MIT"
7 | homepage = "https://sop.github.io/wslscript/"
8 | repository = "https://github.com/sop/wslscript"
9 | edition = "2021"
10 |
11 | [dependencies]
12 | num_enum = "0.7.3"
13 | once_cell = "1.20"
14 | widestring = "1.1"
15 | wchar = "0.11"
16 | log = { version = "0.4", features = ["release_max_level_off"] }
17 | simple-logging = "2.0"
18 |
19 | [dependencies.wslscript_common]
20 | version = "*"
21 | path = "../wslscript_common"
22 |
23 | [dependencies.winapi]
24 | version = "0.3.9"
25 | features = ["winuser", "winbase", "errhandlingapi", "commctrl", "processenv"]
26 |
27 | [features]
28 | debug = []
29 |
30 | [build-dependencies]
31 | winres = "0.1"
32 | toml = "0.8"
33 | serde = "1"
34 | serde_derive = "1"
35 | chrono = "0.4"
36 |
--------------------------------------------------------------------------------
/wslscript/build.rs:
--------------------------------------------------------------------------------
1 | use serde_derive::Deserialize;
2 | use std::env;
3 | use std::fs::File;
4 | use std::io::prelude::*;
5 | use std::io::Read;
6 | use std::path::PathBuf;
7 | use winres::VersionInfo;
8 |
9 | #[derive(Deserialize)]
10 | struct Cargo {
11 | package: CargoPackage,
12 | }
13 |
14 | #[derive(Deserialize)]
15 | struct CargoPackage {
16 | name: String,
17 | description: String,
18 | version: String,
19 | }
20 |
21 | fn main() {
22 | let cargo = read_cargo();
23 | let icon = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
24 | .parent()
25 | .unwrap()
26 | .join("assets/icon/terminal.ico");
27 | let manifest_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("manifest.xml");
28 | let mut f = File::create(manifest_path.clone()).unwrap();
29 | f.write_all(get_manifest(&cargo).as_bytes()).unwrap();
30 | let now = chrono::Local::now();
31 | let version = parse_version(&cargo.package.version);
32 | winres::WindowsResource::new()
33 | .set_manifest_file(manifest_path.to_str().unwrap())
34 | .set_icon_with_id(icon.to_str().unwrap(), "app")
35 | .set("ProductName", "WSL Script")
36 | .set("FileDescription", &cargo.package.description)
37 | .set("FileVersion", &cargo.package.version)
38 | .set_version_info(VersionInfo::FILEVERSION, version)
39 | .set("ProductVersion", &cargo.package.version)
40 | .set_version_info(VersionInfo::PRODUCTVERSION, version)
41 | .set("InternalName", &format!("{}.exe", cargo.package.name))
42 | .set(
43 | "LegalCopyright",
44 | &format!("Joni Kollani © {}", now.format("%Y")),
45 | )
46 | .compile()
47 | .unwrap();
48 | }
49 |
50 | /// Parse version string to resource version.
51 | ///
52 | /// See: https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
53 | fn parse_version(s: &str) -> u64 {
54 | // take first 3 numbers
55 | let mut parts = s
56 | .split(".")
57 | .filter_map(|s| {
58 | s.chars()
59 | .take_while(|c| c.is_digit(10))
60 | .collect::()
61 | .parse::()
62 | .ok()
63 | })
64 | .take(3)
65 | .collect::>();
66 | // insert 0 as a fourth component
67 | parts.push(0);
68 | assert!(parts.len() == 4);
69 | (parts[0] as u64) << 48 | (parts[1] as u64) << 32 | (parts[2] as u64) << 16 | (parts[3] as u64)
70 | }
71 |
72 | /// Format resource version to _m.n.o.p_ string.
73 | ///
74 | /// See: https://docs.microsoft.com/en-us/windows/win32/sbscs/assembly-versions
75 | fn format_version(v: u64) -> String {
76 | format!(
77 | "{}.{}.{}.{}",
78 | (v >> 48) & 0xffff,
79 | (v >> 32) & 0xffff,
80 | (v >> 16) & 0xffff,
81 | v & 0xffff
82 | )
83 | }
84 |
85 | fn get_manifest(cargo: &Cargo) -> String {
86 | format!(
87 | r#"
88 |
90 |
93 | {description}
94 |
95 |
96 |
102 |
103 |
104 | "#,
105 | name = format!("github.sop.{}", cargo.package.name),
106 | description = cargo.package.description,
107 | version = format_version(parse_version(&cargo.package.version))
108 | )
109 | }
110 |
111 | fn read_cargo() -> Cargo {
112 | let mut toml = String::new();
113 | File::open(PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("Cargo.toml"))
114 | .unwrap()
115 | .read_to_string(&mut toml)
116 | .unwrap();
117 | toml::from_str::(&toml).unwrap()
118 | }
119 |
--------------------------------------------------------------------------------
/wslscript/src/gui/listview.rs:
--------------------------------------------------------------------------------
1 | use crate::gui;
2 | use std::mem;
3 | use std::ptr;
4 | use wchar::*;
5 | use widestring::*;
6 | use winapi::shared::ntdef;
7 | use winapi::shared::windef;
8 | use winapi::um::commctrl;
9 | use winapi::um::libloaderapi;
10 | use winapi::um::winuser;
11 | use wslscript_common::registry;
12 | use wslscript_common::wcstring;
13 | use wslscript_common::win32;
14 |
15 | pub(crate) struct ExtensionsListView {
16 | hwnd: windef::HWND,
17 | }
18 |
19 | impl Default for ExtensionsListView {
20 | fn default() -> Self {
21 | Self {
22 | hwnd: ptr::null_mut(),
23 | }
24 | }
25 | }
26 |
27 | impl ExtensionsListView {
28 | pub fn create(main: &gui::MainWindow) -> Self {
29 | use commctrl::*;
30 | use winuser::*;
31 | #[rustfmt::skip]
32 | let hwnd = unsafe { CreateWindowExW(
33 | LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES,
34 | wcstring(WC_LISTVIEW).as_ptr(), ptr::null_mut(),
35 | WS_CHILD | WS_VISIBLE | WS_BORDER | LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS,
36 | 0, 0, 0, 0, main.hwnd,
37 | gui::Control::ListViewExtensions as u16 as _,
38 | libloaderapi::GetModuleHandleW(ptr::null_mut()), ptr::null_mut(),
39 | ) };
40 | let lv = Self { hwnd };
41 | gui::set_window_font(hwnd, &main.caption_font);
42 | unsafe {
43 | SendMessageW(
44 | hwnd,
45 | LVM_SETEXTENDEDLISTVIEWSTYLE,
46 | LVS_EX_FULLROWSELECT as _,
47 | LVS_EX_FULLROWSELECT as _,
48 | )
49 | };
50 | // insert columns
51 | let mut col = LV_COLUMNW {
52 | mask: LVCF_FMT | LVCF_WIDTH | LVCF_TEXT,
53 | fmt: LVCFMT_LEFT,
54 | cx: 80,
55 | pszText: wchz!("Filetype").as_ptr() as _,
56 | ..unsafe { mem::zeroed() }
57 | };
58 | unsafe { SendMessageW(hwnd, LVM_INSERTCOLUMNW, 0, &col as *const _ as _) };
59 | col.pszText = wchz!("Distribution").as_ptr() as _;
60 | col.cx = 130;
61 | unsafe { SendMessageW(hwnd, LVM_INSERTCOLUMNW, 1, &col as *const _ as _) };
62 | // insert items
63 | match registry::query_registered_extensions().map(|exts| {
64 | exts.iter()
65 | .filter_map(|ext| registry::get_extension_config(ext).ok())
66 | .collect::>()
67 | }) {
68 | Ok(configs) => {
69 | for (i, cfg) in configs.iter().enumerate() {
70 | if let Some(item) = lv.insert_item(i, &wcstring(&cfg.extension)) {
71 | let name = main.get_distro_label(cfg.distro.as_ref());
72 | lv.set_subitem_text(item, 1, &wcstring(name));
73 | }
74 | }
75 | }
76 | Err(e) => {
77 | let s = wcstring(format!("Failed to query registry: {}", e));
78 | win32::error_message(&s);
79 | }
80 | }
81 | lv
82 | }
83 |
84 | /// Insert item to listview.
85 | ///
86 | /// Returns the index of the new item.
87 | ///
88 | /// * `idx` - Index at which the the new item is inserted
89 | /// * `label` - Item label
90 | pub fn insert_item(&self, idx: usize, label: &WideCStr) -> Option {
91 | let lvi = commctrl::LV_ITEMW {
92 | mask: commctrl::LVIF_TEXT,
93 | iItem: idx as _,
94 | pszText: label.as_ptr() as _,
95 | ..unsafe { mem::zeroed() }
96 | };
97 | let rv = unsafe {
98 | winuser::SendMessageW(
99 | self.hwnd,
100 | commctrl::LVM_INSERTITEMW,
101 | 0,
102 | &lvi as *const _ as _,
103 | )
104 | };
105 | match rv {
106 | -1 => None,
107 | _ => Some(rv as usize),
108 | }
109 | }
110 |
111 | /// Delete item from listview.
112 | pub fn delete_item(&self, idx: usize) {
113 | unsafe { winuser::SendMessageW(self.hwnd, commctrl::LVM_DELETEITEM, idx, 0) };
114 | }
115 |
116 | /// Set text to subitem.
117 | ///
118 | /// * `idx` - Item index
119 | /// * `sub_idx` - Subitem index
120 | /// * `label` - Text to insert
121 | pub fn set_subitem_text(&self, idx: usize, sub_idx: usize, label: &WideCStr) {
122 | let lvi = commctrl::LV_ITEMW {
123 | mask: commctrl::LVIF_TEXT,
124 | iItem: idx as _,
125 | iSubItem: sub_idx as _,
126 | pszText: label.as_ptr() as _,
127 | ..unsafe { mem::zeroed() }
128 | };
129 | unsafe {
130 | winuser::SendMessageW(self.hwnd, commctrl::LVM_SETITEMW, 0, &lvi as *const _ as _)
131 | };
132 | }
133 |
134 | /// Find extension from listview.
135 | ///
136 | /// Returns listview index or None if extension wasn't found.
137 | pub fn find_ext(&self, ext: &str) -> Option {
138 | let s = wcstring(ext);
139 | let lvf = commctrl::LVFINDINFOW {
140 | flags: commctrl::LVFI_STRING,
141 | psz: s.as_ptr(),
142 | ..unsafe { mem::zeroed() }
143 | };
144 | let idx = unsafe {
145 | winuser::SendMessageW(
146 | self.hwnd,
147 | commctrl::LVM_FINDITEMW,
148 | -1_isize as usize,
149 | &lvf as *const _ as _,
150 | )
151 | };
152 | match idx {
153 | -1 => None,
154 | _ => Some(idx as usize),
155 | }
156 | }
157 |
158 | /// Get listview text by index.
159 | pub fn get_item_text(&self, idx: usize) -> Option {
160 | let mut buf: Vec = Vec::with_capacity(32);
161 | let lvi = commctrl::LV_ITEMW {
162 | pszText: buf.as_mut_ptr(),
163 | cchTextMax: buf.capacity() as _,
164 | ..unsafe { mem::zeroed() }
165 | };
166 | unsafe {
167 | let len = winuser::SendMessageW(
168 | self.hwnd,
169 | commctrl::LVM_GETITEMTEXTW,
170 | idx,
171 | &lvi as *const _ as _,
172 | );
173 | buf.set_len(len as usize);
174 | };
175 | WideCString::from_vec(buf).ok().map(|u| u.to_string_lossy())
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/wslscript/src/gui/mod.rs:
--------------------------------------------------------------------------------
1 | use num_enum::{IntoPrimitive, TryFromPrimitive};
2 | use once_cell::sync::Lazy;
3 | use std::mem;
4 | use std::pin::Pin;
5 | use std::ptr;
6 | use std::str::FromStr;
7 | use wchar::*;
8 | use widestring::*;
9 | use winapi::shared::basetsd;
10 | use winapi::shared::minwindef as win;
11 | use winapi::shared::ntdef;
12 | use winapi::shared::windef;
13 | use winapi::um::commctrl;
14 | use winapi::um::errhandlingapi;
15 | use winapi::um::libloaderapi;
16 | use winapi::um::wingdi;
17 | use winapi::um::winuser::*;
18 | use wslscript_common::error::*;
19 | use wslscript_common::font::Font;
20 | use wslscript_common::icon::ShellIcon;
21 | use wslscript_common::registry;
22 | use wslscript_common::win32;
23 | use wslscript_common::{wcstr, wcstring};
24 |
25 | mod listview;
26 |
27 | /// Default extension to register.
28 | static DEFAULT_EXTENSION: Lazy = Lazy::new(|| wcstring("sh"));
29 |
30 | /// Start WSL Script GUI app.
31 | pub fn start_gui() -> Result<(), Error> {
32 | let wnd = MainWindow::new(wcstr(wchz!("WSL Script")))?;
33 | wnd.run()
34 | }
35 |
36 | pub trait WindowProc {
37 | /// Window procedure callback.
38 | ///
39 | /// If None is returned, underlying wrapper calls `DefWindowProcW`.
40 | fn window_proc(
41 | &mut self,
42 | hwnd: windef::HWND,
43 | msg: win::UINT,
44 | wparam: win::WPARAM,
45 | lparam: win::LPARAM,
46 | ) -> Option;
47 | }
48 |
49 | /// Window procedure wrapper that stores struct pointer to window attributes.
50 | ///
51 | /// Proxies messages to `window_proc()` with *self*.
52 | extern "system" fn window_proc_wrapper(
53 | hwnd: windef::HWND,
54 | msg: win::UINT,
55 | wparam: win::WPARAM,
56 | lparam: win::LPARAM,
57 | ) -> win::LRESULT {
58 | // get pointer to T from userdata
59 | let mut ptr = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as *mut T;
60 | // not yet set, initialize from CREATESTRUCT
61 | if ptr.is_null() && msg == WM_NCCREATE {
62 | let cs = unsafe { &*(lparam as LPCREATESTRUCTW) };
63 | ptr = cs.lpCreateParams as *mut T;
64 | unsafe { errhandlingapi::SetLastError(0) };
65 | if 0 == unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, ptr as *const _ as _) }
66 | && unsafe { errhandlingapi::GetLastError() } != 0
67 | {
68 | return win::FALSE as _;
69 | }
70 | }
71 | // call wrapped window proc
72 | if !ptr.is_null() {
73 | let this = unsafe { &mut *ptr };
74 | if let Some(result) = this.window_proc(hwnd, msg, wparam, lparam) {
75 | return result;
76 | }
77 | }
78 | unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
79 | }
80 |
81 | /// Main window.
82 | pub(crate) struct MainWindow {
83 | /// Main window handle.
84 | hwnd: windef::HWND,
85 | /// Font for captions.
86 | caption_font: Font,
87 | /// Font for filetype extension.
88 | ext_font: Font,
89 | /// Currently selected extension index in the listview.
90 | current_ext_idx: Option,
91 | /// Configuration of the currently selected extension.
92 | current_ext_cfg: Option,
93 | /// List of available WSL distributions.
94 | distros: registry::Distros,
95 | /// Extensions listview.
96 | lv_extensions: listview::ExtensionsListView,
97 | /// Message to display on GUI.
98 | message: Option,
99 | }
100 |
101 | impl Default for MainWindow {
102 | fn default() -> Self {
103 | Self {
104 | hwnd: ptr::null_mut(),
105 | caption_font: Default::default(),
106 | ext_font: Default::default(),
107 | current_ext_idx: None,
108 | current_ext_cfg: None,
109 | distros: registry::query_distros().unwrap_or_else(|_| registry::Distros::default()),
110 | lv_extensions: Default::default(),
111 | message: None,
112 | }
113 | }
114 | }
115 |
116 | /// Window control ID's.
117 | #[derive(IntoPrimitive, TryFromPrimitive, PartialEq)]
118 | #[repr(u16)]
119 | pub(crate) enum Control {
120 | /// Message area.
121 | StaticMsg = 100,
122 | /// Label for extension input.
123 | RegisterLabel,
124 | /// Input for extension.
125 | EditExtension,
126 | /// Register button.
127 | BtnRegister,
128 | /// Listview of registered extensions.
129 | ListViewExtensions,
130 | /// Icon for extension.
131 | StaticIcon,
132 | /// Label for icon.
133 | IconLabel,
134 | /// Combo box for hold mode.
135 | HoldModeCombo,
136 | /// Label for hold mode.
137 | HoldModeLabel,
138 | /// Checkbox for interactive shell.
139 | InteractiveCheckbox,
140 | /// Label for interactive shell checkbox.
141 | InteractiveLabel,
142 | /// Combo box for distro.
143 | DistroCombo,
144 | /// Label for distro.
145 | DistroLabel,
146 | /// Save button.
147 | BtnSave,
148 | }
149 |
150 | /// Menu item ID's.
151 | #[derive(IntoPrimitive, TryFromPrimitive, PartialEq)]
152 | #[repr(u32)]
153 | enum MenuItem {
154 | /// Unregister extension.
155 | Unregister = 100,
156 | /// Edit extension.
157 | EditExtension,
158 | }
159 |
160 | /// System menu item ID's.
161 | #[derive(IntoPrimitive, TryFromPrimitive, PartialEq)]
162 | #[repr(u32)]
163 | enum SystemMenu {
164 | /// About application.
165 | About = 100,
166 | /// Visit website.
167 | Homepage,
168 | }
169 |
170 | /// Minimum and initial main window size.
171 | const MIN_WINDOW_SIZE: (i32, i32) = (300, 315);
172 |
173 | impl MainWindow {
174 | /// Create application window.
175 | fn new(title: &WideCStr) -> Result>, Error> {
176 | let wnd = Pin::new(Box::new(Self::default()));
177 | let instance = unsafe { libloaderapi::GetModuleHandleW(ptr::null_mut()) };
178 | let class_name = wchz!("WSLScript");
179 | // register window class
180 | let wc = WNDCLASSEXW {
181 | cbSize: mem::size_of::() as _,
182 | style: CS_OWNDC | CS_HREDRAW | CS_VREDRAW,
183 | hbrBackground: (COLOR_WINDOW + 1) as _,
184 | lpfnWndProc: Some(window_proc_wrapper::),
185 | hInstance: instance,
186 | lpszClassName: class_name.as_ptr(),
187 | hIcon: unsafe { LoadIconW(instance, wchz!("app").as_ptr()) },
188 | hCursor: unsafe { LoadCursorW(ptr::null_mut(), IDC_ARROW) },
189 | ..unsafe { mem::zeroed() }
190 | };
191 | if 0 == unsafe { RegisterClassExW(&wc) } {
192 | return Err(win32::last_error());
193 | }
194 | // create window
195 | #[rustfmt::skip]
196 | let hwnd = unsafe { CreateWindowExW(
197 | 0, class_name.as_ptr(), title.as_ptr(),
198 | WS_OVERLAPPEDWINDOW & !WS_MAXIMIZEBOX | WS_VISIBLE,
199 | CW_USEDEFAULT, CW_USEDEFAULT, MIN_WINDOW_SIZE.0, MIN_WINDOW_SIZE.1,
200 | ptr::null_mut(), ptr::null_mut(), instance, &*wnd as *const Self as _) };
201 | if hwnd.is_null() {
202 | return Err(win32::last_error());
203 | }
204 | Ok(wnd)
205 | }
206 |
207 | /// Run message loop.
208 | fn run(&self) -> Result<(), Error> {
209 | loop {
210 | let mut msg: MSG = unsafe { mem::zeroed() };
211 | match unsafe { GetMessageW(&mut msg, ptr::null_mut(), 0, 0) } {
212 | 1..=std::i32::MAX => {
213 | unsafe { TranslateMessage(&msg) };
214 | unsafe { DispatchMessageW(&msg) };
215 | }
216 | std::i32::MIN..=-1 => return Err(win32::last_error()),
217 | 0 => return Ok(()),
218 | }
219 | }
220 | }
221 |
222 | /// Create window controls.
223 | fn create_window_controls(&mut self) -> Result<(), Error> {
224 | let instance = unsafe { GetWindowLongW(self.hwnd, GWL_HINSTANCE) as win::HINSTANCE };
225 | self.caption_font = Font::new_default_caption()?;
226 | self.ext_font = Font::new_caption(24)?;
227 | // init common controls
228 | let icex = commctrl::INITCOMMONCONTROLSEX {
229 | dwSize: mem::size_of::() as _,
230 | dwICC: commctrl::ICC_LISTVIEW_CLASSES,
231 | };
232 | unsafe { commctrl::InitCommonControlsEx(&icex) };
233 |
234 | // static message area
235 | #[rustfmt::skip]
236 | let hwnd = unsafe { CreateWindowExW(
237 | 0, wchz!("STATIC").as_ptr(), ptr::null_mut(),
238 | SS_CENTER | WS_CHILD | WS_VISIBLE,
239 | 0, 0, 0, 0, self.hwnd,
240 | Control::StaticMsg as u16 as _, instance, ptr::null_mut(),
241 | ) };
242 | set_window_font(hwnd, &self.caption_font);
243 |
244 | // register button
245 | #[rustfmt::skip]
246 | let hwnd = unsafe { CreateWindowExW(
247 | 0, wchz!("BUTTON").as_ptr(), wchz!("Register").as_ptr(),
248 | WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
249 | 0, 0, 0, 0, self.hwnd,
250 | Control::BtnRegister as u16 as _, instance, ptr::null_mut()
251 | ) };
252 | set_window_font(hwnd, &self.caption_font);
253 |
254 | // register label
255 | #[rustfmt::skip]
256 | let hwnd = unsafe { CreateWindowExW(
257 | 0, wchz!("STATIC").as_ptr(), wchz!("Extension:").as_ptr(),
258 | SS_CENTERIMAGE | SS_RIGHT | WS_CHILD | WS_VISIBLE,
259 | 0, 0, 0, 0, self.hwnd,
260 | Control::RegisterLabel as u16 as _, instance, ptr::null_mut(),
261 | ) };
262 | set_window_font(hwnd, &self.caption_font);
263 |
264 | // extension input
265 | #[rustfmt::skip]
266 | let hwnd = unsafe { CreateWindowExW(
267 | 0, wchz!("EDIT").as_ptr(), ptr::null_mut(),
268 | ES_LEFT | ES_LOWERCASE | WS_CHILD | WS_VISIBLE | WS_BORDER,
269 | 0, 0, 0, 0, self.hwnd,
270 | Control::EditExtension as u16 as _, instance, ptr::null_mut(),
271 | ) };
272 | set_window_font(hwnd, &self.caption_font);
273 | let self_ptr = self as *const _;
274 | // use custom window proc
275 | unsafe { commctrl::SetWindowSubclass(hwnd, Some(extension_input_proc), 0, self_ptr as _) };
276 | // if no extensions are registered, set default value to input box
277 | if registry::query_registered_extensions()
278 | .unwrap_or_default()
279 | .is_empty()
280 | {
281 | unsafe { SetWindowTextW(hwnd, DEFAULT_EXTENSION.as_ptr()) };
282 | }
283 |
284 | // extensions listview
285 | self.lv_extensions = listview::ExtensionsListView::create(self);
286 |
287 | // extension icon
288 | #[rustfmt::skip]
289 | unsafe { CreateWindowExW(
290 | 0, wchz!("STATIC").as_ptr(), ptr::null_mut(),
291 | SS_ICON | SS_CENTERIMAGE | SS_NOTIFY | WS_CHILD | WS_VISIBLE,
292 | 0, 0, 0, 0, self.hwnd,
293 | Control::StaticIcon as u16 as _, instance, ptr::null_mut(),
294 | ) };
295 |
296 | // icon tooltip
297 | self.create_control_tooltip(
298 | Control::StaticIcon,
299 | wcstr(wchz!("Double click to select an icon for the extension.")),
300 | );
301 |
302 | // icon label
303 | #[rustfmt::skip]
304 | let hwnd = unsafe { CreateWindowExW(
305 | 0, wchz!("STATIC").as_ptr(), wchz!("Icon").as_ptr(),
306 | SS_CENTER | WS_CHILD | WS_VISIBLE,
307 | 0, 0, 0, 0, self.hwnd,
308 | Control::IconLabel as u16 as _, instance, ptr::null_mut()
309 | ) };
310 | set_window_font(hwnd, &self.caption_font);
311 |
312 | // hold mode combo box
313 | #[rustfmt::skip]
314 | let hwnd = unsafe { CreateWindowExW(
315 | 0, wchz!("COMBOBOX").as_ptr(), ptr::null_mut(),
316 | CBS_DROPDOWNLIST | WS_VSCROLL | WS_CHILD | WS_VISIBLE,
317 | 0, 0, 0, 0, self.hwnd,
318 | Control::HoldModeCombo as u16 as _, instance, ptr::null_mut()
319 | ) };
320 | set_window_font(hwnd, &self.caption_font);
321 | let insert_item = |mode: registry::HoldMode, label: &[wchar_t]| {
322 | let idx =
323 | unsafe { SendMessageW(hwnd, CB_INSERTSTRING, -1_isize as _, label.as_ptr() as _) };
324 | let s = mode.as_wcstr();
325 | unsafe { SendMessageW(hwnd, CB_SETITEMDATA, idx as _, s.as_ptr() as _) };
326 | };
327 | insert_item(registry::HoldMode::Error, wchz!("Close on success"));
328 | insert_item(registry::HoldMode::Never, wchz!("Always close"));
329 | insert_item(registry::HoldMode::Always, wchz!("Keep open"));
330 |
331 | // hold mode label
332 | #[rustfmt::skip]
333 | let hwnd = unsafe { CreateWindowExW(
334 | 0, wchz!("STATIC").as_ptr(), wchz!("Exit behaviour").as_ptr(),
335 | SS_CENTER | WS_CHILD | WS_VISIBLE,
336 | 0, 0, 0, 0, self.hwnd,
337 | Control::HoldModeLabel as u16 as _, instance, ptr::null_mut()
338 | ) };
339 | set_window_font(hwnd, &self.caption_font);
340 |
341 | // hold more tooltip
342 | self.create_control_tooltip(
343 | Control::HoldModeCombo,
344 | wcstr(wchz!("Console window behaviour when the script exits.")),
345 | );
346 |
347 | // interactive shell checkbox
348 | #[rustfmt::skip]
349 | unsafe { CreateWindowExW(
350 | 0, wchz!("BUTTON").as_ptr(), ptr::null_mut(),
351 | WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_AUTOCHECKBOX,
352 | 0, 0, 0, 0, self.hwnd,
353 | Control::InteractiveCheckbox as u16 as _, instance, ptr::null_mut()
354 | ) };
355 |
356 | // interactive shell label
357 | #[rustfmt::skip]
358 | let hwnd = unsafe { CreateWindowExW(
359 | 0, wchz!("STATIC").as_ptr(), wchz!("Interactive").as_ptr(),
360 | SS_LEFT | SS_CENTERIMAGE | SS_NOTIFY | WS_CHILD | WS_VISIBLE,
361 | 0, 0, 0, 0, self.hwnd,
362 | Control::InteractiveLabel as u16 as _, instance, ptr::null_mut()
363 | ) };
364 | set_window_font(hwnd, &self.caption_font);
365 |
366 | // tooltip for interactive shell
367 | self.create_control_tooltip(
368 | Control::InteractiveCheckbox,
369 | wcstr(wchz!(
370 | "Run bash as an interactive shell and execute \
371 | profile scripts (eg. ~/.bashrc)."
372 | )),
373 | );
374 |
375 | // distro combo box
376 | #[rustfmt::skip]
377 | let hwnd = unsafe { CreateWindowExW(
378 | 0, wchz!("COMBOBOX").as_ptr(), ptr::null_mut(),
379 | CBS_DROPDOWNLIST | WS_VSCROLL | WS_CHILD | WS_VISIBLE,
380 | 0, 0, 0, 0, self.hwnd,
381 | Control::DistroCombo as u16 as _, instance, ptr::null_mut()
382 | ) };
383 | set_window_font(hwnd, &self.caption_font);
384 | let insert_item = |guid: Option<®istry::DistroGUID>, name: &str| {
385 | unsafe {
386 | let s = WideCString::from_str_unchecked(name);
387 | let idx = SendMessageW(hwnd, CB_INSERTSTRING, -1_isize as _, s.as_ptr() as _);
388 | if let Some(guid) = guid {
389 | SendMessageW(
390 | hwnd,
391 | CB_SETITEMDATA,
392 | idx as _,
393 | guid.as_wcstr().as_ptr() as _,
394 | );
395 | } else {
396 | SendMessageW(hwnd, CB_SETITEMDATA, idx as _, 0);
397 | }
398 | };
399 | };
400 | insert_item(None, &self.get_distro_label(None));
401 | for (guid, name) in self.distros.sorted_pairs() {
402 | insert_item(Some(guid), name);
403 | }
404 |
405 | // distro label
406 | #[rustfmt::skip]
407 | let hwnd = unsafe { CreateWindowExW(
408 | 0, wchz!("STATIC").as_ptr(), wchz!("Distribution").as_ptr(),
409 | SS_CENTER | WS_CHILD | WS_VISIBLE,
410 | 0, 0, 0, 0, self.hwnd,
411 | Control::DistroLabel as u16 as _, instance, ptr::null_mut()
412 | ) };
413 | set_window_font(hwnd, &self.caption_font);
414 |
415 | // distro tooltip
416 | self.create_control_tooltip(
417 | Control::DistroCombo,
418 | wcstr(wchz!("WSL distribution on which to run the script.")),
419 | );
420 |
421 | // save button
422 | #[rustfmt::skip]
423 | let hwnd = unsafe { CreateWindowExW(
424 | 0, wchz!("BUTTON").as_ptr(), wchz!("Save").as_ptr(),
425 | WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
426 | 0, 0, 0, 0, self.hwnd,
427 | Control::BtnSave as u16 as _, instance, ptr::null_mut()
428 | ) };
429 | set_window_font(hwnd, &self.caption_font);
430 |
431 | self.update_control_states();
432 | Ok(())
433 | }
434 |
435 | /// Create a tooltip and assign it to given control.
436 | fn create_control_tooltip(&self, control: Control, text: &WideCStr) {
437 | use commctrl::*;
438 | let instance = unsafe { GetWindowLongW(self.hwnd, GWL_HINSTANCE) as win::HINSTANCE };
439 | #[rustfmt::skip]
440 | let hwnd_tt = unsafe { CreateWindowExW(
441 | 0, wchz!("tooltips_class32").as_ptr(), ptr::null_mut(),
442 | WS_POPUP | TTS_ALWAYSTIP | TTS_BALLOON,
443 | CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, self.hwnd,
444 | ptr::null_mut(), instance, ptr::null_mut()
445 | ) };
446 | let ti = TOOLINFOW {
447 | cbSize: mem::size_of::() as _,
448 | hwnd: self.hwnd,
449 | uFlags: TTF_IDISHWND | TTF_SUBCLASS,
450 | uId: self.get_control_handle(control) as _,
451 | lpszText: text.as_ptr() as _,
452 | ..unsafe { mem::zeroed() }
453 | };
454 | unsafe { SendMessageW(hwnd_tt, TTM_ADDTOOLW, 0, &ti as *const _ as _) };
455 | unsafe { SendMessageW(hwnd_tt, TTM_ACTIVATE, win::TRUE as _, 0) };
456 | }
457 |
458 | /// Update control states.
459 | fn update_control_states(&self) {
460 | // set message
461 | let hwnd = self.get_control_handle(Control::StaticMsg);
462 | if let Some(mut ext) = self.get_current_extension() {
463 | // if extension is registered for WSL, but handler is in another directory
464 | if !registry::is_registered_for_current_executable(&ext).unwrap_or(true) {
465 | let exe = std::env::current_exe()
466 | .ok()
467 | .and_then(|p| p.file_name().map(|s| s.to_os_string()))
468 | .and_then(|s| s.into_string().ok())
469 | .unwrap_or_default();
470 | let s = wcstring(format!(
471 | ".{} handler found in another directory!\n\
472 | Did you move {}?",
473 | ext, exe
474 | ));
475 | unsafe { SetWindowTextW(hwnd, s.as_ptr()) };
476 | set_window_font(hwnd, &self.caption_font);
477 | } else if let Some(msg) = &self.message {
478 | unsafe { SetWindowTextW(hwnd, wcstring(msg).as_ptr()) };
479 | set_window_font(hwnd, &self.caption_font);
480 | } else {
481 | ext.insert(0, '.');
482 | unsafe { SetWindowTextW(hwnd, wcstring(ext).as_ptr()) };
483 | set_window_font(hwnd, &self.ext_font);
484 | }
485 | } else {
486 | let s = wchz!(
487 | "Enter the extension and click \
488 | Register to associate a filetype with WSL."
489 | );
490 | unsafe { SetWindowTextW(hwnd, s.as_ptr()) };
491 | set_window_font(hwnd, &self.caption_font);
492 | };
493 | let visible = self.current_ext_cfg.is_some();
494 | // hold mode label
495 | self.set_control_visibility(Control::HoldModeLabel, visible);
496 | // hold mode combo
497 | self.set_control_visibility(Control::HoldModeCombo, visible);
498 | if let Some(mode) = self.current_ext_cfg.as_ref().map(|cfg| cfg.hold_mode) {
499 | self.set_selected_hold_mode(mode);
500 | }
501 | // interactive shell label
502 | self.set_control_visibility(Control::InteractiveLabel, visible);
503 | // interactive shell checkbox
504 | self.set_control_visibility(Control::InteractiveCheckbox, visible);
505 | // set button state
506 | if let Some(state) = self.current_ext_cfg.as_ref().map(|cfg| cfg.interactive) {
507 | self.set_interactive_state(state);
508 | }
509 | // distro label
510 | self.set_control_visibility(Control::DistroLabel, visible);
511 | // distro combo
512 | self.set_control_visibility(Control::DistroCombo, visible);
513 | self.set_selected_distro(
514 | self.current_ext_cfg
515 | .as_ref()
516 | .and_then(|cfg| cfg.distro.as_ref()),
517 | );
518 | // set icon
519 | self.set_control_visibility(Control::StaticIcon, visible);
520 | let hwnd = self.get_control_handle(Control::StaticIcon);
521 | if let Some(icon) = self
522 | .current_ext_cfg
523 | .as_ref()
524 | .and_then(|cfg| cfg.icon.as_ref())
525 | {
526 | unsafe { SendMessageW(hwnd, STM_SETICON, icon.handle() as _, 0) };
527 | } else {
528 | // NOTE: DestroyIcon not needed for shared icons
529 | let hicon = unsafe { LoadIconW(ptr::null_mut(), IDI_WARNING) };
530 | unsafe { SendMessageW(hwnd, STM_SETICON, hicon as _, 0) };
531 | }
532 | // icon label
533 | self.set_control_visibility(Control::IconLabel, visible);
534 | // save button
535 | self.set_control_visibility(Control::BtnSave, visible);
536 | }
537 |
538 | /// Set control visibility.
539 | fn set_control_visibility(&self, control: Control, visible: bool) {
540 | let visibility = if visible { SW_SHOW } else { SW_HIDE };
541 | unsafe {
542 | ShowWindow(self.get_control_handle(control), visibility);
543 | }
544 | }
545 |
546 | /// Add items to system menu.
547 | fn extend_system_menu(&self) -> Result<(), Error> {
548 | let menu = unsafe { GetSystemMenu(self.hwnd, win::FALSE) };
549 | unsafe {
550 | AppendMenuW(menu, MF_SEPARATOR, 0, ptr::null());
551 | AppendMenuW(
552 | menu,
553 | MF_ENABLED | MF_STRING,
554 | SystemMenu::About as _,
555 | wchz!("About WSL Script").as_ptr(),
556 | );
557 | AppendMenuW(
558 | menu,
559 | MF_ENABLED | MF_STRING,
560 | SystemMenu::Homepage as _,
561 | wchz!("Visit website").as_ptr(),
562 | );
563 | }
564 | Ok(())
565 | }
566 |
567 | /// Handle WM_SYSCOMMAND message when custom menu item was selected.
568 | fn on_system_menu_command(&self, id: SystemMenu) -> win::LRESULT {
569 | match id {
570 | SystemMenu::About => {
571 | let mut text = format!("WSL Script");
572 | if let Ok(p) = std::env::current_exe() {
573 | if let Some(version) = wslscript_common::ver::product_version(&p) {
574 | text.push_str(&format!("\nVersion {}", version));
575 | }
576 | };
577 | unsafe {
578 | MessageBoxW(
579 | self.hwnd,
580 | wcstring(text).as_ptr(),
581 | wchz!("About WSL Script").as_ptr(),
582 | MB_OK | MB_ICONINFORMATION,
583 | );
584 | }
585 | 0
586 | }
587 | SystemMenu::Homepage => {
588 | unsafe {
589 | winapi::um::shellapi::ShellExecuteW(
590 | ptr::null_mut(),
591 | wchz!("open").as_ptr(),
592 | wchz!("https://sop.github.io/wslscript/").as_ptr(),
593 | ptr::null(),
594 | ptr::null(),
595 | SW_SHOWNORMAL,
596 | );
597 | }
598 | 0
599 | }
600 | }
601 | }
602 |
603 | /// Handle WM_SIZE message.
604 | ///
605 | /// * `width` - Window width
606 | /// * `height` - Window height
607 | fn on_resize(&self, width: i32, _height: i32) {
608 | self.move_control(Control::StaticMsg, 10, 10, width - 20, 40);
609 | self.move_control(Control::RegisterLabel, 10, 50, 60, 25);
610 | self.move_control(Control::EditExtension, 80, 50, width - 90 - 100, 25);
611 | self.move_control(Control::BtnRegister, width - 100, 50, 90, 25);
612 | self.move_control(Control::ListViewExtensions, 10, 85, width - 20, 75);
613 | self.move_control(Control::HoldModeLabel, 10, 170, 130, 20);
614 | self.move_control(Control::HoldModeCombo, 10, 190, 130, 100);
615 | self.move_control(Control::InteractiveLabel, 170, 190, 130, 20);
616 | self.move_control(Control::InteractiveCheckbox, 150, 190, 20, 20);
617 | self.move_control(Control::DistroLabel, 10, 220, 130, 20);
618 | self.move_control(Control::DistroCombo, 10, 240, 130, 100);
619 | self.move_control(Control::IconLabel, 150, 220, 32, 16);
620 | self.move_control(Control::StaticIcon, 150, 236, 32, 32);
621 | self.move_control(Control::BtnSave, width - 90, 240, 80, 25);
622 | }
623 |
624 | /// Move window control.
625 | fn move_control(&self, control: Control, x: i32, y: i32, width: i32, height: i32) {
626 | let hwnd = self.get_control_handle(control);
627 | unsafe { MoveWindow(hwnd, x, y, width, height, win::TRUE) };
628 | }
629 |
630 | /// Handle WM_COMMAND message from a control.
631 | ///
632 | /// * `hwnd` - Handle of the sending control
633 | /// * `control_id` - ID of the sending control
634 | /// * `code` - Notification code
635 | fn on_control(
636 | &mut self,
637 | _hwnd: windef::HWND,
638 | control_id: Control,
639 | code: win::WORD,
640 | ) -> Result {
641 | #[allow(clippy::single_match)]
642 | match control_id {
643 | Control::BtnRegister => match code {
644 | BN_CLICKED => return self.on_register_button_clicked(),
645 | _ => {}
646 | },
647 | Control::HoldModeCombo => match code {
648 | CBN_SELCHANGE => {
649 | if let Some(mode) = self.get_selected_hold_mode() {
650 | if let Some(cfg) = &mut self.current_ext_cfg {
651 | cfg.hold_mode = mode;
652 | }
653 | }
654 | }
655 | _ => {}
656 | },
657 | Control::InteractiveCheckbox => match code {
658 | BN_CLICKED => {
659 | let state = self.get_interactive_state();
660 | if let Some(cfg) = &mut self.current_ext_cfg {
661 | cfg.interactive = state;
662 | }
663 | }
664 | _ => {}
665 | },
666 | Control::InteractiveLabel => match code {
667 | // when interactive shell label is clicked
668 | STN_CLICKED => {
669 | let state = !self.get_interactive_state();
670 | if let Some(cfg) = &mut self.current_ext_cfg {
671 | cfg.interactive = state;
672 | }
673 | self.set_interactive_state(state);
674 | }
675 | _ => {}
676 | },
677 | Control::DistroCombo => match code {
678 | CBN_SELCHANGE => {
679 | let distro = self.get_selected_distro();
680 | if let Some(cfg) = &mut self.current_ext_cfg {
681 | cfg.distro = distro;
682 | }
683 | }
684 | _ => {}
685 | },
686 | Control::StaticIcon => match code {
687 | STN_DBLCLK => {
688 | if let Some(icon) = self.pick_icon_dlg() {
689 | if let Some(cfg) = &mut self.current_ext_cfg {
690 | cfg.icon = Some(icon);
691 | }
692 | self.update_control_states();
693 | }
694 | }
695 | _ => {}
696 | },
697 | Control::BtnSave => match code {
698 | BN_CLICKED => return self.on_save_button_clicked(),
699 | _ => {}
700 | },
701 | _ => {}
702 | }
703 | Ok(0)
704 | }
705 |
706 | /// Handle register button click.
707 | fn on_register_button_clicked(&mut self) -> Result {
708 | let ext = self
709 | .get_extension_input_text()
710 | .trim_matches('.')
711 | .to_string();
712 | if ext.is_empty() {
713 | return Ok(0);
714 | }
715 | if registry::is_registered_for_other(&ext)? {
716 | let s = wcstring(format!(
717 | ".{} extension is already registered for another application.\n\
718 | Register anyway?",
719 | ext
720 | ));
721 | let result = unsafe {
722 | MessageBoxW(
723 | self.hwnd,
724 | s.as_ptr(),
725 | wchz!("Confirm extension registration.").as_ptr(),
726 | MB_YESNO | MB_ICONQUESTION | MB_DEFBUTTON2,
727 | )
728 | };
729 | if result == IDNO {
730 | return Ok(0);
731 | }
732 | }
733 | let icon = ShellIcon::load_default()?;
734 | let config = registry::ExtConfig {
735 | extension: ext.clone(),
736 | icon: Some(icon),
737 | hold_mode: registry::HoldMode::Error,
738 | interactive: false,
739 | distro: None,
740 | };
741 | registry::register_extension(&config)?;
742 | // clear extension input
743 | self.set_extension_input_text(wcstr(wchz!("")));
744 | let idx = self.lv_extensions.find_ext(&ext).or_else(|| {
745 | // insert to listview
746 | if let Some(item) = self.lv_extensions.insert_item(0, &wcstring(&ext)) {
747 | let name = self.get_distro_label(None);
748 | self.lv_extensions
749 | .set_subitem_text(item, 1, &wcstring(name));
750 | return Some(item);
751 | }
752 | None
753 | });
754 | self.set_current_extension(idx);
755 | self.message = Some(format!("Registered .{} extension.", &ext));
756 | self.update_control_states();
757 | Ok(0)
758 | }
759 |
760 | /// Handle save button click.
761 | fn on_save_button_clicked(&mut self) -> Result {
762 | if let Some(config) = self.current_ext_cfg.as_ref() {
763 | registry::register_extension(config)?;
764 | self.message = Some(format!("Saved .{} extension.", config.extension));
765 | self.update_control_states();
766 | if let Some(item) = self.current_ext_idx {
767 | let name = self.get_distro_label(config.distro.as_ref());
768 | self.lv_extensions
769 | .set_subitem_text(item, 1, &wcstring(name));
770 | }
771 | }
772 | Ok(0)
773 | }
774 |
775 | /// Handle message from a menu.
776 | ///
777 | /// * `hmenu` - Handle to the menu
778 | /// * `item_id` - ID of the clicked menu item
779 | fn on_menucommand(&mut self, hmenu: windef::HMENU, item_id: MenuItem) -> win::LRESULT {
780 | match item_id {
781 | MenuItem::Unregister => {
782 | let idx = Self::get_menu_data::(hmenu);
783 | if let Some(ext) = self.lv_extensions.get_item_text(idx) {
784 | if let Err(e) = registry::unregister_extension(&ext) {
785 | let s = wcstring(format!("Failed to unregister extension: {}", e));
786 | win32::error_message(&s);
787 | return 0;
788 | }
789 | }
790 | self.lv_extensions.delete_item(idx);
791 | self.set_current_extension(None);
792 | self.update_control_states();
793 | // if there's no more registered extensions, and if extension
794 | // input was empty, reset to default extension
795 | if registry::query_registered_extensions()
796 | .unwrap_or_default()
797 | .is_empty()
798 | && self.get_extension_input_text().is_empty()
799 | {
800 | self.set_extension_input_text(&DEFAULT_EXTENSION);
801 | }
802 | }
803 | MenuItem::EditExtension => {
804 | let idx = Self::get_menu_data::(hmenu);
805 | self.set_current_extension(Some(idx));
806 | self.update_control_states();
807 | }
808 | }
809 | 0
810 | }
811 |
812 | /// Get application-defined value associated with a menu.
813 | fn get_menu_data(hmenu: windef::HMENU) -> T
814 | where
815 | T: From,
816 | {
817 | let mut mi = MENUINFO {
818 | cbSize: mem::size_of::() as u32,
819 | fMask: MIM_MENUDATA,
820 | ..unsafe { mem::zeroed() }
821 | };
822 | unsafe { GetMenuInfo(hmenu, &mut mi) };
823 | T::from(mi.dwMenuData)
824 | }
825 |
826 | /// Handle WM_NOTIFY message.
827 | ///
828 | /// * `hwnd` - Handle of the sending control
829 | /// * `control_id` - ID of the sending control
830 | /// * `code` - Notification code
831 | /// * `lparam` - Notification specific parameter
832 | fn on_notify(
833 | &mut self,
834 | hwnd: windef::HWND,
835 | control_id: Control,
836 | code: u32,
837 | lparam: *const isize,
838 | ) -> win::LRESULT {
839 | use commctrl::*;
840 | #[allow(clippy::single_match)]
841 | match control_id {
842 | Control::ListViewExtensions => match code {
843 | // when listview item is activated (eg. double clicked)
844 | LVN_ITEMACTIVATE => {
845 | let nmia = unsafe { &*(lparam as LPNMITEMACTIVATE) };
846 | if nmia.iItem < 0 {
847 | return 0;
848 | }
849 | self.set_current_extension(Some(nmia.iItem as usize));
850 | self.update_control_states();
851 | }
852 | // when listview item is right-clicked
853 | NM_RCLICK => {
854 | let nmia = unsafe { &*(lparam as LPNMITEMACTIVATE) };
855 | if nmia.iItem < 0 {
856 | return 0;
857 | }
858 | let hmenu = unsafe { CreatePopupMenu() };
859 | let mi = MENUINFO {
860 | cbSize: mem::size_of::() as _,
861 | fMask: MIM_MENUDATA | MIM_STYLE,
862 | dwStyle: MNS_NOTIFYBYPOS,
863 | dwMenuData: nmia.iItem as usize,
864 | ..unsafe { mem::zeroed() }
865 | };
866 | unsafe { SetMenuInfo(hmenu, &mi) };
867 | let mut mii = MENUITEMINFOW {
868 | cbSize: mem::size_of::() as _,
869 | fMask: MIIM_TYPE | MIIM_ID,
870 | fType: MFT_STRING,
871 | ..unsafe { mem::zeroed() }
872 | };
873 | mii.wID = MenuItem::EditExtension as _;
874 | mii.dwTypeData = wchz!("Edit").as_ptr() as _;
875 | unsafe { InsertMenuItemW(hmenu, 0, win::TRUE, &mii) };
876 | mii.wID = MenuItem::Unregister as _;
877 | mii.dwTypeData = wchz!("Unregister").as_ptr() as _;
878 | unsafe { InsertMenuItemW(hmenu, 1, win::TRUE, &mii) };
879 | let mut pos: windef::POINT = nmia.ptAction;
880 | unsafe { ClientToScreen(hwnd, &mut pos) };
881 | unsafe { TrackPopupMenuEx(hmenu, 0, pos.x, pos.y, self.hwnd, ptr::null_mut()) };
882 | }
883 | _ => {}
884 | },
885 | _ => {}
886 | }
887 | 0
888 | }
889 |
890 | /// Get currently selected extension.
891 | fn get_current_extension(&self) -> Option {
892 | self.current_ext_idx
893 | .and_then(|item| self.lv_extensions.get_item_text(item))
894 | }
895 |
896 | /// Get window handle to control.
897 | fn get_control_handle(&self, control: Control) -> windef::HWND {
898 | unsafe { GetDlgItem(self.hwnd, control as _) }
899 | }
900 |
901 | /// Get text from extension text input.
902 | fn get_extension_input_text(&self) -> String {
903 | let mut buf: Vec = Vec::with_capacity(32);
904 | unsafe {
905 | // NOTE: if text is longer than buffer, it's truncated
906 | let len = GetDlgItemTextW(
907 | self.hwnd,
908 | Control::EditExtension as _,
909 | buf.as_mut_ptr(),
910 | buf.capacity() as _,
911 | );
912 | buf.set_len(len as usize);
913 | }
914 | WideCString::from_vec(buf).unwrap().to_string_lossy()
915 | }
916 |
917 | /// Set text to extension input control.
918 | fn set_extension_input_text(&self, text: &WideCStr) {
919 | unsafe {
920 | SetDlgItemTextW(self.hwnd, Control::EditExtension as _, text.as_ptr());
921 | }
922 | }
923 |
924 | /// Set extension that is currently selected for edit.
925 | fn set_current_extension(&mut self, item: Option) {
926 | self.current_ext_idx = item;
927 | self.current_ext_cfg = self
928 | .get_current_extension()
929 | .and_then(|ext| registry::get_extension_config(&ext).ok());
930 | self.message = None;
931 | }
932 |
933 | /// Launch icon picker dialog.
934 | ///
935 | /// Returns ShellIcon or None if no icon was selected.
936 | fn pick_icon_dlg(&self) -> Option {
937 | let mut buf = [0_u16; win::MAX_PATH];
938 | let mut idx: std::os::raw::c_int = 0;
939 | if let Some(si) = self
940 | .current_ext_cfg
941 | .as_ref()
942 | .and_then(|cfg| cfg.icon.as_ref())
943 | {
944 | let mut path = si.path();
945 | if let Ok(p) = path.expand() {
946 | path = p;
947 | }
948 | let s = path.to_wide();
949 | if s.len() < buf.len() {
950 | unsafe { std::ptr::copy_nonoverlapping(s.as_ptr(), buf.as_mut_ptr(), s.len()) };
951 | idx = si.index() as i32;
952 | }
953 | }
954 | let result = unsafe { PickIconDlg(self.hwnd, buf.as_mut_ptr(), buf.len() as _, &mut idx) };
955 | if result == 0 {
956 | return None;
957 | }
958 | match buf.iter().position(|&c| c == 0) {
959 | Some(pos) => {
960 | let path = unsafe { WideCString::from_vec_unchecked(&buf[..=pos as usize]) };
961 | if let Ok(p) = win32::WinPathBuf::from(path.as_ucstr()).expand() {
962 | match ShellIcon::load(p, idx as u32) {
963 | Ok(icon) => Some(icon),
964 | Err(e) => {
965 | let s = wcstring(format!("Failed load icon: {}", e));
966 | win32::error_message(&s);
967 | None
968 | }
969 | }
970 | } else {
971 | None
972 | }
973 | }
974 | None => None,
975 | }
976 | }
977 |
978 | /// Get currently select hold mode.
979 | fn get_selected_hold_mode(&self) -> Option {
980 | let hwnd = self.get_control_handle(Control::HoldModeCombo);
981 | let idx = unsafe { SendMessageW(hwnd, CB_GETCURSEL, 0, 0) };
982 | let data = unsafe { SendMessageW(hwnd, CB_GETITEMDATA, idx as _, 0) };
983 | let cs = unsafe { WideCStr::from_ptr_str(data as *const ntdef::WCHAR) };
984 | registry::HoldMode::from_wcstr(cs)
985 | }
986 |
987 | /// Set hold mode to control.
988 | fn set_selected_hold_mode(&self, mode: registry::HoldMode) -> Option {
989 | let hwnd = self.get_control_handle(Control::HoldModeCombo);
990 | let count = unsafe { SendMessageW(hwnd, CB_GETCOUNT, 0, 0) as usize };
991 | for idx in 0..count {
992 | let data = unsafe { SendMessageW(hwnd, CB_GETITEMDATA, idx as _, 0) };
993 | let cs = unsafe { WideCStr::from_ptr_str(data as *const ntdef::WCHAR) };
994 | if let Some(m) = registry::HoldMode::from_wcstr(cs) {
995 | if m == mode {
996 | unsafe { SendMessageW(hwnd, CB_SETCURSEL, idx as _, 0) };
997 | return Some(idx);
998 | }
999 | }
1000 | }
1001 | None
1002 | }
1003 |
1004 | /// Get the interactive shell checkbox state.
1005 | fn get_interactive_state(&self) -> bool {
1006 | let result = unsafe { IsDlgButtonChecked(self.hwnd, Control::InteractiveCheckbox as _) };
1007 | result == 1
1008 | }
1009 |
1010 | /// Set the interactive shell checkbox state.
1011 | fn set_interactive_state(&self, state: bool) {
1012 | unsafe { CheckDlgButton(self.hwnd, Control::InteractiveCheckbox as _, state as _) };
1013 | }
1014 |
1015 | /// Set selected distro in combo box.
1016 | fn set_selected_distro(&self, distro: Option<®istry::DistroGUID>) -> Option {
1017 | let hwnd = self.get_control_handle(Control::DistroCombo);
1018 | let mut sel: usize = 0;
1019 | if let Some(guid) = distro {
1020 | let count = unsafe { SendMessageW(hwnd, CB_GETCOUNT, 0, 0) as usize };
1021 | for idx in 1..count {
1022 | let data = unsafe { SendMessageW(hwnd, CB_GETITEMDATA, idx as _, 0) };
1023 | let guid_str = unsafe { WideCStr::from_ptr_str(data as *const ntdef::WCHAR) };
1024 | if guid_str == guid.as_wcstr() {
1025 | sel = idx;
1026 | break;
1027 | }
1028 | }
1029 | }
1030 | unsafe { SendMessageW(hwnd, CB_SETCURSEL, sel as _, 0) };
1031 | Some(sel)
1032 | }
1033 |
1034 | /// Get currently selected GUID in distro combo box.
1035 | fn get_selected_distro(&self) -> Option {
1036 | let hwnd = self.get_control_handle(Control::DistroCombo);
1037 | let idx = unsafe { SendMessageW(hwnd, CB_GETCURSEL, 0, 0) };
1038 | if idx == 0 || idx == CB_ERR {
1039 | return None;
1040 | }
1041 | let data = unsafe { SendMessageW(hwnd, CB_GETITEMDATA, idx as _, 0) };
1042 | let cs = unsafe { WideCStr::from_ptr_str(data as *const ntdef::WCHAR) };
1043 | let s = cs.to_string_lossy();
1044 | registry::DistroGUID::from_str(&s).ok()
1045 | }
1046 |
1047 | /// Get label for distribution GUID.
1048 | fn get_distro_label(&self, guid: Option<®istry::DistroGUID>) -> String {
1049 | guid.and_then(|guid| self.distros.list.get(guid).map(|s| s.to_owned()))
1050 | .or_else(|| Some(String::from("Default")))
1051 | .unwrap_or_default()
1052 | }
1053 | }
1054 |
1055 | /// Set font to given window.
1056 | fn set_window_font(hwnd: windef::HWND, font: &Font) {
1057 | unsafe { SendMessageW(hwnd, WM_SETFONT, font.handle as _, win::TRUE as _) };
1058 | }
1059 |
1060 | impl WindowProc for MainWindow {
1061 | fn window_proc(
1062 | &mut self,
1063 | hwnd: windef::HWND,
1064 | msg: win::UINT,
1065 | wparam: win::WPARAM,
1066 | lparam: win::LPARAM,
1067 | ) -> Option {
1068 | match msg {
1069 | WM_NCCREATE => {
1070 | // store main window handle
1071 | self.hwnd = hwnd;
1072 | // WM_NCCREATE must be passed to DefWindowProc
1073 | None
1074 | }
1075 | WM_CREATE => {
1076 | if self.create_window_controls().is_err() {
1077 | return Some(-1);
1078 | }
1079 | if self.extend_system_menu().is_err() {
1080 | log::error!("Failed to extend system menu.");
1081 | }
1082 | Some(0)
1083 | }
1084 | WM_SIZE => {
1085 | self.on_resize(
1086 | i32::from(win::LOWORD(lparam as _)),
1087 | i32::from(win::HIWORD(lparam as _)),
1088 | );
1089 | Some(0)
1090 | }
1091 | WM_GETMINMAXINFO => {
1092 | let mmi = unsafe { &mut *(lparam as LPMINMAXINFO) };
1093 | mmi.ptMinTrackSize.x = MIN_WINDOW_SIZE.0;
1094 | mmi.ptMinTrackSize.y = MIN_WINDOW_SIZE.1;
1095 | Some(0)
1096 | }
1097 | WM_CTLCOLORSTATIC => Some(unsafe { wingdi::GetStockObject(COLOR_WINDOW + 1_i32) } as _),
1098 | WM_COMMAND => {
1099 | // if lParam is non-zero, message is from a control
1100 | if lparam != 0 {
1101 | if let Ok(id) = Control::try_from(win::LOWORD(wparam as _)) {
1102 | match self.on_control(lparam as _, id, win::HIWORD(wparam as _)) {
1103 | Err(e) => {
1104 | win32::error_message(&e.to_wide());
1105 | return Some(0);
1106 | }
1107 | Ok(l) => return Some(l),
1108 | }
1109 | }
1110 | }
1111 | // if lParam is zero and HIWORD of wParam is zero, message is from a menu
1112 | else if win::HIWORD(wparam as u32) == 0 {
1113 | if let Ok(id) = MenuItem::try_from(wparam as u32) {
1114 | return Some(self.on_menucommand(ptr::null_mut(), id));
1115 | }
1116 | }
1117 | None
1118 | }
1119 | WM_MENUCOMMAND => {
1120 | let hmenu = lparam as windef::HMENU;
1121 | let item_id = unsafe { GetMenuItemID(hmenu, wparam as _) };
1122 | if let Ok(id) = MenuItem::try_from(item_id) {
1123 | return Some(self.on_menucommand(hmenu, id));
1124 | }
1125 | None
1126 | }
1127 | WM_SYSCOMMAND => {
1128 | if let Ok(id) = SystemMenu::try_from(wparam as u32) {
1129 | return Some(self.on_system_menu_command(id));
1130 | }
1131 | None
1132 | }
1133 | WM_NOTIFY => {
1134 | let hdr = unsafe { &*(lparam as LPNMHDR) };
1135 | if let Ok(id) = Control::try_from(hdr.idFrom as u16) {
1136 | return Some(self.on_notify(hdr.hwndFrom, id, hdr.code, lparam as *const _));
1137 | }
1138 | None
1139 | }
1140 | WM_CLOSE => {
1141 | unsafe { DestroyWindow(hwnd) };
1142 | Some(0)
1143 | }
1144 | WM_DESTROY => {
1145 | unsafe { PostQuitMessage(0) };
1146 | Some(0)
1147 | }
1148 | _ => None,
1149 | }
1150 | }
1151 | }
1152 |
1153 | /// Subclass callback for the extension input control.
1154 | extern "system" fn extension_input_proc(
1155 | hwnd: windef::HWND,
1156 | msg: win::UINT,
1157 | wparam: win::WPARAM,
1158 | lparam: win::LPARAM,
1159 | _subclass_id: basetsd::UINT_PTR,
1160 | data: basetsd::DWORD_PTR,
1161 | ) -> win::LRESULT {
1162 | let wnd = unsafe { &mut *(data as *mut MainWindow) };
1163 | #[allow(clippy::single_match)]
1164 | match msg {
1165 | // TODO: filter dots etc.
1166 | WM_KEYDOWN => match wparam as i32 {
1167 | VK_RETURN => {
1168 | if let Err(e) = wnd.on_register_button_clicked() {
1169 | win32::error_message(&e.to_wide());
1170 | }
1171 | return 0;
1172 | }
1173 | _ => {}
1174 | },
1175 | WM_CHAR => match wparam as i32 {
1176 | VK_RETURN => {
1177 | return 0;
1178 | }
1179 | _ => {
1180 | if let Some(ch) = std::char::from_u32(wparam as _) {
1181 | match ch {
1182 | // illegal filename characters
1183 | '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*' => return 0,
1184 | // space
1185 | ' ' => return 0,
1186 | // no periods in extension
1187 | '.' => return 0,
1188 | _ => {}
1189 | }
1190 | }
1191 | }
1192 | },
1193 | _ => {}
1194 | }
1195 | unsafe { commctrl::DefSubclassProc(hwnd, msg, wparam, lparam) }
1196 | }
1197 |
1198 | extern "system" {
1199 | /// https://docs.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-pickicondlg
1200 | pub fn PickIconDlg(
1201 | hwnd: windef::HWND,
1202 | pszIconPath: ntdef::PWSTR,
1203 | cchIconPath: win::UINT,
1204 | piIconIndex: *mut std::os::raw::c_int,
1205 | ) -> std::os::raw::c_int;
1206 | }
1207 |
--------------------------------------------------------------------------------
/wslscript/src/main.rs:
--------------------------------------------------------------------------------
1 | #![windows_subsystem = "windows"]
2 |
3 | use std::env;
4 | use std::ffi::OsString;
5 | use std::path::PathBuf;
6 | use wchar::*;
7 | use wslscript_common::error::*;
8 | use wslscript_common::wsl;
9 |
10 | mod gui;
11 |
12 | fn main() {
13 | if let Err(e) = run_app() {
14 | log::error!("{}", e);
15 | unsafe {
16 | use winapi::um::winuser::*;
17 | MessageBoxW(
18 | std::ptr::null_mut(),
19 | e.to_wide().as_ptr(),
20 | wchz!("Error").as_ptr(),
21 | MB_OK | MB_ICONERROR | MB_SERVICE_NOTIFICATION,
22 | );
23 | }
24 | }
25 | }
26 |
27 | fn run_app() -> Result<(), Error> {
28 | // set up logging
29 | #[cfg(feature = "debug")]
30 | if let Ok(mut exe) = env::current_exe() {
31 | let stem = exe.file_stem().map_or_else(
32 | || "debug.log".to_string(),
33 | |s| s.to_string_lossy().into_owned(),
34 | );
35 | exe.pop();
36 | exe.push(format!("{}.log", stem));
37 | simple_logging::log_to_file(exe, log::LevelFilter::Debug)?;
38 | }
39 | // log command line arguments
40 | #[cfg(feature = "debug")]
41 | env::args_os()
42 | .enumerate()
43 | .for_each(|(n, arg)| log::debug!("Arg {}: {}", n, arg.to_string_lossy()));
44 | // if program was started with the first and only argument being a .sh file
45 | // or one of the registered extensions.
46 | // this handles a script file being dragged and dropped to wslscript.exe.
47 | if env::args_os().len() == 2 {
48 | if let Some(arg) = env::args_os()
49 | .nth(1)
50 | .filter(|arg| PathBuf::from(arg).exists())
51 | {
52 | let path = PathBuf::from(&arg);
53 | let ext = path.extension().unwrap_or_default().to_string_lossy();
54 | // check whether extension is registered
55 | let opts = match wsl::WSLOptions::from_ext(&ext) {
56 | Some(opts) => Some(opts),
57 | // if extension is ".sh", use default options
58 | None if ext == "sh" => Some(wsl::WSLOptions::default()),
59 | _ => None,
60 | };
61 | if let Some(opts) = opts {
62 | return execute_wsl(vec![arg], opts);
63 | }
64 | }
65 | }
66 | // seek for -E flag and collect all arguments after that
67 | let wsl_args: Vec = env::args_os()
68 | .skip_while(|arg| arg != "-E")
69 | .skip(1)
70 | .collect();
71 | if !wsl_args.is_empty() {
72 | // collect arguments preceding -E
73 | let opts: Vec = env::args_os().take_while(|arg| arg != "-E").collect();
74 | return execute_wsl(wsl_args, wsl::WSLOptions::from_args(opts));
75 | }
76 | // start Windows GUI
77 | gui::start_gui()
78 | }
79 |
80 | fn execute_wsl(args: Vec, opts: wsl::WSLOptions) -> Result<(), Error> {
81 | // convert args to paths, canonicalize when possible
82 | let paths: Vec = args
83 | .iter()
84 | .map(PathBuf::from)
85 | .map(|p| p.canonicalize().unwrap_or(p))
86 | .collect();
87 | // ensure not trying to invoke self
88 | if let Some(exe_os) = env::current_exe().ok().and_then(|p| p.canonicalize().ok()) {
89 | if paths[0] == exe_os {
90 | return Err(Error::InvalidPathError);
91 | }
92 | }
93 | // convert paths to WSL equivalents
94 | let wsl_paths = wsl::paths_to_wsl(&paths, &opts, None)?;
95 | wsl::run_wsl(&wsl_paths[0], &wsl_paths[1..], &opts)
96 | }
97 |
--------------------------------------------------------------------------------
/wslscript_common/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 |
--------------------------------------------------------------------------------
/wslscript_common/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 = "wslscript_common"
7 | version = "0.1.0"
8 |
--------------------------------------------------------------------------------
/wslscript_common/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "wslscript_common"
3 | description = "Common libraries for WSL Script."
4 | version = "0.1.0"
5 | authors = ["Joni Kollani "]
6 | license = "MIT"
7 | homepage = "https://sop.github.io/wslscript/"
8 | repository = "https://github.com/sop/wslscript"
9 | edition = "2021"
10 |
11 | [dependencies]
12 | thiserror = "2.0"
13 | anyhow = "1.0"
14 | once_cell = "1.20"
15 | widestring = "1.1"
16 | wchar = "0.11"
17 | guid_win = "0.2.0"
18 | libloading = "0.8"
19 | log = { version = "0.4", features = ["release_max_level_off"] }
20 | simple-logging = "2.0"
21 |
22 | [dependencies.winapi]
23 | version = "0.3.9"
24 | features = [
25 | "winuser",
26 | "winbase",
27 | "winerror",
28 | "winver",
29 | "errhandlingapi",
30 | "commctrl",
31 | "processenv",
32 | "shellapi",
33 | ]
34 |
35 | [dependencies.winreg]
36 | version = "0.55"
37 | features = ["transactions"]
38 |
39 | [features]
40 | debug = []
41 |
--------------------------------------------------------------------------------
/wslscript_common/src/error.rs:
--------------------------------------------------------------------------------
1 | use crate::wcstring;
2 | use thiserror::Error;
3 |
4 | #[derive(Debug, Error)]
5 | pub enum Error {
6 | #[error("Path contains invalid UTF-8 characters.")]
7 | StringToPathUTF8Error,
8 |
9 | #[error("Failed to convert Windows path to WSL path.")]
10 | WinToUnixPathError,
11 |
12 | #[error("WSL not found or not installed.")]
13 | WSLNotFound,
14 |
15 | #[error("Failed to start WSL process.")]
16 | WSLProcessError,
17 |
18 | #[error("Invalid path.")]
19 | InvalidPathError,
20 |
21 | #[error("Command is too long.")]
22 | CommandTooLong,
23 |
24 | #[error("String is not nul terminated.")]
25 | MissingNulError,
26 |
27 | #[error("Operation was cancelled.")]
28 | Cancel,
29 |
30 | #[error("Registry error: {0}")]
31 | RegistryError(std::io::Error),
32 |
33 | #[error("IO error: {0}")]
34 | IOError(std::io::Error),
35 |
36 | #[error("Dynamic library error: {0}")]
37 | LibraryError(String),
38 |
39 | #[error("WinAPI error: {0}")]
40 | WinAPIError(String),
41 |
42 | #[error("Drop handler error: {0}")]
43 | DropHandlerError(String),
44 |
45 | #[error("Error: {0}")]
46 | GenericError(String),
47 |
48 | #[error("Logic error: {0}")]
49 | LogicError(&'static str),
50 | }
51 |
52 | impl Error {
53 | pub fn to_wide(&self) -> widestring::WideCString {
54 | wcstring(self.to_string())
55 | }
56 | }
57 |
58 | impl From for Error {
59 | fn from(e: anyhow::Error) -> Error {
60 | e.downcast::()
61 | .unwrap_or_else(|e: anyhow::Error| Error::GenericError(e.to_string()))
62 | }
63 | }
64 |
65 | impl From for Error {
66 | fn from(e: std::io::Error) -> Error {
67 | Error::IOError(e)
68 | }
69 | }
70 |
71 | impl From for Error {
72 | fn from(_: widestring::error::MissingNulTerminator) -> Error {
73 | Error::MissingNulError
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/wslscript_common/src/font.rs:
--------------------------------------------------------------------------------
1 | use crate::error::*;
2 | use crate::win32;
3 | use std::mem;
4 | use std::ptr;
5 | use winapi::shared::minwindef as win;
6 | use winapi::shared::windef;
7 | use winapi::um::wingdi;
8 | use winapi::um::winuser;
9 |
10 | /// Logical font.
11 | pub struct Font {
12 | pub handle: windef::HFONT,
13 | }
14 |
15 | impl Default for Font {
16 | fn default() -> Self {
17 | Self {
18 | handle: ptr::null_mut(),
19 | }
20 | }
21 | }
22 |
23 | impl Font {
24 | pub fn new_default_caption() -> Result {
25 | Font::new_caption(0)
26 | }
27 |
28 | /// Get default caption font with given size.
29 | ///
30 | /// See: https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-logfonta
31 | pub fn new_caption(size: i32) -> Result {
32 | use winuser::*;
33 | let mut metrics = NONCLIENTMETRICSW {
34 | cbSize: mem::size_of::() as _,
35 | ..unsafe { mem::zeroed() }
36 | };
37 | if win::FALSE
38 | == unsafe {
39 | SystemParametersInfoW(
40 | SPI_GETNONCLIENTMETRICS,
41 | metrics.cbSize,
42 | &mut metrics as *mut _ as *mut _,
43 | 0,
44 | )
45 | }
46 | {
47 | return Err(win32::last_error());
48 | }
49 | let mut lf: wingdi::LOGFONTW = metrics.lfCaptionFont;
50 | if size > 0 {
51 | lf.lfHeight = size;
52 | }
53 | let font = unsafe { wingdi::CreateFontIndirectW(&lf) };
54 | if font.is_null() {
55 | return Err(win32::last_error());
56 | }
57 | Ok(Self { handle: font })
58 | }
59 | }
60 |
61 | impl Drop for Font {
62 | fn drop(&mut self) {
63 | if !self.handle.is_null() {
64 | unsafe { wingdi::DeleteObject(self.handle as _) };
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/wslscript_common/src/icon.rs:
--------------------------------------------------------------------------------
1 | use crate::error::*;
2 | use crate::win32::*;
3 | use std::ptr::null_mut;
4 | use std::str::FromStr;
5 | use wchar::*;
6 | use widestring::*;
7 | use winapi::shared::windef;
8 | use winapi::um::libloaderapi;
9 | use winapi::um::shellapi;
10 | use winapi::um::winuser;
11 |
12 | /// The Old New Thing - How the shell converts an icon location into an icon
13 | /// https://devblogs.microsoft.com/oldnewthing/20100505-00/?p=14153
14 |
15 | #[derive(Clone)]
16 | pub struct ShellIcon {
17 | /// Handle to loaded icon.
18 | handle: windef::HICON,
19 | /// Path to file containing icon.
20 | path: WinPathBuf,
21 | /// Icon index in a file.
22 | index: u32,
23 | }
24 |
25 | impl ShellIcon {
26 | pub fn load(path: WinPathBuf, index: u32) -> Result {
27 | let s = path.to_wide();
28 | let handle = unsafe {
29 | shellapi::ExtractIconW(
30 | libloaderapi::GetModuleHandleW(null_mut()),
31 | s.as_ptr(),
32 | index,
33 | )
34 | };
35 | if handle.is_null() {
36 | return Err(Error::WinAPIError(String::from(
37 | "No icon found from the file.",
38 | )));
39 | }
40 | if handle == 1 as _ {
41 | return Err(Error::WinAPIError(String::from("File not found.")));
42 | }
43 | Ok(Self {
44 | handle,
45 | path,
46 | index,
47 | })
48 | }
49 |
50 | /// Load default icon.
51 | pub fn load_default() -> Result {
52 | use std::os::windows::ffi::OsStrExt;
53 | let s: Vec = std::env::current_exe()?
54 | .canonicalize()?
55 | .as_os_str()
56 | .encode_wide()
57 | .collect();
58 | // remove UNC prefix
59 | let ws = if &s[0..4] == wch!(r"\\?\") {
60 | WideStr::from_slice(&s[4..])
61 | } else {
62 | WideStr::from_slice(&s)
63 | };
64 | Self::load(WinPathBuf::from(ws), 0)
65 | }
66 |
67 | pub fn handle(&self) -> windef::HICON {
68 | self.handle
69 | }
70 |
71 | pub fn path(&self) -> WinPathBuf {
72 | self.path.clone()
73 | }
74 |
75 | pub fn index(&self) -> u32 {
76 | self.index
77 | }
78 |
79 | pub fn shell_path(&self) -> WideCString {
80 | let mut p = self.path.to_wide().to_os_string();
81 | p.push(format!(",{}", self.index));
82 | unsafe { WideCString::from_os_str_unchecked(p) }
83 | }
84 | }
85 |
86 | impl Drop for ShellIcon {
87 | fn drop(&mut self) {
88 | unsafe { winuser::DestroyIcon(self.handle) };
89 | }
90 | }
91 |
92 | impl FromStr for ShellIcon {
93 | type Err = Error;
94 |
95 | fn from_str(s: &str) -> Result {
96 | let path: String;
97 | let index: u32;
98 | if let Some(i) = s.rfind(',') {
99 | path = s[0..i].to_string();
100 | index = s[i + 1..].parse::().unwrap_or(0);
101 | } else {
102 | path = s.to_owned();
103 | index = 0;
104 | }
105 | Self::load(WinPathBuf::from(path.as_str()), index)
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/wslscript_common/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod error;
2 | pub mod font;
3 | pub mod icon;
4 | pub mod registry;
5 | pub mod ver;
6 | pub mod win32;
7 | pub mod wsl;
8 |
9 | pub use registry::DROP_HANDLER_CLSID;
10 | pub use win32::{wcstr, wcstring};
11 |
--------------------------------------------------------------------------------
/wslscript_common/src/registry.rs:
--------------------------------------------------------------------------------
1 | use crate::error::*;
2 | use crate::icon::ShellIcon;
3 | use crate::win32::*;
4 | use guid_win::Guid;
5 | use once_cell::sync::Lazy;
6 | use std::collections::HashMap;
7 | use std::ffi::OsString;
8 | use std::path::{Path, PathBuf};
9 | use std::pin::Pin;
10 | use std::str::FromStr;
11 | use wchar::*;
12 | use widestring::*;
13 | use winapi::shared::minwindef;
14 | use winapi::shared::winerror;
15 | use winapi::um::winnt;
16 | use winreg::enums::*;
17 | use winreg::transaction::Transaction;
18 | use winreg::RegKey;
19 |
20 | const HANDLER_PREFIX: &str = "wslscript";
21 | const CLASSES_SUBKEY: &str = r"Software\Classes";
22 | const LXSS_SUBKEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Lxss";
23 |
24 | /// Drop handler shell extension GUID: {81521ebe-a2d4-450b-9bf8-5c23ed8730d0}
25 | pub static DROP_HANDLER_CLSID: Lazy =
26 | Lazy::new(|| Guid::from_str("81521ebe-a2d4-450b-9bf8-5c23ed8730d0").unwrap());
27 |
28 | /// Configuration for registered file name extension.
29 | #[derive(Clone)]
30 | pub struct ExtConfig {
31 | /// Filetype extension without leading dot.
32 | pub extension: String,
33 | /// Icon for the filetype.
34 | pub icon: Option,
35 | /// Hold mode.
36 | pub hold_mode: HoldMode,
37 | /// Whether to run bash as an interactive shell.
38 | pub interactive: bool,
39 | /// WSL distribution to run.
40 | pub distro: Option,
41 | }
42 |
43 | /// Terminal window hold mode after script exits.
44 | #[derive(Clone, Copy, PartialEq)]
45 | pub enum HoldMode {
46 | /// Always close terminal window on exit.
47 | Never,
48 | /// Always wait for keypress on exit.
49 | Always,
50 | /// Wait for keypress when exit code != 0.
51 | Error,
52 | }
53 |
54 | impl HoldMode {
55 | const WCSTR_NEVER: &'static [WideChar] = wchz!("never");
56 | const WCSTR_ALWAYS: &'static [WideChar] = wchz!("always");
57 | const WCSTR_ERROR: &'static [WideChar] = wchz!("error");
58 |
59 | /// Create from nul terminated wide string.
60 | pub fn from_wcstr(s: &WideCStr) -> Option {
61 | match s.as_slice_with_nul() {
62 | Self::WCSTR_NEVER => Some(Self::Never),
63 | Self::WCSTR_ALWAYS => Some(Self::Always),
64 | Self::WCSTR_ERROR => Some(Self::Error),
65 | _ => None,
66 | }
67 | }
68 |
69 | /// Create from &str.
70 | pub fn from_str(s: &str) -> Option {
71 | WideCString::from_str(s)
72 | .ok()
73 | .and_then(|s| Self::from_wcstr(&s))
74 | }
75 |
76 | /// Get mode string as a nul terminated wide string.
77 | pub fn as_wcstr(self) -> &'static WideCStr {
78 | match self {
79 | Self::Never => unsafe { WideCStr::from_slice_unchecked(Self::WCSTR_NEVER) },
80 | Self::Always => unsafe { WideCStr::from_slice_unchecked(Self::WCSTR_ALWAYS) },
81 | Self::Error => unsafe { WideCStr::from_slice_unchecked(Self::WCSTR_ERROR) },
82 | }
83 | }
84 |
85 | /// Get mode as a utf-8 string.
86 | pub fn as_string(self) -> String {
87 | self.as_wcstr().to_string_lossy()
88 | }
89 | }
90 |
91 | impl Default for HoldMode {
92 | fn default() -> Self {
93 | Self::Error
94 | }
95 | }
96 |
97 | /// GUID of the WSL distribution.
98 | #[derive(Clone, Eq)]
99 | pub struct DistroGUID {
100 | guid: Guid,
101 | /// Pinned wide c-string of the GUID for win32 usage. Enclosed in `{`...`}`.
102 | wcs: Pin,
103 | }
104 |
105 | impl DistroGUID {
106 | /// Get reference to the pinned wide c-string of the GUID.
107 | pub fn as_wcstr(&self) -> &WideCStr {
108 | &self.wcs
109 | }
110 | }
111 |
112 | impl std::ops::Deref for DistroGUID {
113 | type Target = Guid;
114 | fn deref(&self) -> &Self::Target {
115 | &self.guid
116 | }
117 | }
118 |
119 | impl std::fmt::Display for DistroGUID {
120 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 | let s = self.wcs.to_string().map_err(|_| std::fmt::Error)?;
122 | f.write_str(&s)
123 | }
124 | }
125 |
126 | impl FromStr for DistroGUID {
127 | type Err = ();
128 | fn from_str(s: &str) -> Result {
129 | let guid = Guid::from_str(s).map_err(|_| ())?;
130 | let s = guid.to_string().to_ascii_lowercase();
131 | let wcs = unsafe { WideCString::from_str_unchecked(s) };
132 | Ok(Self {
133 | guid,
134 | wcs: Pin::new(wcs),
135 | })
136 | }
137 | }
138 |
139 | impl std::cmp::PartialEq for DistroGUID {
140 | fn eq(&self, other: &Self) -> bool {
141 | self.guid.eq(&other.guid)
142 | }
143 | }
144 |
145 | impl std::hash::Hash for DistroGUID {
146 | fn hash(&self, state: &mut H) {
147 | self.guid.hash(state);
148 | }
149 | }
150 |
151 | /// List of available WSL distributions mapped from GUID to name.
152 | pub struct Distros {
153 | pub list: HashMap,
154 | pub default: Option,
155 | }
156 |
157 | impl Default for Distros {
158 | fn default() -> Self {
159 | Self {
160 | list: HashMap::new(),
161 | default: None,
162 | }
163 | }
164 | }
165 |
166 | impl Distros {
167 | /// Get a list of _(GUID, name)_ pairs sorted for GUI listing.
168 | pub fn sorted_pairs(&self) -> Vec<(&DistroGUID, &str)> {
169 | let mut pairs = self
170 | .list
171 | .iter()
172 | .map(|(k, v)| (k, v.as_str()))
173 | .collect::>();
174 | pairs.sort_by(|&a, &b| {
175 | use std::cmp::Ordering::*;
176 | if let Some(default) = self.default.as_ref() {
177 | if a.0 == default {
178 | return Less;
179 | }
180 | if b.0 == default {
181 | return Greater;
182 | }
183 | }
184 | a.1.cmp(b.1)
185 | });
186 | pairs
187 | }
188 | }
189 |
190 | /// Registers WSL Script as a handler for given file extension.
191 | ///
192 | /// See https://docs.microsoft.com/en-us/windows/win32/shell/fa-file-types
193 | /// See https://docs.microsoft.com/en-us/windows/win32/shell/fa-progids
194 | /// See https://docs.microsoft.com/en-us/windows/win32/shell/fa-perceivedtypes
195 | ///
196 | pub fn register_extension(config: &ExtConfig) -> Result<(), Error> {
197 | let ext = config.extension.as_str();
198 | if ext.is_empty() {
199 | return Err(Error::LogicError("No extension."));
200 | }
201 | register_server()?;
202 | let tx = Transaction::new().map_err(|e| Error::RegistryError(e))?;
203 | let base = RegKey::predef(HKEY_CURRENT_USER)
204 | .open_subkey_transacted_with_flags(CLASSES_SUBKEY, &tx, KEY_ALL_ACCESS)
205 | .map_err(|e| Error::RegistryError(e))?;
206 | let name = format!("{}.{}", HANDLER_PREFIX, ext);
207 | // delete previous handler key in a transaction
208 | // see https://docs.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regdeletekeytransactedw#remarks
209 | if let Ok(key) = base.open_subkey_transacted_with_flags(&name, &tx, KEY_ALL_ACCESS) {
210 | key.delete_subkey_all("")
211 | .map_err(|e| Error::RegistryError(e))?;
212 | }
213 | let cmd = get_command(config)?.to_os_string();
214 | let icon: Option = config
215 | .icon
216 | .as_ref()
217 | .map(|icon| icon.shell_path().to_os_string());
218 | let handler_desc = format!("WSL Shell Script (.{})", ext);
219 | let hold_mode = config.hold_mode.as_string();
220 | let interactive = config.interactive as u32;
221 | // Software\Classes\wslscript.ext
222 | set_value(&tx, &base, &name, "", &handler_desc)?;
223 | set_value(&tx, &base, &name, "EditFlags", &0x30u32)?;
224 | set_value(&tx, &base, &name, "FriendlyTypeName", &handler_desc)?;
225 | set_value(&tx, &base, &name, "HoldMode", &hold_mode)?;
226 | set_value(&tx, &base, &name, "Interactive", &interactive)?;
227 | if let Some(distro) = &config.distro {
228 | set_value(&tx, &base, &name, "Distribution", &distro.to_string())?;
229 | }
230 | // Software\Classes\wslscript.ext\DefaultIcon
231 | if let Some(s) = &icon {
232 | let path = format!(r"{}\DefaultIcon", name);
233 | set_value(&tx, &base, &path, "", &s.as_os_str())?;
234 | }
235 | // Software\Classes\wslscript.ext\shell
236 | let path = format!(r"{}\shell", name);
237 | set_value(&tx, &base, &path, "", &"open")?;
238 | // Software\Classes\wslscript.ext\shell\open - Open command
239 | let path = format!(r"{}\shell\open", name);
240 | set_value(&tx, &base, &path, "", &"Run in WSL")?;
241 | if let Some(s) = &icon {
242 | set_value(&tx, &base, &path, "Icon", &s.as_os_str())?;
243 | }
244 | // Software\Classes\wslscript.ext\shell\open\command
245 | let path = format!(r"{}\shell\open\command", name);
246 | set_value(&tx, &base, &path, "", &cmd.as_os_str())?;
247 | // Software\Classes\wslscript.ext\shell\runas - Run as administrator
248 | let path = format!(r"{}\shell\runas", name);
249 | set_value(&tx, &base, &path, "Extended", &"")?;
250 | if let Some(s) = &icon {
251 | set_value(&tx, &base, &path, "Icon", &s.as_os_str())?;
252 | }
253 | // Software\Classes\wslscript.ext\shell\runas\command
254 | let path = format!(r"{}\shell\runas\command", name);
255 | set_value(&tx, &base, &path, "", &cmd.as_os_str())?;
256 | // Software\Classes\wslscript.ext\shellex\DropHandler - Drop handler
257 | let path = format!(r"{}\shellex\DropHandler", name);
258 | // {60254CA5-953B-11CF-8C96-00AA00B8708C} (WSH DropHandler)
259 | // {86C86720-42A0-1069-A2E8-08002B30309D} (EXE DropHandler)
260 | let value = DROP_HANDLER_CLSID.to_string();
261 | set_value(&tx, &base, &path, "", &value)?;
262 | // Software\Classes\.ext - Register handler for extension
263 | let path = format!(".{}", ext);
264 | set_value(&tx, &base, &path, "", &name)?;
265 | set_value(&tx, &base, &path, "PerceivedType", &"application")?;
266 | // Software\Classes\.ext\OpenWithProgIds - Add extension to open with list
267 | let path = format!(r".{}\OpenWithProgIds", ext);
268 | set_value(&tx, &base, &path, &name, &"")?;
269 | tx.commit().map_err(|e| Error::RegistryError(e))?;
270 | notify_shell_change();
271 | Ok(())
272 | }
273 |
274 | /// Unregister extension.
275 | pub fn unregister_extension(ext: &str) -> Result<(), Error> {
276 | let tx = Transaction::new().map_err(|e| Error::RegistryError(e))?;
277 | let base = RegKey::predef(HKEY_CURRENT_USER)
278 | .open_subkey_transacted_with_flags(CLASSES_SUBKEY, &tx, KEY_ALL_ACCESS)
279 | .map_err(|e| Error::RegistryError(e))?;
280 | let name = format!("{}.{}", HANDLER_PREFIX, ext);
281 | // delete handler
282 | if let Ok(key) = base.open_subkey_transacted_with_flags(&name, &tx, KEY_ALL_ACCESS) {
283 | key.delete_subkey_all("")
284 | .map_err(|e| Error::RegistryError(e))?;
285 | base.delete_subkey_transacted(&name, &tx)
286 | .map_err(|e| Error::RegistryError(e))?;
287 | }
288 | let ext_name = format!(".{}", ext);
289 | if let Ok(ext_key) = base.open_subkey_transacted_with_flags(&ext_name, &tx, KEY_ALL_ACCESS) {
290 | // if extension has handler as a default
291 | if let Ok(val) = ext_key.get_value::("") {
292 | if val == name {
293 | // set default handler to unset
294 | ext_key
295 | .delete_value("")
296 | .map_err(|e| Error::RegistryError(e))?;
297 | }
298 | }
299 | // cleanup OpenWithProgids
300 | let open_with_name = "OpenWithProgIds";
301 | if let Ok(open_with_key) =
302 | ext_key.open_subkey_transacted_with_flags(open_with_name, &tx, KEY_ALL_ACCESS)
303 | {
304 | // remove handler
305 | if let Some(progid) = open_with_key
306 | .enum_values()
307 | .find_map(|item| item.ok().filter(|(k, _)| *k == name).map(|(k, _)| k))
308 | {
309 | open_with_key
310 | .delete_value(progid)
311 | .map_err(|e| Error::RegistryError(e))?;
312 | }
313 | // if OpenWithProgids was left empty
314 | if let Ok(info) = open_with_key.query_info() {
315 | if info.sub_keys == 0 && info.values == 0 {
316 | ext_key
317 | .delete_subkey_transacted(open_with_name, &tx)
318 | .map_err(|e| Error::RegistryError(e))?;
319 | }
320 | }
321 | }
322 | // if default handler is unset
323 | if ext_key.get_value::(&"").is_err() {
324 | // ... and extension has no subkeys
325 | if let Ok(info) = ext_key.query_info() {
326 | if info.sub_keys == 0 {
327 | // ... remove extension key altogether
328 | base.delete_subkey_transacted(&ext_name, &tx)
329 | .map_err(|e| Error::RegistryError(e))?;
330 | }
331 | }
332 | }
333 | }
334 | tx.commit().map_err(|e| Error::RegistryError(e))?;
335 | // if there's no registered extensions, unregister shell extension
336 | if let Ok(exts) = query_registered_extensions() {
337 | if exts.is_empty() {
338 | remove_server_from_registry()?;
339 | }
340 | }
341 | notify_shell_change();
342 | Ok(())
343 | }
344 |
345 | extern "system" {
346 | fn SHChangeNotify(
347 | weventid: winnt::LONG,
348 | uflags: minwindef::UINT,
349 | dwitem1: minwindef::LPCVOID,
350 | dwitem2: minwindef::LPCVOID,
351 | );
352 | }
353 |
354 | /// Notify the system that file associations have been changed.
355 | ///
356 | /// See: https://docs.microsoft.com/en-us/windows/win32/shell/fa-file-types
357 | /// See: https://docs.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shchangenotify
358 | fn notify_shell_change() {
359 | const SHCNE_ASSOCCHANGED: winnt::LONG = 0x08000000;
360 | const SHCNF_IDLIST: minwindef::UINT = 0;
361 | unsafe {
362 | SHChangeNotify(
363 | SHCNE_ASSOCCHANGED,
364 | SHCNF_IDLIST,
365 | std::ptr::null(),
366 | std::ptr::null(),
367 | )
368 | };
369 | }
370 |
371 | /// Get the wslscript command for filetype registry.
372 | fn get_command(config: &ExtConfig) -> Result {
373 | let exe = WinPathBuf::new(std::env::current_exe()?)
374 | .canonicalize()?
375 | .without_extended();
376 | let mut cmd = WideString::new();
377 | cmd.push(exe.quoted());
378 | cmd.push_slice(wch!(r#" --ext ""#));
379 | cmd.push_str(&config.extension);
380 | cmd.push_slice(wch!(r#"""#));
381 | cmd.push_slice(wch!(r#" -E "%0" %*"#));
382 | Ok(cmd)
383 | }
384 |
385 | /// Set registry value.
386 | fn set_value(
387 | tx: &Transaction,
388 | base: &RegKey,
389 | path: &str,
390 | name: &str,
391 | value: &T,
392 | ) -> Result<(), Error> {
393 | base.create_subkey_transacted(path, tx)
394 | .and_then(|(key, _)| key.set_value(name, value))
395 | .map_err(|e| Error::from(Error::RegistryError(e)))
396 | }
397 |
398 | /// Query list of registered extensions.
399 | ///
400 | /// Extensions don't have a leading dot.
401 | pub fn query_registered_extensions() -> Result, Error> {
402 | let base = RegKey::predef(HKEY_CURRENT_USER)
403 | .open_subkey(CLASSES_SUBKEY)
404 | .map_err(|e| Error::RegistryError(e))?;
405 | let extensions: Vec = base
406 | .enum_keys()
407 | .filter_map(Result::ok)
408 | .filter(|k| k.starts_with(HANDLER_PREFIX))
409 | .map(|k| {
410 | k.trim_start_matches(HANDLER_PREFIX)
411 | .trim_start_matches('.')
412 | .to_string()
413 | })
414 | .filter(|ext| is_extension_registered_for_wsl(ext).unwrap_or(false))
415 | .collect();
416 | Ok(extensions)
417 | }
418 |
419 | /// Query installed WSL distributions.
420 | pub fn query_distros() -> Result {
421 | let base = RegKey::predef(HKEY_CURRENT_USER)
422 | .open_subkey(LXSS_SUBKEY)
423 | .map_err(|e| Error::RegistryError(e))?;
424 | let mut distros = Distros::default();
425 | base.enum_keys().filter_map(Result::ok).for_each(|s| {
426 | if let Ok(name) = base
427 | .open_subkey(&s)
428 | .and_then(|k| k.get_value::("DistributionName"))
429 | {
430 | if let Ok(guid) = DistroGUID::from_str(&s) {
431 | distros.list.insert(guid, name);
432 | }
433 | }
434 | });
435 | if let Ok(s) = base.get_value::("DefaultDistribution") {
436 | if let Ok(guid) = DistroGUID::from_str(&s) {
437 | distros.default = Some(guid);
438 | }
439 | }
440 | Ok(distros)
441 | }
442 |
443 | /// Query distribution name by GUID.
444 | pub fn distro_guid_to_name(guid: DistroGUID) -> Option {
445 | if let Ok(key) = RegKey::predef(HKEY_CURRENT_USER)
446 | .open_subkey(LXSS_SUBKEY)
447 | .and_then(|k| k.open_subkey(guid.to_string()))
448 | {
449 | return key.get_value::("DistributionName").ok();
450 | }
451 | None
452 | }
453 |
454 | /// Get configuration for given registered extension.
455 | ///
456 | /// `ext` is the registered filename extension without a leading dot.
457 | pub fn get_extension_config(ext: &str) -> Result {
458 | let handler_key = RegKey::predef(HKEY_CURRENT_USER)
459 | .open_subkey(CLASSES_SUBKEY)
460 | .and_then(|key| key.open_subkey(format!("{}.{}", HANDLER_PREFIX, ext)))
461 | .map_err(|e| Error::RegistryError(e))?;
462 | let mut icon: Option = None;
463 | if let Ok(key) = handler_key.open_subkey("DefaultIcon") {
464 | if let Ok(s) = key.get_value::("") {
465 | icon = s.parse::().ok();
466 | }
467 | }
468 | let hold_mode = handler_key
469 | .get_value::("HoldMode")
470 | .ok()
471 | .and_then(|s| HoldMode::from_str(&s))
472 | .unwrap_or_default();
473 | let distro = handler_key
474 | .get_value::("Distribution")
475 | .ok()
476 | .and_then(|s| DistroGUID::from_str(&s).ok());
477 | let interactive = handler_key
478 | .get_value::("Interactive")
479 | .ok()
480 | .map(|v| v != 0)
481 | .unwrap_or(false);
482 | Ok(ExtConfig {
483 | extension: ext.to_owned(),
484 | icon,
485 | hold_mode,
486 | interactive,
487 | distro,
488 | })
489 | }
490 |
491 | /// Check whether extension is registered for WSL Script.
492 | pub fn is_extension_registered_for_wsl(ext: &str) -> Result {
493 | RegKey::predef(HKEY_CURRENT_USER)
494 | .open_subkey(CLASSES_SUBKEY)
495 | .map_err(|e| Error::RegistryError(e))?
496 | // try to open .ext key
497 | .open_subkey(format!(".{}", ext))
498 | .and_then(|key| key.get_value::(""))
499 | .map(|val| val == format!("{}.{}", HANDLER_PREFIX, ext))
500 | // if .ext registry key didn't exist
501 | .or(Ok(false))
502 | }
503 |
504 | /// Check whether extension is associated with other than WSL Script.
505 | pub fn is_registered_for_other(ext: &str) -> Result {
506 | RegKey::predef(HKEY_CURRENT_USER)
507 | .open_subkey(CLASSES_SUBKEY)
508 | .map_err(|e| Error::RegistryError(e))?
509 | // try to open .ext key
510 | .open_subkey(format!(".{}", ext))
511 | .and_then(|key| key.get_value::(""))
512 | .map(|val| val != format!("{}.{}", HANDLER_PREFIX, ext))
513 | // if .ext registry key didn't exist
514 | .or(Ok(false))
515 | }
516 |
517 | /// Get executable path of the WSL Script handler.
518 | pub fn get_handler_executable_path(ext: &str) -> Result {
519 | RegKey::predef(HKEY_CURRENT_USER)
520 | .open_subkey(CLASSES_SUBKEY)
521 | .and_then(|key| key.open_subkey(format!(r"{}.{}\shell\open\command", HANDLER_PREFIX, ext)))
522 | .and_then(|key| key.get_value::(""))
523 | .map_err(|e| Error::from(Error::RegistryError(e)))
524 | .and_then(|cmd| {
525 | // remove quotes
526 | cmd.trim_start_matches('"')
527 | .split_terminator('"')
528 | .next()
529 | .map(PathBuf::from)
530 | .ok_or_else(|| Error::InvalidPathError)
531 | })
532 | }
533 |
534 | /// Whether extension is registered for current wslscript executable.
535 | ///
536 | /// Returns an error if extension is not registered for WSLScript, or some
537 | /// error occurs.
538 | pub fn is_registered_for_current_executable(ext: &str) -> Result {
539 | let registered_exe = get_handler_executable_path(ext)?;
540 | let registered_exe = registered_exe.canonicalize().unwrap_or(registered_exe);
541 | let current_exe = std::env::current_exe()?;
542 | let current_exe = current_exe.canonicalize().unwrap_or(current_exe);
543 | if current_exe == registered_exe {
544 | return Ok(true);
545 | }
546 | Ok(false)
547 | }
548 |
549 | /// Call DllRegisterServer from shell extension handler library.
550 | fn register_server() -> Result<(), Error> {
551 | use libloading::{Library, Symbol};
552 | let lib = unsafe { Library::new("wslscript_handler.dll") }
553 | .map_err(|e| Error::LibraryError(format!("{}", e)))?;
554 | let dll_register_server: Symbol i32> =
555 | unsafe { lib.get(b"DllRegisterServer\0") }
556 | .map_err(|e| Error::LibraryError(format!("{}", e)))?;
557 | let rv = unsafe { dll_register_server() };
558 | if rv != winerror::S_OK {
559 | log::debug!("DllRegisterServer returned {}", rv);
560 | return Err(Error::GenericError(
561 | "Failed to register shell extension.".to_string(),
562 | ));
563 | }
564 | Ok(())
565 | }
566 |
567 | /// Register in-process server for drop handler shell extension.
568 | ///
569 | /// See: https://docs.microsoft.com/en-us/windows/win32/com/inprocserver32
570 | pub fn add_server_to_registry(dll_path: &Path) -> Result<(), Error> {
571 | let tx = Transaction::new().map_err(|e| Error::RegistryError(e))?;
572 | let base = RegKey::predef(HKEY_CURRENT_USER)
573 | .open_subkey_transacted_with_flags(CLASSES_SUBKEY, &tx, KEY_ALL_ACCESS)
574 | .map_err(|e| Error::RegistryError(e))?;
575 | let clsid = format!(r"CLSID\{}", DROP_HANDLER_CLSID.to_string());
576 | set_value(&tx, &base, &clsid, "", &"WSLScript Drop Handler")?;
577 | let path = format!(r"{}\InProcServer32", clsid);
578 | let val = dll_path.to_string_lossy().to_string();
579 | set_value(&tx, &base, &path, "", &val)?;
580 | set_value(&tx, &base, &path, "ThreadingModel", &"Apartment")?;
581 | tx.commit().map_err(|e| Error::RegistryError(e))?;
582 | Ok(())
583 | }
584 |
585 | /// Remove registry keys related to drop handler shell extension.
586 | pub fn remove_server_from_registry() -> Result<(), Error> {
587 | let tx = Transaction::new().map_err(|e| Error::RegistryError(e))?;
588 | let base = RegKey::predef(HKEY_CURRENT_USER)
589 | .open_subkey_transacted_with_flags(CLASSES_SUBKEY, &tx, KEY_ALL_ACCESS)
590 | .map_err(|e| Error::RegistryError(e))?;
591 | let clsid = format!(r"CLSID\{}", DROP_HANDLER_CLSID.to_string());
592 | if let Ok(key) = base.open_subkey_transacted_with_flags(&clsid, &tx, KEY_ALL_ACCESS) {
593 | key.delete_subkey_all("")
594 | .map_err(|e| Error::RegistryError(e))?;
595 | base.delete_subkey_transacted(&clsid, &tx)
596 | .map_err(|e| Error::RegistryError(e))?;
597 | }
598 | tx.commit().map_err(|e| Error::RegistryError(e))?;
599 | Ok(())
600 | }
601 |
--------------------------------------------------------------------------------
/wslscript_common/src/ver.rs:
--------------------------------------------------------------------------------
1 | use crate::error::*;
2 | use crate::win32::*;
3 | use std::path::Path;
4 | use std::ptr;
5 | use widestring::WideCStr;
6 | use widestring::WideChar;
7 | use winapi::shared::minwindef as win;
8 | use winapi::um::winver;
9 |
10 | /// Get version string from file.
11 | pub fn product_version(path: &Path) -> Option {
12 | let filever = FileVersion::try_new(path).ok()?;
13 | let translations = filever
14 | .query::(r"\VarFileInfo\Translation")
15 | .ok()?;
16 | for translation in translations {
17 | let sub_block = format!(
18 | r"\StringFileInfo\{:04x}{:04x}\ProductVersion",
19 | translation.lang, translation.cp
20 | );
21 | if let Ok(s) = filever.query::(&sub_block) {
22 | let version = WideCStr::from_slice_truncate(s).unwrap_or_default();
23 | return Some(version.to_string_lossy());
24 | }
25 | }
26 | None
27 | }
28 |
29 | #[repr(C)]
30 | struct LANGANDCODEPAGE {
31 | lang: win::WORD,
32 | cp: win::WORD,
33 | }
34 |
35 | struct FileVersion {
36 | /// File version information.
37 | ///
38 | /// See: https://docs.microsoft.com/en-us/windows/win32/api/winver/nf-winver-getfileversioninfow
39 | data: Vec,
40 | }
41 |
42 | impl FileVersion {
43 | pub fn try_new(path: &Path) -> Result {
44 | let path_c = WinPathBuf::new(path.to_owned()).to_wide();
45 | let size = unsafe { winver::GetFileVersionInfoSizeW(path_c.as_ptr(), ptr::null_mut()) };
46 | if size == 0 {
47 | return Err(last_error());
48 | }
49 | let mut data = Vec::::with_capacity(size as _);
50 | let rv = unsafe {
51 | winver::GetFileVersionInfoW(path_c.as_ptr(), 0, size, data.as_mut_ptr() as _)
52 | };
53 | if rv == 0 {
54 | return Err(last_error());
55 | }
56 | unsafe { data.set_len(size as _) };
57 | Ok(Self { data })
58 | }
59 |
60 | /// Query file version value.
61 | ///
62 | /// See: https://docs.microsoft.com/en-us/windows/win32/api/winver/nf-winver-verqueryvaluew
63 | pub fn query(&self, sub_block: &str) -> Result<&[T], Error> {
64 | let mut buf: win::LPVOID = ptr::null_mut();
65 | let mut len: win::UINT = 0;
66 | let rv = unsafe {
67 | winver::VerQueryValueW(
68 | self.data.as_ptr() as _,
69 | wcstring(sub_block).as_ptr(),
70 | &mut buf,
71 | &mut len,
72 | )
73 | };
74 | if rv == 0 {
75 | return Err(Error::GenericError("Version not found.".to_string()));
76 | }
77 | let s = unsafe { std::slice::from_raw_parts::(buf as _, len as _) };
78 | Ok(s)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/wslscript_common/src/win32.rs:
--------------------------------------------------------------------------------
1 | use crate::error::*;
2 | use std::convert::From;
3 | use std::ops::{Deref, DerefMut};
4 | use std::path::PathBuf;
5 | use std::ptr::null_mut;
6 | use wchar::*;
7 | use widestring::*;
8 | use winapi::shared::minwindef as win;
9 | use winapi::um::winnt;
10 |
11 | /// Convert &str to WideCString
12 | pub fn wcstring>(s: T) -> WideCString {
13 | WideCString::from_str(s).unwrap_or_else(|e| {
14 | let p = e.nul_position();
15 | if let Some(mut v) = e.into_vec() {
16 | v.resize(p, 0);
17 | WideCString::from_vec_truncate(v)
18 | } else {
19 | WideCString::default()
20 | }
21 | })
22 | }
23 |
24 | /// Convert WCHAR slice _(usually from `wchz!` macro)_ to WideCStr
25 | pub fn wcstr(s: &[wchar_t]) -> &WideCStr {
26 | WideCStr::from_slice_truncate(s).unwrap_or_default()
27 | }
28 |
29 | #[cfg(test)]
30 | mod tests {
31 | use super::*;
32 | #[test]
33 | fn test_wcstring_with_null() {
34 | assert_eq!(wcstring("with\0null"), wcstring("with"));
35 | }
36 | #[test]
37 | fn test_wcstr() {
38 | assert_eq!(wcstr(wchz!("test")).as_slice(), &wchz!("test")[0..4]);
39 | }
40 | }
41 |
42 | /// Display error message as a message box.
43 | pub fn error_message(msg: &WideCStr) {
44 | use winapi::um::winuser::{MessageBoxW, MB_ICONERROR, MB_OK};
45 | unsafe {
46 | MessageBoxW(
47 | null_mut(),
48 | msg.as_ptr(),
49 | wcstr(wchz!("Error")).as_ptr(),
50 | MB_OK | MB_ICONERROR,
51 | );
52 | }
53 | }
54 |
55 | /// Get the last WinAPI error.
56 | pub fn last_error() -> Error {
57 | use winapi::um::winbase::*;
58 | let mut buf: winnt::LPWSTR = null_mut();
59 | let errno = unsafe { winapi::um::errhandlingapi::GetLastError() };
60 | let res = unsafe {
61 | FormatMessageW(
62 | FORMAT_MESSAGE_FROM_SYSTEM
63 | | FORMAT_MESSAGE_IGNORE_INSERTS
64 | | FORMAT_MESSAGE_ALLOCATE_BUFFER,
65 | null_mut(),
66 | errno,
67 | win::DWORD::from(winnt::MAKELANGID(
68 | winnt::LANG_NEUTRAL,
69 | winnt::SUBLANG_DEFAULT,
70 | )),
71 | &mut buf as *mut winnt::LPWSTR as _,
72 | 0,
73 | null_mut(),
74 | )
75 | };
76 | let s: String = if res == 0 {
77 | format!("Error code {}", errno)
78 | } else {
79 | let s = unsafe { WideCString::from_ptr_str(buf).to_string_lossy() };
80 | unsafe { LocalFree(buf as _) };
81 | s
82 | };
83 | Error::WinAPIError(s)
84 | }
85 |
86 | /// Path buffer with Windows semantics.
87 | #[derive(Clone)]
88 | pub struct WinPathBuf {
89 | buf: PathBuf,
90 | }
91 |
92 | impl WinPathBuf {
93 | pub fn new(buf: PathBuf) -> Self {
94 | Self { buf }
95 | }
96 |
97 | /// Get path as a nul terminated wide string.
98 | pub fn to_wide(&self) -> WideCString {
99 | unsafe { WideCString::from_os_str_unchecked(self.buf.as_os_str()) }
100 | }
101 |
102 | /// Canonicalize path.
103 | pub fn canonicalize(&self) -> Result {
104 | Ok(Self::new(self.buf.canonicalize().map_err(Error::from)?))
105 | }
106 |
107 | /// Remove extended length path prefix (`\\?\`).
108 | pub fn without_extended(&self) -> Self {
109 | use std::ffi::OsString;
110 | use std::os::windows::ffi::*;
111 | let words = self.buf.as_os_str().encode_wide().collect::>();
112 | let mut s = words.as_slice();
113 | if s.starts_with(wch!(r"\\?\")) {
114 | s = &s[4..];
115 | }
116 | Self::new(PathBuf::from(OsString::from_wide(s)))
117 | }
118 |
119 | /// Get the path as a doubly quoted wide string.
120 | pub fn quoted(&self) -> WideString {
121 | let mut ws = WideString::new();
122 | ws.push_slice(wch!(r#"""#));
123 | ws.push_os_str(self.buf.as_os_str());
124 | ws.push_slice(wch!(r#"""#));
125 | ws
126 | }
127 |
128 | /// Expand environment variables in a path.
129 | pub fn expand(&self) -> Result {
130 | use winapi::um::fileapi::*;
131 | use winapi::um::processenv::*;
132 | let mut buf = [0_u16; 2048];
133 | let len = unsafe {
134 | ExpandEnvironmentStringsW(self.to_wide().as_ptr(), buf.as_mut_ptr(), buf.len() as _)
135 | };
136 | if len == 0 {
137 | return Err(last_error());
138 | }
139 | let path = unsafe { WideCString::from_ptr_unchecked(buf.as_ptr(), len as _) };
140 | let len = unsafe { GetLongPathNameW(path.as_ptr(), buf.as_mut_ptr(), buf.len() as _) };
141 | if len == 0 {
142 | return Err(last_error());
143 | }
144 | let path = unsafe { WideCString::from_ptr_unchecked(buf.as_ptr(), (len + 1) as _) };
145 | Ok(Self::from(path.as_ucstr()))
146 | }
147 | }
148 |
149 | impl From<&WideCStr> for WinPathBuf {
150 | fn from(s: &WideCStr) -> Self {
151 | Self::from(WideStr::from_slice(s.as_slice()))
152 | }
153 | }
154 |
155 | impl From<&WideStr> for WinPathBuf {
156 | fn from(s: &WideStr) -> Self {
157 | Self {
158 | buf: PathBuf::from(s.to_os_string()),
159 | }
160 | }
161 | }
162 |
163 | impl From<&str> for WinPathBuf {
164 | fn from(s: &str) -> Self {
165 | Self {
166 | buf: PathBuf::from(s),
167 | }
168 | }
169 | }
170 |
171 | impl Deref for WinPathBuf {
172 | type Target = PathBuf;
173 |
174 | fn deref(&self) -> &Self::Target {
175 | &self.buf
176 | }
177 | }
178 |
179 | impl DerefMut for WinPathBuf {
180 | fn deref_mut(&mut self) -> &mut Self::Target {
181 | &mut self.buf
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/wslscript_common/src/wsl.rs:
--------------------------------------------------------------------------------
1 | use crate::error::*;
2 | use crate::registry::{self, HoldMode};
3 | use crate::wcstring;
4 | use crate::win32::*;
5 | use anyhow::Context;
6 | use std::env;
7 | use std::ffi::{OsStr, OsString};
8 | use std::os::windows::ffi::{OsStrExt, OsStringExt};
9 | use std::os::windows::process::CommandExt;
10 | use std::path::{Path, PathBuf};
11 | use std::process::{self, Stdio};
12 | use wchar::*;
13 | use widestring::*;
14 | use winapi::shared::minwindef::MAX_PATH;
15 | use winapi::um::winbase;
16 |
17 | /// Maximum command line length on Windows.
18 | const MAX_CMD_LEN: usize = 8191;
19 |
20 | /// Maximum number of paths to convert per single bash invocation.
21 | #[cfg(not(feature = "debug"))]
22 | const MAX_PATHS_CONVERT_PER_PROCESS: usize = 100;
23 | #[cfg(feature = "debug")]
24 | const MAX_PATHS_CONVERT_PER_PROCESS: usize = 1;
25 |
26 | /// Run script with optional arguments in a WSL.
27 | ///
28 | /// Paths must be in WSL context.
29 | pub fn run_wsl(script_path: &Path, args: &[PathBuf], opts: &WSLOptions) -> Result<(), Error> {
30 | // maximum length of the bash command
31 | const MAX_BASH_LEN: usize = MAX_CMD_LEN - MAX_PATH - MAX_PATH - 20;
32 | let mut bash_cmd = compose_bash_command(script_path, args, opts, false)?;
33 | // if arguments won't fit into command line
34 | if bash_cmd.cmd.len() > MAX_BASH_LEN {
35 | // retry and force to write arguments into temporary file
36 | bash_cmd = compose_bash_command(script_path, args, opts, true)?;
37 | if bash_cmd.cmd.len() > MAX_BASH_LEN {
38 | return Err(Error::CommandTooLong);
39 | }
40 | }
41 | log::debug!("Bash command: {}", bash_cmd.cmd.to_string_lossy());
42 | // build command to start WSL process in a terminal window
43 | let mut cmd = process::Command::new(cmd_bin_path().as_os_str());
44 | cmd.args(&[OsStr::new("/C"), wsl_bin_path()?.as_os_str()]);
45 | if let Some(distro) = &opts.distribution {
46 | cmd.args(&[OsStr::new("-d"), distro]);
47 | }
48 | cmd.args(&[OsStr::new("-e"), OsStr::new("bash")]);
49 | if opts.interactive {
50 | cmd.args(&[OsStr::new("-i")]);
51 | }
52 | cmd.args(&[OsStr::new("-c"), &bash_cmd.cmd.to_os_string()]);
53 | // start as a detached process in a new process group so we can safely
54 | // exit this program and have the script execute on it's own
55 | cmd.creation_flags(winbase::DETACHED_PROCESS | winbase::CREATE_NEW_PROCESS_GROUP);
56 | let mut proc: process::Child = cmd
57 | .stdin(Stdio::null())
58 | .stdout(Stdio::null())
59 | .stderr(Stdio::null())
60 | .spawn()
61 | .context(Error::WSLProcessError)?;
62 | // always wait on debug to spot errors
63 | #[cfg(feature = "debug")]
64 | let _ = proc.wait();
65 | // if a temporary file was created for the arguments
66 | if let Some(tmpfile) = bash_cmd.tmpfile {
67 | // wait for the process to exit
68 | let _ = proc.wait();
69 | log::debug!("Removing temporary file {}", tmpfile.to_string_lossy());
70 | if std::fs::remove_file(tmpfile).is_err() {
71 | log::debug!("Failed to remove temporary file");
72 | }
73 | }
74 | Ok(())
75 | }
76 |
77 | struct BashCmdResult {
78 | /// Command line for bash.
79 | cmd: WideString,
80 | /// Path to temporary file containing the script arguments.
81 | tmpfile: Option,
82 | }
83 |
84 | /// Build bash command to execute script with given arguments.
85 | ///
86 | /// If arguments are too long to fit on a command line, write them to temporary
87 | /// file and fetch on WSL side using bash's `mapfile` builtin.
88 | fn compose_bash_command(
89 | script_path: &Path,
90 | args: &[PathBuf],
91 | opts: &WSLOptions,
92 | force_args_in_file: bool,
93 | ) -> Result {
94 | let script_dir = script_path
95 | .parent()
96 | .ok_or(Error::InvalidPathError)?
97 | .as_os_str();
98 | let script_file = script_path.file_name().ok_or(Error::InvalidPathError)?;
99 | // command line to invoke in WSL
100 | let mut cmd = WideString::new();
101 | let tmpfile = if force_args_in_file ||
102 | // heuristic test whether argument list is too long to be passed on command line
103 | args.iter().fold(0, |acc, s| acc + s.as_os_str().len()) > (MAX_CMD_LEN / 2)
104 | {
105 | let argfile = write_args_to_temp_file(args)?;
106 | let path = path_to_wsl(&argfile, opts)?;
107 | // read arguments from temporary file into $args variable
108 | cmd.push_slice(wch!("mapfile -d '' -t args < '"));
109 | cmd.push_os_str(single_quote_escape(path.as_os_str()));
110 | cmd.push_slice(wch!("' && "));
111 | Some(argfile)
112 | } else {
113 | None
114 | };
115 | // cd 'dir' && './progname'
116 | cmd.push_slice(wch!("cd '"));
117 | cmd.push_os_str(single_quote_escape(script_dir));
118 | cmd.push_slice(wch!("' && './"));
119 | cmd.push_os_str(single_quote_escape(script_file));
120 | cmd.push_slice(wch!("'"));
121 | // if arguments are being passed via temporary file
122 | if tmpfile.is_some() {
123 | cmd.push_slice(wch!(" \"${args[@]}\""));
124 | }
125 | // insert arguments to command line
126 | else {
127 | for arg in args {
128 | cmd.push_slice(wch!(" '"));
129 | cmd.push_os_str(single_quote_escape(arg.as_os_str()));
130 | cmd.push_slice(wch!("'"));
131 | }
132 | }
133 | // commands after script exits
134 | match opts.hold_mode {
135 | HoldMode::Never => {}
136 | HoldMode::Always | HoldMode::Error => {
137 | if opts.hold_mode == HoldMode::Always {
138 | cmd.push_slice(wch!(";"));
139 | } else {
140 | cmd.push_slice(wch!(" ||"))
141 | }
142 | cmd.push_os_str(OsString::from_wide(wch!(
143 | r#" { printf >&2 '\n[Process exited - exit code %d] ' "$?"; read -n 1 -s; }"#
144 | )));
145 | }
146 | }
147 | Ok(BashCmdResult { cmd, tmpfile })
148 | }
149 |
150 | /// Write arguments to temporary file as a nul separated list.
151 | fn write_args_to_temp_file(args: &[PathBuf]) -> Result {
152 | use std::io::prelude::*;
153 | let temp = create_temp_file()?;
154 | let paths: Result, _> = args
155 | .iter()
156 | .map(|p| p.to_str().ok_or_else(|| Error::StringToPathUTF8Error))
157 | .collect();
158 | let s = match paths {
159 | Err(e) => return Err(e),
160 | Ok(p) => p.join("\0"),
161 | };
162 | let mut file = std::fs::OpenOptions::new()
163 | .write(true)
164 | .truncate(true)
165 | .open(&temp)?;
166 | file.write_all(s.as_bytes())?;
167 | log::debug!("Args written to: {}", temp.to_string_lossy());
168 | Ok(temp)
169 | }
170 |
171 | /// Create a temporary file.
172 | ///
173 | /// Returned path is an empty file in Windows's temp file directory.
174 | fn create_temp_file() -> Result {
175 | use winapi::um::fileapi as fa;
176 | let mut buf = [0u16; MAX_PATH + 1];
177 | let len = unsafe { fa::GetTempPathW(buf.len() as _, buf.as_mut_ptr()) };
178 | if len == 0 {
179 | return Err(last_error());
180 | }
181 | let temp_dir = unsafe { WideCString::from_ptr_truncate(buf.as_ptr(), len as usize + 1) };
182 | let uniq = unsafe {
183 | fa::GetTempFileNameW(
184 | temp_dir.as_ptr(),
185 | wcstring("wsl").as_ptr(),
186 | 0,
187 | buf.as_mut_ptr(),
188 | )
189 | };
190 | if uniq == 0 {
191 | return Err(last_error());
192 | }
193 | let temp_path = unsafe { WideCString::from_ptr_truncate(buf.as_ptr(), buf.len()) };
194 | log::debug!("Temp path {}", temp_path.to_string_lossy());
195 | Ok(PathBuf::from(temp_path.to_string_lossy()))
196 | }
197 |
198 | /// Escape single quotes in an OsString.
199 | fn single_quote_escape(s: &OsStr) -> OsString {
200 | let mut w: Vec = vec![];
201 | for c in s.encode_wide() {
202 | // escape ' to '\''
203 | if c == '\'' as u16 {
204 | w.extend_from_slice(wch!(r"'\''"));
205 | } else {
206 | w.push(c);
207 | }
208 | }
209 | OsString::from_wide(&w)
210 | }
211 |
212 | /// Convert single Windows path to WSL equivalent.
213 | fn path_to_wsl(path: &Path, opts: &WSLOptions) -> Result {
214 | let mut paths = paths_to_wsl(&[path.to_owned()], opts, None)?;
215 | let p = paths.pop().ok_or_else(|| Error::WinToUnixPathError)?;
216 | Ok(p)
217 | }
218 |
219 | /// Path conversion progress callback.
220 | ///
221 | /// Callback must return true to continue processing.
222 | /// Conversion may be cancelled by returning false.
223 | pub type PathProgressCallback = Box bool + 'static>;
224 |
225 | /// Convert Windows paths to WSL equivalents.
226 | ///
227 | /// Multiple paths can be converted on a single WSL invocation.
228 | /// Converted paths are returned in the same order as given.
229 | ///
230 | /// Optional progress callback function shall be called with a number of
231 | /// paths converted so far.
232 | pub fn paths_to_wsl(
233 | paths: &[PathBuf],
234 | opts: &WSLOptions,
235 | progress_callback: Option,
236 | ) -> Result, Error> {
237 | let mut wsl_paths: Vec = Vec::with_capacity(paths.len());
238 | let mut path_idx = 0;
239 | while path_idx < paths.len() {
240 | // build a printf command that prints null separated results
241 | let mut printf = WideString::new();
242 | printf.push_slice(wch!(r"printf '%s\0'"));
243 | let mut n = 0;
244 | // convert multiple paths on single WSL invocation up to maximum command line length
245 | while path_idx < paths.len()
246 | && printf.len() < MAX_CMD_LEN - MAX_PATH - 100
247 | && n < MAX_PATHS_CONVERT_PER_PROCESS
248 | {
249 | printf.push_slice(wch!(r#" "$(wslpath -u '"#));
250 | printf.push_os_str(single_quote_escape(paths[path_idx].as_os_str()));
251 | printf.push_slice(wch!(r#"')""#));
252 | path_idx += 1;
253 | n += 1;
254 | }
255 | log::debug!("printf command length {}", printf.len());
256 | let mut cmd = process::Command::new(wsl_bin_path()?);
257 | cmd.creation_flags(winbase::CREATE_NO_WINDOW);
258 | if let Some(distro) = &opts.distribution {
259 | cmd.args(&[OsStr::new("-d"), distro]);
260 | }
261 | cmd.args(&[
262 | OsStr::new("-e"),
263 | OsStr::new("bash"),
264 | OsStr::new("-c"),
265 | &printf.to_os_string(),
266 | ]);
267 | let output = cmd.output().context(Error::WinToUnixPathError)?;
268 | if !output.status.success() {
269 | return Err(Error::WinToUnixPathError);
270 | }
271 | wsl_paths.extend(
272 | std::str::from_utf8(&output.stdout)
273 | .context(Error::StringToPathUTF8Error)?
274 | .trim()
275 | .trim_matches('\0')
276 | .split('\0')
277 | .map(PathBuf::from),
278 | );
279 | if let Some(cb) = &progress_callback {
280 | if !cb(path_idx) {
281 | log::debug!("Progress callback returned false, cancelling");
282 | return Err(Error::Cancel);
283 | }
284 | }
285 | }
286 | log::debug!("Converted {} Windows paths to WSL", wsl_paths.len());
287 | Ok(wsl_paths)
288 | }
289 |
290 | /// Returns the path to Windows command prompt executable.
291 | fn cmd_bin_path() -> PathBuf {
292 | // if %COMSPEC% points to existing file
293 | if let Some(p) = env::var_os("COMSPEC")
294 | .map(PathBuf::from)
295 | .filter(|p| p.is_file())
296 | {
297 | return p;
298 | }
299 | // try %SYSTEMROOT\System32\cmd.exe
300 | if let Some(mut p) = env::var_os("SYSTEMROOT").map(PathBuf::from) {
301 | p.push(r"System32\cmd.exe");
302 | if p.is_file() {
303 | return p;
304 | }
305 | }
306 | // hardcoded fallback
307 | PathBuf::from(r"C:\Windows\System32\cmd.exe")
308 | }
309 |
310 | /// Returns the path to WSL executable.
311 | fn wsl_bin_path() -> Result {
312 | // try %SYSTEMROOT\System32\wsl.exe
313 | if let Some(mut p) = env::var_os("SYSTEMROOT").map(PathBuf::from) {
314 | p.push(r"System32\wsl.exe");
315 | if p.is_file() {
316 | return Ok(p);
317 | }
318 | }
319 | // no dice
320 | Err(Error::WSLNotFound)
321 | }
322 |
323 | /// Options for WSL invocation.
324 | pub struct WSLOptions {
325 | /// Mode after the command exits.
326 | hold_mode: HoldMode,
327 | /// Whether to run bash as an interactive shell.
328 | interactive: bool,
329 | /// Name of the WSL distribution to invoke.
330 | distribution: Option,
331 | }
332 |
333 | impl WSLOptions {
334 | pub fn from_args(args: Vec) -> Self {
335 | let mut hold_mode = HoldMode::default();
336 | let mut interactive = false;
337 | let mut distribution = None;
338 | let mut iter = args.iter();
339 | while let Some(arg) = iter.next() {
340 | // If extension parameter is present, load from registry.
341 | // This is the default after 0.5.0 version. Other arguments are
342 | // kept just for backwards compatibility for now.
343 | if arg == "--ext" {
344 | if let Some(ext) = iter.next().map(|s| s.to_string_lossy().into_owned()) {
345 | if let Some(opts) = Self::from_ext(&ext) {
346 | return opts;
347 | }
348 | }
349 | } else if arg == "-h" {
350 | if let Some(mode) = iter
351 | .next()
352 | .and_then(|s| WideCString::from_os_str(s).ok())
353 | .and_then(|s| HoldMode::from_wcstr(&s))
354 | {
355 | hold_mode = mode;
356 | }
357 | } else if arg == "-i" {
358 | interactive = true;
359 | } else if arg == "-d" {
360 | distribution = iter.next().map(|s| s.to_owned());
361 | }
362 | }
363 | Self {
364 | hold_mode,
365 | interactive,
366 | distribution,
367 | }
368 | }
369 |
370 | /// Load options for registered extension.
371 | ///
372 | /// `ext` is the filename extension without a leading dot.
373 | pub fn from_ext(ext: &str) -> Option {
374 | if let Ok(config) = registry::get_extension_config(ext) {
375 | let distro = config
376 | .distro
377 | .and_then(registry::distro_guid_to_name)
378 | .map(OsString::from);
379 | Some(Self {
380 | hold_mode: config.hold_mode,
381 | interactive: config.interactive,
382 | distribution: distro,
383 | })
384 | } else {
385 | None
386 | }
387 | }
388 | }
389 |
390 | impl Default for WSLOptions {
391 | fn default() -> Self {
392 | Self {
393 | hold_mode: HoldMode::default(),
394 | interactive: false,
395 | distribution: None,
396 | }
397 | }
398 | }
399 |
--------------------------------------------------------------------------------
/wslscript_handler/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 |
--------------------------------------------------------------------------------
/wslscript_handler/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 = "cfg-if"
7 | version = "1.0.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
10 |
11 | [[package]]
12 | name = "com"
13 | version = "0.6.0"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6"
16 | dependencies = [
17 | "com_macros",
18 | ]
19 |
20 | [[package]]
21 | name = "com_macros"
22 | version = "0.6.0"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5"
25 | dependencies = [
26 | "com_macros_support",
27 | "proc-macro2",
28 | "syn",
29 | ]
30 |
31 | [[package]]
32 | name = "com_macros_support"
33 | version = "0.6.0"
34 | source = "registry+https://github.com/rust-lang/crates.io-index"
35 | checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c"
36 | dependencies = [
37 | "proc-macro2",
38 | "quote",
39 | "syn",
40 | ]
41 |
42 | [[package]]
43 | name = "comedy"
44 | version = "0.2.0"
45 | source = "registry+https://github.com/rust-lang/crates.io-index"
46 | checksum = "74428ae4f7f05f32f4448e9f42d371538196919c4834979f4f96d1fdebffcb47"
47 | dependencies = [
48 | "winapi",
49 | ]
50 |
51 | [[package]]
52 | name = "guid_win"
53 | version = "0.2.0"
54 | source = "registry+https://github.com/rust-lang/crates.io-index"
55 | checksum = "d87f4be87a557b98b4e4316f2009834f4448652938a950c1e8b33ae25f6f183b"
56 | dependencies = [
57 | "comedy",
58 | "winapi",
59 | ]
60 |
61 | [[package]]
62 | name = "lazy_static"
63 | version = "1.4.0"
64 | source = "registry+https://github.com/rust-lang/crates.io-index"
65 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
66 |
67 | [[package]]
68 | name = "libc"
69 | version = "0.2.118"
70 | source = "registry+https://github.com/rust-lang/crates.io-index"
71 | checksum = "06e509672465a0504304aa87f9f176f2b2b716ed8fb105ebe5c02dc6dce96a94"
72 |
73 | [[package]]
74 | name = "log"
75 | version = "0.4.14"
76 | source = "registry+https://github.com/rust-lang/crates.io-index"
77 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
78 | dependencies = [
79 | "cfg-if",
80 | ]
81 |
82 | [[package]]
83 | name = "once_cell"
84 | version = "1.9.0"
85 | source = "registry+https://github.com/rust-lang/crates.io-index"
86 | checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
87 |
88 | [[package]]
89 | name = "proc-macro2"
90 | version = "1.0.36"
91 | source = "registry+https://github.com/rust-lang/crates.io-index"
92 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
93 | dependencies = [
94 | "unicode-xid",
95 | ]
96 |
97 | [[package]]
98 | name = "quote"
99 | version = "1.0.15"
100 | source = "registry+https://github.com/rust-lang/crates.io-index"
101 | checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
102 | dependencies = [
103 | "proc-macro2",
104 | ]
105 |
106 | [[package]]
107 | name = "redox_syscall"
108 | version = "0.1.57"
109 | source = "registry+https://github.com/rust-lang/crates.io-index"
110 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
111 |
112 | [[package]]
113 | name = "simple-logging"
114 | version = "2.0.2"
115 | source = "registry+https://github.com/rust-lang/crates.io-index"
116 | checksum = "b00d48e85675326bb182a2286ea7c1a0b264333ae10f27a937a72be08628b542"
117 | dependencies = [
118 | "lazy_static",
119 | "log",
120 | "thread-id",
121 | ]
122 |
123 | [[package]]
124 | name = "syn"
125 | version = "1.0.86"
126 | source = "registry+https://github.com/rust-lang/crates.io-index"
127 | checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
128 | dependencies = [
129 | "proc-macro2",
130 | "quote",
131 | "unicode-xid",
132 | ]
133 |
134 | [[package]]
135 | name = "thread-id"
136 | version = "3.3.0"
137 | source = "registry+https://github.com/rust-lang/crates.io-index"
138 | checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1"
139 | dependencies = [
140 | "libc",
141 | "redox_syscall",
142 | "winapi",
143 | ]
144 |
145 | [[package]]
146 | name = "unicode-xid"
147 | version = "0.2.2"
148 | source = "registry+https://github.com/rust-lang/crates.io-index"
149 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
150 |
151 | [[package]]
152 | name = "widestring"
153 | version = "0.5.1"
154 | source = "registry+https://github.com/rust-lang/crates.io-index"
155 | checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983"
156 |
157 | [[package]]
158 | name = "winapi"
159 | version = "0.3.9"
160 | source = "registry+https://github.com/rust-lang/crates.io-index"
161 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
162 | dependencies = [
163 | "winapi-i686-pc-windows-gnu",
164 | "winapi-x86_64-pc-windows-gnu",
165 | ]
166 |
167 | [[package]]
168 | name = "winapi-i686-pc-windows-gnu"
169 | version = "0.4.0"
170 | source = "registry+https://github.com/rust-lang/crates.io-index"
171 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
172 |
173 | [[package]]
174 | name = "winapi-x86_64-pc-windows-gnu"
175 | version = "0.4.0"
176 | source = "registry+https://github.com/rust-lang/crates.io-index"
177 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
178 |
179 | [[package]]
180 | name = "wslscript_handler"
181 | version = "0.1.0"
182 | dependencies = [
183 | "com",
184 | "guid_win",
185 | "log",
186 | "once_cell",
187 | "simple-logging",
188 | "widestring",
189 | "winapi",
190 | ]
191 |
--------------------------------------------------------------------------------
/wslscript_handler/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "wslscript_handler"
3 | description = "Drop handler shell extension for WSL Script."
4 | version = "0.1.0"
5 | authors = ["Joni Kollani "]
6 | license = "MIT"
7 | homepage = "https://sop.github.io/wslscript/"
8 | repository = "https://github.com/sop/wslscript"
9 | edition = "2021"
10 |
11 | [dependencies]
12 | guid_win = "0.2.0"
13 | num_enum = "0.7.3"
14 | once_cell = "1.20"
15 | bitflags = "2.8"
16 | widestring = "1.1"
17 | wchar = "0.11"
18 | log = { version = "0.4", features = ["release_max_level_off"] }
19 | simple-logging = "2.0"
20 |
21 | [dependencies.wslscript_common]
22 | version = "*"
23 | path = "../wslscript_common"
24 |
25 | [dependencies.winapi]
26 | version = "0.3.9"
27 | features = ["unknwnbase", "winerror", "winuser", "oleidl"]
28 |
29 | [dependencies.windows]
30 | version = "0.59"
31 | features = [
32 | "Win32_System_Com",
33 | "Win32_System_Com_StructuredStorage",
34 | "Win32_System_Ole",
35 | "Win32_System_SystemServices",
36 | "Win32_Graphics_Gdi",
37 | "Win32_UI_Shell",
38 | ]
39 |
40 | [dependencies.windows-core]
41 | version = "0.59"
42 |
43 | [lib]
44 | crate-type = ["cdylib"]
45 |
46 | [features]
47 | debug = []
48 |
49 | [build-dependencies]
50 | winres = "0.1"
51 | toml = "0.8"
52 | serde = "1"
53 | serde_derive = "1"
54 | chrono = "0.4"
55 |
--------------------------------------------------------------------------------
/wslscript_handler/build.rs:
--------------------------------------------------------------------------------
1 | use serde_derive::Deserialize;
2 | use std::env;
3 | use std::fs::File;
4 | use std::io::prelude::*;
5 | use std::io::Read;
6 | use std::path::PathBuf;
7 | use winres::VersionInfo;
8 |
9 | #[derive(Deserialize)]
10 | struct Cargo {
11 | package: CargoPackage,
12 | }
13 |
14 | #[derive(Deserialize)]
15 | struct CargoPackage {
16 | name: String,
17 | description: String,
18 | version: String,
19 | }
20 |
21 | fn main() {
22 | println!("cargo:rerun-if-changed=../wslscript/Cargo.toml");
23 | let handler_cargo = handler_cargo();
24 | let wslscript_cargo = wslscript_cargo();
25 | let manifest_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("manifest.xml");
26 | let mut f = File::create(manifest_path.clone()).unwrap();
27 | f.write_all(get_manifest(&handler_cargo, &wslscript_cargo).as_bytes())
28 | .unwrap();
29 | let now = chrono::Local::now();
30 | let version = parse_version(&wslscript_cargo.package.version);
31 | winres::WindowsResource::new()
32 | .set_manifest_file(manifest_path.to_str().unwrap())
33 | .set("ProductName", "WSL Script")
34 | .set("FileDescription", &handler_cargo.package.description)
35 | .set("FileVersion", &wslscript_cargo.package.version)
36 | .set_version_info(VersionInfo::FILEVERSION, version)
37 | .set("ProductVersion", &wslscript_cargo.package.version)
38 | .set_version_info(VersionInfo::PRODUCTVERSION, version)
39 | .set(
40 | "InternalName",
41 | &format!("{}.dll", handler_cargo.package.name),
42 | )
43 | .set(
44 | "LegalCopyright",
45 | &format!("Joni Kollani © {}", now.format("%Y")),
46 | )
47 | .compile()
48 | .unwrap();
49 | }
50 |
51 | /// Parse version string to resource version.
52 | ///
53 | /// See: https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
54 | fn parse_version(s: &str) -> u64 {
55 | // take first 3 numbers
56 | let mut parts = s
57 | .split(".")
58 | .filter_map(|s| {
59 | s.chars()
60 | .take_while(|c| c.is_digit(10))
61 | .collect::()
62 | .parse::()
63 | .ok()
64 | })
65 | .take(3)
66 | .collect::>();
67 | // insert 0 as a fourth component
68 | parts.push(0);
69 | assert!(parts.len() == 4);
70 | (parts[0] as u64) << 48 | (parts[1] as u64) << 32 | (parts[2] as u64) << 16 | (parts[3] as u64)
71 | }
72 |
73 | /// Format resource version to _m.n.o.p_ string.
74 | ///
75 | /// See: https://docs.microsoft.com/en-us/windows/win32/sbscs/assembly-versions
76 | fn format_version(v: u64) -> String {
77 | format!(
78 | "{}.{}.{}.{}",
79 | (v >> 48) & 0xffff,
80 | (v >> 32) & 0xffff,
81 | (v >> 16) & 0xffff,
82 | v & 0xffff
83 | )
84 | }
85 |
86 | fn get_manifest(handler_cargo: &Cargo, wslscript_cargo: &Cargo) -> String {
87 | format!(
88 | r#"
89 |
91 |
94 | {description}
95 |
96 |
97 |
103 |
104 |
105 | "#,
106 | name = format!("github.sop.{}", handler_cargo.package.name),
107 | description = handler_cargo.package.description,
108 | version = format_version(parse_version(&wslscript_cargo.package.version))
109 | )
110 | }
111 |
112 | fn handler_cargo() -> Cargo {
113 | let mut toml = String::new();
114 | File::open(PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("Cargo.toml"))
115 | .unwrap()
116 | .read_to_string(&mut toml)
117 | .unwrap();
118 | toml::from_str::(&toml).unwrap()
119 | }
120 |
121 | fn wslscript_cargo() -> Cargo {
122 | let mut toml = String::new();
123 | File::open(
124 | PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
125 | .parent()
126 | .unwrap()
127 | .join("wslscript/Cargo.toml"),
128 | )
129 | .unwrap()
130 | .read_to_string(&mut toml)
131 | .unwrap();
132 | toml::from_str::(&toml).unwrap()
133 | }
134 |
--------------------------------------------------------------------------------
/wslscript_handler/src/interface.rs:
--------------------------------------------------------------------------------
1 | //! All the nitty gritty details regarding COM interface for the shell extension
2 | //! are defined here.
3 | //!
4 | //! See: https://docs.microsoft.com/en-us/windows/win32/shell/handlers#implementing-shell-extension-handlers
5 |
6 | use guid_win::Guid;
7 | use once_cell::sync::Lazy;
8 | use std::cell::RefCell;
9 | use std::path::PathBuf;
10 | use std::str::FromStr;
11 | use std::sync::atomic::{AtomicUsize, Ordering};
12 | use wchar::wchar_t;
13 | use widestring::WideCStr;
14 | use winapi::shared::guiddef;
15 | use winapi::shared::minwindef as win;
16 | use winapi::shared::winerror;
17 | use winapi::um::oleidl;
18 | use winapi::um::winnt;
19 | use winapi::um::winuser;
20 | use windows::core as wc;
21 | use windows::core::Interface;
22 | use windows::Win32::UI::Shell;
23 | use windows::Win32::{Foundation, System::Com, System::Ole, System::SystemServices};
24 | use wslscript_common::error::*;
25 |
26 | use crate::progress::ProgressWindow;
27 |
28 | /// IClassFactory GUID.
29 | ///
30 | /// See: https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iclassfactory
31 | ///
32 | /// Windows requests this interface via `DllGetClassObject` to further query
33 | /// relevant COM interfaces.
34 | static CLASS_FACTORY_CLSID: Lazy =
35 | Lazy::new(|| Guid::from_str("00000001-0000-0000-c000-000000000046").unwrap());
36 |
37 | /// Semaphore to keep track of running WSL threads.
38 | ///
39 | /// DLL shall not be released if there are threads running.
40 | pub(crate) static THREAD_COUNTER: AtomicUsize = AtomicUsize::new(0);
41 |
42 | /// Handle to loaded DLL module.
43 | static mut DLL_HANDLE: win::HINSTANCE = std::ptr::null_mut();
44 |
45 | /// DLL module entry point.
46 | ///
47 | /// See: https://docs.microsoft.com/en-us/windows/win32/dlls/dllmain
48 | #[no_mangle]
49 | extern "system" fn DllMain(
50 | hinstance: win::HINSTANCE,
51 | reason: win::DWORD,
52 | _reserved: win::LPVOID,
53 | ) -> win::BOOL {
54 | match reason {
55 | winnt::DLL_PROCESS_ATTACH => {
56 | // store module instance to global variable
57 | unsafe { DLL_HANDLE = hinstance };
58 | // set up logging
59 | #[cfg(feature = "debug")]
60 | if let Ok(mut path) = get_module_path(hinstance) {
61 | let stem = path.file_stem().map_or_else(
62 | || "debug.log".to_string(),
63 | |s| s.to_string_lossy().into_owned(),
64 | );
65 | path.pop();
66 | path.push(format!("{}.log", stem));
67 | if simple_logging::log_to_file(&path, log::LevelFilter::Debug).is_err() {
68 | unsafe {
69 | use winapi::um::winuser::*;
70 | let text = wslscript_common::wcstring(format!(
71 | "Failed to set up logging to {}",
72 | path.to_string_lossy()
73 | ));
74 | MessageBoxW(
75 | std::ptr::null_mut(),
76 | text.as_ptr(),
77 | wchar::wchz!("Error").as_ptr(),
78 | MB_OK | MB_ICONERROR | MB_SERVICE_NOTIFICATION,
79 | );
80 | }
81 | }
82 | }
83 | log::debug!("DLL_PROCESS_ATTACH");
84 | return win::TRUE;
85 | }
86 | winnt::DLL_PROCESS_DETACH => {
87 | log::debug!("DLL_PROCESS_DETACH");
88 | ProgressWindow::unregister_window_class();
89 | }
90 | winnt::DLL_THREAD_ATTACH => {}
91 | winnt::DLL_THREAD_DETACH => {}
92 | _ => {}
93 | }
94 | win::FALSE
95 | }
96 |
97 | /// Called to check whether DLL can be unloaded from memory.
98 | ///
99 | /// See: https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-dllcanunloadnow
100 | #[no_mangle]
101 | extern "system" fn DllCanUnloadNow() -> winnt::HRESULT {
102 | let n = THREAD_COUNTER.load(Ordering::SeqCst);
103 | if n > 0 {
104 | log::info!("{} WSL threads running, denying DLL unload", n);
105 | winerror::S_FALSE
106 | } else {
107 | log::info!("Permitting DLL unload");
108 | winerror::S_OK
109 | }
110 | }
111 |
112 | /// Exposes class factory.
113 | ///
114 | /// See: https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-dllgetclassobject
115 | #[no_mangle]
116 | extern "system" fn DllGetClassObject(
117 | class_id: guiddef::REFCLSID,
118 | iid: guiddef::REFIID,
119 | result: *mut win::LPVOID,
120 | ) -> winnt::HRESULT {
121 | let class_guid = guid_from_ref(class_id);
122 | let interface_guid = guid_from_ref(iid);
123 | // expect our registered class ID
124 | if wslscript_common::DROP_HANDLER_CLSID.eq(&class_guid) {
125 | // expect IClassFactory interface to be requested
126 | if !CLASS_FACTORY_CLSID.eq(&interface_guid) {
127 | log::warn!("Expected IClassFactory, got {}", interface_guid);
128 | }
129 | let cls: Com::IClassFactory = Handler::default().into();
130 | let rv = unsafe { cls.query(iid as _, result as _) };
131 | log::debug!(
132 | "QueryInterface for {} returned {}, address={:p}",
133 | interface_guid,
134 | rv,
135 | result
136 | );
137 | return rv.0;
138 | } else {
139 | log::warn!("Unsupported class: {}", class_guid);
140 | }
141 | winerror::CLASS_E_CLASSNOTAVAILABLE
142 | }
143 |
144 | /// Add in-process server keys into registry.
145 | ///
146 | /// See: https://docs.microsoft.com/en-us/windows/win32/api/olectl/nf-olectl-dllregisterserver
147 | #[no_mangle]
148 | extern "system" fn DllRegisterServer() -> winnt::HRESULT {
149 | let hinstance = unsafe { DLL_HANDLE };
150 | let path = match get_module_path(hinstance) {
151 | Ok(p) => p,
152 | Err(_) => return winerror::E_UNEXPECTED,
153 | };
154 | log::debug!("DllRegisterServer for {}", path.to_string_lossy());
155 | match wslscript_common::registry::add_server_to_registry(&path) {
156 | Ok(_) => (),
157 | Err(e) => {
158 | log::error!("Failed to register server: {}", e);
159 | return winerror::E_UNEXPECTED;
160 | }
161 | }
162 | winerror::S_OK
163 | }
164 |
165 | /// Remove in-process server keys from registry.
166 | ///
167 | /// See: https://docs.microsoft.com/en-us/windows/win32/api/olectl/nf-olectl-dllunregisterserver
168 | #[no_mangle]
169 | extern "system" fn DllUnregisterServer() -> winnt::HRESULT {
170 | match wslscript_common::registry::remove_server_from_registry() {
171 | Ok(_) => (),
172 | Err(e) => {
173 | log::error!("Failed to unregister server: {}", e);
174 | return winerror::E_UNEXPECTED;
175 | }
176 | }
177 | winerror::S_OK
178 | }
179 |
180 | /// Convert Win32 GUID pointer to Guid struct.
181 | const fn guid_from_ref(clsid: *const guiddef::GUID) -> Guid {
182 | Guid {
183 | 0: unsafe { *clsid },
184 | }
185 | }
186 |
187 | /// Get path to loaded DLL file.
188 | fn get_module_path(hinstance: win::HINSTANCE) -> Result {
189 | use std::ffi::OsString;
190 | use std::os::windows::ffi::OsStringExt;
191 | use winapi::shared::ntdef;
192 | use winapi::um::libloaderapi::GetModuleFileNameW as GetModuleFileName;
193 | let mut buf: Vec = Vec::with_capacity(win::MAX_PATH);
194 | let len = unsafe { GetModuleFileName(hinstance, buf.as_mut_ptr(), buf.capacity() as _) };
195 | if len == 0 {
196 | return Err(wslscript_common::win32::last_error());
197 | }
198 | unsafe { buf.set_len(len as _) };
199 | Ok(PathBuf::from(OsString::from_wide(&buf)))
200 | }
201 |
202 | bitflags::bitflags! {
203 | /// Key state flags.
204 | #[derive(Debug)]
205 | pub struct KeyState: win::DWORD {
206 | const MK_CONTROL = winuser::MK_CONTROL as _;
207 | const MK_SHIFT = winuser::MK_SHIFT as _;
208 | const MK_ALT = oleidl::MK_ALT;
209 | const MK_LBUTTON = winuser::MK_LBUTTON as _;
210 | const MK_MBUTTON = winuser::MK_MBUTTON as _;
211 | const MK_RBUTTON = winuser::MK_RBUTTON as _;
212 | }
213 | }
214 |
215 | #[wc::implement(Com::IClassFactory, Com::IPersistFile, Ole::IDropTarget)]
216 | #[derive(Default)]
217 | #[allow(non_camel_case_types)]
218 | struct Handler {
219 | target: RefCell,
220 | }
221 |
222 | /// IClassFactory interface.
223 | ///
224 | /// https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iclassfactory
225 | impl Com::IClassFactory_Impl for Handler_Impl {
226 | /// https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iclassfactory-createinstance
227 | fn CreateInstance(
228 | &self,
229 | punkouter: wc::Ref,
230 | riid: *const wc::GUID,
231 | ppvobject: *mut *mut ::core::ffi::c_void,
232 | ) -> wc::Result<()> {
233 | log::debug!("IClassFactory::CreateInstance");
234 | if punkouter.is_some() {
235 | return Err(wc::Error::from(Foundation::CLASS_E_NOAGGREGATION));
236 | }
237 | unsafe { *ppvobject = ::core::ptr::null_mut() };
238 | if riid.is_null() {
239 | return Err(wc::Error::from(Foundation::E_INVALIDARG));
240 | }
241 | unsafe { self.cast::()?.query(riid, ppvobject).ok() }
242 | }
243 |
244 | /// https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iclassfactory-lockserver
245 | fn LockServer(&self, _flock: Foundation::BOOL) -> wc::Result<()> {
246 | log::debug!("IClassFactory::LockServer");
247 | Err(wc::Error::from(Foundation::E_NOTIMPL))
248 | }
249 | }
250 |
251 | /// IPersist interface.
252 | ///
253 | /// https://learn.microsoft.com/en-us/windows/win32/api/objidl/nn-objidl-ipersist
254 | impl Com::IPersist_Impl for Handler_Impl {
255 | /// https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-ipersist-getclassid
256 | fn GetClassID(&self) -> wc::Result {
257 | log::debug!("IPersist::GetClassID");
258 | let guid = wslscript_common::DROP_HANDLER_CLSID.0;
259 | wc::Result::Ok(wc::GUID::from_values(
260 | guid.Data1, guid.Data2, guid.Data3, guid.Data4,
261 | ))
262 | }
263 | }
264 |
265 | /// IPersistFile interface.
266 | ///
267 | /// https://learn.microsoft.com/en-us/windows/win32/api/objidl/nn-objidl-ipersistfile
268 | impl Com::IPersistFile_Impl for Handler_Impl {
269 | /// https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-ipersistfile-isdirty
270 | fn IsDirty(&self) -> wc::HRESULT {
271 | log::debug!("IPersistFile::IsDirty");
272 | Foundation::S_FALSE
273 | }
274 |
275 | /// https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-ipersistfile-load
276 | fn Load(&self, pszfilename: &wc::PCWSTR, _dwmode: Com::STGM) -> wc::Result<()> {
277 | // path to the file that is being dragged over, ie. the registered script file
278 | let filename = unsafe { WideCStr::from_ptr_str(pszfilename.as_ptr()) };
279 | let path = PathBuf::from(filename.to_os_string());
280 | log::debug!("IPersistFile::Load {}", path.to_string_lossy());
281 | if let Ok(mut target) = self.target.try_borrow_mut() {
282 | *target = path;
283 | } else {
284 | return Err(wc::Error::from(Foundation::E_FAIL));
285 | }
286 | Ok(())
287 | }
288 |
289 | /// https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-ipersistfile-save
290 | fn Save(&self, _pszfilename: &wc::PCWSTR, _fremember: Foundation::BOOL) -> wc::Result<()> {
291 | log::debug!("IPersistFile::Save");
292 | Err(wc::Error::from(Foundation::S_FALSE))
293 | }
294 |
295 | /// https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-ipersistfile-savecompleted
296 | fn SaveCompleted(&self, _pszfilename: &wc::PCWSTR) -> wc::Result<()> {
297 | log::debug!("IPersistFile::SaveCompleted");
298 | Ok(())
299 | }
300 |
301 | /// https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-ipersistfile-getcurfile
302 | fn GetCurFile(&self) -> wc::Result {
303 | // TODO: return target file
304 | // https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-ipersistfile-getcurfile#remarks
305 | log::debug!("IPersistFile::GetCurFile");
306 | Err(wc::Error::from(Foundation::E_FAIL))
307 | }
308 | }
309 |
310 | /// IDropTarget interface.
311 | ///
312 | /// https://learn.microsoft.com/en-us/windows/win32/api/oleidl/nn-oleidl-idroptarget
313 | impl Ole::IDropTarget_Impl for Handler_Impl {
314 | /// https://learn.microsoft.com/en-us/windows/win32/api/oleidl/nf-oleidl-idroptarget-dragenter
315 | fn DragEnter(
316 | &self,
317 | pdataobj: wc::Ref,
318 | _grfkeystate: SystemServices::MODIFIERKEYS_FLAGS,
319 | _pt: &Foundation::POINTL,
320 | pdweffect: *mut Ole::DROPEFFECT,
321 | ) -> wc::Result<()> {
322 | log::debug!("IDropTarget::DragEnter");
323 | let obj = pdataobj
324 | .as_ref()
325 | .ok_or_else(|| wc::Error::from(Foundation::E_UNEXPECTED))?;
326 | let format = Com::FORMATETC {
327 | cfFormat: Ole::CF_HDROP.0,
328 | ptd: std::ptr::null_mut(),
329 | dwAspect: Com::DVASPECT_CONTENT.0,
330 | lindex: -1,
331 | tymed: Com::TYMED_HGLOBAL.0 as _,
332 | };
333 | let result = unsafe { obj.QueryGetData(&format) };
334 | log::debug!("IDataObject::QueryGetData returned {}", result);
335 | let effect = if result != Foundation::S_OK {
336 | Ole::DROPEFFECT_NONE
337 | } else {
338 | Ole::DROPEFFECT_COPY
339 | };
340 | unsafe { *pdweffect = effect };
341 | Ok(())
342 | }
343 |
344 | /// https://learn.microsoft.com/en-us/windows/win32/api/oleidl/nf-oleidl-idroptarget-dragover
345 | fn DragOver(
346 | &self,
347 | grfkeystate: SystemServices::MODIFIERKEYS_FLAGS,
348 | _pt: &Foundation::POINTL,
349 | _pdweffect: *mut Ole::DROPEFFECT,
350 | ) -> wc::Result<()> {
351 | log::debug!(
352 | "IDropTarget::DragOver {:?}",
353 | KeyState::from_bits_truncate(grfkeystate.0)
354 | );
355 | Ok(())
356 | }
357 |
358 | /// https://learn.microsoft.com/en-us/windows/win32/api/oleidl/nf-oleidl-idroptarget-dragleave
359 | fn DragLeave(&self) -> wc::Result<()> {
360 | log::debug!("IDropTarget::DragLeave");
361 | Ok(())
362 | }
363 |
364 | /// https://learn.microsoft.com/en-us/windows/win32/api/oleidl/nf-oleidl-idroptarget-drop
365 | fn Drop(
366 | &self,
367 | pdataobj: wc::Ref,
368 | grfkeystate: SystemServices::MODIFIERKEYS_FLAGS,
369 | _pt: &Foundation::POINTL,
370 | pdweffect: *mut Ole::DROPEFFECT,
371 | ) -> wc::Result<()> {
372 | log::debug!("IDropTarget::Drop");
373 | let target = match self.target.try_borrow() {
374 | Ok(t) => t.clone(),
375 | Err(_) => return Err(wc::Error::from(Foundation::E_UNEXPECTED)),
376 | };
377 | let obj = pdataobj
378 | .as_ref()
379 | .ok_or_else(|| wc::Error::from(Foundation::E_UNEXPECTED))?;
380 | let paths = get_paths_from_data_obj(obj)?;
381 | let keys = KeyState::from_bits_truncate(grfkeystate.0);
382 | super::handle_dropped_files(target, paths, keys)
383 | .and_then(|_| {
384 | unsafe { *pdweffect = Ole::DROPEFFECT_COPY };
385 | Ok(())
386 | })
387 | .map_err(|e| {
388 | log::debug!("Drop failed: {}", e);
389 | wc::Error::from(Foundation::E_UNEXPECTED)
390 | })
391 | }
392 | }
393 |
394 | /// Query IDataObject for dropped file names.
395 | fn get_paths_from_data_obj(obj: &Com::IDataObject) -> wc::Result> {
396 | // https://learn.microsoft.com/en-us/windows/win32/api/objidl/ns-objidl-formatetc
397 | let format = Com::FORMATETC {
398 | // https://docs.microsoft.com/en-us/windows/win32/shell/clipboard#cf_hdrop
399 | cfFormat: Ole::CF_HDROP.0,
400 | ptd: std::ptr::null_mut(),
401 | dwAspect: Com::DVASPECT_CONTENT.0,
402 | lindex: -1,
403 | tymed: Com::TYMED_HGLOBAL.0 as _,
404 | };
405 | log::debug!("Calling IDataObject::QueryGetData()");
406 | // https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-idataobject-querygetdata
407 | let result = unsafe { obj.QueryGetData(&format) };
408 | if result != Foundation::S_OK {
409 | log::debug!("IDataObject::QueryGetData returned {}", result);
410 | return Err(wc::Error::from(result));
411 | }
412 | log::debug!("Calling IDataObject::GetData()");
413 | // https://docs.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-idataobject-getdata
414 | let mut medium = unsafe { obj.GetData(&format) }?;
415 | // ensure data was transfered via global memory handle
416 | if medium.tymed != Com::TYMED_HGLOBAL.0 as _ {
417 | return Err(wc::Error::from(Foundation::E_UNEXPECTED));
418 | }
419 | let ptr = unsafe { medium.u.hGlobal.0 };
420 | // https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/ns-shlobj_core-dropfiles
421 | let dropfiles = unsafe { &*(ptr as *const Shell::DROPFILES) };
422 | if !dropfiles.fWide.as_bool() {
423 | log::warn!("ANSI not supported");
424 | return Err(wc::Error::from(Foundation::E_UNEXPECTED));
425 | }
426 | // file name array follows the DROPFILES structure
427 | let farray = unsafe { ptr.cast::().offset(dropfiles.pFiles as _) };
428 | let paths = parse_filename_array_wide(farray as *const wchar_t);
429 | if medium.pUnkForRelease.is_some() {
430 | log::debug!("Calling IUnknown::Release()");
431 | unsafe { std::mem::ManuallyDrop::drop(&mut medium.pUnkForRelease) }
432 | } else {
433 | log::debug!("No release interface, calling GlobalFree()");
434 | let _ = unsafe { Foundation::GlobalFree(Some(medium.u.hGlobal)) }.inspect_err(|e| {
435 | log::debug!("GlobalFree(): {}", e);
436 | });
437 | }
438 | Ok(paths)
439 | }
440 |
441 | /// Parse file name array to list of paths.
442 | ///
443 | /// See: https://docs.microsoft.com/en-us/windows/win32/shell/clipboard#cf_hdrop
444 | fn parse_filename_array_wide(mut ptr: *const wchar_t) -> Vec {
445 | let mut paths = Vec::::new();
446 | loop {
447 | let s = unsafe { WideCStr::from_ptr_str(ptr) };
448 | // terminated by double null, so last slice is empty
449 | if s.is_empty() {
450 | break;
451 | }
452 | // advance pointer
453 | ptr = unsafe { ptr.offset(s.len() as isize + 1) };
454 | paths.push(PathBuf::from(s.to_os_string()));
455 | }
456 | paths
457 | }
458 |
--------------------------------------------------------------------------------
/wslscript_handler/src/lib.rs:
--------------------------------------------------------------------------------
1 | use std::path::{Path, PathBuf};
2 | use std::sync::atomic::Ordering;
3 | use std::sync::mpsc;
4 | use std::thread;
5 | use winapi::shared::windef;
6 | use winapi::um::winuser;
7 | use wslscript_common::error::*;
8 | use wslscript_common::wsl;
9 |
10 | use crate::progress::ProgressWindow;
11 |
12 | mod interface;
13 | mod progress;
14 |
15 | /// Number of paths to convert without displaying a graphical progress indicator.
16 | #[cfg(not(feature = "debug"))]
17 | const CONVERT_WITH_PROGRESS_THRESHOLD: usize = 10;
18 | #[cfg(feature = "debug")]
19 | const CONVERT_WITH_PROGRESS_THRESHOLD: usize = 1;
20 |
21 | /// Handle files dropped to registered filetype.
22 | ///
23 | /// See: https://docs.microsoft.com/en-us/windows/win32/api/oleidl/nf-oleidl-idroptarget-drop
24 | fn handle_dropped_files(
25 | target: PathBuf,
26 | mut paths: Vec,
27 | key_state: interface::KeyState,
28 | ) -> Result<(), Error> {
29 | log::debug!(
30 | "Dropped {} items to {} with keys {:?}",
31 | paths.len(),
32 | target.to_string_lossy(),
33 | key_state
34 | );
35 | let opts = get_wsl_options(&target)?;
36 | paths.insert(0, target);
37 | // increment thread counter
38 | interface::THREAD_COUNTER.fetch_add(1, Ordering::SeqCst);
39 | // move further processing to thread
40 | thread::spawn(move || {
41 | log::debug!("Spawned thread to invoke WSL");
42 | if let Err(e) = run_wsl(paths, opts) {
43 | log::error!("Failed to invoke WSL: {}", e);
44 | }
45 | // Decrement counter when thread finishes. Here all moved variables
46 | // (paths and opts) have already been dropped, so DLL may be safely unloaded.
47 | interface::THREAD_COUNTER.fetch_sub(1, Ordering::SeqCst);
48 | });
49 | Ok(())
50 | }
51 |
52 | /// Invoke WSL with given path arguments.
53 | ///
54 | /// Paths are in Win32 context.
55 | fn run_wsl(win_paths: Vec, opts: wsl::WSLOptions) -> Result<(), Error> {
56 | let wsl_paths = if win_paths.len() > CONVERT_WITH_PROGRESS_THRESHOLD {
57 | convert_paths_with_progress(win_paths, &opts)?
58 | } else {
59 | wsl::paths_to_wsl(&win_paths, &opts, None)?
60 | };
61 | wsl::run_wsl(&wsl_paths[0], &wsl_paths[1..], &opts)
62 | }
63 |
64 | /// Wrapped progress window handle.
65 | struct ProgressWindowHandle(windef::HWND);
66 | /// Window handles are safe to send across threads.
67 | unsafe impl Send for ProgressWindowHandle {}
68 |
69 | /// Convert paths to WSL context with a graphical progress indicator.
70 | fn convert_paths_with_progress(
71 | win_paths: Vec,
72 | opts: &wsl::WSLOptions,
73 | ) -> Result, Error> {
74 | let path_count = win_paths.len();
75 | // channel to transfer current progress as in number of paths converted
76 | let (tx_progress, rx_progress) = mpsc::channel::();
77 | // channel to signal cancellation
78 | let (tx_cancel, rx_cancel) = mpsc::channel::<()>();
79 | // wait for progress updates in a seperate thread
80 | let progress_joiner = thread::spawn(move || {
81 | // channel to transfer progress window handle to this thread
82 | let (tx_hwnd, rx_hwnd) = mpsc::channel::();
83 | // run window in a seperate thread
84 | let window_joiner = thread::spawn(move || {
85 | let wnd = match ProgressWindow::new(path_count, tx_cancel) {
86 | Ok(wnd) => wnd,
87 | Err(e) => {
88 | log::error!("Failed to create progress window: {}", e);
89 | return;
90 | }
91 | };
92 | // send window handle to parent thread
93 | if tx_hwnd
94 | .send(ProgressWindowHandle { 0: wnd.handle() })
95 | .is_err()
96 | {
97 | log::error!("Failed to send progress window handle to parent thread");
98 | wnd.close();
99 | }
100 | drop(tx_hwnd);
101 | // run message loop
102 | if let Err(e) = wnd.run() {
103 | log::error!("Window thread returned error: {}", e);
104 | }
105 | });
106 | // wait for progress window handle
107 | let hwnd = match rx_hwnd.recv() {
108 | Ok(h) => h.0,
109 | Err(_) => {
110 | log::error!("Failed to receive progress window handle");
111 | return;
112 | }
113 | };
114 | drop(rx_hwnd);
115 | // post progress to window
116 | let update_progress = |n: usize| {
117 | // post WM_PROGRESS message to window's queue
118 | unsafe { winuser::PostMessageW(hwnd, progress::WM_PROGRESS, n, path_count as _) };
119 | };
120 | // blocking receive progress updates
121 | while let Ok(count) = rx_progress.recv() {
122 | update_progress(count);
123 | }
124 | // flush remaining messages
125 | while let Ok(count) = rx_progress.try_recv() {
126 | update_progress(count);
127 | }
128 | // close progress window
129 | unsafe { winuser::PostMessageW(hwnd, winuser::WM_CLOSE, 0, 0) };
130 | // wait for window to be destroyed
131 | window_joiner.join().unwrap_or_else(|_| {
132 | log::error!("Progress window thread panicked");
133 | });
134 | });
135 | // convert paths and send progress via channel
136 | let result = wsl::paths_to_wsl(
137 | &win_paths,
138 | &opts,
139 | Some(Box::new(move |count| {
140 | // if conversion was cancelled
141 | if rx_cancel.try_recv().is_ok() {
142 | return false;
143 | }
144 | tx_progress.send(count).unwrap_or_else(|_| {
145 | log::error!("Failed to communicate with channel");
146 | });
147 | // artificial delay while developing
148 | #[cfg(feature = "debug")]
149 | std::thread::sleep(std::time::Duration::from_secs(1));
150 | true
151 | })),
152 | );
153 | // wait for progress thread to finish
154 | progress_joiner.join().unwrap_or_else(|_| {
155 | log::error!("Path conversion progress thread panicked");
156 | });
157 | result
158 | }
159 |
160 | /// Get WSL options from registry based on given filename's extension.
161 | fn get_wsl_options(path: &Path) -> Result {
162 | path.extension()
163 | .ok_or_else(|| Error::DropHandlerError("No filename extension".to_owned()))
164 | .and_then(|s| {
165 | wsl::WSLOptions::from_ext(&s.to_string_lossy()).ok_or_else(|| {
166 | Error::DropHandlerError(format!(
167 | "Extension {} not registered.",
168 | s.to_string_lossy()
169 | ))
170 | })
171 | })
172 | }
173 |
--------------------------------------------------------------------------------
/wslscript_handler/src/progress.rs:
--------------------------------------------------------------------------------
1 | use num_enum::IntoPrimitive;
2 | use once_cell::sync::Lazy;
3 | use std::sync::mpsc::Sender;
4 | use std::{mem, pin::Pin, ptr};
5 | use wchar::*;
6 | use widestring::*;
7 | use winapi::shared::basetsd;
8 | use winapi::shared::minwindef as win;
9 | use winapi::shared::windef::*;
10 | use winapi::um::commctrl;
11 | use winapi::um::errhandlingapi;
12 | use winapi::um::libloaderapi;
13 | use winapi::um::wingdi;
14 | use winapi::um::winuser;
15 | use wslscript_common::error::*;
16 | use wslscript_common::font::Font;
17 | use wslscript_common::wcstring;
18 | use wslscript_common::win32;
19 |
20 | pub struct ProgressWindow {
21 | /// Maximum value for progress.
22 | high_limit: usize,
23 | /// Sender to signal for cancellation.
24 | cancel_sender: Option>,
25 | /// Window handle.
26 | hwnd: HWND,
27 | /// Default font.
28 | font: Font,
29 | }
30 |
31 | impl Default for ProgressWindow {
32 | fn default() -> Self {
33 | Self {
34 | high_limit: 0,
35 | cancel_sender: None,
36 | hwnd: ptr::null_mut(),
37 | font: Font::default(),
38 | }
39 | }
40 | }
41 |
42 | /// Progress window class name.
43 | static WND_CLASS: Lazy = Lazy::new(|| wcstring("WSLScriptProgress"));
44 |
45 | /// Window message for progress update.
46 | pub const WM_PROGRESS: win::UINT = winuser::WM_USER + 1;
47 |
48 | /// Child window identifiers.
49 | #[derive(IntoPrimitive, PartialEq)]
50 | #[repr(u16)]
51 | enum Control {
52 | ProgressBar = 100,
53 | Message,
54 | Title,
55 | }
56 |
57 | /// Minimum and initial main window size as a (width, height) tuple.
58 | const MIN_WINDOW_SIZE: (i32, i32) = (300, 150);
59 |
60 | impl ProgressWindow {
61 | pub fn new(high_limit: usize, cancel_sender: Sender<()>) -> Result>, Error> {
62 | use winuser::*;
63 | // register window class
64 | if !Self::is_window_class_registered() {
65 | Self::register_window_class()?;
66 | }
67 | let mut wnd = Pin::new(Box::new(Self::default()));
68 | wnd.high_limit = high_limit;
69 | wnd.cancel_sender = Some(cancel_sender);
70 | let instance = unsafe { libloaderapi::GetModuleHandleW(ptr::null_mut()) };
71 | let title = wchz!("WSL Script");
72 | // create window
73 | #[rustfmt::skip]
74 | let hwnd = unsafe { CreateWindowExW(
75 | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, WND_CLASS.as_ptr(), title.as_ptr(),
76 | WS_OVERLAPPEDWINDOW & !WS_MAXIMIZEBOX | WS_VISIBLE,
77 | CW_USEDEFAULT, CW_USEDEFAULT, MIN_WINDOW_SIZE.0, MIN_WINDOW_SIZE.1,
78 | ptr::null_mut(), ptr::null_mut(), instance,
79 | // self as a `CREATESTRUCT`'s `lpCreateParams`
80 | &*wnd as *const Self as win::LPVOID)
81 | };
82 | if hwnd.is_null() {
83 | return Err(win32::last_error());
84 | }
85 | Ok(wnd)
86 | }
87 |
88 | /// Get handle to main window.
89 | pub fn handle(&self) -> HWND {
90 | self.hwnd
91 | }
92 |
93 | /// Run message loop.
94 | pub fn run(&self) -> Result<(), Error> {
95 | log::debug!("Starting message loop");
96 | loop {
97 | let mut msg: winuser::MSG = unsafe { mem::zeroed() };
98 | match unsafe { winuser::GetMessageW(&mut msg, ptr::null_mut(), 0, 0) } {
99 | 1..=std::i32::MAX => unsafe {
100 | winuser::TranslateMessage(&msg);
101 | winuser::DispatchMessageW(&msg);
102 | },
103 | std::i32::MIN..=-1 => return Err(win32::last_error()),
104 | 0 => {
105 | log::debug!("Received WM_QUIT");
106 | return Ok(());
107 | }
108 | }
109 | }
110 | }
111 |
112 | /// Signal that progress should be cancelled.
113 | pub fn cancel(&self) {
114 | if let Some(tx) = &self.cancel_sender {
115 | tx.send(()).unwrap_or_else(|_| {
116 | log::error!("Failed to send cancel signal");
117 | });
118 | }
119 | }
120 |
121 | /// Close main window.
122 | pub fn close(&self) {
123 | unsafe { winuser::PostMessageW(self.hwnd, winuser::WM_CLOSE, 0, 0) };
124 | }
125 |
126 | /// Create child control windows.
127 | fn create_window_controls(&mut self) -> Result<(), Error> {
128 | use winuser::*;
129 | let instance = unsafe { GetWindowLongPtrW(self.hwnd, GWLP_HINSTANCE) as win::HINSTANCE };
130 | self.font = Font::new_caption(20)?;
131 | // init common controls
132 | let icex = commctrl::INITCOMMONCONTROLSEX {
133 | dwSize: mem::size_of::() as u32,
134 | dwICC: commctrl::ICC_PROGRESS_CLASS,
135 | };
136 | unsafe { commctrl::InitCommonControlsEx(&icex) };
137 | // progress bar
138 | #[rustfmt::skip]
139 | let hwnd = unsafe { CreateWindowExW(
140 | 0, wcstring(commctrl::PROGRESS_CLASS).as_ptr(), ptr::null_mut(),
141 | WS_CHILD | WS_VISIBLE | commctrl::PBS_MARQUEE,
142 | 0, 0, 0, 0, self.hwnd,
143 | Control::ProgressBar as u16 as _, instance, ptr::null_mut(),
144 | ) };
145 | unsafe { SendMessageW(hwnd, commctrl::PBM_SETRANGE32, 0, self.high_limit as _) };
146 | unsafe { SendMessageW(hwnd, commctrl::PBM_SETMARQUEE, 1, 0) };
147 | // static message area
148 | #[rustfmt::skip]
149 | let hwnd = unsafe { CreateWindowExW(
150 | 0, wchz!("STATIC").as_ptr(), ptr::null_mut(),
151 | SS_CENTER | WS_CHILD | WS_VISIBLE,
152 | 0, 0, 0, 0, self.hwnd,
153 | Control::Message as u16 as _, instance, ptr::null_mut(),
154 | ) };
155 | Self::set_window_font(hwnd, &self.font);
156 | // static title
157 | #[rustfmt::skip]
158 | let hwnd = unsafe { CreateWindowExW(
159 | 0, wchz!("STATIC").as_ptr(), ptr::null_mut(),
160 | SS_CENTER | WS_CHILD | WS_VISIBLE,
161 | 0, 0, 0, 0, self.hwnd,
162 | Control::Title as u16 as _, instance, ptr::null_mut(),
163 | ) };
164 | Self::set_window_font(hwnd, &self.font);
165 | unsafe { SetWindowTextW(hwnd, wchz!("Converting paths...").as_ptr()) };
166 | Ok(())
167 | }
168 |
169 | /// Called when client was resized.
170 | fn on_resize(&self, width: i32, _height: i32) {
171 | self.move_control(Control::Title, 10, 10, width - 20, 20);
172 | self.move_control(Control::ProgressBar, 10, 40, width - 20, 30);
173 | self.move_control(Control::Message, 10, 80, width - 20, 20);
174 | }
175 |
176 | /// Move control relative to main window.
177 | fn move_control(&self, control: Control, x: i32, y: i32, width: i32, height: i32) {
178 | let hwnd = self.get_control_handle(control);
179 | unsafe { winuser::MoveWindow(hwnd, x, y, width, height, win::TRUE) };
180 | }
181 |
182 | /// Get window handle of given control.
183 | fn get_control_handle(&self, control: Control) -> HWND {
184 | unsafe { winuser::GetDlgItem(self.hwnd, control as i32) }
185 | }
186 |
187 | /// Set font to given window.
188 | fn set_window_font(hwnd: HWND, font: &Font) {
189 | unsafe {
190 | winuser::SendMessageW(hwnd, winuser::WM_SETFONT, font.handle as _, win::TRUE as _)
191 | };
192 | }
193 |
194 | /// Update controls to display given progress.
195 | fn update_progress(&mut self, current: usize, max: usize) {
196 | use commctrl::*;
197 | use winuser::*;
198 | log::debug!("Progress update: {}/{}", current, max);
199 | let msg = format!("{} / {}", current, max);
200 | unsafe {
201 | SetWindowTextW(
202 | self.get_control_handle(Control::Message),
203 | wcstring(msg).as_ptr(),
204 | )
205 | };
206 | if self.is_marquee_progress() {
207 | self.set_progress_to_range_mode();
208 | }
209 | let hwnd = self.get_control_handle(Control::ProgressBar);
210 | unsafe { SendMessageW(hwnd, PBM_SETPOS, current, 0) };
211 | // if done, close cancellation channel
212 | if current == max {
213 | self.cancel_sender.take();
214 | }
215 | }
216 |
217 | /// Check whether progress bar is in marquee mode.
218 | fn is_marquee_progress(&self) -> bool {
219 | let style = unsafe {
220 | winuser::GetWindowLongW(
221 | self.get_control_handle(Control::ProgressBar),
222 | winuser::GWL_STYLE,
223 | )
224 | } as u32;
225 | style & commctrl::PBS_MARQUEE != 0
226 | }
227 |
228 | /// Set progress bar to range mode.
229 | fn set_progress_to_range_mode(&self) {
230 | use commctrl::*;
231 | use winuser::*;
232 | let hwnd = self.get_control_handle(Control::ProgressBar);
233 | let mut style = unsafe { GetWindowLongW(hwnd, GWL_STYLE) } as u32;
234 | style &= !PBS_MARQUEE;
235 | style |= PBS_SMOOTH;
236 | unsafe { SetWindowLongW(hwnd, GWL_STYLE, style as _) };
237 | unsafe { SendMessageW(hwnd, PBM_SETMARQUEE, 0, 0) };
238 | }
239 | }
240 |
241 | impl ProgressWindow {
242 | /// Check whether window class is registered.
243 | pub fn is_window_class_registered() -> bool {
244 | unsafe {
245 | let instance = libloaderapi::GetModuleHandleW(ptr::null_mut());
246 | let mut wc: winuser::WNDCLASSEXW = mem::zeroed();
247 | winuser::GetClassInfoExW(instance, WND_CLASS.as_ptr(), &mut wc) != 0
248 | }
249 | }
250 |
251 | /// Register window class.
252 | pub fn register_window_class() -> Result<(), Error> {
253 | use winuser::*;
254 | log::debug!("Registering {} window class", WND_CLASS.to_string_lossy());
255 | let instance = unsafe { libloaderapi::GetModuleHandleW(ptr::null_mut()) };
256 | let wc = WNDCLASSEXW {
257 | cbSize: mem::size_of::() as u32,
258 | style: CS_OWNDC | CS_HREDRAW | CS_VREDRAW,
259 | hbrBackground: (COLOR_WINDOW + 1) as HBRUSH,
260 | lpfnWndProc: Some(window_proc_wrapper::),
261 | hInstance: instance,
262 | lpszClassName: WND_CLASS.as_ptr(),
263 | hIcon: ptr::null_mut(),
264 | hCursor: unsafe { LoadCursorW(ptr::null_mut(), IDC_ARROW) },
265 | ..unsafe { mem::zeroed() }
266 | };
267 | if 0 == unsafe { RegisterClassExW(&wc) } {
268 | Err(win32::last_error())
269 | } else {
270 | Ok(())
271 | }
272 | }
273 |
274 | /// Unregister window class.
275 | pub fn unregister_window_class() {
276 | log::debug!("Unregistering {} window class", WND_CLASS.to_string_lossy());
277 | unsafe {
278 | let instance = libloaderapi::GetModuleHandleW(ptr::null_mut());
279 | winuser::UnregisterClassW(WND_CLASS.as_ptr(), instance);
280 | }
281 | }
282 | }
283 |
284 | trait WindowProc {
285 | /// Window procedure callback.
286 | ///
287 | /// If None is returned, underlying wrapper calls `DefWindowProcW`.
288 | fn window_proc(
289 | &mut self,
290 | hwnd: HWND,
291 | msg: win::UINT,
292 | wparam: win::WPARAM,
293 | lparam: win::LPARAM,
294 | ) -> Option;
295 | }
296 |
297 | /// Window proc wrapper that manages the `&self` pointer to `ProgressWindow` object.
298 | ///
299 | /// Must be `extern "system"` because the function is called by Windows.
300 | extern "system" fn window_proc_wrapper(
301 | hwnd: HWND,
302 | msg: win::UINT,
303 | wparam: win::WPARAM,
304 | lparam: win::LPARAM,
305 | ) -> win::LRESULT {
306 | use winuser::*;
307 | // get pointer to T from userdata
308 | let mut ptr = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as *mut T;
309 | // not yet set, initialize from CREATESTRUCT
310 | if ptr.is_null() && msg == WM_NCCREATE {
311 | let cs = unsafe { &*(lparam as LPCREATESTRUCTW) };
312 | ptr = cs.lpCreateParams as *mut T;
313 | log::debug!("Initialize window pointer {:p}", ptr);
314 | unsafe { errhandlingapi::SetLastError(0) };
315 | if 0 == unsafe {
316 | SetWindowLongPtrW(hwnd, GWLP_USERDATA, ptr as *const _ as basetsd::LONG_PTR)
317 | } && unsafe { errhandlingapi::GetLastError() } != 0
318 | {
319 | return win::FALSE as win::LRESULT;
320 | }
321 | }
322 | // call wrapped window proc
323 | if !ptr.is_null() {
324 | let this = unsafe { &mut *(ptr as *mut T) };
325 | if let Some(result) = this.window_proc(hwnd, msg, wparam, lparam) {
326 | return result;
327 | }
328 | }
329 | unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
330 | }
331 |
332 | impl WindowProc for ProgressWindow {
333 | fn window_proc(
334 | &mut self,
335 | hwnd: HWND,
336 | msg: win::UINT,
337 | wparam: win::WPARAM,
338 | lparam: win::LPARAM,
339 | ) -> Option {
340 | use winuser::*;
341 | match msg {
342 | // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-nccreate
343 | WM_NCCREATE => {
344 | // store main window handle
345 | self.hwnd = hwnd;
346 | // WM_NCCREATE must be passed to DefWindowProc
347 | None
348 | }
349 | // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-create
350 | WM_CREATE => match self.create_window_controls() {
351 | Err(e) => {
352 | log::error!("Failed to create window controls: {}", e);
353 | Some(-1)
354 | }
355 | Ok(()) => Some(0),
356 | },
357 | // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-size
358 | WM_SIZE => {
359 | self.on_resize(
360 | i32::from(win::LOWORD(lparam as u32)),
361 | i32::from(win::HIWORD(lparam as u32)),
362 | );
363 | Some(0)
364 | }
365 | // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-getminmaxinfo
366 | WM_GETMINMAXINFO => {
367 | let mmi = unsafe { &mut *(lparam as LPMINMAXINFO) };
368 | mmi.ptMinTrackSize.x = MIN_WINDOW_SIZE.0;
369 | mmi.ptMinTrackSize.y = MIN_WINDOW_SIZE.1;
370 | Some(0)
371 | }
372 | // https://docs.microsoft.com/en-us/windows/win32/controls/wm-ctlcolorstatic
373 | WM_CTLCOLORSTATIC => {
374 | Some(unsafe { wingdi::GetStockObject(COLOR_WINDOW + 1) } as win::LPARAM)
375 | }
376 | // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-close
377 | WM_CLOSE => {
378 | self.cancel();
379 | unsafe { DestroyWindow(hwnd) };
380 | Some(0)
381 | }
382 | // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-destroy
383 | WM_DESTROY => {
384 | unsafe { PostQuitMessage(0) };
385 | Some(0)
386 | }
387 | WM_PROGRESS => {
388 | self.update_progress(wparam, lparam as _);
389 | Some(0)
390 | }
391 | _ => None,
392 | }
393 | }
394 | }
395 |
--------------------------------------------------------------------------------