Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@ jobs:
with:
name: installers-${{ matrix.os }}
path: |
dist/*.exe
dist/*.dmg
dist/*.AppImage
dist/*.deb
dist/*.zip
dist/*.blockmap
dist/latest*.yml
dist/release/*.exe
dist/release/*.dmg
dist/release/*.AppImage
dist/release/*.deb
dist/release/*.zip
dist/release/*.blockmap
dist/release/latest*.yml
if-no-files-found: error
retention-days: 7

Expand Down
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,19 @@
"browsable",
"CHUNKID",
"CHUNKIDS",
"downsample",
"EXPORTMAP",
"fguid",
"gatherable",
"icns",
"ICONDIR",
"ICONDIRENTRY",
"iconutil",
"IEND",
"konsole",
"Nwnp",
"OFPA",
"Paeth",
"Qfecheck",
"scrollview",
"SERIALSIZES",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ This project reproduces the Windows 95 look and feel, some of the icons, fonts,
originate from Microsoft software and are not covered by the MIT License in [LICENSE](LICENSE), read
[NOTICE.md](NOTICE.md) before forking this project or redistributing a build of it.

Cybearl is not affiliated with, endorsed by, or sponsored by Microsoft Corporation. Windows and
Cybearl is not affiliated with, endorsed by, or sponsored by Microsoft Corporation, Windows and
Windows 95 are trademarks of Microsoft Corporation.
9 changes: 6 additions & 3 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ productName: GitGame
copyright: Copyright © ${author}

directories:
output: dist
buildResources: build
output: dist/release
buildResources: resources

files:
- out/**/*
- dist/app/**/*
- package.json
- "!**/*.map"

Expand All @@ -20,6 +20,7 @@ publish:
releaseType: release

win:
icon: resources/icon.ico
target:
- target: nsis
arch: [x64]
Expand All @@ -33,6 +34,7 @@ nsis:
deleteAppDataOnUninstall: false

mac:
icon: resources/icon.icns
category: public.app-category.developer-tools
target:
- target: dmg
Expand All @@ -42,6 +44,7 @@ mac:
gatekeeperAssess: false

linux:
icon: resources/icons
target:
- target: AppImage
arch: [x64]
Expand Down
6 changes: 6 additions & 0 deletions electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const rendererRoot = path.resolve(srcRoot, "renderer")
const react95IconsRoot = path.resolve(__dirname, "node_modules", "@react95", "icons", "png")
const react95FontsRoot = path.resolve(__dirname, "node_modules", "react95", "dist", "fonts")

// Every bundle lands under a single output root
const outRoot = path.resolve(__dirname, "dist", "app")

