diff --git a/deno.json b/deno.json
new file mode 100644
index 0000000..c3fd439
--- /dev/null
+++ b/deno.json
@@ -0,0 +1,9 @@
+{
+ "tasks": {
+ "dev": "zx index.mts"
+ },
+ "imports": {
+ "@std/assert": "jsr:@std/assert@1",
+ "zx": "npm:zx@^8.8.5"
+ }
+}
diff --git a/deno.lock b/deno.lock
new file mode 100644
index 0000000..7dde75a
--- /dev/null
+++ b/deno.lock
@@ -0,0 +1,31 @@
+{
+ "version": "5",
+ "specifiers": {
+ "jsr:@std/assert@1": "1.0.19",
+ "jsr:@std/internal@^1.0.12": "1.0.12",
+ "npm:zx@^8.8.5": "8.8.5"
+ },
+ "jsr": {
+ "@std/assert@1.0.19": {
+ "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e",
+ "dependencies": [
+ "jsr:@std/internal"
+ ]
+ },
+ "@std/internal@1.0.12": {
+ "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
+ }
+ },
+ "npm": {
+ "zx@8.8.5": {
+ "integrity": "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==",
+ "bin": true
+ }
+ },
+ "workspace": {
+ "dependencies": [
+ "jsr:@std/assert@1",
+ "npm:zx@^8.8.5"
+ ]
+ }
+}
diff --git a/grade.mjs b/grade.mjs
deleted file mode 100755
index 71336cc..0000000
--- a/grade.mjs
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/usr/bin/env zx
-import "fs";
-
-await $`rm -f cache.json`;
-// await $`lutgen apply -p catppuccin-latte -l 8 -L 0.001 -P -r 21.5 ./orig/* -o ./graded`;
-cd("./graded");
-
-const files = await fs.promises.opendir(".");
-
-let json = {};
-
-// set up the data storage
-let lightnessData = [];
-for (var i = 0; i < 10; i += 2) {
- lightnessData.push({ lightness: i / 10, paths: [] });
-}
-
-let chromaData = [];
-for (var i = 0; i < 10; i += 2) {
- chromaData.push({ chroma: i / 100, paths: [] });
-}
-
-let hueData = [];
-for (var i = 0; i < 360; i += 90) {
- hueData.push({ hue: i, paths: [] });
-}
-
-// analyze the files
-for await (const file of files) {
- const stat = await fs.promises.stat(file.name);
- if (stat.isFile()) {
- let output =
- await $`magick ${file.name} -colorspace Oklch -kmeans 10 -format "%c" histogram:info:`.then(
- (s) =>
- s.stdout
- .split("\n")
- .map((substr) => substr.trim())
- .filter((n) => n),
- );
-
- if (output.length < 5) {
- // do it again but with more violence
- console.log(
- `-kmeans 10 of ${file.name} didn't give enough colours, trying again with a higher value...`,
- );
- output =
- await $`magick ${file.name} -colorspace Oklch -kmeans 40 -format "%c" histogram:info:`.then(
- (s) =>
- s.stdout
- .split("\n")
- .map((substr) => substr.trim())
- .filter((n) => n),
- );
- }
-
- let colours = output.map((entry) => {
- let arr = entry.split(" ");
- arr.pop();
- // [# of pixels, oklab colour, hex code]
- return [parseInt(arr[0]), arr[1].replace(/[()]/g, "").split(","), arr[2]];
- });
-
- colours.sort((a, b) => b[0] - a[0]);
-
- const dominantColour = colours[0][1];
- console.log(
- `Got oklch(${dominantColour}) as dominant colour of ${file.name}!`,
- );
- const lightness = dominantColour[0];
- const chroma = dominantColour[1];
- const hue = dominantColour[2];
-
- lightnessData.forEach((n) => {
- let targetLightness = Math.floor(lightness * 5) / 5;
- if (n.lightness == targetLightness) {
- n.paths.push(file.name);
- return;
- }
- });
-
- chromaData.forEach((n) => {
- let targetChroma = Math.floor(chroma * 50) / 50;
- if (n.chroma == targetChroma) {
- n.paths.push(file.name);
- return;
- }
- });
-
- hueData.forEach((n) => {
- let targetHue = Math.floor(hue / 90) * 90;
- if (n.hue == targetHue) {
- n.paths.push(file.name);
- return;
- }
- });
-
- json["lightnessData"] = lightnessData;
- json["chromaData"] = chromaData;
- json["hueData"] = hueData;
-
- fs.writeFile("./cache.json", JSON.stringify(json), err => {})
-
- // past 0.3, the image is way too dark to have any discernable colour
- }
-}
-
-
diff --git a/grade.mts b/grade.mts
new file mode 100755
index 0000000..09482fd
--- /dev/null
+++ b/grade.mts
@@ -0,0 +1,149 @@
+#!/usr/bin/env zx
+///
+
+export interface Config {
+ imageDir: string
+ lightnessStep: number
+ chromaStep: number
+ hueStep: number
+ preAnalysisCommands: string[]
+}
+
+export class ImageAnalysisData {
+ lightnessData: { lightness: number, paths: string[] }[]
+ chromaData: { chroma: number, paths: string[] }[]
+ hueData: { hue: number, paths: string[] }[]
+
+ constructor(lightnessStep: number, chromaStep: number, hueStep: number) {
+ this.lightnessData = []
+ this.chromaData = []
+ this.hueData = []
+
+ for (let i = 0; i < 1; i += lightnessStep) {
+ i = roundToStep(i, lightnessStep) // mitigate floating point precision errors
+ this.lightnessData.push({ lightness: i, paths: [] })
+ }
+
+ for (let i = 0; i < 1; i += chromaStep) {
+ i = roundToStep(i, chromaStep)
+ this.chromaData.push({ chroma: i, paths: [] })
+ }
+ for (let i = 0; i < 360; i += hueStep) {
+ this.hueData.push({ hue: i, paths: [] })
+ }
+ }
+}
+
+export function setupConfig(configDir: string) {
+ const defaultConfig : Config = {
+ imageDir: `${os.homedir()}/Pictures`,
+ lightnessStep: 0.1,
+ chromaStep: 0.01,
+ hueStep: 90,
+ preAnalysisCommands: []
+ }
+
+ fs.writeFile(`${configDir}/config.jsonc`, JSON.stringify(defaultConfig))
+ console.info(`Generated default config at ${configDir}/config.jsonc!`)
+ return defaultConfig
+}
+
+export function runPreAnalysis(hooks: string[]) {
+ hooks.forEach(async (hook) => {
+ await $`${hook}` // Scary!
+ })
+}
+
+export async function analyzeImages(imageDir: string, analysisOutput: ImageAnalysisData, outputPath: string, lightnessStep: number, chromaStep: number, hueStep: number) {
+ const images = await fs.promises.opendir(`${imageDir}`)
+ // await fs.rm(`${}/cache.json`)
+
+ for await (const image of images) {
+ const path = imageDir + image.name
+ const stat = await fs.promises.stat(path)
+ if (stat.isFile()) {
+ let output =
+ await $`magick ${path} -colorspace Oklch -kmeans 10 -format "%c" histogram:info:`.then(
+ (s) =>
+ s.stdout
+ .split("\n")
+ .map((substr) => substr.trim())
+ .filter((n) => n),
+ ) as string[]
+
+ if (output.length < 5) {
+ // do it again but with more violence
+ console.log(
+ `-kmeans 10 of ${image.name} didn't give enough colours, trying again with a higher value...`,
+ )
+ output =
+ await $`magick ${path} -colorspace Oklch -kmeans 40 -format "%c" histogram:info:`.then(
+ (s) =>
+ s.stdout
+ .split("\n") // split the output by newlines
+ .map((substr) => substr.trim()) // trim the whitespace on each line
+ .filter((n) => n), // and then filter out any blank elements generated
+ )
+ }
+
+ const colours = output.map((entry) => {
+ const arr = entry.split(" ")
+ arr.pop()
+ // [# of pixels, oklab colour, hex code]
+ return {
+ pixels: parseInt(arr[0]),
+ oklab: arr[1].replace(/[()]/g, "").split(","), // (0,0,0) -> Array [0, 0, 0]
+ hex: arr[2]
+ }
+ })
+
+ colours.sort((a, b) => (b.pixels) - (a.pixels))
+
+ const dominantColour = colours[0].oklab
+
+ console.log(
+ `Got oklch(${dominantColour}) as dominant colour of ${image.name}!`,
+ )
+ const lightness = Number(dominantColour[0])
+ const chroma = Number(dominantColour[1])
+ const hue = Number(dominantColour[2])
+
+ analysisOutput.lightnessData.forEach((n) => {
+ const targetLightness = floorToStep(lightness, lightnessStep)
+ if (n.lightness == targetLightness) {
+ n.paths.push(path)
+ return
+ }
+ })
+
+ analysisOutput.chromaData.forEach((n) => {
+ const targetChroma = floorToStep(chroma, chromaStep)
+ if (n.chroma == targetChroma) {
+ n.paths.push(path)
+ return
+ }
+ })
+
+ analysisOutput.hueData.forEach((n) => {
+ const targetHue = floorToStep(hue, hueStep)
+ if (n.hue == targetHue) {
+ n.paths.push(path)
+ return
+ }
+ })
+
+ // flush every time we finish analyzing a file, because it just takes so long
+ fs.writeFile(outputPath, JSON.stringify(analysisOutput), (err: Error) => {
+ if (err) throw new Error(err.message)
+ })
+ }
+ }
+}
+
+function floorToStep(num: number, step: number) {
+ return Math.floor(num / step) * step
+}
+
+function roundToStep(num: number, step: number) {
+ return Math.round(num / step) * step
+}
\ No newline at end of file
diff --git a/index.mts b/index.mts
new file mode 100755
index 0000000..9c6db6e
--- /dev/null
+++ b/index.mts
@@ -0,0 +1,32 @@
+#!/usr/bin/env zx
+///
+
+import { analyzeImages, Config, ImageAnalysisData, runPreAnalysis, setupConfig } from "./grade.mts";
+
+const configDir = process.env.XDG_CONFIG_HOME ? `${process.env.XDG_CONFIG_HOME}/rainwall` : `${os.homedir()}/.config/rainwall`
+const cacheDir = process.env.XDG_CACHE_HOME ? `${process.env.XDG_CACHE_HOME}/rainwall` : `${os.homedir()}/.cache/rainwall`
+
+fs.mkdir(configDir)
+fs.mkdir(cacheDir)
+
+let config : Config | undefined
+
+
+fs.readFile(`${configDir}/config.jsonc`, (err: Error, data: string) => {
+ if (err) {
+ console.warn(`Got ${err.message} when trying to load config, generating a new one...`)
+ config = setupConfig(configDir)
+ } else {
+ config = JSON.parse(data)
+ }
+})
+
+if (config == undefined) {
+ throw new Error("Couldn't initialize config!")
+}
+
+let imageData = new ImageAnalysisData(config.lightnessStep, config.chromaStep, config.hueStep)
+
+runPreAnalysis(config.preAnalysisCommands)
+
+await analyzeImages(config.imageDir, imageData, `${cacheDir}/cache.json`, config.lightnessStep, config.chromaStep, config.hueStep)
\ No newline at end of file