diff --git a/package.json b/package.json
index fb23c98..95e5364 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,10 @@
         "eslint-plugin-prettier": "^5.0.1",
         "eslint-plugin-simple-import-sort": "^10.0.0",
         "eslint-plugin-unused-imports": "^3.0.0",
+        "fast-glob": "3.3",
         "prettier": "^3.1.0",
+        "sharp": "^0.33.0",
+        "sharp-ico": "^0.1.5",
         "source-map-support": "^0.5.21",
         "tsx": "^4.6.0",
         "type-fest": "^4.8.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6d089f4..d1b72d0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -69,9 +69,18 @@ devDependencies:
   eslint-plugin-unused-imports:
     specifier: ^3.0.0
     version: 3.0.0(@typescript-eslint/eslint-plugin@6.13.1)(eslint@8.54.0)
+  fast-glob:
+    specifier: '3.3'
+    version: 3.3.1
   prettier:
     specifier: ^3.1.0
     version: 3.1.0
+  sharp:
+    specifier: ^0.33.0
+    version: 0.33.0
+  sharp-ico:
+    specifier: ^0.1.5
+    version: 0.1.5
   source-map-support:
     specifier: ^0.5.21
     version: 0.5.21
@@ -99,6 +108,10 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /@canvas/image-data@1.0.0:
+    resolution: {integrity: sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==}
+    dev: true
+
   /@develar/schema-utils@2.6.5:
     resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
     engines: {node: '>= 8.9.0'}
@@ -175,6 +188,14 @@ packages:
       - supports-color
     dev: true
 
+  /@emnapi/runtime@0.44.0:
+    resolution: {integrity: sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==}
+    requiresBuild: true
+    dependencies:
+      tslib: 2.6.2
+    dev: true
+    optional: true
+
   /@esbuild/android-arm64@0.18.20:
     resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
     engines: {node: '>=12'}
@@ -632,6 +653,194 @@ packages:
     resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
     dev: true
 
