├── .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 |

16 | wslscript.exe 17 |

18 |

{{ site.description | default: site.github.project_tagline }}

19 | 20 |
21 | Download 22 | 23 | View on GitHub 24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 | {{ content }} 32 |
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 | --------------------------------------------------------------------------------