🌈️ apply a wallpaper based on the weather outside
0

Configure Feed

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

fix: imports, use npm

Angel Wang (Jun 20, 2026, 10:49 PM -0600) 5c910d0c 6813f413

+174 -143
+2
.gitignore
··· 1 + /node_modules 2 + /build
-84
analyze/analysis.mts
··· 1 - #!/usr/bin/env zx 2 - 3 - /// <reference types="zx/globals" /> 4 - 5 - export interface AnalysisConfig { 6 - imageDir: string 7 - preAnalysisCommands: string[] 8 - } 9 - 10 - export interface ImageAnalysisData { 11 - files: { path: string; oklch: number[] }[] 12 - } 13 - 14 - export const defaultConfig: AnalysisConfig = { 15 - imageDir: `${os.homedir()}/Pictures`, 16 - preAnalysisCommands: [], 17 - } 18 - 19 - export async function runPreAnalysis(hooks: string[]) { 20 - for await (const hook of hooks) { 21 - await $`eval ${hook}` // Scary! 22 - } 23 - } 24 - 25 - export async function analyzeImages(imageDir: string, analysisOutput: ImageAnalysisData, outputPath: string) { 26 - const images = await fs.promises.opendir(`${imageDir}`) 27 - 28 - for await (const image of images) { 29 - const path = `${imageDir}/${image.name}` 30 - const stat = await fs.promises.stat(path) 31 - if (stat.isFile()) { 32 - let output = await $`magick ${path} -colorspace Oklch -kmeans 10 -format "%c" histogram:info:`.then( 33 - (s) => 34 - s.stdout 35 - .split("\n") 36 - .map((substr) => substr.trim()) 37 - .filter((n) => n), 38 - ) as string[] 39 - 40 - if (output.length < 5) { 41 - // do it again but with more violence 42 - console.log( 43 - `-kmeans 10 of ${image.name} didn't give enough colours, trying again with a higher value...`, 44 - ) 45 - output = await $`magick ${path} -colorspace Oklch -kmeans 40 -format "%c" histogram:info:`.then( 46 - (s) => 47 - s.stdout 48 - .split("\n") // split the output by newlines 49 - .map((substr) => substr.trim()) // trim the whitespace on each line 50 - .filter((n) => n), // and then filter out any blank elements generated 51 - ) 52 - } 53 - 54 - const colours = output.map((entry) => { 55 - const arr = entry.split(" ") 56 - arr.pop() 57 - // [# of pixels, oklab colour, hex code] 58 - return { 59 - pixels: parseInt(arr[0]), 60 - oklab: arr[1].replace(/[()]/g, "").split(","), // (0,0,0) -> Array [0, 0, 0] 61 - hex: arr[2], 62 - } 63 - }) 64 - 65 - colours.sort((a, b) => (b.pixels) - (a.pixels)) 66 - 67 - const dominantColour = colours[0].oklab 68 - 69 - console.log( 70 - `Got oklch(${dominantColour}) as dominant colour of ${image.name}!`, 71 - ) 72 - const lightness = Number(dominantColour[0]) * 100 73 - const chroma = Number(dominantColour[1]) * 100 74 - const hue = Number(dominantColour[2]) 75 - 76 - analysisOutput.files.push({ path: path, oklch: [hue, chroma, lightness] }) 77 - 78 - // flush every time we finish analyzing a file, because it just takes so long 79 - fs.writeFile(outputPath, JSON.stringify(analysisOutput, null, 4), (err: Error) => { 80 - if (err) throw new Error(err.message) 81 - }) 82 - } 83 - } 84 - }
+83
analyze/analysis.ts
··· 1 + #!/usr/bin/env zx 2 + import "zx/globals" 3 + 4 + export interface AnalysisConfig { 5 + imageDir: string 6 + preAnalysisCommands: string[] 7 + } 8 + 9 + export interface ImageAnalysisData { 10 + files: { path: string; oklch: number[] }[] 11 + } 12 + 13 + export const defaultConfig: AnalysisConfig = { 14 + imageDir: `${os.homedir()}/Pictures`, 15 + preAnalysisCommands: [], 16 + } 17 + 18 + export async function runPreAnalysis(hooks: string[]) { 19 + for await (const hook of hooks) { 20 + await $`eval ${hook}` // Scary! 21 + } 22 + } 23 + 24 + export async function analyzeImages(imageDir: string, analysisOutput: ImageAnalysisData, outputPath: string) { 25 + const images = await fs.promises.opendir(`${imageDir}`) 26 + 27 + for await (const image of images) { 28 + const path = `${imageDir}/${image.name}` 29 + const stat = await fs.promises.stat(path) 30 + if (stat.isFile()) { 31 + let output = await $`magick ${path} -colorspace Oklch -kmeans 10 -format "%c" histogram:info:`.then( 32 + (s) => 33 + s.stdout 34 + .split("\n") 35 + .map((substr) => substr.trim()) 36 + .filter((n) => n), 37 + ) as string[] 38 + 39 + if (output.length < 5) { 40 + // do it again but with more violence 41 + console.log( 42 + `-kmeans 10 of ${image.name} didn't give enough colours, trying again with a higher value...`, 43 + ) 44 + output = await $`magick ${path} -colorspace Oklch -kmeans 40 -format "%c" histogram:info:`.then( 45 + (s) => 46 + s.stdout 47 + .split("\n") // split the output by newlines 48 + .map((substr) => substr.trim()) // trim the whitespace on each line 49 + .filter((n) => n), // and then filter out any blank elements generated 50 + ) 51 + } 52 + 53 + const colours = output.map((entry) => { 54 + const arr = entry.split(" ") 55 + arr.pop() 56 + // [# of pixels, oklab colour, hex code] 57 + return { 58 + pixels: parseInt(arr[0]), 59 + oklab: arr[1].replace(/[()]/g, "").split(","), // (0,0,0) -> Array [0, 0, 0] 60 + hex: arr[2], 61 + } 62 + }) 63 + 64 + colours.sort((a, b) => (b.pixels) - (a.pixels)) 65 + 66 + const dominantColour = colours[0].oklab 67 + 68 + console.log( 69 + `Got oklch(${dominantColour}) as dominant colour of ${image.name}!`, 70 + ) 71 + const lightness = Number(dominantColour[0]) * 100 72 + const chroma = Number(dominantColour[1]) * 100 73 + const hue = Number(dominantColour[2]) 74 + 75 + analysisOutput.files.push({ path: path, oklch: [hue, chroma, lightness] }) 76 + 77 + // flush every time we finish analyzing a file, because it just takes so long 78 + fs.writeFile(outputPath, JSON.stringify(analysisOutput, null, 4), (err: Error) => { 79 + if (err) throw new Error(err.message) 80 + }) 81 + } 82 + } 83 + }
-31
analyze/index.mts
··· 1 - #!/usr/bin/env zx 2 - /// <reference types="zx/globals" /> 3 - 4 - import { cacheDir, configDir, loadConfig } from "../utils.mts" 5 - import { AnalysisConfig, analyzeImages, defaultConfig, ImageAnalysisData, runPreAnalysis } from "./analysis.mts" 6 - 7 - const pathToConfig = `${configDir}/analyze-config.json` 8 - 9 - console.debug(`Trying to load config from ${pathToConfig}...`) 10 - 11 - const config = await loadConfig(pathToConfig, defaultConfig) as AnalysisConfig 12 - 13 - if (config == undefined) { 14 - throw new Error("Couldn't initialize config!") 15 - } 16 - 17 - console.debug(`Config: \n${JSON.stringify(config, null, 4)}`) 18 - 19 - await fs.cp(`${cacheDir}/analysis.json`, `${cacheDir}/analysis.json.prev`, { force: true }, (err: Error) => { 20 - if (err) { 21 - console.log(err) 22 - } 23 - }) 24 - 25 - const imageData = { files: [] } as ImageAnalysisData 26 - 27 - await runPreAnalysis(config.preAnalysisCommands) 28 - 29 - await analyzeImages(config.imageDir, imageData, `${cacheDir}/analysis.json`) 30 - 31 - await fs.rm(`${cacheDir}/analysis.json.prev`, { force: true })
+31
analyze/index.ts
··· 1 + #!/usr/bin/env zx 2 + 3 + import { cacheDir, configDir, loadConfig } from "../utils.ts" 4 + import { AnalysisConfig, analyzeImages, defaultConfig, ImageAnalysisData, runPreAnalysis } from "./analysis.ts" 5 + import "zx/globals" 6 + 7 + const pathToConfig = `${configDir}/analyze-config.json` 8 + 9 + console.debug(`Trying to load config from ${pathToConfig}...`) 10 + 11 + const config = await loadConfig(pathToConfig, defaultConfig) as AnalysisConfig 12 + 13 + if (config == undefined) { 14 + throw new Error("Couldn't initialize config!") 15 + } 16 + 17 + console.debug(`Config: \n${JSON.stringify(config, null, 4)}`) 18 + 19 + await fs.cp(`${cacheDir}/analysis.json`, `${cacheDir}/analysis.json.prev`, { force: true }, (err: Error) => { 20 + if (err) { 21 + console.log(err) 22 + } 23 + }) 24 + 25 + const imageData = { files: [] } as ImageAnalysisData 26 + 27 + await runPreAnalysis(config.preAnalysisCommands) 28 + 29 + await analyzeImages(config.imageDir, imageData, `${cacheDir}/analysis.json`) 30 + 31 + await fs.rm(`${cacheDir}/analysis.json.prev`, { force: true })
+1 -5
apply/application.mts apply/application.ts
··· 1 - #!/usr/bin/env zx 2 - 3 - import { ImageAnalysisData } from "../analyze/analysis.mts" 4 - 5 - /// <reference types="zx/globals" /> 1 + import { ImageAnalysisData } from "../analyze/analysis.ts" 6 2 7 3 export interface ApplicationConfig { 8 4 latitude: number
+5 -5
apply/index.mts apply/index.ts
··· 1 1 #!/usr/bin/env zx 2 - /// <reference types="zx/globals" /> 3 2 4 - import { ImageAnalysisData } from "../analyze/analysis.mts" 5 - import { cacheDir, configDir, loadConfig, map, mapEaseInExpo, mapEaseOutExpo } from "../utils.mts" 6 - import { ApplicationConfig, defaultConfig, findMatchingImages, getOpenMeteoData } from "./application.mts" 7 - import * as SunCalc from "npm:suncalc" 3 + import { ImageAnalysisData } from "../analyze/analysis.ts" 4 + import { cacheDir, configDir, loadConfig, map, mapEaseInExpo, mapEaseOutExpo } from "../utils.ts" 5 + import { ApplicationConfig, defaultConfig, findMatchingImages, getOpenMeteoData } from "./application.ts" 6 + import * as SunCalc from "suncalc" 7 + import "zx/globals" 8 8 9 9 const pathToConfig = `${configDir}/apply-config.json` 10 10 const pathToCache = `${cacheDir}/analysis.json`
+3 -5
deno.json
··· 1 1 { 2 2 "tasks": { 3 - "analyze": "zx analyze/index.mts", 4 - "apply": "zx apply/index.mts" 3 + "analyze": "zx analyze/index.ts", 4 + "apply": "zx apply/index.ts" 5 5 }, 6 6 "imports": { 7 - "@std/assert": "jsr:@std/assert@1", 8 - "suncalc": "npm:suncalc@^2.0.0", 9 - "zx": "npm:zx@^8.8.5" 7 + "@std/assert": "jsr:@std/assert@1" 10 8 }, 11 9 "fmt": { "semiColons": false, "indentWidth": 4, "useTabs": true, "lineWidth": 120 } 12 10 }
+11 -13
deno.lock
··· 2 2 "version": "5", 3 3 "specifiers": { 4 4 "jsr:@std/assert@1": "1.0.19", 5 - "jsr:@std/internal@^1.0.12": "1.0.12", 6 - "npm:suncalc@2": "2.0.0", 7 - "npm:zx@^8.8.5": "8.8.5" 5 + "jsr:@std/internal@^1.0.12": "1.0.14", 6 + "npm:suncalc@2": "2.0.0" 8 7 }, 9 8 "jsr": { 10 9 "@std/assert@1.0.19": { ··· 13 12 "jsr:@std/internal" 14 13 ] 15 14 }, 16 - "@std/internal@1.0.12": { 17 - "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 15 + "@std/internal@1.0.14": { 16 + "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" 18 17 } 19 18 }, 20 19 "npm": { 21 20 "suncalc@2.0.0": { 22 21 "integrity": "sha512-RcO28GOMXNnB+sr+c7/+zv5UPMa9wxIsUVGE3HddjQykqCVEC2rc4t4IjBB7qaa7K7dx5Cxp8UZwexyXrX8JkA==" 23 - }, 24 - "zx@8.8.5": { 25 - "integrity": "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==", 26 - "bin": true 27 22 } 28 23 }, 29 24 "workspace": { 30 25 "dependencies": [ 31 - "jsr:@std/assert@1", 32 - "npm:suncalc@2", 33 - "npm:zx@^8.8.5" 34 - ] 26 + "jsr:@std/assert@1" 27 + ], 28 + "packageJson": { 29 + "dependencies": [ 30 + "npm:suncalc@2" 31 + ] 32 + } 35 33 } 36 34 }
+30
package-lock.json
··· 1 + { 2 + "name": "rainwall", 3 + "lockfileVersion": 3, 4 + "requires": true, 5 + "packages": { 6 + "": { 7 + "dependencies": { 8 + "suncalc": "^2.0.0", 9 + "zx": "^8.8.5" 10 + } 11 + }, 12 + "node_modules/suncalc": { 13 + "version": "2.0.0", 14 + "resolved": "https://registry.npmjs.org/suncalc/-/suncalc-2.0.0.tgz", 15 + "integrity": "sha512-RcO28GOMXNnB+sr+c7/+zv5UPMa9wxIsUVGE3HddjQykqCVEC2rc4t4IjBB7qaa7K7dx5Cxp8UZwexyXrX8JkA==" 16 + }, 17 + "node_modules/zx": { 18 + "version": "8.8.5", 19 + "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.5.tgz", 20 + "integrity": "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==", 21 + "license": "Apache-2.0", 22 + "bin": { 23 + "zx": "build/cli.js" 24 + }, 25 + "engines": { 26 + "node": ">= 12.17.0" 27 + } 28 + } 29 + } 30 + }
+6
package.json
··· 1 + { 2 + "dependencies": { 3 + "suncalc": "^2.0.0", 4 + "zx": "^8.8.5" 5 + } 6 + }
+2
utils.mts utils.ts
··· 1 + import "zx/globals" 2 + 1 3 export const configDir = process.env.XDG_CONFIG_HOME 2 4 ? `${process.env.XDG_CONFIG_HOME}/rainwall` 3 5 : `${os.homedir()}/.config/rainwall`