/**
* Path aliases for module resolution.
*/
Expand All @@ -26,16 +29,19 @@ const aliases = {
export default defineConfig({
main: {
resolve: { alias: aliases },
build: { outDir: path.join(outRoot, "main") },
},
preload: {
resolve: { alias: aliases },
build: { outDir: path.join(outRoot, "preload") },
},
renderer: {
root: path.resolve(__dirname, "src", "renderer"),
base: "./",
resolve: { alias: aliases },
plugins: [react(), tailwindcss()],
build: {
outDir: path.join(outRoot, "renderer"),
rollupOptions: {
input: path.resolve(__dirname, "src", "renderer", "index.html"),
},
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
"engines": {
"node": ">=22.0.0"
},
"main": "./out/main/index.js",
"main": "./dist/app/main/index.js",
"scripts": {
"postinstall": "node node_modules/electron/install.js",
"dev": "electron-vite dev",
"build": "electron-vite build",
"preview": "electron-vite preview",
"typecheck": "tsc --noEmit",
"generate-icons": "tsx scripts/generateIcons.ts",
"test": "npm run test:unit",
"test:unit": "vitest",
"lint": "biome check && eslint \"src/**/*.{ts,tsx}\"",
Expand Down Expand Up @@ -61,6 +62,7 @@
"react": "^19.2.5",
"react-dom": "^19.2.5",
"tailwindcss": "^4.3.1",
"tsx": "^4.23.9",
"typescript": "^5.8.3",
"vite": "^7.0.0",
"vite-tsconfig-paths": "^5.1.4",
Expand Down
Binary file added resources/icon.icns
Binary file not shown.
Binary file added resources/icon.ico
Binary file not shown.
Binary file added resources/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/icons/128x128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/icons/16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/icons/24x24.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/icons/256x256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/icons/32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/icons/48x48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/icons/512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/icons/64x64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 77 additions & 0 deletions scripts/generateIcons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import fs from "node:fs"
import path from "node:path"
import CONSTANTS from "@scripts/lib/constants"
import { encodeIcns } from "@scripts/lib/utils/icns"
import { encodeIco } from "@scripts/lib/utils/ico"
import { decodePng, encodePng } from "@scripts/lib/utils/png"
import { resizeIcon } from "@scripts/lib/utils/raster"
import type { IconSources, RasterImage } from "@scripts/types/icons"

/**
* Reads the source art off disk.
* @returns The decoded source images.
*/
function readSources(): IconSources {
const { dir, smallFile, largeFile } = CONSTANTS.ICON_SOURCE

return {
small: decodePng(fs.readFileSync(path.join(dir, smallFile))),
large: decodePng(fs.readFileSync(path.join(dir, largeFile))),
}
}

/**
* Renders the app art at a given size, taking the hand-drawn 16x16 variant where it
* applies and scaling the 32x32 one everywhere else.
* @param sources The decoded source images.
* @param size The square size to render at, in pixels.
* @returns The rendered image.
*/
function renderIcon(sources: IconSources, size: number): RasterImage {
return size === sources.small.width ? sources.small : resizeIcon(sources.large, size)
}

/**
* Renders every icon the packaging and the main process need.
* @param sources The decoded source images.
* @returns Each output file paired with the bytes to write to it.
*/
function renderOutputs(sources: IconSources): [string, Buffer][] {
const icnsSlots = Object.entries(CONSTANTS.ICNS_SLOT_SIZES).map(([type, size]) => ({
type,
image: renderIcon(sources, size),
}))

return [
[
path.join(CONSTANTS.ICON_OUTPUT.packagingDir, "icon.png"),
encodePng(renderIcon(sources, CONSTANTS.ICON_MASTER_SIZE)),
],
[
path.join(CONSTANTS.ICON_OUTPUT.packagingDir, "icon.ico"),
encodeIco(CONSTANTS.ICO_SIZES.map(size => renderIcon(sources, size))),
],
[path.join(CONSTANTS.ICON_OUTPUT.packagingDir, "icon.icns"), encodeIcns(icnsSlots)],
[CONSTANTS.ICON_OUTPUT.runtimeFile, encodePng(renderIcon(sources, CONSTANTS.ICON_RUNTIME_SIZE))],
...CONSTANTS.LINUX_ICON_SIZES.map((size): [string, Buffer] => [
path.join(CONSTANTS.ICON_OUTPUT.linuxDir, `${size}x${size}.png`),
encodePng(renderIcon(sources, size)),
]),
]
}

/**
* Regenerates every packaging icon from the React95 source art.
*/
function generateIcons() {
const outputs = renderOutputs(readSources())

for (const [file, contents] of outputs) {
fs.mkdirSync(path.dirname(file), { recursive: true })
fs.writeFileSync(file, contents)

console.log(`${path.relative(process.cwd(), file).replace(/\\/g, "/")} (${contents.length} bytes)`)
}
}

generateIcons()
109 changes: 109 additions & 0 deletions scripts/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Buffer } from "node:buffer"
import path from "node:path"
import { fileURLToPath } from "node:url"

/**
* The project root, resolved from this file's own location.
*/
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..")

/**
* The constants for the build scripts.
*/
const CONSTANTS = {
/**
* Where the app art is taken from, the React95 set ships a hand-drawn 16x16 variant
* next to the 32x32 one every other size is scaled from.
*/
ICON_SOURCE: {
dir: path.join(ROOT_DIR, "node_modules", "@react95", "icons", "png"),
smallFile: "Computer3_16x16_4.png",
largeFile: "Computer3_32x32_4.png",
},

/**
* Where the generated icons land.
*/
ICON_OUTPUT: {
packagingDir: path.join(ROOT_DIR, "resources"),
linuxDir: path.join(ROOT_DIR, "resources", "icons"),
runtimeFile: path.join(ROOT_DIR, "src", "main", "assets", "icon.png"),
},

/**
* The size of the master PNG, which is what electron-builder falls back to for any
* platform left without a dedicated icon file.
*/
ICON_MASTER_SIZE: 1024,

/**
* The size of the icon the main process hands to its windows.
*/
ICON_RUNTIME_SIZE: 256,

/**
* The sizes packed into the Windows `.ico`.
*/
ICO_SIZES: [16, 24, 32, 48, 64, 128, 256],

/**
* The sizes emitted as loose PNGs for Linux.
*/
LINUX_ICON_SIZES: [16, 24, 32, 48, 64, 128, 256, 512],

/**
* The macOS `.icns` slots, each pairing an OSType with the size it holds, covering
* every 1x and 2x variant `iconutil` would emit.
*/
ICNS_SLOT_SIZES: {
icp4: 16,
icp5: 32,
ic11: 32,
ic12: 64,
ic07: 128,
ic13: 256,
ic08: 256,
ic09: 512,
ic14: 512,
ic10: 1024,
},

/**
* The eight byte signature every PNG file opens with.
*/
PNG_SIGNATURE: Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),

/**
* The number of bytes a pixel takes in the 8-bit RGBA buffers used throughout.
*/
PNG_BYTES_PER_PIXEL: 4,

/**
* The CRC-32 lookup table guarding PNG chunks.
*/
PNG_CRC_TABLE: new Uint32Array(256).map((_, index) => {
let value = index
for (let bit = 0; bit < 8; bit++) value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1
return value
}),

/**
* The size of the `BITMAPINFOHEADER` opening every bitmap backed `.ico` entry.
*/
ICO_BITMAP_HEADER_SIZE: 40,

/**
* The size of the `ICONDIR` opening an `.ico`, followed by one `ICONDIRENTRY` of
* `ICO_DIRECTORY_ENTRY_SIZE` bytes per packed image.
*/
ICO_DIRECTORY_SIZE: 6,
ICO_DIRECTORY_ENTRY_SIZE: 16,

/**
* The smallest entry stored as a PNG rather than as a bitmap, which is how the
* format has carried its largest sizes since Vista.
*/
ICO_PNG_THRESHOLD: 256,
} as const

export default CONSTANTS
26 changes: 26 additions & 0 deletions scripts/lib/utils/icns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Buffer } from "node:buffer"
import { encodePng } from "@scripts/lib/utils/png"
import type { IcnsSlot } from "@scripts/types/icons"

/**
* Packs slots into a macOS `.icns`, one PNG backed chunk each.
* @param slots The slots to pack.
* @returns The ICNS bytes.
*/
export function encodeIcns(slots: IcnsSlot[]): Buffer {
const chunks = slots.map(({ type, image }) => {
const data = encodePng(image)
const chunk = Buffer.alloc(8 + data.length)
chunk.write(type, 0, "ascii")
chunk.writeUInt32BE(chunk.length, 4)
data.copy(chunk, 8)

return chunk
})

const header = Buffer.alloc(8)
header.write("icns", 0, "ascii")
header.writeUInt32BE(header.length + chunks.reduce((total, chunk) => total + chunk.length, 0), 4)

return Buffer.concat([header, ...chunks])
}
Loading
Loading