🌈️ apply a wallpaper based on the weather outside
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: rewrite in ts, add deno

Angel Wang (Jun 20, 2026, 1:42 PM -0600) eb854553 6642b97e

+221 -107
+9
deno.json
··· 1 + { 2 + "tasks": { 3 + "dev": "zx index.mts" 4 + }, 5 + "imports": { 6 + "@std/assert": "jsr:@std/assert@1", 7 + "zx": "npm:zx@^8.8.5" 8 + } 9 + }
+31
deno.lock
··· 1 + { 2 + "version": "5", 3 + "specifiers": { 4 + "jsr:@std/assert@1": "1.0.19", 5 + "jsr:@std/internal@^1.0.12": "1.0.12", 6 + "npm:zx@^8.8.5": "8.8.5" 7 + }, 8 + "jsr": { 9 + "@std/assert@1.0.19": { 10 + "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", 11 + "dependencies": [ 12 + "jsr:@std/internal" 13 + ] 14 + }, 15 + "@std/internal@1.0.12": { 16 + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 17 + } 18 + }, 19 + "npm": { 20 + "zx@8.8.5": { 21 + "integrity": "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==", 22 + "bin": true 23 + } 24 + }, 25 + "workspace": { 26 + "dependencies": [ 27 + "jsr:@std/assert@1", 28 + "npm:zx@^8.8.5" 29 + ] 30 + } 31 + }
-107
grade.mjs
··· 1 - #!/usr/bin/env zx 2 - import "fs"; 3 - 4 - await $`rm -f cache.json`; 5 - // await $`lutgen apply -p catppuccin-latte -l 8 -L 0.001 -P -r 21.5 ./orig/* -o ./graded`; 6 - cd("./graded"); 7 - 8 - const files = await fs.promises.opendir("."); 9 - 10 - let json = {}; 11 - 12 - // set up the data storage 13 - let lightnessData = []; 14 - for (var i = 0; i < 10; i += 2) { 15 - lightnessData.push({ lightness: i / 10, paths: [] }); 16 - } 17 - 18 - let chromaData = []; 19 - for (var i = 0; i < 10; i += 2) { 20 - chromaData.push({ chroma: i / 100, paths: [] }); 21 - } 22 - 23 - let hueData = []; 24 - for (var i = 0; i < 360; i += 90) { 25 - hueData.push({ hue: i, paths: [] }); 26 - } 27 - 28 - // analyze the files 29 - for await (const file of files) { 30 - const stat = await fs.promises.stat(file.name); 31 - if (stat.isFile()) { 32 - let output = 33 - await $`magick ${file.name} -colorspace Oklch -kmeans 10 -format "%c" histogram:info:`.then( 34 - (s) => 35 - s.stdout 36 - .split("\n") 37 - .map((substr) => substr.trim()) 38 - .filter((n) => n), 39 - ); 40 - 41 - if (output.length < 5) { 42 - // do it again but with more violence 43 - console.log( 44 - `-kmeans 10 of ${file.name} didn't give enough colours, trying again with a higher value...`, 45 - ); 46 - output = 47 - await $`magick ${file.name} -colorspace Oklch -kmeans 40 -format "%c" histogram:info:`.then( 48 - (s) => 49 - s.stdout 50 - .split("\n") 51 - .map((substr) => substr.trim()) 52 - .filter((n) => n), 53 - ); 54 - } 55 - 56 - let colours = output.map((entry) => { 57 - let arr = entry.split(" "); 58 - arr.pop(); 59 - // [# of pixels, oklab colour, hex code] 60 - return [parseInt(arr[0]), arr[1].replace(/[()]/g, "").split(","), arr[2]]; 61 - }); 62 - 63 - colours.sort((a, b) => b[0] - a[0]); 64 - 65 - const dominantColour = colours[0][1]; 66 - console.log( 67 - `Got oklch(${dominantColour}) as dominant colour of ${file.name}!`, 68 - ); 69 - const lightness = dominantColour[0]; 70 - const chroma = dominantColour[1]; 71 - const hue = dominantColour[2]; 72 - 73 - lightnessData.forEach((n) => { 74 - let targetLightness = Math.floor(lightness * 5) / 5; 75 - if (n.lightness == targetLightness) { 76 - n.paths.push(file.name); 77 - return; 78 - } 79 - }); 80 - 81 - chromaData.forEach((n) => { 82 - let targetChroma = Math.floor(chroma * 50) / 50; 83 - if (n.chroma == targetChroma) { 84 - n.paths.push(file.name); 85 - return; 86 - } 87 - }); 88 - 89 - hueData.forEach((n) => { 90 - let targetHue = Math.floor(hue / 90) * 90; 91 - if (n.hue == targetHue) { 92 - n.paths.push(file.name); 93 - return; 94 - } 95 - }); 96 - 97 - json["lightnessData"] = lightnessData; 98 - json["chromaData"] = chromaData; 99 - json["hueData"] = hueData; 100 - 101 - fs.writeFile("./cache.json", JSON.stringify(json), err => {}) 102 - 103 - // past 0.3, the image is way too dark to have any discernable colour 104 - } 105 - } 106 - 107 -
+149
grade.mts
··· 1 + #!/usr/bin/env zx 2 + /// <reference types="zx/globals" /> 3 + 4 + export interface Config { 5 + imageDir: string 6 + lightnessStep: number 7 + chromaStep: number 8 + hueStep: number 9 + preAnalysisCommands: string[] 10 + } 11 + 12 + export class ImageAnalysisData { 13 + lightnessData: { lightness: number, paths: string[] }[] 14 + chromaData: { chroma: number, paths: string[] }[] 15 + hueData: { hue: number, paths: string[] }[] 16 + 17 + constructor(lightnessStep: number, chromaStep: number, hueStep: number) { 18 + this.lightnessData = [] 19 + this.chromaData = [] 20 + this.hueData = [] 21 + 22 + for (let i = 0; i < 1; i += lightnessStep) { 23 + i = roundToStep(i, lightnessStep) // mitigate floating point precision errors 24 + this.lightnessData.push({ lightness: i, paths: [] }) 25 + } 26 + 27 + for (let i = 0; i < 1; i += chromaStep) { 28 + i = roundToStep(i, chromaStep) 29 + this.chromaData.push({ chroma: i, paths: [] }) 30 + } 31 + for (let i = 0; i < 360; i += hueStep) { 32 + this.hueData.push({ hue: i, paths: [] }) 33 + } 34 + } 35 + } 36 + 37 + export function setupConfig(configDir: string) { 38 + const defaultConfig : Config = { 39 + imageDir: `${os.homedir()}/Pictures`, 40 + lightnessStep: 0.1, 41 + chromaStep: 0.01, 42 + hueStep: 90, 43 + preAnalysisCommands: [] 44 + } 45 + 46 + fs.writeFile(`${configDir}/config.jsonc`, JSON.stringify(defaultConfig)) 47 + console.info(`Generated default config at ${configDir}/config.jsonc!`) 48 + return defaultConfig 49 + } 50 + 51 + export function runPreAnalysis(hooks: string[]) { 52 + hooks.forEach(async (hook) => { 53 + await $`${hook}` // Scary! 54 + }) 55 + } 56 + 57 + export async function analyzeImages(imageDir: string, analysisOutput: ImageAnalysisData, outputPath: string, lightnessStep: number, chromaStep: number, hueStep: number) { 58 + const images = await fs.promises.opendir(`${imageDir}`) 59 + // await fs.rm(`${}/cache.json`) 60 + 61 + for await (const image of images) { 62 + const path = imageDir + image.name 63 + const stat = await fs.promises.stat(path) 64 + if (stat.isFile()) { 65 + let output = 66 + await $`magick ${path} -colorspace Oklch -kmeans 10 -format "%c" histogram:info:`.then( 67 + (s) => 68 + s.stdout 69 + .split("\n") 70 + .map((substr) => substr.trim()) 71 + .filter((n) => n), 72 + ) as string[] 73 + 74 + if (output.length < 5) { 75 + // do it again but with more violence 76 + console.log( 77 + `-kmeans 10 of ${image.name} didn't give enough colours, trying again with a higher value...`, 78 + ) 79 + output = 80 + await $`magick ${path} -colorspace Oklch -kmeans 40 -format "%c" histogram:info:`.then( 81 + (s) => 82 + s.stdout 83 + .split("\n") // split the output by newlines 84 + .map((substr) => substr.trim()) // trim the whitespace on each line 85 + .filter((n) => n), // and then filter out any blank elements generated 86 + ) 87 + } 88 + 89 + const colours = output.map((entry) => { 90 + const arr = entry.split(" ") 91 + arr.pop() 92 + // [# of pixels, oklab colour, hex code] 93 + return { 94 + pixels: parseInt(arr[0]), 95 + oklab: arr[1].replace(/[()]/g, "").split(","), // (0,0,0) -> Array [0, 0, 0] 96 + hex: arr[2] 97 + } 98 + }) 99 + 100 + colours.sort((a, b) => (b.pixels) - (a.pixels)) 101 + 102 + const dominantColour = colours[0].oklab 103 + 104 + console.log( 105 + `Got oklch(${dominantColour}) as dominant colour of ${image.name}!`, 106 + ) 107 + const lightness = Number(dominantColour[0]) 108 + const chroma = Number(dominantColour[1]) 109 + const hue = Number(dominantColour[2]) 110 + 111 + analysisOutput.lightnessData.forEach((n) => { 112 + const targetLightness = floorToStep(lightness, lightnessStep) 113 + if (n.lightness == targetLightness) { 114 + n.paths.push(path) 115 + return 116 + } 117 + }) 118 + 119 + analysisOutput.chromaData.forEach((n) => { 120 + const targetChroma = floorToStep(chroma, chromaStep) 121 + if (n.chroma == targetChroma) { 122 + n.paths.push(path) 123 + return 124 + } 125 + }) 126 + 127 + analysisOutput.hueData.forEach((n) => { 128 + const targetHue = floorToStep(hue, hueStep) 129 + if (n.hue == targetHue) { 130 + n.paths.push(path) 131 + return 132 + } 133 + }) 134 + 135 + // flush every time we finish analyzing a file, because it just takes so long 136 + fs.writeFile(outputPath, JSON.stringify(analysisOutput), (err: Error) => { 137 + if (err) throw new Error(err.message) 138 + }) 139 + } 140 + } 141 + } 142 + 143 + function floorToStep(num: number, step: number) { 144 + return Math.floor(num / step) * step 145 + } 146 + 147 + function roundToStep(num: number, step: number) { 148 + return Math.round(num / step) * step 149 + }
+32
index.mts
··· 1 + #!/usr/bin/env zx 2 + /// <reference types="zx/globals" /> 3 + 4 + import { analyzeImages, Config, ImageAnalysisData, runPreAnalysis, setupConfig } from "./grade.mts"; 5 + 6 + const configDir = process.env.XDG_CONFIG_HOME ? `${process.env.XDG_CONFIG_HOME}/rainwall` : `${os.homedir()}/.config/rainwall` 7 + const cacheDir = process.env.XDG_CACHE_HOME ? `${process.env.XDG_CACHE_HOME}/rainwall` : `${os.homedir()}/.cache/rainwall` 8 + 9 + fs.mkdir(configDir) 10 + fs.mkdir(cacheDir) 11 + 12 + let config : Config | undefined 13 + 14 + 15 + fs.readFile(`${configDir}/config.jsonc`, (err: Error, data: string) => { 16 + if (err) { 17 + console.warn(`Got ${err.message} when trying to load config, generating a new one...`) 18 + config = setupConfig(configDir) 19 + } else { 20 + config = JSON.parse(data) 21 + } 22 + }) 23 + 24 + if (config == undefined) { 25 + throw new Error("Couldn't initialize config!") 26 + } 27 + 28 + let imageData = new ImageAnalysisData(config.lightnessStep, config.chromaStep, config.hueStep) 29 + 30 + runPreAnalysis(config.preAnalysisCommands) 31 + 32 + await analyzeImages(config.imageDir, imageData, `${cacheDir}/cache.json`, config.lightnessStep, config.chromaStep, config.hueStep)