├── .github └── workflows │ ├── publish-github-release.yaml │ └── test.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── Makefile ├── README.md ├── examples ├── png-Yosemite │ ├── apple_folder_256.png │ ├── cube_folder_256.png │ ├── octocat_folder_256.png │ ├── rhombic_hexecontahedron_folder_256.png │ └── sysprefs_folder_256.png ├── png │ ├── apple.gif │ ├── apple_folder_256.png │ ├── cube_folder_256.png │ ├── explanation.png │ ├── octocat_folder_256.png │ ├── rhombic_hexecontahedron_folder_256.png │ └── sysprefs_folder_256.png └── src │ ├── .gitignore │ ├── apple.png │ ├── cube.png │ ├── folder_outline.png │ ├── octocat.png │ ├── rhombic_hexecontahedron.png │ └── sysprefs.png ├── rust-toolchain.toml ├── src ├── command.rs ├── error.rs ├── icon_conversion.rs ├── magick.rs ├── main.rs ├── options.rs ├── output_paths.rs ├── primitives.rs ├── resources.rs └── resources │ ├── badges │ ├── AliasBadgeIcon.iconset │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ └── LockedBadgeIcon.iconset │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ └── folders │ ├── GenericFolderIcon.BigSur.dark.iconset │ ├── icon_128x128.png │ ├── icon_128x128@2x.png │ ├── icon_16x16.png │ ├── icon_16x16@2x.png │ ├── icon_256x256.png │ ├── icon_256x256@2x.png │ ├── icon_32x32.png │ ├── icon_32x32@2x.png │ ├── icon_512x512.png │ └── icon_512x512@2x.png │ └── GenericFolderIcon.BigSur.iconset │ ├── icon_128x128.png │ ├── icon_128x128@2x.png │ ├── icon_16x16.png │ ├── icon_16x16@2x.png │ ├── icon_256x256.png │ ├── icon_256x256@2x.png │ ├── icon_32x32.png │ ├── icon_32x32@2x.png │ ├── icon_512x512.png │ └── icon_512x512@2x.png ├── test ├── helpers.sh └── test-behaviour.sh └── tools └── Quicksilver └── Use as icon mask (folderify)….scpt /.github/workflows/publish-github-release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish GitHub release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | Publish: 10 | permissions: 11 | contents: write 12 | if: startsWith(github.ref, 'refs/tags/v') 13 | uses: cubing/actions-workflows/.github/workflows/publish-github-release.yaml@main 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test on macOS 3 | 4 | jobs: 5 | test: 6 | strategy: 7 | matrix: 8 | os: [macos-13, macos-14, macos-15] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v4 13 | - uses: oven-sh/setup-bun@v1 14 | - name: Install ImageMagick 15 | run: env HOMEBREW_NO_AUTO_UPDATE=1 brew install imagemagick 16 | - run: make build 17 | - run: make test-behaviour 18 | - run: make lint 19 | - run: make check-readme-cli-help 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /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 = "bitflags" 7 | version = "1.3.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 10 | 11 | [[package]] 12 | name = "cfg-if" 13 | version = "1.0.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 16 | 17 | [[package]] 18 | name = "clap" 19 | version = "4.1.8" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" 22 | dependencies = [ 23 | "bitflags", 24 | "clap_derive", 25 | "clap_lex", 26 | "is-terminal", 27 | "once_cell", 28 | "strsim", 29 | "termcolor", 30 | ] 31 | 32 | [[package]] 33 | name = "clap_complete" 34 | version = "4.2.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "01c22dcfb410883764b29953103d9ef7bb8fe21b3fa1158bc99986c2067294bd" 37 | dependencies = [ 38 | "clap", 39 | ] 40 | 41 | [[package]] 42 | name = "clap_derive" 43 | version = "4.1.8" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0" 46 | dependencies = [ 47 | "heck", 48 | "proc-macro-error", 49 | "proc-macro2", 50 | "quote", 51 | "syn", 52 | ] 53 | 54 | [[package]] 55 | name = "clap_lex" 56 | version = "0.3.2" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" 59 | dependencies = [ 60 | "os_str_bytes", 61 | ] 62 | 63 | [[package]] 64 | name = "console" 65 | version = "0.15.5" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" 68 | dependencies = [ 69 | "encode_unicode", 70 | "lazy_static", 71 | "libc", 72 | "unicode-width", 73 | "windows-sys 0.42.0", 74 | ] 75 | 76 | [[package]] 77 | name = "encode_unicode" 78 | version = "0.3.6" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 81 | 82 | [[package]] 83 | name = "errno" 84 | version = "0.3.5" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" 87 | dependencies = [ 88 | "libc", 89 | "windows-sys 0.48.0", 90 | ] 91 | 92 | [[package]] 93 | name = "folderify" 94 | version = "4.0.1" 95 | dependencies = [ 96 | "clap", 97 | "clap_complete", 98 | "include_dir", 99 | "indicatif", 100 | "mktemp", 101 | ] 102 | 103 | [[package]] 104 | name = "getrandom" 105 | version = "0.2.8" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 108 | dependencies = [ 109 | "cfg-if", 110 | "libc", 111 | "wasi", 112 | ] 113 | 114 | [[package]] 115 | name = "heck" 116 | version = "0.4.1" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 119 | 120 | [[package]] 121 | name = "hermit-abi" 122 | version = "0.3.1" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 125 | 126 | [[package]] 127 | name = "include_dir" 128 | version = "0.7.3" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" 131 | dependencies = [ 132 | "include_dir_macros", 133 | ] 134 | 135 | [[package]] 136 | name = "include_dir_macros" 137 | version = "0.7.3" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" 140 | dependencies = [ 141 | "proc-macro2", 142 | "quote", 143 | ] 144 | 145 | [[package]] 146 | name = "indicatif" 147 | version = "0.17.7" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" 150 | dependencies = [ 151 | "console", 152 | "instant", 153 | "number_prefix", 154 | "portable-atomic", 155 | "unicode-width", 156 | ] 157 | 158 | [[package]] 159 | name = "instant" 160 | version = "0.1.12" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 163 | dependencies = [ 164 | "cfg-if", 165 | ] 166 | 167 | [[package]] 168 | name = "io-lifetimes" 169 | version = "1.0.6" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" 172 | dependencies = [ 173 | "libc", 174 | "windows-sys 0.45.0", 175 | ] 176 | 177 | [[package]] 178 | name = "is-terminal" 179 | version = "0.4.4" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" 182 | dependencies = [ 183 | "hermit-abi", 184 | "io-lifetimes", 185 | "rustix", 186 | "windows-sys 0.45.0", 187 | ] 188 | 189 | [[package]] 190 | name = "lazy_static" 191 | version = "1.4.0" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 194 | 195 | [[package]] 196 | name = "libc" 197 | version = "0.2.139" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 200 | 201 | [[package]] 202 | name = "linux-raw-sys" 203 | version = "0.1.4" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 206 | 207 | [[package]] 208 | name = "mktemp" 209 | version = "0.5.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "4bdc1f74dd7bb717d39f784f844e490d935b3aa7e383008006dbbf29c1f7820a" 212 | dependencies = [ 213 | "uuid", 214 | ] 215 | 216 | [[package]] 217 | name = "number_prefix" 218 | version = "0.4.0" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 221 | 222 | [[package]] 223 | name = "once_cell" 224 | version = "1.17.1" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 227 | 228 | [[package]] 229 | name = "os_str_bytes" 230 | version = "6.4.1" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" 233 | 234 | [[package]] 235 | name = "portable-atomic" 236 | version = "1.3.3" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794" 239 | 240 | [[package]] 241 | name = "proc-macro-error" 242 | version = "1.0.4" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 245 | dependencies = [ 246 | "proc-macro-error-attr", 247 | "proc-macro2", 248 | "quote", 249 | "syn", 250 | "version_check", 251 | ] 252 | 253 | [[package]] 254 | name = "proc-macro-error-attr" 255 | version = "1.0.4" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 258 | dependencies = [ 259 | "proc-macro2", 260 | "quote", 261 | "version_check", 262 | ] 263 | 264 | [[package]] 265 | name = "proc-macro2" 266 | version = "1.0.51" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" 269 | dependencies = [ 270 | "unicode-ident", 271 | ] 272 | 273 | [[package]] 274 | name = "quote" 275 | version = "1.0.23" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 278 | dependencies = [ 279 | "proc-macro2", 280 | ] 281 | 282 | [[package]] 283 | name = "rustix" 284 | version = "0.36.16" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "6da3636faa25820d8648e0e31c5d519bbb01f72fdf57131f0f5f7da5fed36eab" 287 | dependencies = [ 288 | "bitflags", 289 | "errno", 290 | "io-lifetimes", 291 | "libc", 292 | "linux-raw-sys", 293 | "windows-sys 0.45.0", 294 | ] 295 | 296 | [[package]] 297 | name = "strsim" 298 | version = "0.10.0" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 301 | 302 | [[package]] 303 | name = "syn" 304 | version = "1.0.109" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 307 | dependencies = [ 308 | "proc-macro2", 309 | "quote", 310 | "unicode-ident", 311 | ] 312 | 313 | [[package]] 314 | name = "termcolor" 315 | version = "1.2.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 318 | dependencies = [ 319 | "winapi-util", 320 | ] 321 | 322 | [[package]] 323 | name = "unicode-ident" 324 | version = "1.0.8" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 327 | 328 | [[package]] 329 | name = "unicode-width" 330 | version = "0.1.10" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 333 | 334 | [[package]] 335 | name = "uuid" 336 | version = "1.2.2" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" 339 | dependencies = [ 340 | "getrandom", 341 | ] 342 | 343 | [[package]] 344 | name = "version_check" 345 | version = "0.9.4" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 348 | 349 | [[package]] 350 | name = "wasi" 351 | version = "0.11.0+wasi-snapshot-preview1" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 354 | 355 | [[package]] 356 | name = "winapi" 357 | version = "0.3.9" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 360 | dependencies = [ 361 | "winapi-i686-pc-windows-gnu", 362 | "winapi-x86_64-pc-windows-gnu", 363 | ] 364 | 365 | [[package]] 366 | name = "winapi-i686-pc-windows-gnu" 367 | version = "0.4.0" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 370 | 371 | [[package]] 372 | name = "winapi-util" 373 | version = "0.1.5" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 376 | dependencies = [ 377 | "winapi", 378 | ] 379 | 380 | [[package]] 381 | name = "winapi-x86_64-pc-windows-gnu" 382 | version = "0.4.0" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 385 | 386 | [[package]] 387 | name = "windows-sys" 388 | version = "0.42.0" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 391 | dependencies = [ 392 | "windows_aarch64_gnullvm 0.42.1", 393 | "windows_aarch64_msvc 0.42.1", 394 | "windows_i686_gnu 0.42.1", 395 | "windows_i686_msvc 0.42.1", 396 | "windows_x86_64_gnu 0.42.1", 397 | "windows_x86_64_gnullvm 0.42.1", 398 | "windows_x86_64_msvc 0.42.1", 399 | ] 400 | 401 | [[package]] 402 | name = "windows-sys" 403 | version = "0.45.0" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 406 | dependencies = [ 407 | "windows-targets 0.42.1", 408 | ] 409 | 410 | [[package]] 411 | name = "windows-sys" 412 | version = "0.48.0" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 415 | dependencies = [ 416 | "windows-targets 0.48.5", 417 | ] 418 | 419 | [[package]] 420 | name = "windows-targets" 421 | version = "0.42.1" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 424 | dependencies = [ 425 | "windows_aarch64_gnullvm 0.42.1", 426 | "windows_aarch64_msvc 0.42.1", 427 | "windows_i686_gnu 0.42.1", 428 | "windows_i686_msvc 0.42.1", 429 | "windows_x86_64_gnu 0.42.1", 430 | "windows_x86_64_gnullvm 0.42.1", 431 | "windows_x86_64_msvc 0.42.1", 432 | ] 433 | 434 | [[package]] 435 | name = "windows-targets" 436 | version = "0.48.5" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 439 | dependencies = [ 440 | "windows_aarch64_gnullvm 0.48.5", 441 | "windows_aarch64_msvc 0.48.5", 442 | "windows_i686_gnu 0.48.5", 443 | "windows_i686_msvc 0.48.5", 444 | "windows_x86_64_gnu 0.48.5", 445 | "windows_x86_64_gnullvm 0.48.5", 446 | "windows_x86_64_msvc 0.48.5", 447 | ] 448 | 449 | [[package]] 450 | name = "windows_aarch64_gnullvm" 451 | version = "0.42.1" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 454 | 455 | [[package]] 456 | name = "windows_aarch64_gnullvm" 457 | version = "0.48.5" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 460 | 461 | [[package]] 462 | name = "windows_aarch64_msvc" 463 | version = "0.42.1" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 466 | 467 | [[package]] 468 | name = "windows_aarch64_msvc" 469 | version = "0.48.5" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 472 | 473 | [[package]] 474 | name = "windows_i686_gnu" 475 | version = "0.42.1" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 478 | 479 | [[package]] 480 | name = "windows_i686_gnu" 481 | version = "0.48.5" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 484 | 485 | [[package]] 486 | name = "windows_i686_msvc" 487 | version = "0.42.1" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 490 | 491 | [[package]] 492 | name = "windows_i686_msvc" 493 | version = "0.48.5" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 496 | 497 | [[package]] 498 | name = "windows_x86_64_gnu" 499 | version = "0.42.1" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 502 | 503 | [[package]] 504 | name = "windows_x86_64_gnu" 505 | version = "0.48.5" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 508 | 509 | [[package]] 510 | name = "windows_x86_64_gnullvm" 511 | version = "0.42.1" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 514 | 515 | [[package]] 516 | name = "windows_x86_64_gnullvm" 517 | version = "0.48.5" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 520 | 521 | [[package]] 522 | name = "windows_x86_64_msvc" 523 | version = "0.42.1" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 526 | 527 | [[package]] 528 | name = "windows_x86_64_msvc" 529 | version = "0.48.5" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 532 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "folderify" 3 | version = "4.0.1" 4 | edition = "2021" 5 | description = "Generate a native-style macOS folder icon from a mask file." 6 | license = "MIT" 7 | homepage = "https://github.com/lgarron/folderify" 8 | documentation = "https://github.com/lgarron/folderify" 9 | repository = "https://github.com/lgarron/folderify" 10 | keywords = ["macos", "icons"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | clap = { version = "4.1.8", features = ["derive"] } 16 | clap_complete = "4.2.0" 17 | include_dir = "0.7.3" 18 | indicatif = "0.17.5" 19 | mktemp = "0.5.0" 20 | 21 | [[bin]] 22 | name = "folderify" 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | cargo build --release 4 | 5 | .PHONY: lint 6 | lint: 7 | cargo clippy 8 | cargo fmt --check 9 | 10 | .PHONY: format 11 | format: 12 | cargo fmt 13 | 14 | .PHONY: test 15 | test: test-behaviour lint check-readme-cli-help 16 | 17 | .PHONY: test-behaviour 18 | test-behaviour: 19 | ./test/test-behaviour.sh 20 | 21 | .PHONY: publish 22 | publish: 23 | cargo publish 24 | 25 | .PHONY: install 26 | install: 27 | cargo install --path . 28 | 29 | .PHONY: uninstall 30 | cargo uninstall folderify 31 | 32 | .PHONY: clean 33 | clean: 34 | 35 | .PHONY: readme-cli-help 36 | readme-cli-help: 37 | bun x readme-cli-help "cargo run -- --help" 38 | 39 | .PHONY: check-readme-cli-help 40 | check-readme-cli-help: 41 | bun x readme-cli-help --check-only "cargo run -- --help" 42 | 43 | .PHONY: reset 44 | reset: 45 | rm -rf ./target 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # folderify 2 | 3 | ![mask.png + folder = folderified!](examples/png/explanation.png) 4 | 5 | Generate pixel-perfect macOS folder icons in the native style. 6 | 7 | - Automatically includes all icon sizes from `16x16` through `512x512@2x`. 8 | - Light or dark mode (automatically selected by default). 9 | 10 | **Using `folderify`?** [Let me know](https://mastodon.social/@lgarron) or [let me know](https://github.com/lgarron/folderify/issues/new) and I'd love to feature some real-world uses! 11 | 12 | ## Install 13 | 14 | Install `folderify` using [Homebrew](https://formulae.brew.sh/formula/folderify): 15 | 16 | ```shell 17 | brew install folderify 18 | ``` 19 | 20 | Homebrew install is recommended, and automatically installs `folderify` argument completions for your shell. 21 | 22 | See below for other installation options. 23 | 24 | ## Usage 25 | 26 | Use a mask to assign an icon to a folder: 27 | 28 | ```shell 29 | folderify mask.png /path/to/folder 30 | ``` 31 | 32 | Generate `mask.icns` and `mask.iconset` files: 33 | 34 | ```shell 35 | folderify mask.png 36 | ``` 37 | 38 | By default, `folderify` uses your system's current light/dark mode. Use `--color-scheme` to override this: 39 | 40 | ```shell 41 | folderify --color-scheme dark mask.png 42 | ``` 43 | 44 | Note: 45 | 46 | - There is currently no simple way to set an icon that will automatically switch between light and dark when you switch the entire OS. You can only assign one version of an icon to a folder. 47 | 48 | ### Tips 49 | 50 | For best results: 51 | 52 | - Use a `.png` mask. 53 | - Use a solid black design over a transparent background. 54 | - Make sure the corner pixels of the mask image are transparent. They are used for empty margins. 55 | - Pass the `--no-trim` flag and use a mask: 56 | - with a height of 384px, 57 | - with a width that is a multiple of 128px (up to 768px), 58 | - using a 16px grid. 59 | - Each 64x64 tile will exactly align with 1 pixel at the smallest icon size. 60 | 61 | ### OS X (macOS 10) 62 | 63 | Folder styles from OS X / macOS 10 are no longer supported by `folderify` as of v3: 64 | 65 | - Leopard-style (OS X 10.5 to OS X 10.9) 66 | - Yosemite-style (OS X 10.10 to macOS 10.15) 67 | 68 | To generate these, please use `folderify` v2. For example: 69 | 70 | ```shell 71 | # Using `uvx`: https://docs.astral.sh/uv/guides/tools/ 72 | uvx --from folderify folderify-v2 --macOS 10.5 path/to/icon.png 73 | 74 | # Using `pip` 75 | pip3 install folderify 76 | python3 -m folderify --macOS 10.5 path/to/icon.png 77 | ``` 78 | 79 | ## Other installation options 80 | 81 | If you don't have Homebrew but you already have ImageMagick (the `magick` 82 | binary) on your system, you can use the following: 83 | 84 | ### Install using Rust 85 | 86 | ```shell 87 | cargo install folderify 88 | ``` 89 | 90 | ### From source 91 | 92 | Or download the code directly: 93 | 94 | ```shell 95 | git clone https://github.com/lgarron/folderify && cd folderify 96 | 97 | # Run directly 98 | cargo run -- --reveal examples/src/folder_outline.png . 99 | 100 | # Install (assuming the `cargo` bin is in your path) 101 | cargo install --path . 102 | folderify --reveal examples/src/folder_outline.png . 103 | ``` 104 | 105 | The repository folder should now have a custom icon. 106 | 107 | ```shell 108 | # bash 109 | for file in examples/src/*.png; do cargo run -- $file; done 110 | open examples/src/ 111 | ``` 112 | 113 | You should see a bunch of new `.iconset` folders and `.icns` files that were automatically generated from the `.png` masks. 114 | 115 | ### Dependencies 116 | 117 | - [ImageMagick](https://www.imagemagick.org/) - for image processing (you should be able to run `magick` and `identify` on the commandline). 118 | - Included with macOS: 119 | - `iconutil` 120 | - Optional: 121 | - [`fileicon`](https://github.com/mklement0/fileicon/) 122 | - `sips`, `DeRez`, `Rez`, `SetFile` (You need Xcode command line tools for some of these.) 123 | 124 | ## Full options 125 | 126 | ````cli-help 127 | Generate a native-style macOS folder icon from a mask file. 128 | 129 | Usage: folderify [OPTIONS] [MASK] [TARGET] 130 | 131 | Arguments: 132 | [MASK] 133 | Mask image file. For best results: 134 | - Use a .png mask. 135 | - Use a solid black design over a transparent background. 136 | - Make sure the corner pixels of the mask image are transparent. They are used for empty margins. 137 | - Make sure the non-transparent pixels span a height of 384px, using a 16px grid. 138 | If the height is 384px and the width is a multiple of 128px, each 64x64 tile will exactly align with 1 pixel at the smallest folder size. 139 | 140 | [TARGET] 141 | Target file or folder. If a target is specified, the resulting icon will 142 | be applied to the target file/folder. Else (unless --output-icns or 143 | --output-iconset is specified), a .iconset folder and .icns file will be 144 | created in the same folder as the mask (you can use "Get Info" in Finder 145 | to copy the icon from the .icns file). 146 | 147 | Options: 148 | --output-icns 149 | Write the `.icns` file to the given path. 150 | (Will be written even if a target is also specified.) 151 | 152 | --output-iconset 153 | Write the `.iconset` folder to the given path. 154 | (Will be written even if a target is also specified.) 155 | 156 | -r, --reveal 157 | Reveal either the target, `.icns`, or `.iconset` (in that order of preference) in Finder 158 | 159 | --macOS 160 | Version of the macOS folder icon, e.g. "14.2.1". Defaults to the version currently running 161 | 162 | --color-scheme 163 | Color scheme — auto matches the current system value 164 | 165 | [default: auto] 166 | [possible values: auto, light, dark] 167 | 168 | --no-trim 169 | Don't trim margins from the mask. 170 | By default (i.e. without this flag), transparent margins are trimmed from all 4 sides. 171 | 172 | --no-progress 173 | Don't show progress bars 174 | 175 | --badge 176 | Add a badge to the icon. Currently only supports one badge at a time 177 | 178 | [possible values: alias, locked] 179 | 180 | -v, --verbose 181 | Detailed output. Also sets `--no-progress` 182 | 183 | --completions 184 | Print completions for the given shell (instead of generating any icons). 185 | These can be loaded/stored permanently (e.g. when using Homebrew), but they can also be sourced directly, e.g.: 186 | 187 | folderify --completions fish | source # fish 188 | source <(folderify --completions zsh) # zsh 189 | 190 | [possible values: bash, elvish, fish, powershell, zsh] 191 | 192 | -h, --help 193 | Print help (see a summary with '-h') 194 | 195 | -V, --version 196 | Print version 197 | ```` 198 | 199 | ## Example 200 | 201 | Example generated from the Apple logo: 202 | ![Icons from apple.iconset at resolutions from 16x16 up to 512x5125@2x, shown in Quicklook on macOS](examples/png/apple.gif) 203 | -------------------------------------------------------------------------------- /examples/png-Yosemite/apple_folder_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/png-Yosemite/apple_folder_256.png -------------------------------------------------------------------------------- /examples/png-Yosemite/cube_folder_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/png-Yosemite/cube_folder_256.png -------------------------------------------------------------------------------- /examples/png-Yosemite/octocat_folder_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/png-Yosemite/octocat_folder_256.png -------------------------------------------------------------------------------- /examples/png-Yosemite/rhombic_hexecontahedron_folder_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/png-Yosemite/rhombic_hexecontahedron_folder_256.png -------------------------------------------------------------------------------- /examples/png-Yosemite/sysprefs_folder_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/png-Yosemite/sysprefs_folder_256.png -------------------------------------------------------------------------------- /examples/png/apple.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/png/apple.gif -------------------------------------------------------------------------------- /examples/png/apple_folder_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/png/apple_folder_256.png -------------------------------------------------------------------------------- /examples/png/cube_folder_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/png/cube_folder_256.png -------------------------------------------------------------------------------- /examples/png/explanation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/png/explanation.png -------------------------------------------------------------------------------- /examples/png/octocat_folder_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/png/octocat_folder_256.png -------------------------------------------------------------------------------- /examples/png/rhombic_hexecontahedron_folder_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/png/rhombic_hexecontahedron_folder_256.png -------------------------------------------------------------------------------- /examples/png/sysprefs_folder_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/png/sysprefs_folder_256.png -------------------------------------------------------------------------------- /examples/src/.gitignore: -------------------------------------------------------------------------------- 1 | *.icns 2 | *.iconset -------------------------------------------------------------------------------- /examples/src/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/src/apple.png -------------------------------------------------------------------------------- /examples/src/cube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/src/cube.png -------------------------------------------------------------------------------- /examples/src/folder_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/src/folder_outline.png -------------------------------------------------------------------------------- /examples/src/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/src/octocat.png -------------------------------------------------------------------------------- /examples/src/rhombic_hexecontahedron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/src/rhombic_hexecontahedron.png -------------------------------------------------------------------------------- /examples/src/sysprefs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/examples/src/sysprefs.png -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.83.0" 3 | -------------------------------------------------------------------------------- /src/command.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::process::Command; 3 | use std::process::Stdio; 4 | use std::str::from_utf8; 5 | 6 | use crate::error::CommandFailedError; 7 | use crate::error::CommandInvalidError; 8 | use crate::error::FolderifyError; 9 | use crate::error::GeneralError; 10 | use crate::magick::CommandArgs; 11 | 12 | const DEBUG_PRINT_ARGS: bool = false; 13 | 14 | const MAGICK_COMMAND: &str = "magick"; 15 | const IDENTIFY_COMMAND: &str = "identify"; 16 | pub(crate) const ICONUTIL_COMMAND: &str = "iconutil"; 17 | pub(crate) const OPEN_COMMAND: &str = "open"; 18 | 19 | pub(crate) const OSASCRIPT_COMMAND: &str = "osascript"; 20 | pub(crate) const FILEICON_COMMAND: &str = "fileicon"; 21 | 22 | pub(crate) const SIPS_COMMAND: &str = "sips"; 23 | pub(crate) const DEREZ_COMMAND: &str = "DeRez"; 24 | pub(crate) const REZ_COMMAND: &str = "Rez"; 25 | pub(crate) const SETFILE_COMMAND: &str = "SetFile"; 26 | 27 | pub(crate) fn run_command( 28 | command_name: &str, 29 | args: &CommandArgs, 30 | stdin: Option<&[u8]>, 31 | ) -> Result, FolderifyError> { 32 | if DEBUG_PRINT_ARGS { 33 | println!("args: {}", args.args.join(" ")); 34 | }; 35 | let child = Command::new(command_name) 36 | .args(args.args.iter()) 37 | .stdin(Stdio::piped()) 38 | .stdout(Stdio::piped()) 39 | .spawn(); 40 | let mut child = match child { 41 | Ok(child) => child, 42 | Err(_) => { 43 | return Err(FolderifyError::CommandInvalid(CommandInvalidError { 44 | command_name: command_name.into(), 45 | })); 46 | } 47 | }; 48 | 49 | if let Some(stdin) = stdin { 50 | let child_stdin = child.stdin.as_mut().unwrap(); // TODO 51 | match child_stdin.write_all(stdin) { 52 | Ok(output) => output, 53 | Err(_) => { 54 | return Err(FolderifyError::General(GeneralError { 55 | message: "Could not write to stdin for a command.".into(), 56 | })) 57 | } 58 | } 59 | } 60 | 61 | let output = match child.wait_with_output() { 62 | Ok(output) => output, 63 | Err(_) => { 64 | return Err(FolderifyError::CommandInvalid(CommandInvalidError { 65 | command_name: command_name.into(), 66 | })) 67 | } 68 | }; 69 | 70 | if !output.status.success() { 71 | return Err(FolderifyError::CommandFailed(CommandFailedError { 72 | command_name: command_name.into(), 73 | stderr: output.stderr, 74 | })); 75 | } 76 | 77 | Ok(output.stdout) 78 | } 79 | 80 | pub(crate) fn run_magick(args: &CommandArgs, stdin: Option<&[u8]>) -> Result<(), FolderifyError> { 81 | run_command(MAGICK_COMMAND, args, stdin)?; 82 | Ok(()) 83 | } 84 | 85 | pub(crate) fn identify_read_u32(args: &CommandArgs) -> Result { 86 | let stdout = run_command(IDENTIFY_COMMAND, args, None)?; 87 | let s: &str = match from_utf8(&stdout) { 88 | Ok(s) => s, 89 | Err(s) => { 90 | println!("errerrerr{}+++++", s); 91 | return Err((GeneralError { 92 | message: "Could not read input dimensions".into(), 93 | }) 94 | .into()); 95 | } 96 | }; 97 | let value = match s.parse::() { 98 | Ok(value) => value, 99 | Err(s) => { 100 | // TODO 101 | println!("errerrerr{}+++++", s); 102 | return Err((GeneralError { 103 | message: "Could not read input dimensions".into(), 104 | }) 105 | .into()); 106 | } 107 | }; 108 | Ok(value) 109 | } 110 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | #[allow(dead_code)] // For debugging 3 | pub enum FolderifyError { 4 | CommandInvalid(CommandInvalidError), 5 | CommandFailed(CommandFailedError), 6 | General(GeneralError), 7 | } 8 | 9 | #[derive(Debug)] 10 | #[allow(dead_code)] // For debugging 11 | pub struct CommandInvalidError { 12 | pub command_name: String, 13 | } 14 | 15 | impl From for FolderifyError { 16 | fn from(value: CommandInvalidError) -> Self { 17 | FolderifyError::CommandInvalid(value) 18 | } 19 | } 20 | 21 | #[derive(Debug)] 22 | #[allow(dead_code)] // For debugging 23 | pub struct CommandFailedError { 24 | pub command_name: String, 25 | pub stderr: Vec, 26 | } 27 | 28 | impl From for FolderifyError { 29 | fn from(value: CommandFailedError) -> Self { 30 | FolderifyError::CommandFailed(value) 31 | } 32 | } 33 | 34 | #[derive(Debug)] 35 | #[allow(dead_code)] // For debugging 36 | pub struct GeneralError { 37 | pub message: String, 38 | } 39 | 40 | impl From for FolderifyError { 41 | fn from(value: GeneralError) -> Self { 42 | FolderifyError::General(value) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/icon_conversion.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Display, 3 | fs::{self, create_dir_all, metadata}, 4 | path::{Path, PathBuf}, 5 | process::exit, 6 | }; 7 | 8 | use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle}; 9 | use mktemp::Temp; 10 | 11 | const RETINA_SCALE: u32 = 2; 12 | 13 | pub enum ProgressBarType { 14 | Input, 15 | Conversion, 16 | OutputWithAssignment, 17 | OutputIcns, 18 | } 19 | 20 | impl ProgressBarType { 21 | pub fn num_steps(&self, options: &Options) -> u64 { 22 | match self { 23 | ProgressBarType::Input => 1, 24 | ProgressBarType::Conversion => 13 + if options.badge.is_some() { 1 } else { 0 }, 25 | ProgressBarType::OutputWithAssignment => { 26 | 2 + if matches!(options.set_icon_using, SetIconUsing::Rez) { 27 | 7 28 | } else { 29 | 0 30 | } 31 | } 32 | ProgressBarType::OutputIcns => 1, 33 | } 34 | } 35 | } 36 | 37 | use crate::{ 38 | command::{ 39 | run_command, run_magick, DEREZ_COMMAND, FILEICON_COMMAND, ICONUTIL_COMMAND, 40 | OSASCRIPT_COMMAND, REZ_COMMAND, SETFILE_COMMAND, SIPS_COMMAND, 41 | }, 42 | error::{FolderifyError, GeneralError}, 43 | magick::{density, BlurDown, CommandArgs, CompositingOperation}, 44 | options::{Badge, ColorScheme, Options, SetIconUsing}, 45 | primitives::{Dimensions, Extent, Offset, RGBColor}, 46 | resources::{get_badge_icon, get_folder_icon}, 47 | }; 48 | 49 | pub struct ScaledMaskInputs { 50 | pub icon_size: u32, 51 | pub mask_dimensions: Dimensions, 52 | pub offset_y: i32, 53 | } 54 | 55 | pub struct BezelInputs { 56 | pub color: RGBColor, 57 | pub blur: BlurDown, 58 | pub mask_operation: CompositingOperation, 59 | pub opacity: f32, 60 | } 61 | 62 | pub struct EngravingInputs { 63 | pub fill_color: RGBColor, 64 | pub top_bezel: BezelInputs, 65 | pub bottom_bezel: BezelInputs, 66 | } 67 | 68 | #[derive(Debug)] 69 | pub struct WorkingDir { 70 | working_dir: Temp, 71 | } 72 | 73 | impl WorkingDir { 74 | pub fn new() -> Self { 75 | Self { 76 | working_dir: Temp::new_dir().expect("Couldn't create a temp dir."), 77 | } 78 | } 79 | 80 | pub fn icon_conversion( 81 | &self, 82 | progress_bar_type: ProgressBarType, 83 | stage_description: &str, 84 | multi_progress_bar: Option, 85 | options: &Options, 86 | ) -> IconConversion { 87 | let progress_bar = match multi_progress_bar { 88 | Some(multi_progress_bar) => { 89 | let progress_bar = ProgressBar::new(progress_bar_type.num_steps(options)); 90 | let progress_bar = match progress_bar_type { 91 | ProgressBarType::Conversion => multi_progress_bar.insert(1, progress_bar), 92 | _ => multi_progress_bar.insert_from_back(0, progress_bar), 93 | }; 94 | let progress_bar = progress_bar.with_finish(ProgressFinish::AndLeave); 95 | // TODO share the progress bar style? 96 | let progress_bar_style = ProgressStyle::with_template( 97 | "{bar:12.cyan/blue} | {pos:>2}/{len:2} | {wide_msg}", 98 | ) 99 | .expect("Could not construct progress bar.") 100 | .progress_chars("=> "); 101 | progress_bar.set_style(progress_bar_style); 102 | progress_bar.tick(); 103 | Some(progress_bar) 104 | } 105 | None => None, 106 | }; 107 | IconConversion { 108 | working_dir: self.working_dir.as_path().to_owned(), 109 | resolution_prefix: stage_description.into(), 110 | progress_bar, 111 | } 112 | } 113 | 114 | pub fn open_in_finder(&self) -> Result<(), FolderifyError> { 115 | let mut open_args = CommandArgs::new(); 116 | open_args.push_path(&self.working_dir); 117 | run_command("open", &open_args, None)?; 118 | Ok(()) 119 | } 120 | 121 | pub fn release(self) { 122 | self.working_dir.release(); // TODO 123 | } 124 | 125 | pub fn icon_file_with_extension(&self, extension: &str) -> PathBuf { 126 | self.working_dir 127 | .as_path() 128 | .join("icon") 129 | .with_extension(extension) 130 | } 131 | 132 | pub fn create_iconset_dir(&self, options: &Options) -> Result { 133 | let iconset_dir = self.icon_file_with_extension("iconset"); 134 | if options.verbose { 135 | println!("[Iconset] {}", iconset_dir.display()); 136 | }; 137 | if let Err(e) = create_dir_all(&iconset_dir) { 138 | println!("Error: {}", (e)); 139 | return Err(FolderifyError::General(GeneralError { 140 | message: "Could not create iconset dir".into(), 141 | })); 142 | }; 143 | Ok(iconset_dir) 144 | } 145 | } 146 | 147 | pub enum IconResolution { 148 | NonRetina16, 149 | Retina16, 150 | NonRetina32, 151 | Retina32, 152 | NonRetina128, 153 | Retina128, 154 | NonRetina256, 155 | Retina256, 156 | NonRetina512, 157 | Retina512, 158 | } 159 | 160 | impl IconResolution { 161 | // TODO: return iterator? 162 | pub fn values() -> Vec { 163 | vec![ 164 | Self::Retina512, 165 | Self::NonRetina512, 166 | Self::Retina256, 167 | Self::NonRetina256, 168 | Self::Retina128, 169 | Self::NonRetina128, 170 | Self::Retina32, 171 | Self::NonRetina32, 172 | Self::Retina16, 173 | Self::NonRetina16, 174 | ] 175 | } 176 | 177 | pub fn size(&self) -> u32 { 178 | match self { 179 | IconResolution::NonRetina16 => 16, 180 | IconResolution::Retina16 => 16 * RETINA_SCALE, 181 | IconResolution::NonRetina32 => 32, 182 | IconResolution::Retina32 => 32 * RETINA_SCALE, 183 | IconResolution::NonRetina128 => 128, 184 | IconResolution::Retina128 => 128 * RETINA_SCALE, 185 | IconResolution::NonRetina256 => 256, 186 | IconResolution::Retina256 => 256 * RETINA_SCALE, 187 | IconResolution::NonRetina512 => 512, 188 | IconResolution::Retina512 => 512 * RETINA_SCALE, 189 | } 190 | } 191 | 192 | pub fn offset_y(&self) -> i32 { 193 | match self { 194 | IconResolution::NonRetina16 => -2, 195 | IconResolution::Retina16 => -2, 196 | IconResolution::NonRetina32 => -2, 197 | IconResolution::Retina32 => -3, 198 | IconResolution::NonRetina128 => -6, 199 | IconResolution::Retina128 => -12, 200 | IconResolution::NonRetina256 => -12, 201 | IconResolution::Retina256 => -24, 202 | IconResolution::NonRetina512 => -24, 203 | IconResolution::Retina512 => -48, 204 | } 205 | } 206 | 207 | pub fn bottom_bezel_blur_down(&self) -> BlurDown { 208 | match self { 209 | IconResolution::NonRetina16 => BlurDown { 210 | spread_px: 1, 211 | page_y: 0, 212 | }, 213 | _ => BlurDown { 214 | spread_px: 2, 215 | page_y: 1, 216 | }, 217 | } 218 | } 219 | 220 | pub fn bottom_bezel_alpha(&self) -> f32 { 221 | match self { 222 | IconResolution::NonRetina16 => 0.5, 223 | IconResolution::Retina16 => 0.35, 224 | IconResolution::NonRetina32 => 0.35, 225 | IconResolution::Retina32 => 0.6, 226 | IconResolution::NonRetina128 => 0.6, 227 | IconResolution::Retina128 => 0.6, 228 | IconResolution::NonRetina256 => 0.6, 229 | IconResolution::Retina256 => 0.75, 230 | IconResolution::NonRetina512 => 0.75, 231 | IconResolution::Retina512 => 0.75, 232 | } 233 | } 234 | 235 | pub fn icon_file(&self) -> String { 236 | format!("icon_{}.png", self) 237 | } 238 | } 239 | 240 | impl Display for IconResolution { 241 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 242 | write!( 243 | f, 244 | "{}", 245 | match self { 246 | Self::NonRetina16 => "16x16", 247 | Self::Retina16 => "16x16@2x", 248 | Self::NonRetina32 => "32x32", 249 | Self::Retina32 => "32x32@2x", 250 | Self::NonRetina128 => "128x128", 251 | Self::Retina128 => "128x128@2x", 252 | Self::NonRetina256 => "256x256", 253 | Self::Retina256 => "256x256@2x", 254 | Self::NonRetina512 => "512x512", 255 | Self::Retina512 => "512x512@2x", 256 | } 257 | ) 258 | } 259 | } 260 | 261 | pub struct IconConversion { 262 | working_dir: PathBuf, 263 | resolution_prefix: String, 264 | pub progress_bar: Option, 265 | } 266 | 267 | pub struct IconInputs { 268 | pub color_scheme: ColorScheme, 269 | pub resolution: IconResolution, 270 | } 271 | 272 | impl IconConversion { 273 | pub fn step_unincremented(&self, step_description: &str) { 274 | if let Some(progress_bar) = &self.progress_bar { 275 | let wide_msg = format!("{:10} | {}", self.resolution_prefix, step_description); 276 | progress_bar.set_message(wide_msg); 277 | } 278 | } 279 | 280 | pub fn step(&self, step_desciption: &str) { 281 | if let Some(progress_bar) = &self.progress_bar { 282 | self.step_unincremented(step_desciption); 283 | progress_bar.inc(1); 284 | } 285 | } 286 | 287 | fn output_path(&self, file_name: &str) -> PathBuf { 288 | let mut path = self.working_dir.to_path_buf(); 289 | path.push(format!("{}_{}", self.resolution_prefix, file_name)); 290 | path 291 | } 292 | 293 | pub fn full_mask( 294 | &self, 295 | options: &Options, 296 | centering_dimensions: &Dimensions, 297 | ) -> Result { 298 | self.step_unincremented("Preparing icon mask"); 299 | let mut args = CommandArgs::new(); 300 | args.background_transparent(); 301 | args.density(density(&options.mask_path, centering_dimensions)?); 302 | args.push_path(&options.mask_path); 303 | if !options.no_trim { 304 | args.trim() 305 | } 306 | args.resize(centering_dimensions); 307 | args.center(); 308 | args.extent(&Extent::no_offset(centering_dimensions)); 309 | let output_path = self.output_path("0.0_FULL_MASK.png"); 310 | args.push_path(&output_path); 311 | run_magick(&args, None)?; 312 | self.step(""); 313 | Ok(output_path) 314 | } 315 | 316 | pub fn sized_mask( 317 | &self, 318 | input_path: &Path, 319 | inputs: &ScaledMaskInputs, 320 | ) -> Result { 321 | let mut args = CommandArgs::new(); 322 | args.background_transparent(); 323 | args.push_path(input_path); 324 | args.resize(&inputs.mask_dimensions); 325 | args.center(); 326 | args.extent(&Extent { 327 | size: Dimensions::square(inputs.icon_size), 328 | offset: Offset::from_y(inputs.offset_y), 329 | }); 330 | let output_path = self.output_path("1.0_SIZED_MASK.png"); 331 | args.push_path(&output_path); 332 | run_magick(&args, None)?; 333 | Ok(output_path) 334 | } 335 | 336 | fn simple_operation( 337 | &self, 338 | input_path: &Path, 339 | output_filename: &str, 340 | f: impl Fn(&mut CommandArgs), 341 | ) -> Result { 342 | let mut args = CommandArgs::new(); 343 | args.push_path(input_path); 344 | f(&mut args); 345 | let file_name = format!("{}.png", output_filename); 346 | let output_path = self.output_path(&file_name); 347 | args.push_path(&output_path); 348 | run_magick(&args, None)?; 349 | Ok(output_path) 350 | } 351 | 352 | pub fn engrave( 353 | &self, 354 | sized_mask: &Path, 355 | template_icon: &[u8], 356 | output_path: &Path, 357 | inputs: &EngravingInputs, 358 | ) -> Result<(), FolderifyError> { 359 | self.step("Creating colorized fill"); 360 | let fill_colorized = self.simple_operation( 361 | sized_mask, 362 | "2.1_FILL_COLORIZED", 363 | |args: &mut CommandArgs| { 364 | args.fill_colorize(&inputs.fill_color); 365 | }, 366 | )?; 367 | 368 | self.step("Setting fill opacity"); 369 | let fill = 370 | self.simple_operation(&fill_colorized, "2.2_FILL", |args: &mut CommandArgs| { 371 | args.opacity(0.5); 372 | })?; 373 | 374 | self.step("Complementing mask for top bezel"); 375 | let top_bezel_complement = self.simple_operation( 376 | sized_mask, 377 | "3.1_TOP_BEZEL_COMPLEMENT", 378 | |args: &mut CommandArgs| { 379 | args.negate(); 380 | }, 381 | )?; 382 | 383 | self.step("Colorizing top bezel"); 384 | let top_bezel_colorized = self.simple_operation( 385 | &top_bezel_complement, 386 | "3.2_TOP_BEZEL_COLORIZED", 387 | |args: &mut CommandArgs| { 388 | args.fill_colorize(&inputs.top_bezel.color); 389 | }, 390 | )?; 391 | 392 | self.step("Blurring top bezel"); 393 | let top_bezel_blurred = self.simple_operation( 394 | &top_bezel_colorized, 395 | "3.3_TOP_BEZEL_BLURRED", 396 | |args: &mut CommandArgs| { 397 | args.blur_down(&inputs.top_bezel.blur); 398 | }, 399 | )?; 400 | 401 | self.step("Compositing top bezel"); 402 | let top_bezel_masked = self.simple_operation( 403 | &top_bezel_blurred, 404 | "3.4_TOP_BEZEL_MASKED", 405 | |args: &mut CommandArgs| { 406 | args.mask_down(sized_mask, &inputs.top_bezel.mask_operation); 407 | }, 408 | )?; 409 | 410 | self.step("Setting top bezel opacity"); 411 | let top_bezel = self.simple_operation( 412 | &top_bezel_masked, 413 | "3.5_TOP_BEZEL", 414 | |args: &mut CommandArgs| { 415 | args.opacity(inputs.top_bezel.opacity); 416 | }, 417 | )?; 418 | 419 | self.step("Colorizing bottom bezel"); 420 | let bottom_bezel_colorized = self.simple_operation( 421 | sized_mask, 422 | "4.1_BOTTOM_BEZEL_COLORIZED", 423 | |args: &mut CommandArgs| { 424 | args.fill_colorize(&inputs.bottom_bezel.color); 425 | }, 426 | )?; 427 | 428 | self.step("Blurring bottom bezel"); 429 | let bottom_bezel_blurred = self.simple_operation( 430 | &bottom_bezel_colorized, 431 | "4.2_BOTTOM_BEZEL_BLURRED", 432 | |args: &mut CommandArgs| { 433 | args.blur_down(&inputs.bottom_bezel.blur); 434 | }, 435 | )?; 436 | 437 | self.step("Compositing bottom bezel"); 438 | let bottom_bezel_masked = self.simple_operation( 439 | &bottom_bezel_blurred, 440 | "4.3_BOTTOM_BEZEL_MASKED", 441 | |args: &mut CommandArgs| { 442 | args.mask_down(sized_mask, &inputs.bottom_bezel.mask_operation); 443 | }, 444 | )?; 445 | 446 | self.step("Setting bottom bezel opacity"); 447 | let bottom_bezel = self.simple_operation( 448 | &bottom_bezel_masked, 449 | "4.4_BOTTOM_BEZEL", 450 | |args: &mut CommandArgs| { 451 | args.opacity(inputs.bottom_bezel.opacity); 452 | }, 453 | )?; 454 | 455 | self.step("Engraving bezels"); 456 | let mut args = CommandArgs::new(); 457 | args.push("-"); 458 | args.push_path(&bottom_bezel); 459 | args.composite(&CompositingOperation::dissolve); 460 | args.push_path(&fill); 461 | args.composite(&CompositingOperation::dissolve); 462 | args.push_path(&top_bezel); 463 | args.composite(&CompositingOperation::dissolve); 464 | args.push_path(output_path); 465 | run_magick(&args, Some(template_icon))?; 466 | Ok(()) 467 | } 468 | 469 | pub fn badge_in_place( 470 | &self, 471 | icon_path: &Path, 472 | badge: Badge, 473 | resolution: &IconResolution, 474 | ) -> Result<(), FolderifyError> { 475 | self.step("Adding badge"); 476 | 477 | let badge_icon = get_badge_icon(badge, resolution); 478 | 479 | let mut args = CommandArgs::new(); 480 | args.push_path(icon_path); 481 | args.push("-"); 482 | args.composite(&CompositingOperation::dissolve); 483 | args.push_path(icon_path); 484 | run_magick(&args, Some(badge_icon)) 485 | } 486 | 487 | // TODO 488 | pub fn icon( 489 | &self, 490 | options: &Options, 491 | full_mask_path: &Path, 492 | output_path: &Path, 493 | inputs: &IconInputs, 494 | ) -> Result<(), FolderifyError> { 495 | // if options.verbose { 496 | // println!("[Starting] {}", inputs.resolution); 497 | // } 498 | 499 | let size = inputs.resolution.size(); 500 | let offset_y = inputs.resolution.offset_y(); 501 | 502 | self.step_unincremented("Sizing mask"); 503 | let sized_mask_path = self 504 | .sized_mask( 505 | full_mask_path, 506 | &ScaledMaskInputs { 507 | icon_size: size, 508 | mask_dimensions: Dimensions { 509 | width: size * 3 / 4, 510 | height: size / 2, 511 | }, 512 | offset_y, 513 | }, 514 | ) 515 | .unwrap(); 516 | 517 | // TODO 518 | let template_icon = get_folder_icon(inputs.color_scheme, &inputs.resolution); 519 | 520 | let fill_color = match inputs.color_scheme { 521 | ColorScheme::Light => RGBColor::new(8, 134, 206), 522 | ColorScheme::Dark => RGBColor::new(6, 111, 194), 523 | }; 524 | 525 | let engraved = self.engrave( 526 | &sized_mask_path, 527 | template_icon, 528 | output_path, 529 | &EngravingInputs { 530 | fill_color, 531 | top_bezel: BezelInputs { 532 | color: RGBColor::new(58, 152, 208), 533 | blur: BlurDown { 534 | spread_px: 0, 535 | page_y: 2, 536 | }, 537 | mask_operation: CompositingOperation::Dst_In, 538 | opacity: 0.5, 539 | }, 540 | bottom_bezel: BezelInputs { 541 | color: RGBColor::new(174, 225, 253), 542 | blur: inputs.resolution.bottom_bezel_blur_down(), 543 | mask_operation: CompositingOperation::Dst_Out, 544 | opacity: inputs.resolution.bottom_bezel_alpha(), 545 | }, 546 | }, 547 | ); 548 | if let Some(badge) = options.badge { 549 | self.badge_in_place(output_path, badge, &inputs.resolution)?; 550 | }; 551 | 552 | self.step(""); 553 | 554 | if options.verbose { 555 | println!("[{}] {}", options.mask_path.display(), inputs.resolution); 556 | } 557 | engraved 558 | } 559 | 560 | pub fn to_icns( 561 | &self, 562 | options: &Options, 563 | iconset_dir: &Path, 564 | icns_path: &Path, 565 | ) -> Result<(), FolderifyError> { 566 | self.step("Creating .icns file"); 567 | if options.verbose { 568 | println!( 569 | "[{}] Creating the .icns file...", 570 | options.mask_path.display() 571 | ); 572 | } 573 | let mut args = CommandArgs::new(); 574 | args.push_path(iconset_dir); 575 | args.push("--convert"); 576 | args.push("icns"); 577 | args.push("--output"); 578 | args.push_path(icns_path); 579 | run_command(ICONUTIL_COMMAND, &args, None)?; 580 | Ok(()) 581 | } 582 | 583 | pub fn assign_icns( 584 | &self, 585 | options: &Options, 586 | icns_path: &Path, 587 | target_path: &Path, 588 | ) -> Result<(), FolderifyError> { 589 | if options.verbose { 590 | println!( 591 | "[{}] Assigning icon to target: {}", 592 | options.mask_path.display(), 593 | target_path.display(), 594 | ); 595 | } 596 | 597 | let assignment_fn = match options.set_icon_using { 598 | SetIconUsing::Fileicon => Self::assign_icns_using_fileicon, 599 | SetIconUsing::Osascript => Self::assign_icns_using_osascript, 600 | SetIconUsing::Rez => Self::assign_icns_using_rez, 601 | }; 602 | assignment_fn(self, options, icns_path, target_path)?; 603 | 604 | self.step(""); 605 | 606 | Ok(()) 607 | } 608 | 609 | pub fn assign_icns_using_osascript( 610 | &self, 611 | _options: &Options, 612 | icns_path: &Path, 613 | target_path: &Path, 614 | ) -> Result<(), FolderifyError> { 615 | self.step("Using `osascript` to assign the `.icns` file."); 616 | 617 | let target_metadata = match metadata(target_path) { 618 | Ok(target_metadata) => target_metadata, 619 | Err(_) => { 620 | eprintln!("Error: target path does not exist!"); 621 | exit(1); 622 | } 623 | }; 624 | 625 | // Adapted from: 626 | // - https://github.com/mklement0/fileicon/blob/9c41a44fac462f66a1194e223aa26e4c3b9b5ae3/bin/fileicon#L268-L276 627 | // - https://github.com/mklement0/fileicon/issues/32#issuecomment-1074124748 628 | // - https://apple.stackexchange.com/a/161984 629 | // 630 | // In theory, we could try to call the Cocoa framework directly through 631 | // bridging or linking. However, AppleScript is more likely to be 632 | // portable across macOS versions. 633 | let stdin = format!("use framework \"Cocoa\" 634 | 635 | set sourcePath to \"{}\" 636 | set destPath to \"{}\" 637 | 638 | set imageData to (current application's NSImage's alloc()'s initWithContentsOfFile:sourcePath) 639 | (current application's NSWorkspace's sharedWorkspace()'s setIcon:imageData forFile:destPath options:2)", 640 | escape_path_for_applescript(&icns_path.to_string_lossy()), escape_path_for_applescript(&target_path.to_string_lossy()) 641 | ); 642 | 643 | let args = CommandArgs::new(); 644 | run_command(OSASCRIPT_COMMAND, &args, Some(stdin.as_bytes()))?; 645 | 646 | if target_metadata.is_dir() { 647 | // TODO: check for network volume first, only then the appropriate path. 648 | if metadata(target_path.join("Icon\r")).is_err() 649 | && metadata(target_path.join(".VolumeIcon.icns")).is_err() 650 | { 651 | eprintln!("Icon was not successfully assigned to the target folder."); 652 | exit(1); 653 | } 654 | } else { 655 | // TODO: this is usually overwritten by the progress bars. 656 | eprintln!( 657 | "Target is not a folder. Please check manually if the icon was assigned correctly." 658 | ); 659 | }; 660 | 661 | Ok(()) 662 | } 663 | 664 | pub fn assign_icns_using_fileicon( 665 | &self, 666 | _options: &Options, 667 | icns_path: &Path, 668 | target_path: &Path, 669 | ) -> Result<(), FolderifyError> { 670 | self.step("Using `fileicon` to assign the `.icns` file."); 671 | let mut args = CommandArgs::new(); 672 | args.push("set"); 673 | args.push_path(target_path); 674 | args.push_path(icns_path); 675 | run_command(FILEICON_COMMAND, &args, None)?; 676 | 677 | Ok(()) 678 | } 679 | 680 | pub fn assign_icns_using_rez( 681 | &self, 682 | _options: &Options, 683 | icns_path: &Path, 684 | target_path: &Path, 685 | ) -> Result<(), FolderifyError> { 686 | let target_is_dir = metadata(target_path) 687 | .expect("Target does not exist!") 688 | .is_dir(); // TODO: Return `FolderifyError` 689 | 690 | let target_resource_path = if target_is_dir { 691 | target_path.join("Icon\r") 692 | } else { 693 | target_path.to_owned() 694 | }; 695 | 696 | // sips: add an icns resource fork to the icns file 697 | self.step("Adding resource fork to .icns file using sips"); 698 | let mut args = CommandArgs::new(); 699 | args.push("-i"); 700 | args.push_path(icns_path); 701 | run_command(SIPS_COMMAND, &args, None)?; 702 | 703 | // DeRez: export the icns resource from the icns file 704 | self.step("Exporting .icns resource using DeRez"); 705 | let mut args = CommandArgs::new(); 706 | args.push("-only"); 707 | args.push("icns"); 708 | args.push_path(icns_path); 709 | let derezzed = run_command(DEREZ_COMMAND, &args, None)?; 710 | let derezzed_path = self.output_path("derezzed.data"); 711 | if fs::write(&derezzed_path, derezzed).is_err() { 712 | return Err(FolderifyError::General(GeneralError { 713 | message: "Could not write derezzed data".into(), 714 | })); 715 | } 716 | 717 | // Rez: add exported icns resource to the resource fork of target/Icon^M 718 | self.step("Add .icns resource target using Rez"); 719 | let mut args = CommandArgs::new(); 720 | args.push("-append"); 721 | args.push_path(&derezzed_path); 722 | args.push("-o"); 723 | args.push_path(&target_resource_path); 724 | run_command(REZ_COMMAND, &args, None)?; 725 | 726 | // SetFile: set custom icon attribute 727 | self.step("Setting custom icon attribute"); 728 | let mut args = CommandArgs::new(); 729 | args.push("-a"); 730 | args.push("-C"); 731 | args.push_path(target_path); 732 | run_command(SETFILE_COMMAND, &args, None)?; 733 | 734 | if target_is_dir { 735 | self.step("Setting invisible file attribute"); 736 | // SetFile: set invisible file attribute 737 | let mut args = CommandArgs::new(); 738 | args.push("-a"); 739 | args.push("-V"); 740 | args.push_path(&target_resource_path); 741 | run_command(SETFILE_COMMAND, &args, None)?; 742 | } else { 743 | self.step("Skipping invisible file attribute for file target"); 744 | }; 745 | 746 | Ok(()) 747 | } 748 | } 749 | 750 | pub fn escape_path_for_applescript(path: &str) -> String { 751 | // Newlines don't need to be escaped. 752 | path.replace('\\', "\\\\").replace('\"', "\\\"") 753 | } 754 | -------------------------------------------------------------------------------- /src/magick.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::max; 2 | use std::path::Path; 3 | 4 | use crate::command::identify_read_u32; 5 | use crate::error::FolderifyError; 6 | use crate::primitives::{Dimensions, Extent, Offset, RGBColor}; 7 | 8 | const DEFAULT_DENSITY: u32 = 72; 9 | 10 | // TODO: Place a version of this in `command.rs` 11 | pub(crate) struct CommandArgs { 12 | pub args: Vec, 13 | } 14 | 15 | impl CommandArgs { 16 | pub fn new() -> Self { 17 | CommandArgs { args: vec![] } 18 | } 19 | 20 | pub fn push_string(&mut self, s: String) { 21 | self.args.push(s); 22 | } 23 | 24 | pub fn push(&mut self, s: &str) { 25 | self.push_string(s.into()); 26 | } 27 | 28 | pub fn push_path(&mut self, path: &Path) { 29 | self.push(path.to_str().expect("Could not set path for command")); 30 | } 31 | 32 | pub fn background_transparent(&mut self) { 33 | self.push("-background"); 34 | self.push("transparent"); 35 | } 36 | 37 | pub fn background_none(&mut self) { 38 | self.push("-background"); 39 | self.push("transparent"); 40 | } 41 | 42 | pub fn resize(&mut self, dimensions: &Dimensions) { 43 | self.push("-resize"); 44 | self.push(&dimensions.to_string()); 45 | } 46 | 47 | pub fn extent(&mut self, extent: &Extent) { 48 | self.push("-extent"); 49 | self.push(&extent.to_string()); 50 | } 51 | 52 | pub fn format_width(&mut self) { 53 | self.push("-format"); 54 | self.push("%w"); 55 | } 56 | 57 | pub fn format_height(&mut self) { 58 | self.push("-format"); 59 | self.push("%h"); 60 | } 61 | 62 | pub fn density(&mut self, d: u32) { 63 | self.push("-density"); 64 | self.push(&d.to_string()); 65 | } 66 | 67 | pub fn trim(&mut self) { 68 | self.push("-trim"); 69 | } 70 | 71 | pub fn center(&mut self) { 72 | self.push("-gravity"); 73 | self.push("Center"); 74 | } 75 | 76 | pub fn fill_colorize(&mut self, fill_color: &RGBColor) { 77 | self.push("-fill"); 78 | self.push(&fill_color.to_string()); 79 | self.push("-colorize"); 80 | self.push("100, 100, 100"); 81 | } 82 | 83 | pub fn opacity(&mut self, alpha: f32) { 84 | self.push("-channel"); 85 | self.push("Alpha"); 86 | self.push("-evaluate"); 87 | self.push("multiply"); 88 | self.push_string(alpha.to_string()); 89 | } 90 | 91 | pub fn negate(&mut self) { 92 | self.push("-negate"); 93 | } 94 | 95 | pub fn flatten(&mut self) { 96 | self.push("-flatten"); 97 | } 98 | 99 | pub fn page(&mut self, offset: &Offset) { 100 | self.push("-page"); 101 | self.push(&offset.to_string()); 102 | } 103 | 104 | pub fn motion_blur_down(&mut self, spread_px: u32) { 105 | self.push("-motion-blur"); 106 | self.push_string(format!("0x{}-90", spread_px)); 107 | } 108 | 109 | pub fn blur_down(&mut self, blur_down: &BlurDown) { 110 | self.motion_blur_down(blur_down.spread_px); 111 | self.page(&Offset { 112 | x: 0, 113 | y: blur_down.page_y, 114 | }); 115 | self.background_none(); 116 | self.flatten(); 117 | } 118 | 119 | // TODO: take `CompositingOperation` instead of `&CompositingOperation`? 120 | pub fn composite(&mut self, compositing_operation: &CompositingOperation) { 121 | self.push("-compose"); 122 | self.push(match compositing_operation { 123 | CompositingOperation::Dst_In => "Dst_In", 124 | CompositingOperation::Dst_Out => "Dst_Out", 125 | CompositingOperation::dissolve => "dissolve", 126 | }); 127 | self.push("-composite"); 128 | } 129 | 130 | pub fn mask_down(&mut self, mask_path: &Path, compositing_operation: &CompositingOperation) { 131 | self.push_path(mask_path); 132 | self.push("-alpha"); 133 | self.push("Set"); 134 | self.composite(compositing_operation); 135 | } 136 | } 137 | 138 | pub struct BlurDown { 139 | pub spread_px: u32, 140 | pub page_y: i32, 141 | } 142 | 143 | #[allow(non_camel_case_types)] // Match ImageMagick args 144 | pub enum CompositingOperation { 145 | Dst_In, 146 | Dst_Out, 147 | dissolve, 148 | } 149 | 150 | pub(crate) fn density( 151 | mask_path: &Path, 152 | centering_dimensions: &Dimensions, 153 | ) -> Result { 154 | let mut width_args = CommandArgs::new(); 155 | width_args.format_width(); 156 | width_args.push_path(mask_path); 157 | let input_width = identify_read_u32(&width_args)?; 158 | 159 | let mut height_args = CommandArgs::new(); 160 | height_args.format_height(); 161 | height_args.push_path(mask_path); 162 | let input_height = identify_read_u32(&height_args)?; 163 | 164 | Ok(max( 165 | DEFAULT_DENSITY * centering_dimensions.width / input_width, 166 | DEFAULT_DENSITY * centering_dimensions.height / input_height, 167 | )) 168 | } 169 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::thread::{self, JoinHandle}; 2 | 3 | use command::{run_command, OPEN_COMMAND}; 4 | use icon_conversion::{IconInputs, IconResolution, WorkingDir}; 5 | use indicatif::MultiProgress; 6 | use magick::CommandArgs; 7 | 8 | use crate::{output_paths::PotentialOutputPaths, primitives::Dimensions}; 9 | 10 | mod command; 11 | mod error; 12 | mod icon_conversion; 13 | mod magick; 14 | mod options; 15 | mod output_paths; 16 | mod primitives; 17 | mod resources; 18 | 19 | fn main() { 20 | let options = options::get_options(); 21 | 22 | let potential_output_paths = PotentialOutputPaths::new(&options); 23 | 24 | println!( 25 | "[{}] Using folder style: BigSur", 26 | options.mask_path.display() 27 | ); 28 | println!( 29 | "[{}] Using color scheme: {}", 30 | options.mask_path.display(), 31 | options.color_scheme 32 | ); 33 | 34 | let working_dir = WorkingDir::new(); 35 | if options.debug { 36 | working_dir.open_in_finder().unwrap(); 37 | } 38 | 39 | let multi_progress_bar = match options.show_progress { 40 | true => Some(MultiProgress::new()), 41 | false => None, 42 | }; 43 | 44 | let input_icon_conversion = working_dir.icon_conversion( 45 | icon_conversion::ProgressBarType::Input, 46 | "(Input)", 47 | multi_progress_bar.clone(), 48 | &options, 49 | ); 50 | let full_mask_path = input_icon_conversion 51 | .full_mask( 52 | &options, 53 | &Dimensions { 54 | width: 768, 55 | height: 384, 56 | }, 57 | ) 58 | .unwrap(); 59 | 60 | let final_output_paths = potential_output_paths.finalize(&options, &working_dir); 61 | 62 | let mut handles = Vec::>::new(); 63 | for resolution in IconResolution::values() { 64 | let icon_conversion = working_dir.icon_conversion( 65 | icon_conversion::ProgressBarType::Conversion, 66 | &resolution.to_string(), 67 | multi_progress_bar.clone(), 68 | &options, 69 | ); 70 | let options = options.clone(); 71 | let full_mask_path = full_mask_path.clone(); 72 | let output_path = final_output_paths.iconset_dir.join(resolution.icon_file()); 73 | let handle = thread::spawn(move || { 74 | icon_conversion 75 | .icon( 76 | &options, 77 | &full_mask_path, 78 | &output_path, 79 | &IconInputs { 80 | color_scheme: options.color_scheme, 81 | resolution, 82 | }, 83 | ) 84 | .unwrap(); 85 | }); 86 | handles.push(handle); 87 | } 88 | 89 | let output_iconset_only = match ( 90 | &options.target, 91 | &options.output_icns, 92 | &options.output_iconset, 93 | ) { 94 | (None, None, Some(output_iconset)) => Some(output_iconset), 95 | _ => None, 96 | }; 97 | 98 | // Deduplicate this `match` with the one that happens after handle joining. 99 | let output_progress_bar_type = match output_iconset_only { 100 | Some(_) => icon_conversion::ProgressBarType::OutputIcns, 101 | None => icon_conversion::ProgressBarType::OutputWithAssignment, 102 | }; 103 | let output_icon_conversion = working_dir.icon_conversion( 104 | output_progress_bar_type, 105 | "(Output)", 106 | multi_progress_bar, 107 | &options, 108 | ); 109 | output_icon_conversion.step_unincremented("Waiting…"); 110 | 111 | for handle in handles { 112 | handle.join().unwrap(); 113 | } 114 | 115 | let reveal_path = match output_iconset_only { 116 | Some(output_iconset) => { 117 | // TODO: avoid `.icns assignment entirely? 118 | // TODO: Change the number of output steps? 119 | output_iconset 120 | } 121 | None => { 122 | output_icon_conversion 123 | .to_icns( 124 | &options, 125 | &final_output_paths.iconset_dir, 126 | &final_output_paths.icns_path, 127 | ) 128 | .unwrap(); 129 | 130 | let icns_assignment_path = options 131 | .target 132 | .as_ref() 133 | .unwrap_or(&final_output_paths.icns_path); 134 | 135 | output_icon_conversion 136 | .assign_icns( 137 | &options, 138 | &final_output_paths.icns_path, 139 | icns_assignment_path, 140 | ) 141 | .unwrap(); 142 | 143 | icns_assignment_path 144 | } 145 | }; 146 | 147 | if options.reveal { 148 | match options.show_progress { 149 | true => output_icon_conversion.step_unincremented("Revealing in Finder…"), 150 | false => println!("Revealing in Finder…"), 151 | } 152 | let mut args = CommandArgs::new(); 153 | args.push("-R"); 154 | args.push_path(reveal_path); 155 | run_command(OPEN_COMMAND, &args, None).unwrap(); 156 | } 157 | 158 | if options.debug { 159 | working_dir.release(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | use clap::{CommandFactory, Parser, ValueEnum}; 2 | use clap_complete::generator::generate; 3 | use clap_complete::{Generator, Shell}; 4 | use std::io::stdout; 5 | use std::process::exit; 6 | use std::{env::var, fmt::Display, path::PathBuf, process::Command}; 7 | 8 | /// Generate a native-style macOS folder icon from a mask file. 9 | #[derive(Parser, Debug)] 10 | #[command(author, version, about, long_about = None)] 11 | #[clap(name = "folderify")] 12 | struct FolderifyArgs { 13 | #[allow(clippy::doc_lazy_continuation)] // We want concise text. 14 | /// Mask image file. For best results: 15 | /// - Use a .png mask. 16 | /// - Use a solid black design over a transparent background. 17 | /// - Make sure the corner pixels of the mask image are transparent. They are used for empty margins. 18 | /// - Make sure the non-transparent pixels span a height of 384px, using a 16px grid. 19 | /// If the height is 384px and the width is a multiple of 128px, each 64x64 tile will exactly align with 1 pixel at the smallest folder size. 20 | #[clap(verbatim_doc_comment)] 21 | mask: Option, 22 | 23 | /// Target file or folder. If a target is specified, the resulting icon will 24 | /// be applied to the target file/folder. Else (unless --output-icns or 25 | /// --output-iconset is specified), a .iconset folder and .icns file will be 26 | /// created in the same folder as the mask (you can use "Get Info" in Finder 27 | /// to copy the icon from the .icns file). 28 | #[clap(verbatim_doc_comment)] 29 | target: Option, 30 | 31 | /// Write the `.icns` file to the given path. 32 | /// (Will be written even if a target is also specified.) 33 | #[clap(verbatim_doc_comment, long, id = "ICNS_FILE")] 34 | output_icns: Option, 35 | 36 | /// Write the `.iconset` folder to the given path. 37 | /// (Will be written even if a target is also specified.) 38 | #[clap(verbatim_doc_comment, long, id = "ICONSET_FOLDER")] 39 | output_iconset: Option, 40 | 41 | /// Reveal either the target, `.icns`, or `.iconset` (in that order of preference) in Finder. 42 | #[clap(short, long)] 43 | reveal: bool, 44 | 45 | /// Version of the macOS folder icon, e.g. "14.2.1". 46 | /// Defaults to the version currently running. 47 | #[clap(long = "macOS", alias = "osx", short_alias = 'x', id = "MACOS_VERSION")] 48 | mac_os: Option, // TODO: enum, default? 49 | 50 | /// Color scheme — auto matches the current system value. 51 | #[clap(long, value_enum, default_value_t = ColorSchemeOrAuto::Auto)] 52 | color_scheme: ColorSchemeOrAuto, 53 | 54 | /// Don't trim margins from the mask. 55 | /// By default (i.e. without this flag), transparent margins are trimmed from all 4 sides. 56 | #[clap(long, verbatim_doc_comment)] 57 | no_trim: bool, 58 | 59 | /// Don't show progress bars. 60 | #[arg(long)] 61 | no_progress: bool, 62 | 63 | /// Program used to set the icon. `osascript` should work in most circumstances, `fileicon` performs more checks, and `Rez` produces smaller but less accurate icons. 64 | #[arg(long, hide(true))] 65 | set_icon_using: Option, 66 | 67 | /// Add a badge to the icon. Currently only supports one badge at a time. 68 | #[arg(long)] 69 | badge: Option, 70 | 71 | /// Detailed output. Also sets `--no-progress`. 72 | #[clap(short, long)] 73 | verbose: bool, 74 | 75 | /// Print completions for the given shell (instead of generating any icons). 76 | /// These can be loaded/stored permanently (e.g. when using Homebrew), but they can also be sourced directly, e.g.: 77 | /// 78 | /// folderify --completions fish | source # fish 79 | /// source <(folderify --completions zsh) # zsh 80 | #[clap(long, verbatim_doc_comment, id = "SHELL")] 81 | completions: Option, 82 | } 83 | 84 | #[derive(ValueEnum, Clone, Debug, PartialEq, Copy)] 85 | pub enum ColorScheme { 86 | Light, 87 | Dark, 88 | } 89 | 90 | #[derive(ValueEnum, Clone, Debug, PartialEq, Copy)] 91 | pub enum Badge { 92 | Alias, 93 | Locked, 94 | } 95 | 96 | impl Display for ColorScheme { 97 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 98 | write!( 99 | f, 100 | "{}", 101 | match self { 102 | Self::Light => "light", 103 | Self::Dark => "dark", 104 | } 105 | ) 106 | } 107 | } 108 | 109 | #[derive(ValueEnum, Clone, Debug, PartialEq)] 110 | enum ColorSchemeOrAuto { 111 | Auto, 112 | Light, 113 | Dark, 114 | } 115 | 116 | #[derive(ValueEnum, Clone, Debug, PartialEq)] 117 | pub enum SetIconUsing { 118 | Fileicon, 119 | Osascript, 120 | #[clap(name = "Rez")] 121 | Rez, 122 | } 123 | 124 | #[derive(ValueEnum, Clone, Debug, PartialEq)] 125 | enum SetIconUsingOrAuto { 126 | Auto, 127 | Fileicon, 128 | Osascript, 129 | #[clap(name = "Rez")] 130 | Rez, 131 | } 132 | 133 | #[derive(Debug, Clone)] 134 | pub struct Options { 135 | pub mask_path: PathBuf, 136 | pub color_scheme: ColorScheme, 137 | pub no_trim: bool, 138 | pub target: Option, 139 | pub output_icns: Option, 140 | pub output_iconset: Option, 141 | pub set_icon_using: SetIconUsing, 142 | pub show_progress: bool, 143 | pub badge: Option, 144 | pub reveal: bool, 145 | pub verbose: bool, 146 | pub debug: bool, 147 | } 148 | 149 | fn completions_for_shell(cmd: &mut clap::Command, generator: impl Generator) { 150 | generate(generator, cmd, "folderify", &mut stdout()); 151 | } 152 | 153 | fn known_mac_os_version(mac_os: &str) -> bool { 154 | for major_version_string in ["15", "14", "13", "12", "11"] { 155 | if mac_os.starts_with(&format!("{}.", major_version_string)) 156 | || mac_os == major_version_string 157 | { 158 | return true; 159 | } 160 | } 161 | false 162 | } 163 | 164 | pub fn get_options() -> Options { 165 | let mut command = FolderifyArgs::command(); 166 | 167 | let args = FolderifyArgs::parse(); 168 | if let Some(shell) = args.completions { 169 | completions_for_shell(&mut command, shell); 170 | exit(0); 171 | } 172 | 173 | let mask = match args.mask { 174 | Some(mask) => mask, 175 | None => { 176 | command.print_help().unwrap(); 177 | exit(0); 178 | } 179 | }; 180 | 181 | if let Some(mac_os) = &args.mac_os { 182 | let mac_os: &str = mac_os; 183 | // macOS 11.0 reports itself as macOS 10.16 in some APIs. Someone might pass such a value on to `folderify`, so we can't just check for major version 10. 184 | // Instead, we denylist the versions that previously had different folder icons, so that we don't accidentally apply the Big Sur style when one of these versions was specified. 185 | if matches!( 186 | mac_os, 187 | "10.5" 188 | | "10.6" 189 | | "10.7" 190 | | "10.8" 191 | | "10.9" 192 | | "10.10" 193 | | "10.11" 194 | | "10.12" 195 | | "10.13" 196 | | "10.14" 197 | | "10.15" 198 | ) { 199 | eprintln!("Error: OS X / macOS 10 was specified. This is no longer supported by folderify v3.\nTo generate these icons, please use folderify v2: https://github.com/lgarron/folderify/tree/main#os-x-macos-10"); 200 | exit(1) 201 | } 202 | if !known_mac_os_version(mac_os) { 203 | eprintln!("Warning: Unknown macOS version specified. Assuming macOS 11 or later"); 204 | } 205 | } 206 | let debug = var("FOLDERIFY_DEBUG") == Ok("1".into()); 207 | let verbose = args.verbose || debug; 208 | let show_progress = !args.no_progress && !args.verbose; 209 | let set_icon_using = match args.set_icon_using { 210 | Some(SetIconUsingOrAuto::Rez) => SetIconUsing::Rez, 211 | Some(SetIconUsingOrAuto::Fileicon) => SetIconUsing::Fileicon, 212 | _ => SetIconUsing::Osascript, 213 | }; 214 | Options { 215 | mask_path: mask, 216 | color_scheme: map_color_scheme_auto(args.color_scheme), 217 | no_trim: args.no_trim, 218 | target: args.target, 219 | output_icns: args.output_icns, 220 | output_iconset: args.output_iconset, 221 | badge: args.badge, 222 | set_icon_using, 223 | show_progress, 224 | reveal: args.reveal, 225 | verbose, 226 | debug, 227 | } 228 | } 229 | 230 | fn map_color_scheme_auto(color_scheme: ColorSchemeOrAuto) -> ColorScheme { 231 | match color_scheme { 232 | ColorSchemeOrAuto::Dark => return ColorScheme::Dark, 233 | ColorSchemeOrAuto::Light => return ColorScheme::Light, 234 | ColorSchemeOrAuto::Auto => (), 235 | }; 236 | 237 | match Command::new("/usr/bin/env") 238 | .args(["defaults", "read", "-g", "AppleInterfaceStyle"]) 239 | .output() 240 | { 241 | Ok(val) => { 242 | if val.stdout == String::from("Dark\n").into_bytes() { 243 | ColorScheme::Dark 244 | } else { 245 | ColorScheme::Light 246 | } 247 | } 248 | Err(_) => { 249 | println!("Could not compute auto color scheme. Assuming light mode."); 250 | ColorScheme::Light 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/output_paths.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::create_dir_all, path::PathBuf}; 2 | 3 | use crate::{icon_conversion::WorkingDir, options::Options}; 4 | 5 | pub(crate) struct FinalOutputPaths { 6 | pub iconset_dir: PathBuf, 7 | pub icns_path: PathBuf, 8 | } 9 | 10 | // TODO: separate printing from calculation 11 | // Or just output everything to a temp path, and copy the desired results. 12 | pub(crate) struct PotentialOutputPaths { 13 | pub iconset_dir: Option, 14 | pub icns_path: Option, 15 | } 16 | 17 | impl PotentialOutputPaths { 18 | pub fn new(options: &Options) -> PotentialOutputPaths { 19 | let mut output_paths = PotentialOutputPaths { 20 | iconset_dir: None, 21 | icns_path: None, 22 | }; 23 | match ( 24 | &options.target, 25 | &options.output_iconset, 26 | &options.output_icns, 27 | ) { 28 | (Some(target), output_iconset, output_icns) => { 29 | println!( 30 | "[{}] => assign to [{}]", 31 | options.mask_path.display(), 32 | target.display() 33 | ); 34 | Self::alt_outputs(options, &mut output_paths, output_iconset, output_icns); 35 | } 36 | (None, None, None) => { 37 | let iconset_dir_value = options.mask_path.with_extension("iconset"); 38 | let icns_path_value = options.mask_path.with_extension("icns"); 39 | println!( 40 | "[{}] => [{}]", 41 | options.mask_path.display(), 42 | iconset_dir_value.display() 43 | ); 44 | println!( 45 | "[{}] => [{}]", 46 | options.mask_path.display(), 47 | icns_path_value.display() 48 | ); 49 | output_paths.iconset_dir = Some(iconset_dir_value); 50 | output_paths.icns_path = Some(icns_path_value); 51 | } 52 | (None, output_iconset, output_icns) => { 53 | Self::alt_outputs(options, &mut output_paths, output_iconset, output_icns); 54 | } 55 | } 56 | output_paths 57 | } 58 | 59 | fn alt_outputs( 60 | options: &Options, 61 | output_targets: &mut PotentialOutputPaths, 62 | output_iconset: &Option, 63 | output_icns: &Option, 64 | ) { 65 | if let Some(output_iconset) = output_iconset { 66 | println!( 67 | "[{}] => [{}]", 68 | options.mask_path.display(), 69 | output_iconset.display() 70 | ); 71 | output_targets.iconset_dir = Some(output_iconset.to_owned()); 72 | } 73 | if let Some(output_icns) = output_icns { 74 | println!( 75 | "[{}] => [{}]", 76 | options.mask_path.display(), 77 | output_icns.display() 78 | ); 79 | output_targets.icns_path = Some(output_icns.to_owned()); 80 | } 81 | } 82 | 83 | // This creates the iconset dir if needed (but not the icns path). 84 | pub fn finalize(&self, options: &Options, working_dir: &WorkingDir) -> FinalOutputPaths { 85 | let iconset_dir = match &self.iconset_dir { 86 | Some(iconset_dir) => { 87 | create_dir_all(iconset_dir).unwrap(); // TODO 88 | iconset_dir.to_owned() 89 | } 90 | None => working_dir.create_iconset_dir(options).unwrap(), 91 | }; 92 | 93 | let icns_path = match &self.icns_path { 94 | Some(icns_path) => icns_path.to_owned(), 95 | None => working_dir.icon_file_with_extension("icns"), 96 | }; 97 | 98 | FinalOutputPaths { 99 | iconset_dir, 100 | icns_path, 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/primitives.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Display; 3 | 4 | #[derive(Clone)] 5 | pub struct Dimensions { 6 | pub width: u32, 7 | pub height: u32, 8 | } 9 | 10 | impl Dimensions { 11 | pub fn square(side_size: u32) -> Self { 12 | Dimensions { 13 | width: side_size, 14 | height: side_size, 15 | } 16 | } 17 | } 18 | 19 | impl Display for Dimensions { 20 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | write!(f, "{}x{}", self.width, self.height) 22 | } 23 | } 24 | 25 | pub struct RGBColor { 26 | r: u8, 27 | g: u8, 28 | b: u8, 29 | } 30 | 31 | impl RGBColor { 32 | pub fn new(r: u8, g: u8, b: u8) -> Self { 33 | Self { r, g, b } 34 | } 35 | } 36 | 37 | impl Display for RGBColor { 38 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 39 | write!(f, "rgb({}, {}, {})", self.r, self.g, self.b) 40 | } 41 | } 42 | 43 | pub struct Offset { 44 | pub x: i32, 45 | pub y: i32, 46 | } 47 | 48 | impl Offset { 49 | pub fn from_y(y: i32) -> Self { 50 | Offset { x: 0, y } 51 | } 52 | 53 | fn default() -> Offset { 54 | Offset { x: 0, y: 0 } 55 | } 56 | } 57 | 58 | fn sign(v: i32) -> char { 59 | if v < 0 { 60 | '-' 61 | } else { 62 | '+' 63 | } 64 | } 65 | 66 | impl Display for Offset { 67 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | write!( 69 | f, 70 | "{}{}{}{}", 71 | sign(self.x), 72 | self.x.abs(), 73 | sign(self.y), 74 | self.y.abs() 75 | ) 76 | } 77 | } 78 | 79 | pub struct Extent { 80 | pub size: Dimensions, 81 | pub offset: Offset, 82 | } 83 | 84 | impl Extent { 85 | pub fn no_offset(size: &Dimensions) -> Self { 86 | Self { 87 | size: size.clone(), 88 | offset: Offset::default(), 89 | } 90 | } 91 | } 92 | 93 | impl Display for Extent { 94 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 95 | write!(f, "{}{}", self.size, self.offset) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/resources.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use include_dir::{include_dir, Dir}; 4 | 5 | use crate::{ 6 | icon_conversion::IconResolution, 7 | options::{Badge, ColorScheme}, 8 | }; 9 | 10 | static RESOURCES_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src/resources"); 11 | 12 | pub fn get_folder_icon(color_scheme: ColorScheme, resolution: &IconResolution) -> &'static [u8] { 13 | let mut path = PathBuf::new(); 14 | path.push("folders"); 15 | path.push(match color_scheme { 16 | ColorScheme::Light => "GenericFolderIcon.BigSur.iconset", 17 | ColorScheme::Dark => "GenericFolderIcon.BigSur.dark.iconset", 18 | }); 19 | path.push(resolution.icon_file()); 20 | RESOURCES_DIR.get_file(&path).unwrap().contents() 21 | } 22 | 23 | pub fn get_badge_icon(badge: Badge, resolution: &IconResolution) -> &'static [u8] { 24 | let mut path = PathBuf::new(); 25 | path.push("badges"); 26 | path.push(match badge { 27 | Badge::Alias => "AliasBadgeIcon.iconset", 28 | Badge::Locked => "LockedBadgeIcon.iconset", 29 | }); 30 | path.push(resolution.icon_file()); 31 | RESOURCES_DIR.get_file(&path).unwrap().contents() 32 | } 33 | -------------------------------------------------------------------------------- /src/resources/badges/AliasBadgeIcon.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/AliasBadgeIcon.iconset/icon_128x128.png -------------------------------------------------------------------------------- /src/resources/badges/AliasBadgeIcon.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/AliasBadgeIcon.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /src/resources/badges/AliasBadgeIcon.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/AliasBadgeIcon.iconset/icon_16x16.png -------------------------------------------------------------------------------- /src/resources/badges/AliasBadgeIcon.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/AliasBadgeIcon.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /src/resources/badges/AliasBadgeIcon.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/AliasBadgeIcon.iconset/icon_256x256.png -------------------------------------------------------------------------------- /src/resources/badges/AliasBadgeIcon.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/AliasBadgeIcon.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /src/resources/badges/AliasBadgeIcon.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/AliasBadgeIcon.iconset/icon_32x32.png -------------------------------------------------------------------------------- /src/resources/badges/AliasBadgeIcon.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/AliasBadgeIcon.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /src/resources/badges/AliasBadgeIcon.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/AliasBadgeIcon.iconset/icon_512x512.png -------------------------------------------------------------------------------- /src/resources/badges/AliasBadgeIcon.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/AliasBadgeIcon.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /src/resources/badges/LockedBadgeIcon.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/LockedBadgeIcon.iconset/icon_128x128.png -------------------------------------------------------------------------------- /src/resources/badges/LockedBadgeIcon.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/LockedBadgeIcon.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /src/resources/badges/LockedBadgeIcon.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/LockedBadgeIcon.iconset/icon_16x16.png -------------------------------------------------------------------------------- /src/resources/badges/LockedBadgeIcon.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/LockedBadgeIcon.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /src/resources/badges/LockedBadgeIcon.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/LockedBadgeIcon.iconset/icon_256x256.png -------------------------------------------------------------------------------- /src/resources/badges/LockedBadgeIcon.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/LockedBadgeIcon.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /src/resources/badges/LockedBadgeIcon.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/LockedBadgeIcon.iconset/icon_32x32.png -------------------------------------------------------------------------------- /src/resources/badges/LockedBadgeIcon.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/LockedBadgeIcon.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /src/resources/badges/LockedBadgeIcon.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/LockedBadgeIcon.iconset/icon_512x512.png -------------------------------------------------------------------------------- /src/resources/badges/LockedBadgeIcon.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/badges/LockedBadgeIcon.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_128x128.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_16x16.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_256x256.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_32x32.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_512x512.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.dark.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_128x128.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_16x16.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_256x256.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_32x32.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_512x512.png -------------------------------------------------------------------------------- /src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/src/resources/folders/GenericFolderIcon.BigSur.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /test/helpers.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function success { 4 | MESSAGE="${1}" 5 | echo "✅ ${MESSAGE}" 6 | } 7 | 8 | function failure { 9 | MESSAGE="${1}" 10 | echo "❌ ${MESSAGE}" 11 | exit 1 12 | } 13 | 14 | function make_temp_folder { 15 | mktemp -d 16 | } 17 | 18 | function check_file { 19 | FILE="${1}" 20 | if test -f "${FILE}" 21 | then 22 | echo "✅ File ${FILE} exists as expected." 23 | else 24 | echo "❌ File ${FILE} should exist, but doesn't." 25 | exit 1 26 | fi 27 | } 28 | 29 | function check_folder { 30 | FILE="${1}" 31 | if test -d "${FILE}" 32 | then 33 | echo "✅ Folder ${FILE} exists as expected." 34 | else 35 | echo "❌ Folder ${FILE} should exist, but doesn't." 36 | exit 1 37 | fi 38 | } 39 | 40 | function check_folder_icon { 41 | FILE="${1}/" 42 | ICON="${FILE}"Icon$'\r' 43 | # Check for "custom icon" attribute on target 44 | if ! xattr -p com.apple.FinderInfo "${FILE}" > /dev/null 45 | then 46 | echo "❌ Folder ${FILE} should have a FinderInfo attribute, but doesn't." 47 | exit 1 48 | fi 49 | # Check for "invisible" attribute on icon 50 | if ! xattr -p com.apple.FinderInfo "${ICON}" > /dev/null 51 | then 52 | echo "❌ Folder ${FILE} icon should have a FinderInfo attribute, but doesn't." 53 | exit 1 54 | fi 55 | # Check for icns data in icon 56 | if ! xattr -p com.apple.ResourceFork "${ICON}" > /dev/null 57 | then 58 | echo "❌ Folder ${FILE} icon should have a resource fork, but doesn't." 59 | exit 1 60 | fi 61 | echo "✅ Folder ${FILE} has an icon as expected." 62 | } 63 | 64 | function check_no_file_nor_folder { 65 | local TEST_PATH="${1}" 66 | if test -f "${TEST_PATH}" 67 | then 68 | echo "❌ Path ${TEST_PATH} should not exist, but is a file." 69 | exit 1 70 | elif test -d "${TEST_PATH}" 71 | then 72 | echo "❌ Path ${TEST_PATH} should not exist, but is a folder." 73 | exit 1 74 | else 75 | echo "✅ Path ${TEST_PATH} does not exist, as expected." 76 | fi 77 | } 78 | -------------------------------------------------------------------------------- /test/test-behaviour.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | DIR="$(dirname "$0")" 6 | source "${DIR}/helpers.sh" 7 | 8 | echo -e "\nTest help flag." 9 | cargo run -- -h 10 | 11 | echo -e "\nGenerate icon file." 12 | cargo run -- ./examples/src/apple.png 13 | 14 | echo -e "\nCheck generated files." 15 | check_file "./examples/src/apple.icns" 16 | check_folder "./examples/src/apple.iconset" 17 | 18 | check_file "./examples/src/apple.iconset/icon_512x512@2x.png" 19 | check_file "./examples/src/apple.iconset/icon_256x256@2x.png" 20 | check_file "./examples/src/apple.iconset/icon_512x512.png" 21 | check_file "./examples/src/apple.iconset/icon_128x128@2x.png" 22 | check_file "./examples/src/apple.iconset/icon_256x256.png" 23 | check_file "./examples/src/apple.iconset/icon_128x128.png" 24 | check_file "./examples/src/apple.iconset/icon_16x16@2x.png" 25 | check_file "./examples/src/apple.iconset/icon_16x16.png" 26 | check_file "./examples/src/apple.iconset/icon_32x32@2x.png" 27 | check_file "./examples/src/apple.iconset/icon_32x32.png" 28 | 29 | TEMP_DIR=$(make_temp_folder) 30 | echo -e "\nAssign folder icon." 31 | cargo run -- ./examples/src/apple.png "${TEMP_DIR}" 32 | echo -e "\nCheck folder icon." 33 | check_folder_icon "${TEMP_DIR}" 34 | rm -r "${TEMP_DIR}" 35 | 36 | TEMP_DIR_REZ=$(make_temp_folder) 37 | echo -e "\nAssign folder icon with --set-icon-using Rez." 38 | cargo run -- --set-icon-using Rez ./examples/src/apple.png "${TEMP_DIR_REZ}" 39 | echo -e "\nCheck folder icon assigned with --set-icon-using Rez." 40 | check_folder_icon "${TEMP_DIR_REZ}" 41 | rm -r "${TEMP_DIR_REZ}" 42 | 43 | echo -e "\nTest that \`--verbose\` is accepted." 44 | cargo run -- --verbose ./examples/src/apple.png 45 | 46 | echo -e "\nTest that \`--no-trim\` is accepted." 47 | cargo run -- --no-trim ./examples/src/apple.png 48 | 49 | echo -e "\nTest that \`--color-scheme auto\` is accepted." 50 | cargo run -- --color-scheme auto ./examples/src/apple.png 51 | 52 | echo -e "\nTest that \`--color-scheme light\` is accepted." 53 | cargo run -- --color-scheme light ./examples/src/apple.png 54 | 55 | echo -e "\nTest that \`--color-scheme dark\` is accepted." 56 | cargo run -- --color-scheme dark ./examples/src/apple.png 57 | 58 | echo -e "\nTest that \`--no-progress\` is accepted." 59 | cargo run -- --no-progress ./examples/src/apple.png 60 | 61 | echo -e "\nTest that \`--badge alias\` is accepted." 62 | cargo run -- --badge alias ./examples/src/apple.png 63 | 64 | echo -e "\nTest that \`--badge locked\` is accepted." 65 | cargo run -- --badge locked ./examples/src/apple.png 66 | 67 | echo -e "\nTest that \`--output-icns\` is accepted." 68 | cargo run -- --output-icns ./examples/src/folder_outline_custom_path_1.icns ./examples/src/folder_outline.png 69 | check_file ./examples/src/folder_outline_custom_path_1.icns 70 | check_no_file_nor_folder ./examples/src/folder_outline.icns 71 | check_no_file_nor_folder ./examples/src/folder_outline.iconset 72 | 73 | echo -e "\nTest that \`--output-iconset\` is accepted." 74 | cargo run -- --output-iconset ./examples/src/folder_outline_custom_path_2.iconset ./examples/src/folder_outline.png 75 | check_folder ./examples/src/folder_outline_custom_path_2.iconset 76 | check_no_file_nor_folder ./examples/src/folder_outline.icns 77 | check_no_file_nor_folder ./examples/src/folder_outline.iconset 78 | 79 | echo -e "\nTest that \`--output-icns\` and \`--output-iconset\` are accepted together." 80 | cargo run -- --output-icns ./examples/src/folder_outline_custom_path_3.icns --output-iconset ./examples/src/folder_outline_custom_path_4.iconset ./examples/src/folder_outline.png 81 | check_file ./examples/src/folder_outline_custom_path_3.icns 82 | check_folder ./examples/src/folder_outline_custom_path_4.iconset 83 | check_no_file_nor_folder ./examples/src/folder_outline.icns 84 | check_no_file_nor_folder ./examples/src/folder_outline.iconset 85 | 86 | for version in "10.5" "10.8" "10.15" 87 | do 88 | echo -e "\nTest that --macOS ${version} is rejected." 89 | # Wrap command to avoid triggering `pipefail`. 90 | if (cargo run -- --macOS ${version} ./examples/src/apple.png) 91 | then 92 | failure "Not rejected." 93 | else 94 | success "Rejected (expected)." 95 | fi 96 | done 97 | 98 | # Accepted with a warning. 99 | for version in "10.16" "99.0" 100 | do 101 | echo -e "\nTest that --macOS ${version} is accepted with a warning." 102 | # Wrap command to avoid triggering `pipefail`. 103 | if (cargo run -- --macOS ${version} ./examples/src/apple.png 2>&1 | grep "Warning: Unknown macOS version specified\.") 104 | then 105 | success "Accepted with warning." 106 | else 107 | failure "Command failed or warning missing." 108 | fi 109 | done 110 | 111 | for version in "11.0" "12.1" "14.2.1" 112 | do 113 | echo -e "\nTest that --macOS ${version} is accepted without warning" 114 | if (cargo run -- --macOS ${version} ./examples/src/apple.png 2>&1 | grep "Warning: Unknown macOS version specified\.") 115 | then 116 | failure "Command failed or unexpected warning." 117 | else 118 | success "Accepted without warning." 119 | fi 120 | done 121 | -------------------------------------------------------------------------------- /tools/Quicksilver/Use as icon mask (folderify)….scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgarron/folderify/195ed28c98c4712da7e1596a52a2c806fe3ba472/tools/Quicksilver/Use as icon mask (folderify)….scpt --------------------------------------------------------------------------------