From c5823dadc2ccc192064927b9d981f76492c8cca1 Mon Sep 17 00:00:00 2001 From: Angel Wang Date: Sat, 20 Jun 2026 20:20:23 -0600 Subject: [PATCH] feat: implement matching for a suitable wallpaper --- .editorconfig | 3 - analyze/analysis.mts | 14 ++--- apply/application.mts | 140 +++++++++++++++++++++++++++++------------- apply/index.mts | 81 +++++++++++++++++++++--- deno.json | 3 +- utils.mts | 75 ++++++++++++++-------- 6 files changed, 229 insertions(+), 87 deletions(-) delete mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index b8bfb97..0000000 --- a/.editorconfig +++ /dev/null @@ -1,3 +0,0 @@ -[*.mts] -indent_size = 4 -indent_style = tab \ No newline at end of file diff --git a/analyze/analysis.mts b/analyze/analysis.mts index 3942798..7aac025 100755 --- a/analyze/analysis.mts +++ b/analyze/analysis.mts @@ -1,4 +1,7 @@ #!/usr/bin/env zx + +import { floorToStep } from "../utils.mts"; + /// export interface AnalysisConfig { @@ -32,10 +35,10 @@ export class ImageAnalysisData { } } -export const defaultConfig: AnalysisConfig = { - imageDir: `${os.homedir()}/Pictures`, - lightnessStep: 10, - chromaStep: 1, +export const defaultConfig: AnalysisConfig = { + imageDir: `${os.homedir()}/Pictures`, + lightnessStep: 10, + chromaStep: 1, hueStep: 90, preAnalysisCommands: [] } @@ -131,6 +134,3 @@ export async function analyzeImages(imageDir: string, analysisOutput: ImageAnaly } } -function floorToStep(num: number, step: number) { - return Math.floor(num / step) * step -} \ No newline at end of file diff --git a/apply/application.mts b/apply/application.mts index e4462f3..c166a06 100644 --- a/apply/application.mts +++ b/apply/application.mts @@ -1,51 +1,107 @@ #!/usr/bin/env zx + +import { ImageAnalysisData } from "../analyze/analysis.mts" +import _ from "npm:underscore" + /// export interface ApplicationConfig { - latitude: number, - longitude: number, - weatherModel: string, - applyWallpaperCommand: string + latitude: number + longitude: number + weatherModel: string + lightnessRange: { start: number; end: number } + chromaRange: { start: number; end: number } + hueRange: { start: number; end: number } + applyWallpaperCommand: string } -export const defaultConfig: ApplicationConfig = { +export const defaultConfig: ApplicationConfig = { latitude: 0, - longitude: 0, - weatherModel: "best_match", - applyWallpaperCommand: "hyprctl hyprpaper wallpaper , %s" + longitude: 0, + weatherModel: "best_match", + lightnessRange: { + start: 0, + end: 100, + }, + chromaRange: { + start: 0, + end: 100, + }, + hueRange: { + start: 0, + end: 360, + }, + applyWallpaperCommand: "hyprctl hyprpaper wallpaper , %s", +} + +export async function getOpenMeteoData( + latitude: number, + longitude: number, + weatherModel: string = "best_match", +) { + const openMeteoResponse = await fetch( + "https://api.open-meteo.com/v1/forecast", + { + method: "POST", + body: new URLSearchParams({ + latitude: latitude.toString(), + longitude: longitude.toString(), + models: weatherModel, + current: "cloud_cover", + minutely_15: "shortwave_radiation_instant", + }), + }, + ) + + if (!openMeteoResponse.ok) { + throw new Error( + `Open-Meteo sent back an error: ${openMeteoResponse.status}`, + ) + } + + const openMeteoData = await openMeteoResponse.json() + + const cloudCover: string = openMeteoData.current.cloud_cover + + let index = 0 + openMeteoData.minutely_15.time.forEach((time: string) => { + if (Date.parse(time) > Date.now()) { + // if the data point has a time greater than right now, subtract one to find the one just before the current + // time + index = openMeteoData.minutely_15.time.indexOf(time) - 1 + } + }) + const shortwaveRadiation: string = openMeteoData.minutely_15.shortwave_radiation_instant[index] + + return { + cloudCover, + shortwaveRadiation, + } } -export async function getOpenMeteoData(latitude: number, longitude: number, weatherModel: string = "best_match") { - const openMeteoResponse = await fetch("https://api.open-meteo.com/v1/forecast", { - method: "POST", - body: new URLSearchParams({ - latitude: latitude.toString(), - longitude: longitude.toString(), - models: weatherModel, - current: "cloud_cover", - minutely_15: "shortwave_radiation_instant" - }) - }) - - if (!openMeteoResponse.ok) { - throw new Error(`Open-Meteo sent back an error: ${openMeteoResponse.status}`) - } - - const openMeteoData = await openMeteoResponse.json() - - const cloudCover = openMeteoData.current.cloud_cover - - let index = 0 - openMeteoData.minutely_15.time.forEach((time: string) => { - if (Date.parse(time) > Date.now()) { - // if the data point has a time greater than right now, subtract one to find the one just before the current - // time - index = openMeteoData.minutely_15.time.indexOf(time) - 1 - } - }) - const shortwaveRadiation = openMeteoData.minutely_15.shortwave_radiation_instant[index]; - - return { - cloudCover, shortwaveRadiation - } -} \ No newline at end of file +export function findMatchingImages( + imagesData: ImageAnalysisData, + hueValue: number, + chromaValue: number, + lightnessValue: number, +) { + let result = [] + let i = 0 + const chromaIndex = imagesData.chromaData.indexOf( + imagesData.chromaData.find((data) => data.chroma === chromaValue)!, + ) + const lightnessIndex = imagesData.lightnessData.indexOf( + imagesData.lightnessData.find((data) => data.lightness === lightnessValue)!, + ) + + do { + const matches = [ + imagesData.chromaData[chromaIndex + i].paths || imagesData.chromaData[chromaIndex - i].paths, + imagesData.lightnessData[lightnessIndex + i].paths || imagesData.lightnessData[lightnessIndex - i].paths, + ] + console.log(matches) + result = matches.reduce((a, b) => a.filter((c) => b.includes(c))) + + i++ + } while (result.length === 0) +} diff --git a/apply/index.mts b/apply/index.mts index f9d0614..2c8cd2b 100644 --- a/apply/index.mts +++ b/apply/index.mts @@ -1,23 +1,86 @@ #!/usr/bin/env zx /// -import { configDir, loadConfig } from "../utils.mts"; -import { ApplicationConfig, defaultConfig, getOpenMeteoData } from "./application.mts"; -import * as SunCalc from "npm:suncalc" +import { AnalysisConfig, defaultConfig as analysisDefaultConfig, ImageAnalysisData } from "../analyze/analysis.mts" +import { cacheDir, configDir, floorToStep, loadConfig, map } from "../utils.mts" +import { ApplicationConfig, defaultConfig, findMatchingImages, getOpenMeteoData } from "./application.mts" +import * as SunCalc from "npm:suncalc" const pathToConfig = `${configDir}/apply-config.json` +const pathToAnalysisConfig = `${configDir}/analyze-config.json` +const pathToCache = `${cacheDir}/analysis.json` -console.debug(`Trying to load config from ${pathToConfig}...`) -const config = await loadConfig(pathToConfig, defaultConfig) as ApplicationConfig +console.debug(`Trying to load application config from ${pathToConfig}...`) +const config = await loadConfig( + pathToConfig, + defaultConfig, +) as ApplicationConfig if (config == undefined) { - throw new Error("Couldn't initialize config!") + throw new Error("Couldn't initialize config!") +} + +// we also need the analysis config to load the steps +console.debug(`Trying to load analysis config from ${pathToAnalysisConfig}...`) +const analysisConfig = await loadConfig( + pathToAnalysisConfig, + analysisDefaultConfig, +) as AnalysisConfig +if (analysisConfig == undefined) { + throw new Error("Couldn't initialize config!") } console.debug(`Config: \n${JSON.stringify(config, null, 4)}`) -const openMeteoData = await getOpenMeteoData(config.latitude, config.longitude, config.weatherModel) -const sunCalcData = SunCalc.getPosition(new Date(), config.latitude, config.longitude) +const openMeteoData = await getOpenMeteoData( + config.latitude, + config.longitude, + config.weatherModel, +) +const sunCalcData = SunCalc.getPosition( + new Date(), + config.latitude, + config.longitude, +) + console.info(`Current cloud cover percentage is ${openMeteoData.cloudCover}%!`) -console.info(`Current shortwave radiation is ${openMeteoData.shortwaveRadiation} W/m²!`) +console.info( + `Current shortwave radiation is ${openMeteoData.shortwaveRadiation} W/m²!`, +) console.info(`Current sun altitude is ${sunCalcData.altitude}°!`) +// const hueValue = clamp() +const chromaValue = floorToStep( + map( + Number(openMeteoData.cloudCover), + 0, + 100, + config.chromaRange.end, + config.chromaRange.start, + ), + analysisConfig.chromaStep, +) + +const lightnessValue = floorToStep( + map( + Number(openMeteoData.shortwaveRadiation), + 0, + 1000, + config.lightnessRange.start, + config.lightnessRange.end, + ), + analysisConfig.lightnessStep, +) + +console.info(`Got chroma value ${chromaValue}!`) +console.info(`Got lightness value ${lightnessValue}!`) + +console.debug(`Trying to open cache file ${pathToCache}...`) +const cacheFile = await fs.promises.readFile(`${pathToCache}`, { + encoding: "utf8", +}) +const imagesData = JSON.parse(cacheFile) as ImageAnalysisData +console.debug(`Opened cache file ${pathToCache}!`) + + + +findMatchingImages(imagesData, 0, chromaValue, lightnessValue) \ No newline at end of file diff --git a/deno.json b/deno.json index d4ba7f1..3a6e8a8 100644 --- a/deno.json +++ b/deno.json @@ -7,5 +7,6 @@ "@std/assert": "jsr:@std/assert@1", "suncalc": "npm:suncalc@^2.0.0", "zx": "npm:zx@^8.8.5" - } + }, + "fmt": { "semiColons": false, "indentWidth": 4, "useTabs": true, "lineWidth": 120 } } diff --git a/utils.mts b/utils.mts index 28f488a..ac02769 100644 --- a/utils.mts +++ b/utils.mts @@ -1,35 +1,60 @@ -export const configDir = process.env.XDG_CONFIG_HOME ? `${process.env.XDG_CONFIG_HOME}/rainwall` : `${os.homedir()}/.config/rainwall` -export const cacheDir = process.env.XDG_CACHE_HOME ? `${process.env.XDG_CACHE_HOME}/rainwall` : `${os.homedir()}/.cache/rainwall` -if (!fs.statSync(configDir)){ - fs.mkdirSync(configDir); +export const configDir = process.env.XDG_CONFIG_HOME + ? `${process.env.XDG_CONFIG_HOME}/rainwall` + : `${os.homedir()}/.config/rainwall` +export const cacheDir = process.env.XDG_CACHE_HOME + ? `${process.env.XDG_CACHE_HOME}/rainwall` + : `${os.homedir()}/.cache/rainwall` + +if (!fs.statSync(configDir)) { + fs.mkdirSync(configDir) } -if (!fs.statSync(cacheDir)){ - fs.mkdirSync(cacheDir); +if (!fs.statSync(cacheDir)) { + fs.mkdirSync(cacheDir) } export async function loadConfig(pathToConfig: string, defaultConfig: object) { - let config: object | undefined - try { - const stringConfig = await fs.promises.readFile(`${pathToConfig}`, { encoding: 'utf8' }) - if (stringConfig == undefined) { - console.warn(`Current config blank, generating a new one... you may need to stop this process and edit the config file.`) - config = defaultConfig - writeDefaultConfig(pathToConfig, defaultConfig) - } else { - config = JSON.parse(stringConfig) - } - } catch (err) { - console.warn(`Got ${(err as Error).message} when trying to load config, generating a new one... you may need to stop this process and edit the config file.`) - config = defaultConfig - writeDefaultConfig(pathToConfig, defaultConfig) - } - console.info(`Loaded config from ${pathToConfig}!`) - return config + let config: object | undefined + try { + const stringConfig = await fs.promises.readFile(`${pathToConfig}`, { + encoding: "utf8" + }) + if (stringConfig == undefined) { + console.warn( + `Current config blank, generating a new one... you may need to stop this process and edit the config file.` + ) + config = defaultConfig + writeDefaultConfig(pathToConfig, defaultConfig) + } else { + config = JSON.parse(stringConfig) + } + } catch (err) { + console.warn( + `Got ${ + (err as Error).message + } when trying to load config, generating a new one... you may need to stop this process and edit the config file.` + ) + config = defaultConfig + writeDefaultConfig(pathToConfig, defaultConfig) + } + console.info(`Loaded config from ${pathToConfig}!`) + return config } async function writeDefaultConfig(path: string, data: object) { - await fs.writeFile(`${path}`, JSON.stringify(data, null, 4)) - console.info(`Generated default config at ${path}!`) + await fs.writeFile(`${path}`, JSON.stringify(data, null, 4)) + console.info(`Generated default config at ${path}!`) +} + +export function floorToStep(num: number, step: number) { + return Math.floor(num / step) * step +} + +export function clamp(number: number, min: number, max: number) { + return Math.max(min, Math.min(number, max)) +} + +export function map(number: number, min: number, max: number, newMin: number, newMax: number) { + return (((number - min) / (max - min)) * (newMax - newMin)) + newMin } \ No newline at end of file -- 2.51.2