+  /@img/sharp-darwin-arm64@0.33.0:
+    resolution: {integrity: sha512-070tEheekI1LJWTGPC9WlQEa5UoKTXzzlORBHMX4TbfUxMiL336YHR8vBEUNsjse0RJCX8dZ4ZXwT595aEF1ug==}
+    engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [arm64]
+    os: [darwin]
+    requiresBuild: true
+    optionalDependencies:
+      '@img/sharp-libvips-darwin-arm64': 1.0.0
+    dev: true
+    optional: true
+
+  /@img/sharp-darwin-x64@0.33.0:
+    resolution: {integrity: sha512-pu/nvn152F3qbPeUkr+4e9zVvEhD3jhwzF473veQfMPkOYo9aoWXSfdZH/E6F+nYC3qvFjbxbvdDbUtEbghLqw==}
+    engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [x64]
+    os: [darwin]
+    requiresBuild: true
+    optionalDependencies:
+      '@img/sharp-libvips-darwin-x64': 1.0.0
+    dev: true
+    optional: true
+
+  /@img/sharp-libvips-darwin-arm64@1.0.0:
+    resolution: {integrity: sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==}
+    engines: {macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [arm64]
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@img/sharp-libvips-darwin-x64@1.0.0:
+    resolution: {integrity: sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==}
+    engines: {macos: '>=10.13', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [x64]
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@img/sharp-libvips-linux-arm64@1.0.0:
+    resolution: {integrity: sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==}
+    engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [arm64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@img/sharp-libvips-linux-arm@1.0.0:
+    resolution: {integrity: sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==}
+    engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [arm]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@img/sharp-libvips-linux-s390x@1.0.0:
+    resolution: {integrity: sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==}
+    engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [s390x]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@img/sharp-libvips-linux-x64@1.0.0:
+    resolution: {integrity: sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==}
+    engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [x64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@img/sharp-libvips-linuxmusl-arm64@1.0.0:
+    resolution: {integrity: sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==}
+    engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [arm64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@img/sharp-libvips-linuxmusl-x64@1.0.0:
+    resolution: {integrity: sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==}
+    engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [x64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@img/sharp-linux-arm64@0.33.0:
+    resolution: {integrity: sha512-dcomVSrtgF70SyOr8RCOCQ8XGVThXwe71A1d8MGA+mXEVRJ/J6/TrCbBEJh9ddcEIIsrnrkolaEvYSHqVhswQw==}
+    engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [arm64]
+    os: [linux]
+    requiresBuild: true
+    optionalDependencies:
+      '@img/sharp-libvips-linux-arm64': 1.0.0
+    dev: true
+    optional: true
+
+  /@img/sharp-linux-arm@0.33.0:
+    resolution: {integrity: sha512-4horD3wMFd5a0ddbDY8/dXU9CaOgHjEHALAddXgafoR5oWq5s8X61PDgsSeh4Qupsdo6ycfPPSSNBrfVQnwwrg==}
+    engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [arm]
+    os: [linux]
+    requiresBuild: true
+    optionalDependencies:
+      '@img/sharp-libvips-linux-arm': 1.0.0
+    dev: true
+    optional: true
+
+  /@img/sharp-linux-s390x@0.33.0:
+    resolution: {integrity: sha512-TiVJbx38J2rNVfA309ffSOB+3/7wOsZYQEOlKqOUdWD/nqkjNGrX+YQGz7nzcf5oy2lC+d37+w183iNXRZNngQ==}
+    engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [s390x]
+    os: [linux]
+    requiresBuild: true
+    optionalDependencies:
+      '@img/sharp-libvips-linux-s390x': 1.0.0
+    dev: true
+    optional: true
+
+  /@img/sharp-linux-x64@0.33.0:
+    resolution: {integrity: sha512-PaZM4Zi7/Ek71WgTdvR+KzTZpBqrQOFcPe7/8ZoPRlTYYRe43k6TWsf4GVH6XKRLMYeSp8J89RfAhBrSP4itNA==}
+    engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [x64]
+    os: [linux]
+    requiresBuild: true
+    optionalDependencies:
+      '@img/sharp-libvips-linux-x64': 1.0.0
+    dev: true
+    optional: true
+
+  /@img/sharp-linuxmusl-arm64@0.33.0:
+    resolution: {integrity: sha512-1QLbbN0zt+32eVrg7bb1lwtvEaZwlhEsY1OrijroMkwAqlHqFj6R33Y47s2XUv7P6Ie1PwCxK/uFnNqMnkd5kg==}
+    engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [arm64]
+    os: [linux]
+    requiresBuild: true
+    optionalDependencies:
+      '@img/sharp-libvips-linuxmusl-arm64': 1.0.0
+    dev: true
+    optional: true
+
+  /@img/sharp-linuxmusl-x64@0.33.0:
+    resolution: {integrity: sha512-CecqgB/CnkvCWFhmfN9ZhPGMLXaEBXl4o7WtA6U3Ztrlh/s7FUKX4vNxpMSYLIrWuuzjiaYdfU3+Tdqh1xaHfw==}
+    engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [x64]
+    os: [linux]
+    requiresBuild: true
+    optionalDependencies:
+      '@img/sharp-libvips-linuxmusl-x64': 1.0.0
+    dev: true
+    optional: true
+
+  /@img/sharp-wasm32@0.33.0:
+    resolution: {integrity: sha512-Hn4js32gUX9qkISlemZBUPuMs0k/xNJebUNl/L6djnU07B/HAA2KaxRVb3HvbU5fL242hLOcp0+tR+M8dvJUFw==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [wasm32]
+    requiresBuild: true
+    dependencies:
+      '@emnapi/runtime': 0.44.0
+    dev: true
+    optional: true
+
+  /@img/sharp-win32-ia32@0.33.0:
+    resolution: {integrity: sha512-5HfcsCZi3l5nPRF2q3bllMVMDXBqEWI3Q8KQONfzl0TferFE5lnsIG0A1YrntMAGqvkzdW6y1Ci1A2uTvxhfzg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [ia32]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@img/sharp-win32-x64@0.33.0:
+    resolution: {integrity: sha512-i3DtP/2ce1yKFj4OzOnOYltOEL/+dp4dc4dJXJBv6god1AFTcmkaA99H/7SwOmkCOBQkbVvA3lCGm3/5nDtf9Q==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+    cpu: [x64]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
   /@malept/cross-spawn-promise@1.1.1:
     resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==}
     engines: {node: '>= 10'}
@@ -1513,6 +1722,13 @@ packages:
   /color-name@1.1.4:
     resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
 
+  /color-string@1.9.1:
+    resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
+    dependencies:
+      color-name: 1.1.4
+      simple-swizzle: 0.2.2
+    dev: true
+
   /color-support@1.1.3:
     resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
     hasBin: true
@@ -1520,6 +1736,14 @@ packages:
     dev: false
     optional: true
 
+  /color@4.2.3:
+    resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
+    engines: {node: '>=12.5.0'}
+    dependencies:
+      color-convert: 2.0.1
+      color-string: 1.9.1
+    dev: true
+
   /combined-stream@1.0.8:
     resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
     engines: {node: '>= 0.8'}
@@ -1622,6 +1846,23 @@ packages:
     dependencies:
       ms: 2.1.2
 
+  /decode-bmp@0.2.1:
+    resolution: {integrity: sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==}
+    engines: {node: '>=8.6.0'}
+    dependencies:
+      '@canvas/image-data': 1.0.0
+      to-data-view: 1.1.0
+    dev: true
+
+  /decode-ico@0.4.1:
+    resolution: {integrity: sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA==}
+    engines: {node: '>=8.6'}
+    dependencies:
+      '@canvas/image-data': 1.0.0
+      decode-bmp: 0.2.1
+      to-data-view: 1.1.0
+    dev: true
+
   /decode-uri-component@0.2.2:
     resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
     engines: {node: '>=0.10'}
@@ -1723,6 +1964,11 @@ packages:
     dev: false
     optional: true
 
+  /detect-libc@2.0.2:
+    resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
+    engines: {node: '>=8'}
+    dev: true
+
   /detect-node@2.1.0:
     resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
     requiresBuild: true
@@ -2810,6 +3056,10 @@ packages:
     engines: {node: '>=14.18.0'}
     dev: true
 
+  /ico-endec@0.1.6:
+    resolution: {integrity: sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ==}
+    dev: true
+
   /iconv-corefoundation@1.1.7:
     resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
     engines: {node: ^8.11.2 || >=10}
@@ -2899,6 +3149,10 @@ packages:
       is-typed-array: 1.1.12
     dev: true
 
+  /is-arrayish@0.3.2:
+    resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
+    dev: true
+
   /is-bigint@1.0.4:
     resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
     dependencies:
@@ -4020,6 +4274,44 @@ packages:
       split-string: 3.1.0
     dev: true
 
+  /sharp-ico@0.1.5:
+    resolution: {integrity: sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q==}
+    dependencies:
+      decode-ico: 0.4.1
+      ico-endec: 0.1.6
+      sharp: 0.33.0
+    dev: true
+
+  /sharp@0.33.0:
+    resolution: {integrity: sha512-99DZKudjm/Rmz+M0/26t4DKpXyywAOJaayGS9boEn7FvgtG0RYBi46uPE2c+obcJRtA3AZa0QwJot63gJQ1F0Q==}
+    engines: {libvips: '>=8.15.0', node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    requiresBuild: true
+    dependencies:
+      color: 4.2.3
+      detect-libc: 2.0.2
+      semver: 7.5.4
+    optionalDependencies:
+      '@img/sharp-darwin-arm64': 0.33.0
+      '@img/sharp-darwin-x64': 0.33.0
+      '@img/sharp-libvips-darwin-arm64': 1.0.0
+      '@img/sharp-libvips-darwin-x64': 1.0.0
+      '@img/sharp-libvips-linux-arm': 1.0.0
+      '@img/sharp-libvips-linux-arm64': 1.0.0
+      '@img/sharp-libvips-linux-s390x': 1.0.0
+      '@img/sharp-libvips-linux-x64': 1.0.0
+      '@img/sharp-libvips-linuxmusl-arm64': 1.0.0
+      '@img/sharp-libvips-linuxmusl-x64': 1.0.0
+      '@img/sharp-linux-arm': 0.33.0
+      '@img/sharp-linux-arm64': 0.33.0
+      '@img/sharp-linux-s390x': 0.33.0
+      '@img/sharp-linux-x64': 0.33.0
+      '@img/sharp-linuxmusl-arm64': 0.33.0
+      '@img/sharp-linuxmusl-x64': 0.33.0
+      '@img/sharp-wasm32': 0.33.0
+      '@img/sharp-win32-ia32': 0.33.0
+      '@img/sharp-win32-x64': 0.33.0
+    dev: true
+
   /shebang-command@2.0.0:
     resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
     engines: {node: '>=8'}
@@ -4043,6 +4335,12 @@ packages:
   /signal-exit@3.0.7:
     resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
 
+  /simple-swizzle@0.2.2:
+    resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
+    dependencies:
+      is-arrayish: 0.3.2
+    dev: true
+
   /simple-update-notifier@2.0.0:
     resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
     engines: {node: '>=10'}
@@ -4297,6 +4595,10 @@ packages:
       rimraf: 3.0.2
     dev: true
 
+  /to-data-view@1.1.0:
+    resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==}
+    dev: true
+
   /to-object-path@0.3.0:
     resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==}
     engines: {node: '>=0.10.0'}
diff --git a/scripts/build/build.mts b/scripts/build/build.mts
index 27f45cc..a8504ab 100644
--- a/scripts/build/build.mts
+++ b/scripts/build/build.mts
@@ -8,6 +8,7 @@ import { BuildContext, BuildOptions, context } from "esbuild";
 import { copyFile } from "fs/promises";
 
 import vencordDep from "./vencordDep.mjs";
+import { composeTrayIcons } from "./composeTrayIcons.mts";
 
 const isDev = process.argv.includes("--dev");
 
@@ -51,6 +52,12 @@ async function copyVenmic() {
 
 await Promise.all([
     copyVenmic(),
+    composeTrayIcons({
+        icon: "./static/icon.png",
+        badges: "./static/badges/*",
+        outDir: "./static/dist/tray_icons",
+        createEmpty: true
+    }),
     createContext({
         ...NodeCommonOpts,
         entryPoints: ["src/main/index.ts"],
diff --git a/scripts/build/composeTrayIcons.mts b/scripts/build/composeTrayIcons.mts
new file mode 100644
index 0000000..8e71e8a
--- /dev/null
+++ b/scripts/build/composeTrayIcons.mts
@@ -0,0 +1,221 @@
+import sharp, { OutputInfo } from "sharp";
+import fastGlob from "fast-glob";
+import type { ImageData } from "sharp-ico";
+import { parse as pathParse, format as pathFormat } from "node:path";
+
+interface BadgePosition {
+    left?: number;
+    top?: number;
+    anchorX?: "left" | "right" | "center";
+    anchorY?: "top" | "bottom" | "center";
+}
+
+interface BadgeOptions extends BadgePosition {
+    width?: number;
+    height?: number;
+    resizeOptions?: sharp.ResizeOptions;
+}
+
+const DEFAULT_BADGE_OPTIONS: Required<BadgeOptions> = {
+    width: 0.5,
+    height: 0.5,
+    left: 0.8,
+    top: 0.8,
+    anchorX: "center",
+    anchorY: "center",
+    resizeOptions: {
+        kernel: sharp.kernel.cubic
+    }
+};
+
+export async function composeTrayIcons({
+    icon: iconPath,
+    badges: badgeGlob,
+    outDir,
+    outExt = ".png",
+    createEmpty = false,
+    iconOptions = { width: 64, height: 64 },
+    badgeOptions = undefined
+}: {
+    icon: string | Buffer | sharp.Sharp;
+    badges: string;
+    outDir: string;
+    outExt?: string;
+    createEmpty?: boolean;
+    iconOptions?: ImageDim;
+    badgeOptions?: BadgeOptions;
+}) {
+    const badges = await fastGlob.glob(badgeGlob);
+    if (!badges.length) {
+        throw new Error(`No badges matching glob '${badgeGlob}' found!`);
+    }
+
+    const badgeOptionsFilled = { ...DEFAULT_BADGE_OPTIONS, ...badgeOptions };
+    const { data: iconData, info: iconInfo } = await resolveImageOrIco(iconPath, iconOptions);
+    const iconName = typeof iconPath === "string" ? pathParse(iconPath).name : "tray_icon";
+
+    const resizedBadgeDim = {
+        height: Math.round(badgeOptionsFilled.height * iconInfo.height),
+        width: Math.round(badgeOptionsFilled.width * iconInfo.width)
+    };
+
+    async function doCompose(badgePath: string | sharp.Sharp, ensureSize?: ImageDim | false) {
+        const { data: badgeData, info: badgeInfo } = await resolveImageOrIco(badgePath, resizedBadgeDim);
+        if (ensureSize && (badgeInfo.height !== ensureSize.height || badgeInfo.width !== ensureSize.width)) {
+            throw new Error(
+                `Badge loaded from ${badgePath} has size ${badgeInfo.height}x${badgeInfo.height} != ${ensureSize.height}x${ensureSize.height}`
+            );
+        }
+
+        const savePath = pathFormat({
+            name: iconName + (typeof badgePath === "string" ? "_" + pathParse(badgePath).name : ""),
+            dir: outDir,
+            ext: outExt,
+            base: undefined
+        });
+        let out = composeTrayIcon(iconData, iconInfo, badgeData, badgeInfo, badgeOptionsFilled);
+        const outputInfo = await out.toFile(savePath);
+        return {
+            iconInfo,
+            badgeInfo,
+            outputInfo
+        };
+    }
+
+    if (createEmpty) {
+        const firstComposition = await doCompose(badges[0]);
+        return await Promise.all([
+            firstComposition,
+            ...badges.map(badge => doCompose(badge, firstComposition.badgeInfo)),
+            doCompose(emptyImage(firstComposition.badgeInfo).png())
+        ]);
+    } else {
+        return await Promise.all(badges.map(badge => doCompose(badge)));
+    }
+}
+
+type SharpInput = string | Buffer;
+
+interface ImageDim {
+    width: number;
+    height: number;
+}
+
+async function resolveImageOrIco(...args: Parameters<typeof loadFromImageOrIco>) {
+    const image = await loadFromImageOrIco(...args);
+    const { data, info } = await image.toBuffer({ resolveWithObject: true });
+    return {
+        data,
+        info: validDim(info)
+    };
+}
+
+async function loadFromImageOrIco(
+    path: string | Buffer | sharp.Sharp,
+    sizeOptions?: ImageDim & { resizeICO?: boolean }
+): Promise<sharp.Sharp> {
+    if (typeof path === "string" && path.endsWith(".ico")) {
+        const icos = (await import("sharp-ico")).sharpsFromIco(path, undefined, true) as unknown as ImageData[];
+        let icoInfo;
+        if (sizeOptions == null) {
+            icoInfo = icos[icos.length - 1];
+        } else {
+            icoInfo = icos.reduce((best, ico) =>
+                Math.abs(ico.width - sizeOptions.width) < Math.abs(ico.width - best.width) ? ico : best
+            );
+        }
+
+        if (icoInfo.image == null) {
+            throw new Error("Bug: sharps-ico found no image in ICO");
+        }
+
+        const icoImage = icoInfo.image.png();
+        if (sizeOptions?.resizeICO) {
+            return icoImage.resize(sizeOptions);
+        } else {
+            return icoImage;
+        }
+    } else {
+        let image = typeof path !== "string" && "toBuffer" in path ? path : sharp(path);
+        if (sizeOptions) {
+            image = image.resize(sizeOptions);
+        }
+        return image;
+    }
+}
+
+function validDim<T extends Partial<ImageDim>>(meta: T): T & ImageDim {
+    if (meta?.width == null || meta?.height == null) {
+        throw new Error("Failed getting icon dimensions");
+    }
+    return meta as T & ImageDim;
+}
+
+function emptyImage(dim: ImageDim) {
+    return sharp({
+        create: {
+            width: dim.width,
+            height: dim.height,
+            channels: 4,
+            background: { r: 0, b: 0, g: 0, alpha: 0 }
+        }
+    });
+}
+
+function composeTrayIcon(
+    icon: SharpInput,
+    iconDim: ImageDim,
+    badge: SharpInput,
+    badgeDim: ImageDim,
+    badgeOptions: Required<BadgeOptions>
+): sharp.Sharp {
+    let badgeLeft = badgeOptions.left * iconDim.width;
+    switch (badgeOptions.anchorX) {
+        case "left":
+            break;
+        case "right":
+            badgeLeft -= badgeDim.width;
+            break;
+        case "center":
+            badgeLeft -= badgeDim.width / 2;
+            break;
+    }
+    let badgeTop = badgeOptions.top * iconDim.height;
+    switch (badgeOptions.anchorY) {
+        case "top":
+            break;
+        case "bottom":
+            badgeTop -= badgeDim.height / 2;
+            break;
+        case "center":
+            badgeTop -= badgeDim.height / 2;
+            break;
+    }
+
+    badgeTop = Math.round(badgeTop);
+    badgeLeft = Math.round(badgeLeft);
+
+    const padding = Math.max(
+        0,
+        -badgeLeft,
+        badgeLeft + badgeDim.width - iconDim.width,
+        -badgeTop,
+        badgeTop + badgeDim.height - iconDim.height
+    );
+
+    return emptyImage({
+        width: iconDim.width + 2 * padding,
+        height: iconDim.height + 2 * padding
+    }).composite([
+        {
+            input: icon,
+            left: padding,
+            top: padding
+        },
+        {
+            input: badge,
+            left: badgeLeft + padding,
+            top: badgeTop + padding
+        }
+    ]);
+}
diff --git a/src/main/appBadge.ts b/src/main/appBadge.ts
index 46abe1d..63103e6 100644
--- a/src/main/appBadge.ts
+++ b/src/main/appBadge.ts
@@ -6,27 +6,43 @@
 
 import { app, NativeImage, nativeImage } from "electron";
 import { join } from "path";
-import { BADGE_DIR } from "shared/paths";
+import { BADGE_DIR, TRAY_ICON_DIR, TRAY_ICON_PATH } from "shared/paths";
+import { trayContainer } from "./mainWindow";
+import { Settings } from "./settings";
 
-const imgCache = new Map<number, NativeImage>();
-function loadBadge(index: number) {
-    const cached = imgCache.get(index);
+const imgCache = new Map<string, NativeImage>();
+
+function loadImg(path: string) {
+    const cached = imgCache.get(path);
     if (cached) return cached;
 
-    const img = nativeImage.createFromPath(join(BADGE_DIR, `${index}.ico`));
-    imgCache.set(index, img);
+    const img = nativeImage.createFromPath(path);
+    imgCache.set(path, img);
 
     return img;
 }
 
+function loadBadge(index: number) {
+    return loadImg(join(BADGE_DIR, `${index}.ico`));
+}
+
+function loadTrayIcon(index: number) {
+    return loadImg(index === 0 ? TRAY_ICON_PATH : join(TRAY_ICON_DIR, `icon_${index}.png`));
+}
+
 let lastIndex: null | number = -1;
 
 export function setBadgeCount(count: number) {
+    const [index, description] = getBadgeIndexAndDescription(count);
+
+    if (Settings?.store.trayBadge) {
+        trayContainer.tray?.setImage(loadTrayIcon(index ?? 0));
+    }
+
     switch (process.platform) {
         case "linux":
             if (count === -1) count = 0;
-            app.setBadgeCount(count);
-            break;
+            app.setBadgeCount(count); // Only works if libunity is installed
         case "darwin":
             if (count === 0) {
                 app.dock.setBadge("");
@@ -35,7 +51,6 @@ export function setBadgeCount(count: number) {
             app.dock.setBadge(count === -1 ? "•" : count.toString());
             break;
         case "win32":
-            const [index, description] = getBadgeIndexAndDescription(count);
             if (lastIndex === index) break;
 
             lastIndex = index;
diff --git a/src/main/mainWindow.ts b/src/main/mainWindow.ts
index 6e9a05c..17e1605 100644
--- a/src/main/mainWindow.ts
+++ b/src/main/mainWindow.ts
@@ -21,7 +21,7 @@ import { isTruthy } from "shared/utils/guards";
 import { once } from "shared/utils/once";
 import type { SettingsStore } from "shared/utils/SettingsStore";
 
-import { ICON_PATH } from "../shared/paths";
+import { ICON_PATH, TRAY_ICON_PATH } from "../shared/paths";
 import { createAboutWindow } from "./about";
 import { initArRPC } from "./arrpc";
 import {
@@ -41,7 +41,9 @@ import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./u
 import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader";
 
 let isQuitting = false;
-let tray: Tray;
+export const trayContainer: { tray: Tray | null } = {
+    tray: null
+};
 
 applyDeckKeyboardFix();
 
@@ -118,7 +120,7 @@ function initTray(win: BrowserWindow) {
         }
     ]);
 
-    tray = new Tray(ICON_PATH);
+    const tray = (trayContainer.tray = new Tray(TRAY_ICON_PATH));
     tray.setToolTip("Vesktop");
     tray.setContextMenu(trayMenu);
     tray.on("click", () => win.show());
@@ -332,7 +334,7 @@ function initWindowBoundsListeners(win: BrowserWindow) {
 function initSettingsListeners(win: BrowserWindow) {
     addSettingsListener("tray", enable => {
         if (enable) initTray(win);
-        else tray?.destroy();
+        else trayContainer.tray?.destroy();
     });
     addSettingsListener("disableMinSize", disable => {
         if (disable) {
diff --git a/src/preload/VesktopNative.ts b/src/preload/VesktopNative.ts
index d72329d..09eda0d 100644
--- a/src/preload/VesktopNative.ts
+++ b/src/preload/VesktopNative.ts
@@ -23,7 +23,7 @@ export const VesktopNative = {
     app: {
         relaunch: () => invoke<void>(IpcEvents.RELAUNCH),
         getVersion: () => sendSync<void>(IpcEvents.GET_VERSION),
-        setBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count),
+        setAppBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count),
         supportsWindowsTransparency: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY)
     },
     autostart: {
diff --git a/src/renderer/appBadge.ts b/src/renderer/appBadge.ts
index b55d488..0c1b6af 100644
--- a/src/renderer/appBadge.ts
+++ b/src/renderer/appBadge.ts
@@ -13,7 +13,9 @@ let GuildReadStateStore: any;
 let NotificationSettingsStore: any;
 
 export function setBadge() {
-    if (Settings.store.appBadge === false) return;
+    const { appBadge, trayBadge } = Settings.store;
+
+    if (appBadge === false && trayBadge === false) return;
 
     try {
         const mentionCount = GuildReadStateStore.getTotalMentionCount();
@@ -24,7 +26,7 @@ export function setBadge() {
         let totalCount = mentionCount + pendingRequests;
         if (!totalCount && hasUnread && !disableUnreadBadge) totalCount = -1;
 
-        VesktopNative.app.setBadgeCount(totalCount);
+        if (appBadge || trayBadge) VesktopNative.app.setAppBadgeCount(totalCount);
     } catch (e) {
         console.error(e);
     }
diff --git a/src/renderer/components/Settings.tsx b/src/renderer/components/Settings.tsx
index b90c746..13fea9c 100644
--- a/src/renderer/components/Settings.tsx
+++ b/src/renderer/components/Settings.tsx
@@ -93,13 +93,27 @@ export default function SettingsUi() {
                 onChange={v => {
                     Settings.appBadge = v;
                     if (v) setBadge();
-                    else VesktopNative.app.setBadgeCount(0);
+                    else VesktopNative.app.setAppBadgeCount(0);
                 }}
-                note="Show mention badge on the app icon"
+                note="Show mention badge on the app (taskbar/panel) icon"
             >
                 Notification Badge
             </Switch>
 
+            {Settings.tray && (
+                <Switch
+                    value={Settings.trayBadge ?? true}
+                    onChange={v => {
+                        Settings.trayBadge = v;
+                        if (v) setBadge();
+                        else VesktopNative.app.setAppBadgeCount(0);
+                    }}
+                    note="Show mention badge on the tray icon"
+                >
+                    Tray Notification Badge
+                </Switch>
+            )}
+
             {switches.map(([key, text, note, def, predicate]) => (
                 <Switch
                     value={(Settings[key as any] ?? def ?? false) && predicate?.() !== false}
diff --git a/src/shared/paths.ts b/src/shared/paths.ts
index 483250a..004b063 100644
--- a/src/shared/paths.ts
+++ b/src/shared/paths.ts
@@ -10,3 +10,5 @@ export const STATIC_DIR = /* @__PURE__ */ join(__dirname, "..", "..", "static");
 export const VIEW_DIR = /* @__PURE__ */ join(STATIC_DIR, "views");
 export const BADGE_DIR = /* @__PURE__ */ join(STATIC_DIR, "badges");
 export const ICON_PATH = /* @__PURE__ */ join(STATIC_DIR, "icon.png");
+export const TRAY_ICON_DIR = /* @__PURE__ */ join(STATIC_DIR, "dist", "tray_icons");
+export const TRAY_ICON_PATH = /* @__PURE__ */ join(TRAY_ICON_DIR, "icon.png");
diff --git a/src/shared/settings.d.ts b/src/shared/settings.d.ts
index 236cdfc..2350c54 100644
--- a/src/shared/settings.d.ts
+++ b/src/shared/settings.d.ts
@@ -11,6 +11,7 @@ export interface Settings {
     vencordDir?: string;
     transparencyOption?: "none" | "mica" | "tabbed" | "acrylic";
     tray?: boolean;
+    trayBadge?: boolean;
     minimizeToTray?: boolean;
     openLinksWithElectron?: boolean;
     staticTitle?: boolean;
diff --git a/static/dist/.gitignore b/static/dist/.gitignore
index c96a04f..c9b4c1e 100644
--- a/static/dist/.gitignore
+++ b/static/dist/.gitignore
@@ -1,2 +1,3 @@
 *
-!.gitignore
\ No newline at end of file
+!.gitignore
+!tray_icons/.gitkeep