Compare commits

..

No commits in common. "9e7da12bf5c6c6747f5f03b0b7f2c015d84b4f82" and "49937af24e57bfb86f6a71184e05ceb150cf02d5" have entirely different histories.

37 changed files with 768 additions and 1043 deletions

276
package-lock.json generated
View file

@ -107,9 +107,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
"integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
"integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -171,32 +171,32 @@
"dev": true
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
"dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.24"
"@jridgewell/trace-mapping": "^0.3.9"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
@ -219,9 +219,9 @@
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"version": "0.3.22",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
"integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@ -276,9 +276,9 @@
}
},
"node_modules/@types/eslint": {
"version": "8.56.5",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz",
"integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==",
"version": "8.56.2",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz",
"integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==",
"dev": true,
"dependencies": {
"@types/estree": "*",
@ -308,18 +308,18 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.11.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz",
"integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==",
"version": "20.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz",
"integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
"integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
@ -903,9 +903,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"version": "4.22.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
"integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
"dev": true,
"funding": [
{
@ -922,8 +922,8 @@
}
],
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
"caniuse-lite": "^1.0.30001580",
"electron-to-chromium": "^1.4.648",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
},
@ -950,9 +950,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001597",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
"integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
"version": "1.0.30001587",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz",
"integrity": "sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==",
"dev": true,
"funding": [
{
@ -1104,6 +1104,34 @@
"webpack": "^5.1.0"
}
},
"node_modules/copy-webpack-plugin/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/copy-webpack-plugin/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"ajv": "^8.8.2"
}
},
"node_modules/copy-webpack-plugin/node_modules/globby": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz",
@ -1124,6 +1152,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/copy-webpack-plugin/node_modules/path-type": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz",
@ -1136,6 +1170,25 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/copy-webpack-plugin/node_modules/schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
"integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.1.0"
},
"engines": {
"node": ">= 12.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/copy-webpack-plugin/node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
@ -1257,15 +1310,15 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.700",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.700.tgz",
"integrity": "sha512-40dqKQ3F7C8fbBEmjSeJ+qEHCKzPyrP9SkeIBZ3wSCUH9nhWStrDz030XlDzlhNhlul1Z0fz7TpDFnsIzo4Jtg==",
"version": "1.4.668",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.668.tgz",
"integrity": "sha512-ZOBocMYCehr9W31+GpMclR+KBaDZOoAEabLdhpZ8oU1JFDwIaFY0UDbpXVEUFc0BIP2O2Qn3rkfCjQmMR4T/bQ==",
"dev": true
},
"node_modules/enhanced-resolve": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz",
"integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==",
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
"integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
@ -1315,16 +1368,16 @@
}
},
"node_modules/eslint": {
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
"integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.57.0",
"@humanwhocodes/config-array": "^0.11.14",
"@eslint/js": "8.56.0",
"@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
@ -1615,9 +1668,9 @@
}
},
"node_modules/flatted": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"version": "3.2.9",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
"dev": true
},
"node_modules/fs.realpath": {
@ -1766,9 +1819,9 @@
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
"integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
@ -2719,9 +2772,9 @@
]
},
"node_modules/sass": {
"version": "1.71.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
"integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
"version": "1.71.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.71.0.tgz",
"integrity": "sha512-HKKIKf49Vkxlrav3F/w6qRuPcmImGVbIXJ2I3Kg0VMA+3Bav+8yE9G5XmP5lMj6nl4OlqbPftGAscNaNu28b8w==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@ -2736,9 +2789,9 @@
}
},
"node_modules/sass-loader": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz",
"integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.0.tgz",
"integrity": "sha512-LS2mLeFWA+orYxHNu+O18Xe4jR0kyamNOOUsE3NyBP4DvIL+8stHpNX0arYTItdPe80kluIiJ7Wfe/9iHSRO0Q==",
"dev": true,
"dependencies": {
"neo-async": "^2.6.2"
@ -2776,58 +2829,23 @@
}
},
"node_modules/schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
"integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.1.0"
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
},
"engines": {
"node": ">= 12.13.0"
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/schema-utils/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/schema-utils/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"ajv": "^8.8.2"
}
},
"node_modules/schema-utils/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
@ -3005,9 +3023,9 @@
}
},
"node_modules/terser": {
"version": "5.29.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
"integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==",
"version": "5.27.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
"integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@ -3056,24 +3074,6 @@
}
}
},
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -3093,9 +3093,9 @@
}
},
"node_modules/ts-api-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
"integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz",
"integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==",
"dev": true,
"engines": {
"node": ">=16"
@ -3149,9 +3149,9 @@
}
},
"node_modules/typescript": {
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@ -3238,9 +3238,9 @@
}
},
"node_modules/webpack": {
"version": "5.90.3",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz",
"integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==",
"version": "5.90.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.2.tgz",
"integrity": "sha512-ziXu8ABGr0InCMEYFnHrYweinHK2PWrMqnwdHk2oK3rRhv/1B+2FnfwYv5oD+RrknK/Pp/Hmyvu+eAsaMYhzCw==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
@ -3383,24 +3383,6 @@
"node": ">=4.0"
}
},
"node_modules/webpack/node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -1,8 +1,7 @@
import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, UiEventHandler } from "./events";
import { byteArrayToJsSource, formatHex } from "./etc";
import { byte_array_to_js_source, format_hex } from "./etc";
import { Instruction, ISA } from "./instructionSet";
import { m256, u2, u3, u8 } from "./num";
import { DEFAULT_VRAM_BANK } from "./constants";
export type TempInstrState = {
pos: u8;
@ -26,7 +25,6 @@ export class Computer {
private carry_flag: boolean = false;
private program_counter: u8 = 0;
private bank: u2 = 0;
private vram_bank: u2 = DEFAULT_VRAM_BANK;
private current_instr: TempInstrState | null = null;
events: CpuEventHandler = new CpuEventHandler();
@ -40,7 +38,7 @@ export class Computer {
pos: this.program_counter,
code: current_byte,
});
console.log(`Invalid instruction: ${formatHex(current_byte)}`);
console.log(`Invalid instruction: ${format_hex(current_byte)}`);
this.step_forward();
this.events.dispatch(CpuEvent.Cycle);
return;
@ -144,9 +142,9 @@ export class Computer {
return this.call_stack.pop() ?? null;
}
setBank(bank: u2): void {
this.events.dispatch(CpuEvent.SwitchBank, { bank: bank });
this.bank = bank;
setBank(bank_no: u2): void {
this.events.dispatch(CpuEvent.SwitchBank, { bank: bank_no });
this.bank = bank_no;
}
setCarry(state: boolean): void {
@ -158,11 +156,6 @@ export class Computer {
return this.carry_flag;
}
setVramBank(bank: u2): void {
this.vram_bank = bank;
this.events.dispatch(CpuEvent.SetVramBank, { bank });
}
reset(): void {
this.events.dispatch(CpuEvent.Reset);
this.banks = init_banks();
@ -171,25 +164,23 @@ export class Computer {
this.current_instr = null;
this.program_counter = 0;
this.carry_flag = false;
this.vram_bank = 3;
}
initEvents(ui: UiCpuSignalHandler): void {
init_events(ui: UiCpuSignalHandler): void {
ui.listen(UiCpuSignal.RequestCpuCycle, (cycle_count) => {
for (let i = 0; i < cycle_count; i++) this.cycle();
});
ui.listen(UiCpuSignal.RequestMemoryChange, ({ address, bank, value }) => this.setMemory(address, value, bank));
ui.listen(UiCpuSignal.RequestRegisterChange, ({ register_no, value }) => this.setRegister(register_no, value));
ui.listen(UiCpuSignal.RequestMemoryDump, (callback) => callback(this.dumpMemory()));
ui.listen(UiCpuSignal.RequestMemoryDump, () =>
this.events.dispatch(CpuEvent.MemoryDumped, { memory: this.dump_memory() })
);
ui.listen(UiCpuSignal.RequestCpuReset, () => this.reset());
ui.listen(UiCpuSignal.RequestProgramCounterChange, ({ address }) => {
this.setProgramCounter(address);
});
}
loadMemory(program: Array<u8>): void {
load_memory(program: Array<u8>): void {
// TODO allow loading into other banks
console.log(byteArrayToJsSource(program));
console.log(byte_array_to_js_source(program));
const max_loop: u8 = Math.min(255, program.length) as u8;
for (let i: u8 = 0; i < 256; i++) {
// Don't fire event if no change is made
@ -201,7 +192,7 @@ export class Computer {
this.program_counter = 0;
}
dumpMemory(): [Uint8Array, Uint8Array, Uint8Array, Uint8Array] {
dump_memory(): [Uint8Array, Uint8Array, Uint8Array, Uint8Array] {
return this.banks;
}

View file

@ -1,3 +0,0 @@
const DEFAULT_VRAM_BANK = 3;
export { DEFAULT_VRAM_BANK };

View file

@ -3,9 +3,6 @@
* @copyright Alexander Bass 2024
* @license GPL-3.0
*/
import el from "./util/elementMaker";
// Re-export el
export { el };
import { u8 } from "./num";
/**
@ -14,34 +11,52 @@ import { u8 } from "./num";
*/
export const $ = (id: string): HTMLElement => document.getElementById(id) as HTMLElement;
export const formatHex = (n: u8): string => n.toString(16).toUpperCase().padStart(2, "0");
export const format_hex = (n: u8): string => n.toString(16).toUpperCase().padStart(2, "0");
/**
* Converts array of bytes to a JavaScript syntax array of hexadecimal literals
* @param bytes
*/
export const byteArrayToJsSource = (bytes: Array<u8>): string => {
export const byte_array_to_js_source = (bytes: Array<u8>): string => {
let str = "[";
for (const b of bytes) {
str += `0x${formatHex(b)},`;
str += `0x${format_hex(b)},`;
}
str += "]";
return str;
};
/**
* Create an html element
* @param type
* @param id id attribute to set
*/
export function el<E extends keyof HTMLElementTagNameMap>(
type: E,
id?: string,
class_list?: string
): HTMLElementTagNameMap[E];
export function el(type: string, id?: string, class_list?: string): HTMLElement | undefined {
const element = document.createElement(type);
if (id !== undefined) {
element.id = id;
}
if (class_list !== undefined) {
element.className = class_list;
}
return element;
}
export type NonEmptyArray<T> = T[] & { 0: T };
export const SVG_NS = "http://www.w3.org/2000/svg";
export function inRange(check: number, start: number, end: number): boolean {
export function in_range(check: number, start: number, end: number): boolean {
if (check >= start && check <= end) return true;
return false;
}
/**
* Gets the `i`th element in a list. Negative indices return null.
* Out of range indices return null
*/
export function at<T>(l: Array<T>, i: number): T | null {
if (i < 0) {
return null;

View file

@ -22,10 +22,10 @@ export enum CpuEvent {
Print,
Reset,
Halt,
MemoryDumped,
MemoryAccessed,
SwitchBank,
SetFlagCarry,
SetVramBank,
}
type VoidDataCpuEventList = CpuEvent.Halt | CpuEvent.Reset | CpuEvent.Cycle;
@ -40,9 +40,9 @@ interface CpuEventMap {
[CpuEvent.InvalidParsed]: { pos: u8; code: u8 };
[CpuEvent.InstructionExecuted]: { instr: Instruction };
[CpuEvent.SwitchBank]: { bank: u2 };
[CpuEvent.SetVramBank]: { bank: u2 };
[CpuEvent.Print]: string;
[CpuEvent.SetFlagCarry]: boolean;
[CpuEvent.MemoryDumped]: { memory: [Uint8Array, Uint8Array, Uint8Array, Uint8Array] };
}
export interface CpuEventHandler extends EventHandler<CpuEvent> {
@ -68,17 +68,14 @@ export enum UiCpuSignal {
RequestRegisterChange,
RequestCpuReset,
RequestMemoryDump,
RequestProgramCounterChange,
}
type VoidDataUiCpuSignalList = UiCpuSignal.RequestCpuReset;
type VoidDataUiCpuSignalList = UiCpuSignal.RequestCpuReset | UiCpuSignal.RequestMemoryDump;
interface UiCpuSignalMap {
[UiCpuSignal.RequestCpuCycle]: number;
[UiCpuSignal.RequestMemoryChange]: { address: u8; bank: u2; value: u8 };
[UiCpuSignal.RequestRegisterChange]: { register_no: u3; value: u8 };
[UiCpuSignal.RequestProgramCounterChange]: { address: u8 };
[UiCpuSignal.RequestMemoryDump]: (memory: [Uint8Array, Uint8Array, Uint8Array, Uint8Array]) => void;
}
export interface UiCpuSignalHandler extends EventHandler<UiCpuSignal> {

View file

@ -26,40 +26,31 @@
<button type="button" id="edit_button"></button>
</div>
<span id="cycles"></span>
<div id="memory_bank_view"></div>
<div id="reset_buttons"></div>
<div id="memory_bank_view">
<div id="bank_boxes">
<button class="nostyle">1</button>
<button class="nostyle selected">2</button>
<button class="nostyle">3</button>
<button class="nostyle">4</button>
</div>
<script>
const d = document.getElementById("bank_boxes");
const a = [...d.children];
for (const b of a) {
b.addEventListener("click", () => {
a.forEach((ab) => ab.classList.remove("selected"));
b.classList.add("selected");
});
}
console.log(a);
</script>
</div>
</div>
<div id="window_box">
<div id="instruction_explainer"></div>
<div id="printout"></div>
<div id="tv"></div>
<div id="bank_viz">
<div id="visualization">
<!-- TODO make these generated by software -->
<div id="cpu">CPU</div>
<svg id="cpu_bank" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 100">
<polyline points="0,43 7,43 7,12.5 20,12.5" stroke="yellow" />
<polyline points="0,47.5 13,47.5 13,37.5 20,37.5" stroke="gray" />
<polyline points="0,52.5 13,52.5 13,62.5 20,62.5" stroke="gray" />
<polyline points="0,57 7,57 7,87.5 20,87.5" stroke="gray" />
</svg>
<div id="targets">
<div id="target">BANK 0</div>
<div id="target" class="gray">BANK 1</div>
<div id="target" class="gray">BANK 2</div>
<div id="target" class="gray">BANK 3</div>
</div>
<svg id="vram_bank" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 100">
<polyline points="0,43 7,43 7,12.5 20,12.5" stroke="gray" />
<polyline points="0,47.5 13,47.5 13,37.5 20,37.5" stroke="gray" />
<polyline points="0,52.5 13,52.5 13,62.5 20,62.5" stroke="gray" />
<polyline points="0,57 7,57 7,87.5 20,87.5" stroke="yellow" />
</svg>
<div id="cpu">VRAM</div>
</div>
</div>
</div>
</main>

View file

@ -6,7 +6,7 @@
import { Computer } from "./computer";
import { $ } from "./etc";
import { ISA } from "./instructionSet";
import { generateIsa } from "./isaGenerator";
import { generate_isa } from "./isaGenerator";
import { UI } from "./ui";
import { u8 } from "./num";
@ -41,13 +41,13 @@ function main(): void {
const computer = new Computer();
const ui = new UI();
ui.initEvents(computer.events);
computer.loadMemory(program);
computer.initEvents(ui.cpu_signaler);
ui.init_events(computer.events);
computer.load_memory(program);
computer.init_events(ui.cpu_signaler);
window.comp = computer;
window.ui = ui;
$("ISA").textContent = generateIsa(ISA);
$("ISA").textContent = generate_isa(ISA);
let fire = false;
window.firehose = (): void => {

View file

@ -4,7 +4,7 @@
* @license GPL-3.0
*/
import { CpuEvent, CpuEventHandler } from "./events";
import { formatHex, inRange } from "./etc";
import { format_hex, in_range } from "./etc";
import { isU2, isU3, m256, u2, u3, u8 } from "./num";
export enum ParamType {
@ -50,7 +50,6 @@ interface GenericComputer {
setBank: (bank_no: u2) => void;
getCarry(): boolean;
setCarry(state: boolean): void;
setVramBank(bank: u2): void;
}
interface AfterExecutionComputerAction {
@ -86,7 +85,7 @@ export class InstructionSet {
insertInstruction(hexCode: u8, instruction: Instruction): void {
if (this.instructions.has(hexCode)) {
throw new Error(`Instruction "${formatHex(hexCode)}" already exists`);
throw new Error(`Instruction "${format_hex(hexCode)}" already exists`);
}
this.instructions.set(hexCode, instruction);
}
@ -94,7 +93,7 @@ export class InstructionSet {
addCategory(c: InstrCategory): void {
// Check for overlap with existing ranges
for (const r of this.category_ranges) {
if (inRange(c.start, r.start, r.end) || inRange(c.end, r.start, r.end)) {
if (in_range(c.start, r.start, r.end) || in_range(c.end, r.start, r.end)) {
throw new Error(`Range of ${c.start}...${c.end} is already registered`);
}
}
@ -739,15 +738,3 @@ ISA.insertInstruction(0xf1, {
a.dispatch(CpuEvent.Print, byte.toString(10));
},
});
ISA.insertInstruction(0xff, {
name: "Set VRAM Bank",
desc: "Set memory bank which screen gets pixels from memory bank (P1)",
params: [new ConstParam("memory bank to select")],
execute(c, p, a) {
const bank_no = p[0];
if (!isU2(bank_no)) throw new Error("TO2O");
c.setVramBank(bank_no);
a.dispatch(CpuEvent.SetVramBank, { bank: bank_no });
},
});

View file

@ -3,25 +3,11 @@
* @copyright Alexander Bass 2024
* @license GPL-3.0
*/
import { formatHex, inRange } from "./etc";
import { format_hex, in_range } from "./etc";
import { InstrCategory, Instruction, InstructionSet, ParameterType, ParamType } from "./instructionSet";
import { u8 } from "./num";
import { u8 } from "./num.js";
function parameterDescription(params: Array<ParameterType>): string {
let str = "";
if (params.length !== 0) {
str += " ";
}
for (const p of params) {
const p_map = { [ParamType.Const]: "C", [ParamType.Memory]: "M", [ParamType.Register]: "R" };
const char = p_map[p.type];
str += char;
str += " ";
}
return str;
}
export function generateIsa(iset: InstructionSet): string {
export function generate_isa(iset: InstructionSet): string {
const instructions: Array<[u8, Instruction]> = [];
for (const kv of iset.instructions.entries()) instructions.push(kv);
@ -34,7 +20,7 @@ export function generateIsa(iset: InstructionSet): string {
let current_category: InstrCategory | null = null;
for (const instruction of instructions) {
const cat = iset.category_ranges.find((i) => inRange(instruction[0], i.start, i.end));
const cat = iset.category_ranges.find((i) => in_range(instruction[0], i.start, i.end));
if (cat === undefined) {
throw new Error("Instruction found which is not part of category");
}
@ -42,10 +28,10 @@ export function generateIsa(iset: InstructionSet): string {
output_string += `-- ${cat.name.toUpperCase()} --\n`;
current_category = cat;
}
const hex_code = formatHex(instruction[0]);
const hex_code = format_hex(instruction[0]);
const short_description = instruction[1].name.padEnd(max_instr_name_len, " ");
const parameters = parameterDescription(instruction[1].params);
const parameters = parameter_description(instruction[1].params);
const description = instruction[1].desc;
output_string += `0x${hex_code}: ${short_description}`;
if (parameters.length !== 0) {
@ -57,3 +43,17 @@ export function generateIsa(iset: InstructionSet): string {
}
return output_string;
}
function parameter_description(params: Array<ParameterType>): string {
let str = "";
if (params.length !== 0) {
str += " ";
}
for (const p of params) {
const p_map = { [ParamType.Const]: "C", [ParamType.Memory]: "M", [ParamType.Register]: "R" };
const char = p_map[p.type];
str += char;
str += " ";
}
return str;
}

View file

@ -1,12 +1,6 @@
#memory {
.celled_viewer.selected {
grid-template-columns: repeat(16, min-content);
// display: block;
display: grid;
}
.celled_viewer {
display: none;
}
grid-template-columns: repeat(16, min-content);
.program_counter {
outline: 3px solid orange;
}

View file

@ -25,7 +25,6 @@ body {
main {
display: flex;
justify-content: center;
align-items: flex-start;
}
#grid {
@ -34,12 +33,13 @@ main {
grid-template-columns: min-content min-content min-content;
grid-template-rows: min-content min-content min-content;
grid-template-areas:
". regmemlabel . cycles"
". registers . bank "
"reset memory memory memory"
". regmemlabel . cycles "
". registers . bank "
"title memory memory memory"
". buttons buttons buttons";
". buttons buttons buttons ";
#memory {
grid-area: memory;
}
#window_box {
grid-area: windowbox;
}
@ -66,25 +66,6 @@ main {
user-select: none;
transform: scale(-1, -1);
}
#memory {
grid-area: memory;
}
#reset_buttons {
grid-area: reset;
}
}
#reset_buttons {
flex-direction: column;
display: flex;
align-items: end;
button {
margin-bottom: 7px;
margin-right: 7px;
border: 5px solid yellow;
padding: 5px;
width: fit-content;
}
}
#labelcontainer {

View file

@ -4,7 +4,7 @@
height: 18px;
position: absolute;
bottom: 0;
border-bottom: 5px solid var(--border);
border-bottom: 5px solid yellow;
cursor: s-resize;
}
@ -15,12 +15,6 @@
gap: 10px;
margin-left: 10px;
width: 500px;
.window:not(:has(#resize)) {
border-bottom: 5px solid var(--border);
&.collapsed {
border-bottom: unset;
}
}
.window {
overflow-y: hidden;
position: relative;
@ -36,18 +30,16 @@
font-size: 0.6em;
color: lightgray;
border-bottom: 5px solid var(--border);
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAHUlEQVQI12P4/5/h////DP8ZoICJAQ0wIstiVQEA3CAI/c69TucAAAAASUVORK5CYII=");
image-rendering: pixelated;
background: repeating-linear-gradient(
to top,
transparent,
transparent 2px,
transparent 2px,
yellow 2px,
yellow 4px
),
repeating-linear-gradient(to right, transparent, transparent 2px, transparent 2px, yellow 2px, yellow 4px);
// background: repeating-linear-gradient(
// to top,
// transparent,
// transparent 2px,
// transparent 2px,
// yellow 2px,
// yellow 4px
// ),
// repeating-linear-gradient(to right, transparent, transparent 2px, transparent 2px, yellow 2px, yellow 4px);
#text {
word-break: keep-all;
white-space: nowrap;
@ -64,6 +56,9 @@
}
}
}
.window.collapsed > :not(:first-child) {
display: none;
}
}
.window#tv {
@ -74,41 +69,6 @@
}
}
#bank_viz {
#visualization {
user-select: none;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-size: 80%;
// max-width: 500px;
#cpu,
#target {
border: 3px solid yellow;
padding: 5px;
}
svg polyline {
stroke-linecap: butt;
stroke-width: 3;
fill: none;
}
svg {
width: 50px;
}
#vram_bank {
transform: scale(-1, 1);
}
#targets {
white-space: nowrap;
display: flex;
flex-direction: column;
gap: 13px;
}
}
}
#instruction_explainer {
display: flex;
flex-direction: column;
@ -126,7 +86,6 @@
}
#expl_icon {
user-select: none;
margin-inline-end: 0.5em;
font-size: 30px;
padding: 5px;

View file

@ -1,20 +1,15 @@
import { CpuEvent, CpuEventHandler, UiCpuSignalHandler, UiEventHandler } from "./events";
import { CpuEvent, CpuEventHandler, UiCpuSignalHandler, UiEvent, UiEventHandler } from "./events";
import { $ } from "./etc";
import UiComponent, { UiComponentConstructor } from "./ui/uiComponent";
// Components
import MemoryView from "./ui/components/memoryView";
import frequencyIndicator from "./ui/components/frequencyIndicator";
import RegisterView from "./ui/components/registerView";
import BankSelector from "./ui/components/bank_view_selector";
import EditButton from "./ui/components/editButton";
import pausePlay from "./ui/components/pausePlay";
import SaveLoad from "./ui/components/saveLoad";
import ResetButtons from "./ui/components/reset_buttons";
// Window Components
import InstructionExplainer from "./ui/windows/instructionExplainer";
import Screen from "./ui/windows/screen";
import Printout from "./ui/windows/printout";
import BankVisualizer from "./ui/windows/bank_visualizer";
import { InstructionExplainer } from "./ui/windows/instructionExplainer";
import { MemoryView } from "./ui/memoryView";
import { frequencyIndicator } from "./ui/frequencyIndicator";
import { RegisterView } from "./ui/registerView";
import { Screen } from "./ui/windows/screen";
import { EditButton } from "./ui/editButton";
import { UiComponent, UiComponentConstructor } from "./ui/uiComponent";
import { pausePlay } from "./ui/pausePlay";
import { Printout } from "./ui/windows/printout";
import { SaveLoad } from "./ui/saveLoad";
export class UI {
ui_events: UiEventHandler = new UiEventHandler();
@ -31,9 +26,6 @@ export class UI {
this.register_component(EditButton, $("edit_button"));
this.register_component(pausePlay, $("controls_buttons"));
this.register_component(SaveLoad, $("save_load_buttons"));
this.register_component(BankSelector, $("memory_bank_view"));
this.register_component(BankVisualizer, $("bank_viz"));
this.register_component(ResetButtons, $("reset_buttons"));
}
private register_component(ctor: UiComponentConstructor, e: HTMLElement): void {
@ -46,12 +38,12 @@ export class UI {
this.components.push(component);
}
initEvents(cpu_events: CpuEventHandler): void {
init_events(cpu_events: CpuEventHandler): void {
cpu_events.listen(CpuEvent.Reset, () => {
this.reset();
});
for (const c of this.components) if (c.initCpuEvents) c.initCpuEvents(cpu_events);
for (const c of this.components) if (c.init_cpu_events) c.init_cpu_events(cpu_events);
}
reset(): void {

View file

@ -3,77 +3,63 @@
* @copyright Alexander Bass 2024
* @license GPL-3.0
*/
import { NonEmptyArray, el, formatHex } from "../etc";
import { NonEmptyArray, el, format_hex } from "../etc";
import { u8 } from "../num";
import EditorContext from "./editableHex";
interface GenericCell {
el: HTMLElement;
}
export default class CelledViewer {
export abstract class CelledViewer {
cells: Array<GenericCell> = [];
readonly width: number;
readonly height: number;
container: HTMLElement;
editor: EditorContext;
constructor(width: number, height: number, element: HTMLElement, edit_callback: (address: u8, value: u8) => void) {
this.container = element;
width: number;
height: number;
element: HTMLElement;
constructor(width: number, height: number, element: HTMLElement) {
this.element = element;
this.width = width;
this.height = height;
this.container.classList.add("celled_viewer");
this.element.classList.add("celled_viewer");
for (let i = 0; i < this.width * this.height; i++) {
const mem_cell_el = el("div").fin();
const mem_cell_el = el("div");
mem_cell_el.append("0", "0");
this.container.appendChild(mem_cell_el);
this.element.appendChild(mem_cell_el);
const mem_cell = { el: mem_cell_el };
this.cells.push(mem_cell);
}
const list = this.cells.map((c) => c.el);
this.editor = new EditorContext(list, this.width, this.height, (address, value) => {
edit_callback(address, value);
});
}
reset(): void {
for (let i = 0; i < this.height * this.width; i++) {
this.setCellValue(i as u8, 0);
this.set_cell_value(i as u8, 0);
this.cells[i].el.className = "";
}
}
addCellClass(address: u8, ...css_class: NonEmptyArray<string>): void {
add_cell_class(address: u8, ...css_class: NonEmptyArray<string>): void {
for (const str of css_class) {
this.cells[address].el.classList.add(str);
}
}
removeCellClass(address: u8, ...css_class: NonEmptyArray<string>): void {
remove_cell_class(address: u8, ...css_class: NonEmptyArray<string>): void {
for (const str of css_class) {
this.cells[address].el.classList.remove(str);
}
}
removeAllCellClass(css_class: string): void {
remove_all_cell_class(css_class: string): void {
for (const cell of this.cells) {
cell.el.classList.remove(css_class);
}
}
clearAllClasses(): void {
for (const cell of this.cells) {
cell.el.className = "";
}
add_cell_class_exclusive(address: u8, css_class: string): void {
this.remove_all_cell_class(css_class);
this.add_cell_class(address, css_class);
}
addCellClassExclusive(address: u8, css_class: string): void {
this.removeAllCellClass(css_class);
this.addCellClass(address, css_class);
}
setCellValue(address: u8, value: u8): void {
const str = formatHex(value);
set_cell_value(address: u8, value: u8): void {
const str = format_hex(value);
const a = str[0];
const b = str[1];
this.cells[address].el.textContent = "";

View file

@ -1,35 +0,0 @@
import { el } from "../../etc";
import { u2 } from "../../num";
import { UiEventHandler, UiEvent } from "../../events";
import UiComponent from "../uiComponent";
export default class BankSelector implements UiComponent {
container: HTMLElement;
events: UiEventHandler;
private bank_buttons: Array<HTMLButtonElement>;
constructor(element: HTMLElement, events: UiEventHandler) {
this.container = element;
this.events = events;
const bank_boxes = el("div").id("bank_boxes").fin();
this.bank_buttons = [];
for (let i = 0; i < 4; i++) {
const button = el("button").cl("nostyle").fin();
bank_boxes.appendChild(button);
button.addEventListener("click", () => {
for (const b of this.bank_buttons) b.classList.remove("selected");
button.classList.add("selected");
this.events.dispatch(UiEvent.ChangeViewBank, { bank: i as u2 });
});
button.textContent = i.toString();
this.bank_buttons.push(button);
}
this.bank_buttons[0].classList.add("selected");
this.container.appendChild(bank_boxes);
}
reset(): void {
for (const b of this.bank_buttons) b.classList.remove("selected");
this.bank_buttons[0].classList.add("selected");
}
}

View file

@ -1,41 +0,0 @@
import { el, $ } from "../../etc";
import { UiEventHandler, UiEvent, UiCpuSignalHandler, UiCpuSignal } from "../../events";
import UiComponent from "../uiComponent";
export default class EditButton implements UiComponent {
container: HTMLElement;
events: UiEventHandler;
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, event: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
this.container = element;
this.events = event;
this.cpu_signals = cpu_signals;
const image = el("img").at("src", "pencil.png").st("width", "20px").st("height", "20px").fin();
this.container.classList.add("editor_toggle");
this.container.addEventListener("click", () => this.edit_toggle());
this.container.appendChild(image);
}
reset(): void {
const is_on = this.container.classList.contains("on");
if (is_on) {
this.edit_toggle();
}
}
edit_toggle(): void {
const is_on = this.container.classList.contains("on");
if (is_on) {
this.container.classList.remove("on");
$("root").classList.remove("editor");
this.container.classList.add("off");
this.events.dispatch(UiEvent.EditOff);
} else {
this.events.dispatch(UiEvent.EditOn);
$("root").classList.add("editor");
this.container.classList.add("on");
this.container.classList.remove("off");
this.cpu_signals.dispatch(UiCpuSignal.RequestProgramCounterChange, { address: 0 });
}
}
}

View file

@ -1,126 +0,0 @@
import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, UiEventHandler } from "../../events";
import { ParamType } from "../../instructionSet";
import { u2, u8 } from "../../num";
import UiComponent from "../uiComponent";
import { el } from "../../etc";
import CelledViewer from "../celledViewer";
/** Only to be run once */
function createBanks(
element: HTMLElement,
edit_callback: (address: u8, bank: u2, value: u8) => void
): [CelledViewer, CelledViewer, CelledViewer, CelledViewer] {
const list: Array<CelledViewer> = [];
for (let i = 0; i < 4; i++) {
const child = el("div").fin();
list.push(
new CelledViewer(16, 16, child, (address: u8, value: u8) => {
edit_callback(address, i as u2, value);
})
);
element.appendChild(child);
}
list[0].container.classList.add("selected");
return list as [CelledViewer, CelledViewer, CelledViewer, CelledViewer];
}
export default class MemoryView implements UiComponent {
container: HTMLElement;
program_counter: u8 = 0;
last_accessed_cell: { address: u8; bank: u2 } | null = null;
events: UiEventHandler;
cpu_signals: UiCpuSignalHandler;
banks: [CelledViewer, CelledViewer, CelledViewer, CelledViewer];
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
this.container = element;
this.events = events;
this.cpu_signals = cpu_signals;
this.banks = createBanks(element, (address, bank, value) => {
cpu_signals.dispatch(UiCpuSignal.RequestMemoryChange, { address, bank, value });
});
for (const bank of this.banks) {
this.events.listen(UiEvent.EditOn, () => {
bank.editor.enable();
bank.clearAllClasses();
});
this.events.listen(UiEvent.EditOff, () => {
bank.editor.disable();
bank.clearAllClasses();
});
}
this.events.listen(UiEvent.ChangeViewBank, ({ bank }) => this.setBank(bank));
}
get program(): CelledViewer {
return this.banks[0];
}
setBank(bank: u2): void {
for (const bank of this.banks) bank.container.classList.remove("selected");
this.banks[bank].container.classList.add("selected");
}
setProgramCounter(position: u8): void {
this.program.removeCellClass(this.program_counter, "program_counter");
this.program.addCellClass(position, "program_counter");
this.program_counter = position;
}
reset(): void {
for (const viewer of this.banks) viewer.reset();
this.last_accessed_cell = null;
this.setProgramCounter(0);
}
initCpuEvents(c: CpuEventHandler): void {
c.listen(CpuEvent.MemoryAccessed, ({ address, bank, value }) => {
if (this.last_accessed_cell?.address !== address || this.last_accessed_cell?.bank !== bank) {
if (this.last_accessed_cell !== null) {
this.banks[this.last_accessed_cell.bank].removeCellClass(this.last_accessed_cell.address, "last_access");
}
this.banks[bank].addCellClass(address, "last_access");
this.last_accessed_cell = { address, bank };
}
});
c.listen(CpuEvent.MemoryChanged, ({ address, bank, value }) => {
if (bank !== 0) {
return;
}
this.banks[bank].setCellValue(address, value);
});
c.listen(CpuEvent.ProgramCounterChanged, ({ counter }) => {
this.setProgramCounter(counter);
});
c.listen(CpuEvent.ParameterParsed, ({ param, code, pos }) => {
this.program.addCellClass(pos, "instruction_argument");
const t = param.type;
this.program.removeCellClass(pos, "constant", "register", "memory", "instruction", "invalid");
let name: string = "";
if (t === ParamType.Const) {
name = "constant";
} else if (t === ParamType.Memory) {
name = "memory";
} else if (t === ParamType.Register) {
name = "register";
} else {
throw new Error("unreachable");
}
this.program.addCellClass(pos, name);
});
c.listen(CpuEvent.InstructionParsed, ({ instr, code, pos }) => {
this.program.removeAllCellClass("instruction_argument");
this.program.removeAllCellClass("current_instruction");
this.program.removeCellClass(pos, "constant", "register", "memory", "invalid");
this.program.addCellClass(pos, "current_instruction");
this.program.addCellClass(pos, "instruction");
});
c.listen(CpuEvent.InvalidParsed, ({ code, pos }) => {
this.program.removeCellClass(pos, "constant", "register", "memory", "instruction");
this.program.addCellClass(pos, "invalid");
});
}
}

View file

@ -1,31 +0,0 @@
import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, UiEventHandler } from "../../events";
import { isU3, u3, u8 } from "../../num";
import CelledViewer from "../celledViewer";
import UiComponent from "../uiComponent";
export default class RegisterView extends CelledViewer implements UiComponent {
events: UiEventHandler;
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(8, 1, element, (address: u8, value: u8) => {
if (!isU3(address)) throw new Error("unreachable");
this.cpu_signals.dispatch(UiCpuSignal.RequestRegisterChange, { register_no: address as u3, value });
});
this.events = events;
this.cpu_signals = cpu_signals;
this.events.listen(UiEvent.EditOn, () => {
this.editor.enable();
for (const cell of this.cells) cell.el.className = "";
});
this.events.listen(UiEvent.EditOff, () => {
this.editor.disable();
for (const cell of this.cells) cell.el.className = "";
});
}
initCpuEvents(c: CpuEventHandler): void {
c.listen(CpuEvent.RegisterChanged, ({ register_no, value }) => this.setCellValue(register_no, value));
c.listen(CpuEvent.Reset, () => this.reset());
}
}

View file

@ -1,26 +0,0 @@
import { el } from "../../etc";
import { UiEventHandler, UiCpuSignalHandler, UiEvent, UiCpuSignal } from "../../events";
import UiComponent from "../uiComponent";
export default class ResetButtons implements UiComponent {
container: HTMLElement;
events: UiEventHandler;
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
this.container = element;
this.events = events;
this.cpu_signals = cpu_signals;
const reset_button = el("button").cl("nostyle").tx("R").fin();
const trash_button = el("button").cl("nostyle").tx("T").fin();
reset_button.addEventListener("click", () => this.resetClicked());
trash_button.addEventListener("click", () => this.trashClicked());
this.container.append(reset_button, trash_button);
}
resetClicked(): void {}
trashClicked(): void {
this.cpu_signals.dispatch(UiCpuSignal.RequestCpuReset);
}
}

View file

@ -1,90 +0,0 @@
import { el } from "../../etc";
import { UiEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../../events";
import { u2, u8, m256, isU2 } from "../../num";
import UiComponent from "../uiComponent";
export default class SaveLoad implements UiComponent {
container: HTMLElement;
events: UiEventHandler;
save_button: HTMLButtonElement;
binary_upload: HTMLInputElement;
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
this.container = element;
this.events = events;
this.cpu_signals = cpu_signals;
this.save_button = el("button").id("save_button").tx("Save").fin();
this.binary_upload = el("input")
.id("binary_upload")
.at("type", "file")
.at("name", "binary_upload")
.st("display", "none")
.fin();
const label = el("label").cl("button").at("for", "binary_upload").tx("Load Binary").fin();
this.container.append(this.binary_upload, label, this.save_button);
this.save_button.addEventListener("click", () => {
this.download();
});
this.binary_upload.addEventListener("change", (e) => {
this.uploadChanged(e);
});
}
private download(): void {
this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryDump, (memory) => {
const flattened = new Uint8Array(256 * memory.length);
for (let x = 0; x < 4; x++) {
for (let y = 0; y < 256; x++) {
flattened[256 * x + y] = memory[x][y];
}
}
const blob = new Blob([flattened], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "bin.bin";
link.click();
link.remove();
});
}
private uploadChanged(e: Event): void {
const t = e.target;
if (t === null) return;
const file = (t as HTMLInputElement).files?.[0];
if (file === undefined) {
console.error("No files attribute on file input");
return;
}
const reader = new FileReader();
reader.addEventListener("load", (e) => {
const target = e.target;
if (target === null) return;
const data = target.result;
if (!(data instanceof ArrayBuffer)) {
console.error("Data is not arraybuffer");
return;
}
const view = new Uint8Array(data);
const array = [...view] as Array<u8>;
this.cpu_signals.dispatch(UiCpuSignal.RequestCpuReset);
for (const [i, v] of array.entries()) {
const address = m256(i);
const bank = Math.floor(i / 256);
if (!isU2(bank)) {
// throw new Error("Too many banks in data file");
return;
}
this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryChange, { address, bank, value: v });
}
});
reader.readAsArrayBuffer(file);
}
}

41
src/ui/editButton.ts Normal file
View file

@ -0,0 +1,41 @@
import { el, $ } from "../etc";
import { UiEventHandler, UiEvent } from "../events";
import { UiComponent } from "./uiComponent";
export class EditButton implements UiComponent {
element: HTMLElement;
events: UiEventHandler;
constructor(element: HTMLElement, event: UiEventHandler) {
this.element = element;
this.events = event;
const image = el("img");
image.src = "pencil.png";
image.style.width = "20px";
image.style.height = "20px";
this.element.classList.add("editor_toggle");
this.element.addEventListener("click", () => this.edit_toggle());
this.element.appendChild(image);
}
reset(): void {
const is_on = this.element.classList.contains("on");
if (is_on) {
this.edit_toggle();
}
}
edit_toggle(): void {
const is_on = this.element.classList.contains("on");
if (is_on) {
this.element.classList.remove("on");
$("root").classList.remove("editor");
this.element.classList.add("off");
this.events.dispatch(UiEvent.EditOff);
} else {
this.events.dispatch(UiEvent.EditOn);
$("root").classList.add("editor");
this.element.classList.add("on");
this.element.classList.remove("off");
}
}
}

View file

@ -1,23 +1,18 @@
// This file was cobbled together and is the messiest part of this project
import { at } from "../etc";
import { isU8, u8 } from "../num";
import { u8 } from "../num";
const HEX_CHARACTERS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
export default class EditorContext {
export class EditorContext {
private list: Array<HTMLElement>;
private width: number;
private height: number;
private enabled: boolean = false;
private current_cell_info: { left?: string; right?: string; old?: string };
private edit_callback: (n: u8, value: u8) => void;
constructor(list: Array<HTMLElement>, width: number, height: number, callback: (n: u8, value: u8) => void) {
if (!isU8(width * height - 1)) {
throw new RangeError("Grid is too big for editor. Maximum area is 256");
}
private edit_callback: (n: number, value: u8) => void;
constructor(list: Array<HTMLElement>, width: number, callback: (n: number, value: u8) => void) {
this.list = list;
this.height = height;
this.width = width;
this.edit_callback = callback;
this.current_cell_info = {};
@ -40,7 +35,7 @@ export default class EditorContext {
this.current_cell_info.right = undefined;
cell.classList.add("caret_selected");
// Reset cursor position (there's an API for this, but this is a simpler, more robust solution)
// Reset cursor position (I know there's an API for this, but this is a simpler, more robust solution)
cell.textContent = cell.textContent ?? "00";
});
@ -54,7 +49,7 @@ export default class EditorContext {
const text = `${left}${right}`;
cell.textContent = text;
const val = Number.parseInt(text, 16) as u8;
this.edit_callback(i as u8, val);
this.edit_callback(i, val);
cell.classList.add("recent_edit");
}
});

View file

@ -1,15 +1,15 @@
import { CpuEvent, CpuEventHandler, UiEventHandler } from "../../events";
import UiComponent from "../uiComponent";
import { CpuEvent, CpuEventHandler, UiEventHandler } from "../events";
import { UiComponent } from "./uiComponent";
export default class frequencyIndicator implements UiComponent {
container: HTMLElement;
export class frequencyIndicator implements UiComponent {
element: HTMLElement;
private running: number | null = null;
private count: number = 0;
private last_value: number = 0;
private last_time: number = 0;
events: UiEventHandler;
constructor(element: HTMLElement, events: UiEventHandler) {
this.container = element;
this.element = element;
this.events = events;
this.start();
}
@ -34,11 +34,11 @@ export default class frequencyIndicator implements UiComponent {
const value = Math.round(this.count / dt);
if (this.last_value !== value) {
this.container.textContent = `${value}hz`;
this.element.textContent = `${value}hz`;
this.last_value = value;
}
if (value === 0) {
this.container.textContent = "";
this.element.textContent = "";
}
this.last_time = new_time;
this.count = 0;
@ -51,9 +51,8 @@ export default class frequencyIndicator implements UiComponent {
this.stop();
this.count = 0;
this.last_value = 0;
this.start();
}
initCpuEvents(c: CpuEventHandler): void {
init_cpu_events(c: CpuEventHandler): void {
c.listen(CpuEvent.Cycle, () => {
this.count += 1;
});

97
src/ui/memoryView.ts Normal file
View file

@ -0,0 +1,97 @@
import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, UiEventHandler } from "../events";
import { ParamType } from "../instructionSet";
import { u8 } from "../num.js";
import { UiComponent } from "./uiComponent";
import { CelledViewer } from "./celledViewer";
import { EditorContext } from "./editableHex";
export class MemoryView extends CelledViewer implements UiComponent {
program_counter: u8 = 0;
last_accessed_cell: u8 | null = null;
events: UiEventHandler;
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(16, 16, element);
this.program_counter = 0;
this.events = events;
this.cpu_signals = cpu_signals;
const list = this.cells.map((c) => c.el);
const editor = new EditorContext(list, this.width, (i, value) => {
this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryChange, { address: i as u8, bank: 0, value });
});
this.events.listen(UiEvent.EditOn, () => {
editor.enable();
for (const cell of this.cells) {
cell.el.className = "";
}
});
this.events.listen(UiEvent.EditOff, () => {
editor.disable();
for (const cell of this.cells) {
cell.el.className = "";
}
});
}
set_program_counter(position: u8): void {
this.remove_cell_class(this.program_counter, "program_counter");
this.add_cell_class(position, "program_counter");
this.program_counter = position;
}
reset(): void {
super.reset();
this.last_accessed_cell = null;
this.set_program_counter(0);
}
init_cpu_events(c: CpuEventHandler): void {
c.listen(CpuEvent.MemoryAccessed, ({ address, bank, value }) => {
if (bank !== 0) return;
if (this.last_accessed_cell !== address) {
if (this.last_accessed_cell !== null) {
this.remove_cell_class(this.last_accessed_cell, "last_access");
}
this.add_cell_class(address, "last_access");
this.last_accessed_cell = address;
}
});
c.listen(CpuEvent.MemoryChanged, ({ address, bank, value }) => {
if (bank !== 0) {
return;
}
this.set_cell_value(address, value);
});
c.listen(CpuEvent.ProgramCounterChanged, ({ counter }) => {
this.set_program_counter(counter);
});
c.listen(CpuEvent.ParameterParsed, ({ param, code, pos }) => {
this.add_cell_class(pos, "instruction_argument");
const t = param.type;
this.remove_cell_class(pos, "constant", "register", "memory", "instruction", "invalid");
let name: string = "";
if (t === ParamType.Const) {
name = "constant";
} else if (t === ParamType.Memory) {
name = "memory";
} else if (t === ParamType.Register) {
name = "register";
} else {
throw new Error("unreachable");
}
this.add_cell_class(pos, name);
});
c.listen(CpuEvent.InstructionParsed, ({ instr, code, pos }) => {
this.remove_all_cell_class("instruction_argument");
this.remove_all_cell_class("current_instruction");
this.remove_cell_class(pos, "constant", "register", "memory", "invalid");
this.add_cell_class(pos, "current_instruction");
this.add_cell_class(pos, "instruction");
});
c.listen(CpuEvent.InvalidParsed, ({ code, pos }) => {
this.remove_cell_class(pos, "constant", "register", "memory", "instruction");
this.add_cell_class(pos, "invalid");
});
}
}

View file

@ -1,11 +1,11 @@
import { el } from "../../etc";
import { UiEventHandler, UiEvent, CpuEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../../events";
import UiComponent from "../uiComponent";
import { el } from "../etc";
import { UiEventHandler, UiEvent, CpuEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../events";
import { UiComponent } from "./uiComponent";
const MAX_SLIDER = 1000;
export default class pausePlay implements UiComponent {
container: HTMLElement;
export class pausePlay implements UiComponent {
element: HTMLElement;
start_button: HTMLButtonElement;
step_button: HTMLButtonElement;
range: HTMLInputElement;
@ -15,32 +15,35 @@ export default class pausePlay implements UiComponent {
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
this.container = element;
this.element = element;
this.events = events;
this.cpu_signals = cpu_signals;
this.start_button = el("button").id("pause_play_button").tx("Start").fin();
this.step_button = el("button").id("step_button").tx("Step").fin();
this.range = el("input")
.id("speed_range")
.at("type", "range")
.at("min", "0")
.at("max", MAX_SLIDER.toString())
.at("value", "0")
.fin();
this.start_button = el("button", "pause_play_button");
this.step_button = el("button", "step_button");
this.range = el("input", "speed_range");
this.range.max = MAX_SLIDER.toString();
this.range.min = "0";
this.range.type = "range";
this.start_button.addEventListener("click", () => this.toggle());
this.step_button.addEventListener("click", () => this.step());
this.range.addEventListener("input", (e) => {
const delay = MAX_SLIDER - parseInt((e.target as HTMLInputElement).value, 10) + 10;
this.cycle_delay = delay;
});
this.container.appendChild(this.start_button);
this.container.appendChild(this.step_button);
this.container.appendChild(this.range);
this.start_button.textContent = "Start";
this.step_button.textContent = "Step";
this.element.appendChild(this.start_button);
this.element.appendChild(this.step_button);
this.element.appendChild(this.range);
this.cycle_delay = 1000;
this.range.value = "0";
this.events.listen(UiEvent.EditOn, () => this.disable());
this.events.listen(UiEvent.EditOff, () => this.enable());
this.events.listen(UiEvent.EditOn, () => {
this.disable();
});
this.events.listen(UiEvent.EditOff, () => {
this.enable();
});
}
disable(): void {
@ -69,8 +72,9 @@ export default class pausePlay implements UiComponent {
private cycle(): void {
const loop = (): void => {
if (this.on === false) return;
if (this.on === false) {
return;
}
this.cpu_signals.dispatch(UiCpuSignal.RequestCpuCycle, 1);
setTimeout(loop, this.cycle_delay);
};
@ -85,13 +89,15 @@ export default class pausePlay implements UiComponent {
}
start(): void {
if (this.on) return;
this.toggle();
if (!this.on) {
this.toggle();
}
}
stop(): void {
if (!this.on) return;
this.toggle();
if (this.on) {
this.toggle();
}
}
reset(): void {

38
src/ui/registerView.ts Normal file
View file

@ -0,0 +1,38 @@
import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, UiEventHandler } from "../events";
import { u3 } from "../num";
import { CelledViewer } from "./celledViewer";
import { EditorContext } from "./editableHex";
import { UiComponent } from "./uiComponent";
export class RegisterView extends CelledViewer implements UiComponent {
events: UiEventHandler;
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(8, 1, element);
this.events = events;
this.cpu_signals = cpu_signals;
const list = this.cells.map((c) => c.el);
const editor = new EditorContext(list, this.width, (i, value) => {
this.cpu_signals.dispatch(UiCpuSignal.RequestRegisterChange, { register_no: i as u3, value });
});
this.events.listen(UiEvent.EditOn, () => {
editor.enable();
for (const cell of this.cells) {
cell.el.className = "";
}
});
this.events.listen(UiEvent.EditOff, () => {
editor.disable();
for (const cell of this.cells) {
cell.el.className = "";
}
});
}
init_cpu_events(c: CpuEventHandler): void {
c.listen(CpuEvent.RegisterChanged, ({ register_no, value }) => {
this.set_cell_value(register_no, value);
});
}
}

91
src/ui/saveLoad.ts Normal file
View file

@ -0,0 +1,91 @@
import { el } from "../etc";
import { UiEventHandler, CpuEventHandler, CpuEvent, UiCpuSignalHandler, UiCpuSignal } from "../events";
import { u2, u8, m256 } from "../num";
import { UiComponent } from "./uiComponent";
export class SaveLoad implements UiComponent {
element: HTMLElement;
events: UiEventHandler;
save_button: HTMLButtonElement;
binary_upload: HTMLInputElement;
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
this.element = element;
this.events = events;
this.cpu_signals = cpu_signals;
this.save_button = el("button", "save_button");
this.binary_upload = el("input", "binary_upload");
this.binary_upload.type = "file";
this.binary_upload.name = "binary_upload";
this.binary_upload.style.display = "none";
const label = el("label");
this.save_button.textContent = "Save";
label.textContent = "Load Binary";
label.classList.add("button");
label.setAttribute("for", "binary_upload");
this.element.appendChild(this.binary_upload);
this.element.appendChild(label);
this.element.appendChild(this.save_button);
this.save_button.addEventListener("click", () => {
this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryDump);
});
this.binary_upload.addEventListener("change", (e) => {
this.upload_changed(e);
});
}
private upload_changed(e: Event): void {
const t = e.target;
if (t === null) {
return;
}
const file: File | undefined = (t as HTMLInputElement).files?.[0];
if (file === undefined) {
console.log("No files attribute on file input");
return;
}
const reader = new FileReader();
console.log(file);
reader.addEventListener("load", (e) => {
if (e.target !== null) {
const data = e.target.result;
if (data instanceof ArrayBuffer) {
const view = new Uint8Array(data);
const array = [...view] as Array<u8>;
this.cpu_signals.dispatch(UiCpuSignal.RequestCpuReset);
for (const [i, v] of array.entries()) {
const address = m256(i);
const bank = Math.floor(i / 256) as u2;
this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryChange, { address, bank, value: v });
}
} else {
console.log("not array");
}
}
});
reader.readAsArrayBuffer(file);
}
// eslint-disable-next-line class-methods-use-this
init_cpu_events(e: CpuEventHandler): void {
e.listen(CpuEvent.MemoryDumped, ({ memory }) => {
const flattened = new Uint8Array(256 * memory.length);
for (let x = 0; x < 4; x++) {
for (let y = 0; y < 256; x++) {
flattened[256 * x + y] = memory[x][y];
}
}
const blob = new Blob([flattened], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "bin.bin";
link.click();
link.remove();
});
}
}

View file

@ -12,17 +12,17 @@ import { CpuEventHandler, UiCpuSignalHandler, UiEventHandler } from "../events";
// - CpuEventHandler: listen to events created as a result of CPU actions
// - UiCpuEventSignaler: dispatch signals to request actions from the CPU
export default interface UiComponent {
container: HTMLElement;
export interface UiComponent {
element: HTMLElement;
/** Allows listening and emitting UiEvents*/
events: UiEventHandler;
/** Creating signals for the cpu to process */
cpu_signals?: UiCpuSignalHandler;
/** Completely reset the state of the component */
reset?: () => void;
softReset?: () => void;
soft_reset?: () => void;
/** Allows listening CPUEvents*/
initCpuEvents?: (c: CpuEventHandler) => void;
init_cpu_events?: (c: CpuEventHandler) => void;
}
export interface UiComponentConstructor {

View file

@ -1,79 +1,53 @@
import { el } from "../etc";
interface WindowBoxOptions {
collapsed?: boolean;
fit_content?: boolean;
}
const BORDER_STROKE = 5; // px
export default abstract class WindowBox {
container: HTMLElement;
export abstract class WindowBox {
element: HTMLElement;
title_bar: HTMLElement;
readonly title: string;
private resize: HTMLElement;
private collapse_button: HTMLButtonElement;
private collapsed = false;
private fit_content = false;
private resize?: HTMLElement;
private resize_func?: (e: MouseEvent) => void;
private collapsed: boolean = false;
private resize_func: (e: MouseEvent) => void;
constructor(element: HTMLElement, title: string, options?: WindowBoxOptions) {
this.container = element;
constructor(element: HTMLElement, title: string, options?: { collapsed?: boolean }) {
this.element = element;
this.title = title;
this.container.classList.add("window");
this.title_bar = el("div").cl("window_title").fin();
if (this.container.firstChild !== null) {
this.container.firstChild.before(this.title_bar);
} else {
this.container.appendChild(this.title_bar);
}
const title_bar_text_box = el("div").id("text").fin();
this.element.classList.add("window");
this.title_bar = el("div", undefined, "window_title");
this.element.appendChild(this.title_bar);
const title_bar_text_box = el("div", "text");
title_bar_text_box.textContent = title;
this.collapse_button = el("button").id("collapse_button").cl("nostyle").fin();
this.collapse_button.addEventListener("click", () => this.toggleCollapse());
this.title_bar.appendChild(title_bar_text_box);
this.resize = el("div", "resize");
this.element.appendChild(this.resize);
this.resize_func = this.resize_move.bind(this);
this.collapse_button = el("button", "collapse_button", "nostyle");
this.collapse_button.addEventListener("click", () => {
this.toggle_collapse();
});
this.title_bar.appendChild(this.collapse_button);
if (options?.collapsed) this.collapse();
if (options?.fit_content) {
this.fit_content = true;
} else {
this.resize = el("div").id("resize").fin();
this.container.appendChild(this.resize);
this.resize_func = this.resize_move.bind(this);
this.resize.addEventListener("mousedown", (e) => {
if (this.resize_func) window.addEventListener("mousemove", this.resize_func);
});
window.addEventListener("mouseup", () => this.removeResizeListeners());
window.addEventListener("mouseleave", () => this.removeResizeListeners());
this.resize.addEventListener("mousedown", (e) => {
window.addEventListener("mousemove", this.resize_func);
});
window.addEventListener("mouseup", () => {
this.remove_resize_listeners();
});
window.addEventListener("mouseleave", () => {
this.remove_resize_listeners();
});
if (options?.collapsed) {
this.collapse();
}
}
collapse(): void {
this.container.classList.add("collapsed");
this.removeResizeListeners();
if (this.resize) this.resize.style.visibility = "hidden";
this.setHeight(this.title_bar.offsetHeight - BORDER_STROKE);
this.element.classList.add("collapsed");
this.remove_resize_listeners();
this.resize.style.visibility = "hidden";
this.element.style.height = `${this.title_bar.offsetHeight + 4}px`;
this.collapsed = true;
}
correctHeightValue(height: number): number {
if (this.fit_content) {
let height_sum = 0;
for (const c of this.container.children) {
height_sum += (<HTMLElement>c).offsetHeight;
}
return height_sum;
}
return height;
}
toggleCollapse(): void {
toggle_collapse(): void {
if (this.collapsed) {
this.uncollapse();
} else {
@ -81,34 +55,28 @@ export default abstract class WindowBox {
}
}
setHeight(height: number): void {
this.container.style.height = `${height + 2 * BORDER_STROKE}px`;
}
uncollapse(): void {
this.container.classList.remove("collapsed");
if (this.resize) this.resize.style.visibility = "unset";
const new_height = this.correctHeightValue(this.title_bar.offsetHeight + 200);
this.setHeight(new_height);
this.element.classList.remove("collapsed");
this.resize.style.visibility = "unset";
this.element.style.height = `${this.title_bar.offsetHeight + 10 + 200}px`;
this.collapsed = false;
}
removeResizeListeners(): void {
if (this.resize_func) window.removeEventListener("mousemove", this.resize_func);
remove_resize_listeners(): void {
window.removeEventListener("mousemove", this.resize_func);
}
resize_move(e: MouseEvent): void {
if (this.collapsed) {
this.uncollapse();
this.removeResizeListeners();
this.remove_resize_listeners();
return;
}
const distance_to_title = e.clientY - this.container.offsetTop - this.title_bar.offsetHeight + window.scrollY + 5;
const distance_to_title = e.clientY - this.element.offsetTop - this.title_bar.offsetHeight + window.scrollY + 5;
if (distance_to_title <= 5) {
this.collapse();
return;
}
this.setHeight(e.clientY - this.container.offsetTop + window.scrollY);
this.element.style.height = `${e.clientY - this.element.offsetTop + window.scrollY + 8}px`;
}
}

View file

@ -1,35 +0,0 @@
import { CpuEvent, CpuEventHandler, UiEventHandler } from "../../events";
import UiComponent from "../uiComponent";
import WindowBox from "../windowBox";
import { DEFAULT_VRAM_BANK } from "../../constants";
export default class BankVisualizer extends WindowBox implements UiComponent {
events: UiEventHandler;
cpu_banks: Array<SVGPolylineElement>;
vram_banks: Array<SVGPolylineElement>;
constructor(element: HTMLElement, events: UiEventHandler) {
super(element, "Bank Status", { fit_content: true });
this.events = events;
this.cpu_banks = [...element.querySelectorAll("#cpu_bank>polyline")] as Array<SVGPolylineElement>;
this.vram_banks = [...element.querySelectorAll("#vram_bank>polyline")] as Array<SVGPolylineElement>;
}
initCpuEvents(c: CpuEventHandler): void {
c.listen(CpuEvent.SetVramBank, ({ bank }) => {
for (const bank_path of this.vram_banks) bank_path.setAttribute("stroke", "gray");
this.vram_banks[bank].setAttribute("stroke", "yellow");
});
c.listen(CpuEvent.SwitchBank, ({ bank }) => {
for (const bank_path of this.cpu_banks) bank_path.setAttribute("stroke", "gray");
this.cpu_banks[bank].setAttribute("stroke", "yellow");
});
}
reset(): void {
for (const bank_path of this.vram_banks) bank_path.setAttribute("stroke", "gray");
for (const bank_path of this.cpu_banks) bank_path.setAttribute("stroke", "gray");
this.vram_banks[DEFAULT_VRAM_BANK].setAttribute("stroke", "yellow");
this.cpu_banks[0].setAttribute("stroke", "yellow");
}
}

View file

@ -1,11 +1,11 @@
import { el, formatHex } from "../../etc";
import { el, format_hex } from "../../etc";
import { CpuEvent, CpuEventHandler, UiCpuSignalHandler, UiEventHandler } from "../../events";
import { Instruction, ParamType, ParameterType } from "../../instructionSet";
import { u8 } from "../../num";
import WindowBox from "../windowBox";
import UiComponent from "../uiComponent";
import { WindowBox } from "../windowBox";
import { UiComponent } from "../uiComponent";
export default class InstructionExplainer extends WindowBox implements UiComponent {
export class InstructionExplainer extends WindowBox implements UiComponent {
events: UiEventHandler;
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
@ -13,26 +13,25 @@ export default class InstructionExplainer extends WindowBox implements UiCompone
this.cpu_signals = cpu_signals;
this.events = events;
}
addInstruction(instr: Instruction, pos: u8, byte: u8): void {
add_instruction(instr: Instruction, pos: u8, byte: u8): void {
this.reset();
this.addBox(formatHex(byte), instr.name, "instruction");
this.add_box(format_hex(byte), instr.name, "instruction");
}
private addBox(box_icon_text: string, name: string, css_class: string): void {
const instr_box = el("div").id("expl_box").fin();
const instr_icon = el("span")
.id("expl_icon")
.cl(css_class)
.at("title", css_class.toUpperCase())
.tx(box_icon_text)
.fin();
const instr_box_text = el("span").id("expl_text").tx(name).fin();
private add_box(box_icon_text: string, name: string, css_class: string): void {
const instr_box = el("div", "expl_box");
const instr_icon = el("span", "expl_icon");
instr_icon.classList.add(css_class);
instr_icon.setAttribute("title", css_class.toUpperCase());
instr_icon.textContent = box_icon_text;
const instr_box_text = el("span", "expl_text");
instr_box_text.textContent = name;
instr_box.appendChild(instr_icon);
instr_box.appendChild(instr_box_text);
this.container.appendChild(instr_box);
this.element.appendChild(instr_box);
}
addParameter(param: ParameterType, pos: u8, byte: u8): void {
add_parameter(param: ParameterType, pos: u8, byte: u8): void {
const t = param.type;
let name;
if (t === ParamType.Const) {
@ -44,27 +43,27 @@ export default class InstructionExplainer extends WindowBox implements UiCompone
} else {
throw new Error("unreachable");
}
this.addBox(formatHex(byte), param.desc, name);
this.add_box(format_hex(byte), param.desc, name);
}
addInvalid(pos: u8, byte: u8): void {
add_invalid(pos: u8, byte: u8): void {
this.reset();
this.addBox(formatHex(byte), "Invalid Instruction", "invalid");
this.add_box(format_hex(byte), "Invalid Instruction", "invalid");
}
initCpuEvents(c: CpuEventHandler): void {
init_cpu_events(c: CpuEventHandler): void {
c.listen(CpuEvent.ParameterParsed, ({ param, code, pos }) => {
this.addParameter(param, pos, code);
this.add_parameter(param, pos, code);
});
c.listen(CpuEvent.InstructionParsed, ({ instr, code, pos }) => {
this.addInstruction(instr, pos, code);
this.add_instruction(instr, pos, code);
});
c.listen(CpuEvent.InvalidParsed, ({ code, pos }) => {
this.addInvalid(pos, code);
this.add_invalid(pos, code);
});
}
reset(): void {
this.container.querySelectorAll("#expl_box").forEach((e) => e.remove());
this.element.querySelectorAll("#expl_box").forEach((e) => e.remove());
}
}

View file

@ -1,9 +1,9 @@
import { el } from "../../etc";
import { CpuEvent, CpuEventHandler, UiCpuSignalHandler, UiEventHandler } from "../../events";
import WindowBox from "../windowBox";
import UiComponent from "../uiComponent";
import { WindowBox } from "../windowBox";
import { UiComponent } from "../uiComponent";
export default class Printout extends WindowBox implements UiComponent {
export class Printout extends WindowBox implements UiComponent {
events: UiEventHandler;
text_box: HTMLElement;
cpu_signals: UiCpuSignalHandler;
@ -11,11 +11,11 @@ export default class Printout extends WindowBox implements UiComponent {
super(element, "Printout");
this.cpu_signals = cpu_signals;
this.events = events;
this.text_box = el("div").id("printout_text").fin();
this.container.appendChild(this.text_box);
this.text_box = el("div", "printout_text");
this.element.appendChild(this.text_box);
}
initCpuEvents(c: CpuEventHandler): void {
init_cpu_events(c: CpuEventHandler): void {
c.listen(CpuEvent.Print, (c) => {
this.text_box.textContent += c;
});

View file

@ -1,41 +1,35 @@
import { DEFAULT_VRAM_BANK } from "../../constants";
import { el } from "../../etc";
import { UiEventHandler, CpuEventHandler, CpuEvent, UiCpuSignalHandler, UiCpuSignal } from "../../events";
import { u2, u4, u8 } from "../../num";
import UiComponent from "../uiComponent";
import WindowBox from "../windowBox";
const CANVAS_SIZE = 512;
const WIDTH = 16;
export default class Screen extends WindowBox implements UiComponent {
import { UiEventHandler, CpuEventHandler, CpuEvent } from "../../events";
import { u4, u8 } from "../../num";
import { UiComponent } from "../uiComponent";
import { WindowBox } from "../windowBox";
export class Screen extends WindowBox implements UiComponent {
events: UiEventHandler;
screen: HTMLCanvasElement;
cpu_signals: UiCpuSignalHandler;
ctx: CanvasRenderingContext2D;
scale: number;
current_vram_bank: u2 = DEFAULT_VRAM_BANK;
constructor(element: HTMLElement, event: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(element, "TV", { collapsed: true, fit_content: true });
this.cpu_signals = cpu_signals;
scale: [number, number];
constructor(element: HTMLElement, event: UiEventHandler) {
super(element, "TV", { collapsed: true });
this.screen = el("canvas", "screen");
this.events = event;
this.scale = CANVAS_SIZE / WIDTH;
this.screen = el("canvas").id("screen").fin();
this.screen.width = CANVAS_SIZE;
this.screen.height = CANVAS_SIZE;
const canvas_size = [512, 512];
const data_size = [16, 16];
this.scale = [canvas_size[0] / data_size[0], canvas_size[1] / data_size[1]];
[this.screen.width, this.screen.height] = canvas_size;
const ctx = this.screen.getContext("2d");
if (ctx === null) {
throw new Error("could not load screen");
}
this.ctx = ctx;
this.container.appendChild(this.screen);
this.element.appendChild(this.screen);
this.test_pattern();
}
private test_pattern(): void {
for (let x = 0; x < 256; x++) {
this.setPixel(x as u8, x as u8);
for (let x = 0; x < 16; x++) {
for (let y = 0; y < 16; y++) {
this.setPixel(x as u4, y as u4, (x + 16 * y) as u8);
}
}
}
@ -46,27 +40,17 @@ export default class Screen extends WindowBox implements UiComponent {
}
}
initCpuEvents(c: CpuEventHandler): void {
init_cpu_events(c: CpuEventHandler): void {
c.listen(CpuEvent.MemoryChanged, ({ address, bank, value }) => {
if (bank !== 1) return;
this.setPixel(address, value);
});
c.listen(CpuEvent.SetVramBank, ({ bank }) => {
this.current_vram_bank = bank;
this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryDump, (memory) => {
const vram = memory[this.current_vram_bank];
for (const [i, pixel] of vram.entries()) {
this.setPixel(i as u8, pixel as u8);
}
});
const x = (address % 16) as u4;
const y = Math.floor(address / 16) as u4;
this.setPixel(x, y, value);
});
}
setPixel(address: u8, value: u8): void {
const x = (address % 16) as u4;
const y = Math.floor(address / 16) as u4;
const point: [number, number] = [x * this.scale, y * this.scale];
setPixel(x: u4, y: u4, value: u8): void {
const point: [number, number] = [x * this.scale[0], y * this.scale[1]];
const RED_SCALE = 255 / 2 ** 3;
const GREEN_SCALE = 255 / 2 ** 3;
@ -77,6 +61,6 @@ export default class Screen extends WindowBox implements UiComponent {
const color = `rgb(${red},${green},${blue})`;
this.ctx.fillStyle = color;
this.ctx.fillRect(...point, this.scale, this.scale);
this.ctx.fillRect(...point, ...this.scale);
}
}

View file

@ -1,47 +0,0 @@
class ElementInProgress<E extends HTMLElement> {
private element: E;
constructor(el: E) {
this.element = el;
}
/** Set attribute */
at(name: string, value: string): ElementInProgress<E> {
this.element.setAttribute(name, value);
return this;
}
/** Set id */
id(id: string): ElementInProgress<E> {
this.element.id = id;
return this;
}
/** Add class */
cl(class_name: string): ElementInProgress<E> {
this.element.classList.add(class_name);
return this;
}
/** Set textContent */
tx(text_contents: string): ElementInProgress<E> {
this.element.textContent = text_contents;
return this;
}
/** Set style */
st(name: string, value: string): ElementInProgress<E> {
this.element.style.setProperty(name, value);
return this;
}
fin(): E {
return this.element;
}
}
export default function el<E extends keyof HTMLElementTagNameMap>(
name: E
): ElementInProgress<HTMLElementTagNameMap[E]> {
const element = document.createElement(name);
return new ElementInProgress(element);
}

74
svg.html Normal file
View file

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
html {
color: white;
background-color: black;
font-family: monospace;
font-size: 2em;
}
body {
display: flex;
justify-content: center;
margin-top: 100px;
align-content: center;
margin-inline: auto;
margin-block: auto;
}
#cont {
margin-top: 100px;
display: flex;
flex-direction: row;
}
#cpu,
#target {
margin-block: auto;
border: solid 3px yellow;
padding: 6px;
}
#targets {
display: flex;
flex-direction: column;
gap: 9px;
}
#connections {
}
div#target.gray {
border-color: gray;
}
svg polyline {
fill: transparent;
stroke-width: 3;
stroke-linecap: butt;
}
</style>
</head>
<body>
<div id="cont">
<div id="cpu">CPU</div>
<svg
id="connections"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
preserveAspectRatio="none"
width="100"
height="100%"
>
<polyline points="0,43 50,43 50,10, 100,10" stroke="yellow" />
<polyline points="0,48 64,48 64,37, 100,37" stroke="gray" />
<polyline points="0,53 64,53 64,63, 100,63" stroke="gray" />
<polyline points="0,58 50,58 50,90, 100,90" stroke="gray" />
</svg>
<div id="targets">
<div id="target">Main</div>
<div id="target" class="gray">Bank 1</div>
<div id="target" class="gray">Bank 2</div>
<div id="target" class="gray">VRAM</div>
</div>
</div>
</body>
</html>

BIN
test.bin Normal file

Binary file not shown.

26
todo.md
View file

@ -1,5 +1,3 @@
Verify mod256 behavior on negatives
Edit Mode
- Select where program counter is
@ -10,26 +8,18 @@ Speed control slider behavior
Speed control slider styling
Overclock Box
Error log:
error in instruction when number out of range (fix new Error("todo"))
- Allow ignoring errors
- Highlight errored instruction parameters and give description
Hard reset button (resets everything)
Soft reset button. Resets program counter and registers.
Fixed size windows
- TV
- Bank Status
Limit size to printout text buffer
UI for showing which Memory bank is selected
VRAM select instruction
Improve instruction explainer. Clearly show what is an instruction and what is a parameter
Ui showing CPU flag(s) (Carry)
Verify mod256 behavior on negatives
UI showing CPU flag(s) (Carry)
Error log
Responsive layout
@ -38,3 +28,5 @@ standardize names of all things
Documentation with standard names
Example Programs
Ui for togging your mother.