diff --git a/i3/i3status/index.js b/i3/i3status/index.js new file mode 100644 index 0000000..3c4509c --- /dev/null +++ b/i3/i3status/index.js @@ -0,0 +1,532 @@ +import { URL } from "url"; +import { request } from "https"; +import { readFile } from "fs/promises"; +import { cpus as cpuInfo } from "os"; +import { exec } from "child_process"; +import { createInterface } from "readline/promises"; +import { stdin as input, stdout as output } from "process"; +import { createRenderer } from "@aminnairi/i3status"; + +// HELPERS + +const json = { + get: (url) => { + return new Promise((resolve, reject) => { + const { hostname, pathname, search } = new URL(url); + const path = `${pathname}${search}`; + + let data = ""; + + const req = request({ + hostname, + path, + port: 443, + method: "GET" + }, response => { + response.on("data", chunk => { + data += chunk.toString(); + }); + + response.on("end", () => { + try { + resolve(JSON.parse(data)); + } catch (error) { + reject(error); + } + }); + }); + + req.on("error", error => { + reject(error); + }); + + req.end(); + }); + } +}; + +const execute = (command) => { + return new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + return reject(error); + } + + if (stderr) { + return reject(new Error(stderr)); + } + + resolve(stdout); + }); + }); +}; + +const ignore = () => { }; + +// BUSINESS + +const getCov19 = async () => { + return json.get("https://coronavirusapifr.herokuapp.com/data/live/france").then(response => { + return ` ${Math.round(response[0].TO * 10000) / 100}%`; + }); +}; + +const getClock = () => { + const date = new Date(); + return ` ${date.toLocaleDateString("fr-FR")} ${date.toLocaleTimeString("fr-FR")}`; +}; + +const getBitcoinRate = async () => { + return json.get("https://api.coindesk.com/v1/bpi/currentprice.json").then(json => { + return ` ${json.bpi.USD.rate}`; + }); +}; + +const getWeather = async () => { + return json.get("https://api.openweathermap.org/data/2.5/weather?q=paris&appid=f31995fb1e1d69ac2f5d2053ca040a3b&units=metric").then(json => { + return ` ${Math.round((Number(json.main.temp) || 0) * 10) / 10}°C ${json.weather[0].main}`; + }); +}; + +const getCpus = () => { + const cpus = cpuInfo(); + const count = cpus.length; + const speed = Math.round(cpus.reduce((clock, cpu) => clock + cpu.speed, 0) / count) / 1000; + + return ` ${count} CPUs ${speed}GHz`; +}; + +const getBattery = async () => { + return Promise.all([ + readFile("/sys/class/power_supply/BAT0/capacity"), + readFile("/sys/class/power_supply/BAT0/status") + ]).then(([batteryBuffer, statusBuffer]) => { + const battery = batteryBuffer.toString().trim(); + const state = statusBuffer.toString().trim(); + + if (state === "Discharging") { + const full_text = ` ${battery}%`; + + if (battery <= 10) { + return { + color: "#FF0000", + full_text + }; + } + + if (battery <= 20) { + return { + color: "#FF7F00", + full_text + }; + } + + return { + color: "#FFFFFF", + full_text + }; + } + + return { + color: "#00ff00", + full_text: ` ${battery}%` + } + }); +}; + +const getBrightness = async () => { + return execute("cat /sys/class/backlight/intel_backlight/brightness").then(brightness => { + const brightnessLevel = Math.round((Number(brightness.toString().trim()) || 0) / 1000); + + return ` ${brightnessLevel}%`; + }); +}; + +const getVolume = async () => { + return Promise.all([ + execute("pactl get-sink-volume 0 | grep 'Volume' | cut -d '/' -f 2"), + execute("pactl get-sink-mute 0") + ]).then(([volume, muted]) => { + return `${muted.includes("no") ? "" : ""} ${volume.trim()}`; + }); +}; + +const getMemory = async () => { + return Promise.all([ + execute("cat /proc/meminfo | grep MemTotal | cut -d ' ' -f 9"), + execute("cat /proc/meminfo | grep MemFree | cut -d ' ' -f 11") + ]).then(([rawTotalMemory, rawFreeMemory]) => { + const totalMemory = Math.round((Number(rawTotalMemory.trim()) || 0) / 100000) / 10; + const freeMemory = Math.round((Number(rawFreeMemory.trim()) || 0) / 100000) / 10; + + return ` ${freeMemory}Go / ${totalMemory}Go`; + }); +}; + +const getDiskSpace = async () => { + return Promise.all([ + execute("df -h | grep '/\$' | cut -d ' ' -f 7"), + execute("df -h | grep '/\$' | cut -d ' ' -f 3") + ]).then(([rawDiskSpaceRemaining, rawDiskSpaceTotal]) => { + const diskSpaceRemaining = rawDiskSpaceRemaining.trim(); + const diskSpaceTotal = rawDiskSpaceTotal.trim(); + + return ` ${diskSpaceRemaining} / ${diskSpaceTotal}`; + }); +}; + +// SETUP + +const render = createRenderer({ + createInterface, + input, + output +}); + +const dispatch = render({ + initialBlocks: [ + { + name: "disk", + full_text: "", + separator: false + }, + { + name: "memory", + full_text: "", + separator: false + }, + { + name: "cpus", + full_text: getCpus(), + separator: false + }, + { + name: "bitcoin", + full_text: "", + separator: false + }, + { + name: "cov19", + full_text: "", + separator: false + }, + { + name: "weather", + full_text: "", + separator: false + }, + { + name: "battery", + full_text: "", + separator: false + }, + { + name: "brightness", + full_text: "", + separator: false + }, + { + name: "volume", + full_text: "", + separator: false + }, + { + name: "clock", + full_text: getClock(), + separator: false + }, + ], + onEvent: ({ event: { name, button, modifiers }, dispatch }) => { + const leftClick = 1; + + if (name === "clock" && button === leftClick && modifiers.length === 1 && modifiers.includes("Control")) { + return execute("librewolf https://calendar.google.com"); + } + + if (name === "weather" && button === 1 && modifiers.length === 0) { + return getWeather().then(weather => { + dispatch({ + name: "WEATHER_SET_FULL_TEXT", + payload: weather + }); + }).catch(ignore); + } + + if (name === "bitcoin" && button === 1) { + if (modifiers.length === 0) { + return getBitcoinRate().then(rate => { + dispatch({ + name: "BITCOIN_SET_FULL_TEXT", + payload: rate + }); + }).catch(ignore); + } + + if (modifiers.length === 1 && modifiers.includes("Control")) { + return execute("librewolf https://coindesk.com/price/bitcoin"); + } + } + }, + onDispatch: ({ blocks, action: { name, payload } }) => { + switch (name) { + case "CLOCK_SET_FULL_TEXT": + return blocks.map(block => { + if (block.name !== "clock") { + return block; + } + + return { + ...block, + full_text: payload + }; + }) + + case "BITCOIN_SET_FULL_TEXT": + return blocks.map(block => { + if (block.name !== "bitcoin") { + return block; + } + + return { + ...block, + full_text: payload + }; + }); + + case "WEATHER_SET_FULL_TEXT": + return blocks.map(block => { + if (block.name !== "weather") { + return block; + } + + return { + ...block, + full_text: payload + }; + }); + + case "CPUS_SET_FULL_TEXT": + return blocks.map(block => { + if (block.name !== "cpus") { + return block; + } + + return { + ...block, + full_text: payload + }; + }); + + case "BATTERY_SET_FULL_TEXT_COLOR": + return blocks.map(block => { + if (block.name !== "battery") { + return block; + } + + return { + ...block, + full_text: payload.full_text, + color: payload.color + }; + }); + + case "BRIGHTNESS_SET_FULL_TEXT": + return blocks.map(block => { + if (block.name !== "brightness") { + return block; + } + + return { + ...block, + full_text: payload + }; + }); + + case "VOLUME_SET_FULL_TEXT": + return blocks.map(block => { + if (block.name !== "volume") { + return block; + } + + return { + ...block, + full_text: payload + }; + }); + + case "MEMORY_SET_FULL_TEXT": + return blocks.map(block => { + if (block.name !== "memory") { + return block; + } + + return { + ...block, + full_text: payload + }; + }); + + case "DISK_SET_FULL_TEXT": + return blocks.map(block => { + if (block.name !== "disk") { + return block; + } + + return { + ...block, + full_text: payload + }; + }); + + case "COV19_SET_FULL_TEXT": + return blocks.map(block => { + if (block.name !== "cov19") { + return block; + } + + return { + ...block, + full_text: payload + }; + }); + + default: + return blocks; + } + } +}); + +// INTERVALS + +setInterval(() => { + dispatch({ + name: "CLOCK_SET_FULL_TEXT", + payload: getClock() + }); + + dispatch({ + name: "CPUS_SET_FULL_TEXT", + payload: getCpus() + }); + + getBattery().then(battery => { + dispatch({ + name: "BATTERY_SET_FULL_TEXT_COLOR", + payload: battery + }); + }).catch(ignore); + + getBrightness().then(brightness => { + dispatch({ + name: "BRIGHTNESS_SET_FULL_TEXT", + payload: brightness + }); + }).catch(ignore); + + getVolume().then(volume => { + dispatch({ + name: "VOLUME_SET_FULL_TEXT", + payload: volume + }); + }).catch(ignore); + + getMemory().then(memory => { + dispatch({ + name: "MEMORY_SET_FULL_TEXT", + payload: memory + }); + }).catch(ignore); + + getDiskSpace().then(diskSpace => { + dispatch({ + name: "DISK_SET_FULL_TEXT", + payload: diskSpace + }); + }).catch(ignore); +}, 1000); + +setInterval(() => { + getBitcoinRate().then(rate => { + dispatch({ + name: "BITCOIN_SET_FULL_TEXT", + payload: rate + }); + }).catch(ignore); +}, 1000 * 60 * 10); + +setInterval(() => { + getWeather().then(weather => { + dispatch({ + name: "WEATHER_SET_FULL_TEXT", + payload: weather + }); + }).catch(ignore); + +}, 1000 * 60 * 30); + +setInterval(() => { + getCov19().then(payload => { + dispatch({ + name: "COV19_SET_FULL_TEXT", + payload + }); + }).catch(ignore); +}, 1000 * 60 * 60 * 12); + +// INITIAL SETUP + +getBitcoinRate().then(rate => { + dispatch({ + name: "BITCOIN_SET_FULL_TEXT", + payload: rate + }); +}).catch(ignore); + +getWeather().then(weather => { + dispatch({ + name: "WEATHER_SET_FULL_TEXT", + payload: weather + }); +}).catch(ignore); + +getBattery().then(battery => { + dispatch({ + name: "BATTERY_SET_FULL_TEXT_COLOR", + payload: battery + }); +}).catch(ignore); + +getBrightness().then(brightness => { + dispatch({ + name: "BRIGHTNESS_SET_FULL_TEXT", + payload: brightness + }); +}).catch(ignore); + +getVolume().then(volume => { + dispatch({ + name: "VOLUME_SET_FULL_TEXT", + payload: volume + }); +}).catch(ignore); + +getMemory().then(memory => { + dispatch({ + name: "MEMORY_SET_FULL_TEXT", + payload: memory + }); +}).catch(ignore); + +getDiskSpace().then(diskSpace => { + dispatch({ + name: "DISK_SET_FULL_TEXT", + payload: diskSpace + }); +}).catch(ignore); + +getCov19().then(payload => { + dispatch({ + name: "COV19_SET_FULL_TEXT", + payload + }); +}).catch(ignore);