home/natto/ags: init

Signed-off-by: Amneesh Singh <natto@weirdnatto.in>
This commit is contained in:
2024-06-01 17:59:52 +05:30
parent 00ea23f65c
commit c86fb8b6d3
37 changed files with 1658 additions and 33 deletions

View File

@@ -0,0 +1,19 @@
const bluetooth = await Service.import("bluetooth");
export default () =>
Widget.Icon({
setup: (self) =>
self.hook(
bluetooth,
(self) => {
self.tooltipText = bluetooth.connected_devices
.map(({ name }) => name)
.join("\n");
self.visible = bluetooth.connected_devices.length > 0;
},
"notify::connected-devices",
),
icon: bluetooth
.bind("enabled")
.as((on) => `bluetooth-${on ? "active" : "disabled"}-symbolic`),
});

View File

@@ -0,0 +1,38 @@
const hyprland = await Service.import("hyprland");
const gurmukhiNums = {
1: "",
2: "੨",
3: "੩",
4: "",
5: "੫",
6: "੬",
7: "੭",
8: "੮",
9: "੯",
10: "",
};
export default () => {
const activeId = hyprland.active.workspace.bind("id");
const workspaces = hyprland.bind("workspaces").as((ws) =>
ws
.sort((a, b) => a.id - b.id)
.map(({ id }) =>
Widget.Button({
onClicked: () => hyprland.messageAsync(`dispatch workspace ${id}`),
child: Widget.Label(gurmukhiNums[id]),
className: activeId.as(
(i) => `${i === id ? "focused" : "unfocused"}`,
),
cursor: "pointer",
}),
),
);
return Widget.Box({
css: "padding: 1px;",
className: "hyprland",
children: workspaces,
});
};

View File

@@ -0,0 +1,56 @@
import Hyprland from "./hyprland.js";
import Music from "./music.js";
import Tray from "./tray.js";
import Time from "./time.js";
import Network from "./network.js";
import Bluetooth from "./bluetooth.js";
import Settings from "./settings.js";
import { WindowNames } from "../../constants.js";
const { BAR } = WindowNames;
const Left = () => {
return Widget.Box({
className: "bar-left",
spacing: 8,
children: [Hyprland()],
});
};
const Center = (monitor) => {
return Widget.Box({
className: "bar-center",
spacing: 8,
children: [Music(monitor)],
});
};
const Right = (monitor) => {
return Widget.Box({
className: "bar-right",
hpack: "end",
spacing: 10,
children: [
Tray(),
Bluetooth(),
Network(),
Time(monitor),
Settings(monitor),
],
});
};
export default (monitor = 0) =>
Widget.Window({
name: `${BAR}-${monitor}`,
className: BAR,
monitor,
anchor: ["top", "left", "right"],
exclusivity: "exclusive",
child: Widget.CenterBox({
startWidget: Left(),
centerWidget: Center(monitor),
endWidget: Right(monitor),
}),
});

View File

@@ -0,0 +1,65 @@
import Controls from "../music-box/music-controls.js";
import { shrinkText } from "../../utils/text.js";
import { findPlayer } from "../../utils/music.js";
import { WindowNames } from "../../constants.js";
const mpris = await Service.import("mpris");
const players = mpris.bind("players");
/** @param {import('types/service/mpris').MprisPlayer} player */
const Player = (player, monitor) => {
const revealer = Widget.Revealer({
revealChild: false,
transitionDuration: 300,
transition: "slide_left",
child: Controls(player),
});
return Widget.EventBox({
visible: player.bus_name === findPlayer(mpris.players).bus_name,
cursor: "pointer",
setup: (self) => {
self.on("leave-notify-event", () => {
revealer.reveal_child = false;
});
self.on("enter-notify-event", () => {
revealer.reveal_child = true;
});
},
child: Widget.Box({
className: "music",
children: [
Widget.Button({
onClicked: () =>
App.toggleWindow(`${WindowNames.MUSIC_BOX}-${monitor}`),
className: "music-title",
child: Widget.Label().hook(player, (self) => {
self.tooltip_text = player.track_title;
self.label = shrinkText(self.tooltip_text, 50);
}),
}),
revealer,
],
}),
})
.hook(
mpris,
(self, bus_name) => {
self.visible = player.bus_name === bus_name;
},
"player-changed",
)
.hook(
mpris,
(self) => {
self.visible = player === findPlayer(mpris.players);
},
"player-closed",
);
};
export default (monitor) =>
Widget.Box({
visible: players.as((p) => p.length > 0),
children: players.as((ps) => ps.map((p) => Player(p, monitor))),
});

View File

@@ -0,0 +1,40 @@
const network = await Service.import("network");
const WifiIndicator = () =>
Widget.Box({
tooltipText: network.wifi.bind("state").as((s) => `State: ${s}`),
children: [
Widget.Icon({
className: "network-icon",
icon: network.wifi.bind("icon_name"),
}),
Widget.Label({
visible: network.wifi.bind("ssid"),
label: network.wifi.bind("ssid"),
}),
],
});
const WiredIndicator = () =>
Widget.Icon({
className: "network-icon",
tooltipText: network.wired.bind("internet").as((a) => `Internet: ${a}`),
icon: network.wired.bind("icon_name"),
});
export default () =>
Widget.Stack({
className: "network",
children: {
wifi: WifiIndicator(),
wired: WiredIndicator(),
},
shown: Utils.merge(
[network.bind("primary"), network.wired.bind("state")],
(primary, wired) => {
if (primary) return primary;
if (wired == "activated") return "wired";
return "wifi";
},
),
});

View File

@@ -0,0 +1,8 @@
import { WindowNames } from "../../constants.js";
export default (monitor) =>
Widget.Button({
child: Widget.Icon("open-menu-symbolic"),
onPrimaryClick: () =>
App.toggleWindow(`${WindowNames.SETTINGS}-${monitor}`),
});

View File

@@ -0,0 +1,42 @@
const time = Variable("", {
poll: [1000, 'date "+%H:%M:%S"'],
});
const getDate = () => " " + Utils.exec('date "+%a, %b %e"');
const date = Variable(getDate());
export default () => {
const revealer = Widget.Revealer({
revealChild: false,
transitionDuration: 300,
transition: "slide_left",
child: Widget.Label({
label: date.bind(),
}),
});
return Widget.EventBox({
cursor: "pointer",
setup: (self) => {
self.on("leave-notify-event", () => {
revealer.reveal_child = false;
});
self.on("enter-notify-event", () => {
date.value = getDate();
revealer.reveal_child = true;
});
},
child: Widget.Button({
className: "date-wrapper",
onPrimaryClick: () => App.toggleWindow("calendar-0"),
child: Widget.Box({
children: [
Widget.Label({
label: time.bind(),
}),
revealer,
],
}),
}),
});
};

View File

@@ -0,0 +1,17 @@
const systemtray = await Service.import("systemtray");
const SysTrayItem = (item) =>
Widget.Button({
className: "system-tray-item",
child: Widget.Icon().bind("icon", item, "icon"),
tooltipMarkup: item.bind("tooltip_markup"),
onPrimaryClick: (_, event) => item.activate(event),
onSecondaryClick: (_, event) => item.openMenu(event),
cursor: "pointer",
});
export default () =>
Widget.Box({
className: "system-tray-unwrapped",
children: systemtray.bind("items").as((i) => i.map(SysTrayItem)),
});

View File

@@ -0,0 +1,37 @@
import Popup, { Padding, Revealer } from "../utils/popup.js";
import { WindowNames } from "../constants.js";
const Tray = Widget.Calendar({
className: "calendar-unwrapped",
showDayNames: true,
showHeading: true,
});
export default (monitor = 0) => {
const { CALENDAR } = WindowNames;
const name = `${CALENDAR}-${monitor}`;
return Popup({
name,
className: CALENDAR,
monitor,
layout: Widget.Box({
children: [
Padding(name),
Widget.Box({
hexpand: false,
vertical: true,
children: [
Revealer({
name,
child: Tray,
transition: "slide_down",
transitionDuration: 400,
}),
Padding(name),
],
}),
],
}),
});
};

View File

@@ -0,0 +1,183 @@
// Mostly taken from https://github.com/Aylur/ags/blob/11150225e62462bcd431d1e55185e810190a730a/example/media-widget/Media.js
import Popup, { Padding, Revealer } from "../../utils/popup.js";
import { shrinkText } from "../../utils/text.js";
import { lengthStr, blurBg } from "../../utils/music.js";
import { findPlayer } from "../../utils/music.js";
import { WindowNames } from "../../constants.js";
import Controls from "./music-controls.js";
const FALLBACK_ICON = "audio-x-generic-symbolic";
const { MUSIC_BOX } = WindowNames;
const mpris = await Service.import("mpris");
const Player = (player) => {
const img = Widget.Box({
visible: player.bind("cover_path"),
className: "cover-art",
vpack: "start",
css: player
.bind("cover_path")
.as(
(p) =>
(p ? "min-width: 200px; min-height: 200px;" : "") +
`background-image: url('${p}');`,
),
});
const title = Widget.Label({
className: "title",
wrap: true,
hpack: "start",
label: player.bind("track_title").as((t) => shrinkText(t, 40)),
});
const artist = Widget.Label({
className: "artist",
wrap: true,
hpack: "start",
label: player.bind("track_artists").as((a) => shrinkText(a.join(", "), 80)),
});
const positionSlider = Widget.Slider({
className: "position",
drawValue: false,
onChange: ({ value }) => (player.position = value * player.length),
visible: player.bind("length").as((l) => l > 0),
setup: (self) => {
const update = () => {
if (self.dragging) return;
const value = player.position / player.length;
self.value = value > 0 ? value : 0;
};
self
.hook(player, update)
.hook(player, update, "position")
.poll(1000, update);
},
});
const positionLabel = Widget.Label({
ypad: 0,
hpack: "start",
className: "position-label",
setup: (self) => {
const update = (_, time) => {
self.label = lengthStr(time || player.position);
self.visible = player.length > 0;
};
self.hook(player, update, "position");
self.poll(1000, update);
},
});
const lengthLabel = Widget.Label({
ypad: 0,
hpack: "end",
className: "length-label",
visible: player.bind("length").as((l) => l > 0),
label: player.bind("length").as(lengthStr),
});
const icon = Widget.Icon({
className: "icon",
hexpand: true,
hpack: "end",
vpack: "start",
tooltipText: player.identity || "",
icon: player.bind("entry").as((entry) => {
const name = `${entry}-symbolic`;
return Utils.lookUpIcon(name) ? name : FALLBACK_ICON;
}),
});
return Widget.Box(
{
className: "music-player",
visible: player.bus_name === findPlayer(mpris.players).bus_name,
css: player.bind("cover_path").as(blurBg),
},
img,
Widget.CenterBox({
className: "music-details",
vertical: true,
hexpand: true,
spacing: 25,
startWidget: Widget.Box(
{
vertical: true,
spacing: 6,
},
title,
Widget.Box({
className: "icon-wrapper",
spacing: 10,
children: [artist, icon],
}),
),
centerWidget: positionSlider,
endWidget: Widget.CenterBox({
spacing: 6,
startWidget: positionLabel,
centerWidget: Controls(player),
endWidget: lengthLabel,
}),
}),
)
.hook(
mpris,
(self, bus_name) => {
self.visible = player.bus_name === bus_name;
},
"player-changed",
)
.hook(
mpris,
(self) => {
self.visible = player.bus_name === findPlayer(mpris.players).bus_name;
},
"player-closed",
);
};
const PlayerBox = () =>
Widget.Box({
className: `${MUSIC_BOX}-unwrapped`,
vertical: true,
css: "padding: 1px;",
children: mpris.bind("players").as((ps) => ps.map(Player)),
});
export default (monitor = 0) => {
const name = `${MUSIC_BOX}-${monitor}`;
return Popup({
name,
className: MUSIC_BOX,
monitor,
layout: Widget.Box({
children: [
Padding(name), // left
Widget.Box({
hexpand: false,
vexpand: false,
vertical: true,
children: [
Revealer({
name,
child: PlayerBox(),
transition: "slide_down",
transitionDuration: 400,
}),
Padding(name), // down
],
}),
Padding(name), // right
],
}),
});
};

View File

@@ -0,0 +1,47 @@
const PLAY_ICON = "media-playback-start-symbolic";
const PAUSE_ICON = "media-playback-pause-symbolic";
const PREV_ICON = "go-previous-symbolic";
const NEXT_ICON = "go-next-symbolic";
export default (player) => {
const playPause = Widget.Button({
class_name: "play-pause",
on_clicked: () => player.playPause(),
visible: player.bind("can_play"),
cursor: "pointer",
child: Widget.Icon({
icon: player.bind("play_back_status").transform((s) => {
switch (s) {
case "Playing":
return PAUSE_ICON;
case "Paused":
case "Stopped":
return PLAY_ICON;
}
}),
}),
});
const prev = Widget.Button({
yalign: 0.5,
onClicked: () => player.previous(),
visible: player.bind("can_go_prev"),
child: Widget.Icon(PREV_ICON),
cursor: "pointer",
});
const next = Widget.Button({
onClicked: () => player.next(),
visible: player.bind("can_go_next"),
child: Widget.Icon(NEXT_ICON),
cursor: "pointer",
});
return Widget.CenterBox({
vertical: true,
centerWidget: Widget.Box({
className: "music-controls",
children: [prev, playPause, next],
}),
});
};

View File

@@ -0,0 +1,69 @@
const audio = await Service.import("audio");
export default () => {
const isSpeaker = Variable(true);
/** @param {'speaker' | 'microphone'} type */
const VolumeSlider = (type = "speaker") =>
Widget.Slider({
hexpand: true,
drawValue: false,
onChange: ({ value }) => (audio[type].volume = value),
value: audio[type].bind("volume"),
});
const speakerSlider = VolumeSlider("speaker");
const micSlider = VolumeSlider("microphone");
const speakerIndicator = Widget.Button({
on_clicked: () => (audio.speaker.is_muted = !audio.speaker.is_muted),
child: Widget.Icon().hook(audio.speaker, (self) => {
self.className = "volume-icon";
const vol = audio.speaker.volume * 100;
let icon = [
[101, "overamplified"],
[67, "high"],
[34, "medium"],
[1, "low"],
[0, "muted"],
].find(([threshold]) => threshold <= vol)?.[1];
if (audio.speaker.is_muted) icon = "muted";
self.icon = `audio-volume-${icon}-symbolic`;
self.tooltip_text = `Volume ${Math.floor(vol)}%`;
}),
});
const micIndicator = Widget.Button({
on_clicked: () => (audio.microphone.is_muted = !audio.microphone.is_muted),
child: Widget.Icon().hook(audio.microphone, (self) => {
self.className = "volume-icon";
const vol = audio.microphone.volume * 100;
let icon = [
[67, "high"],
[34, "medium"],
[1, "low"],
[0, "muted"],
].find(([threshold]) => threshold <= vol)?.[1];
if (audio.microphone.is_muted) icon = "muted";
self.icon = `microphone-sensitivity-${icon}-symbolic`;
self.tooltip_text = `Volume ${Math.floor(vol)}%`;
}),
});
return Widget.EventBox({
cursor: "pointer",
className: "volume",
onSecondaryClick: () => (isSpeaker.value = !isSpeaker.value),
child: Widget.Stack({
children: {
speaker: Widget.Box({}, speakerSlider, speakerIndicator),
microphone: Widget.Box({}, micSlider, micIndicator),
},
shown: isSpeaker.bind().as((s) => (s ? "speaker" : "microphone")),
}),
});
};

View File

@@ -0,0 +1,48 @@
const hasBacklight = Variable(Utils.exec("ls /sys/class/backlight"));
export default () => {
const getBrightness = () => {
try {
return (
Number(Utils.exec("brightnessctl get")) /
Number(Utils.exec("brightnessctl max"))
);
} catch {
console.log("settings/backlight: failed to get brightness");
}
return 0;
};
const setBrightness = (b) => {
if (b < 0.05) b = 0.05;
else if (b > 1) b = 1;
Utils.exec(`brightnessctl set ${b * 100}%`);
};
const brightness = Variable(getBrightness());
const Slider = Widget.Slider({
hexpand: true,
drawValue: false,
onChange: ({ value }) => setBrightness(value),
value: brightness.bind(),
});
const Indicator = Widget.Button({
on_clicked: brightness.bind().as((b) => () => {
if (b <= 0.5) brightness.value = 1;
else brightness.value = 0.5;
}),
child: Widget.Icon().hook(brightness, (self) => {
self.className = "brightness-icon";
self.icon = `display-brightness-symbolic`;
self.tooltip_text = `Brightness: ${Math.floor(brightness.value * 100)}%`;
}),
});
return Widget.Box({
className: "brightness",
visible: hasBacklight.bind().as((b) => !!b),
children: [Slider, Indicator],
});
};

View File

@@ -0,0 +1,72 @@
import Popup, { Padding, Revealer } from "../../utils/popup.js";
import { WindowNames } from "../../constants.js";
import Audio from "./audio.js";
import Backlight from "./backlight.js";
import {
cpuMetric,
memoryMetric,
diskMetric,
batteryMetric,
} from "./metrics.js";
import Temperature from "./temperature.js";
import Weather from "./weather.js";
import PowerMenu from "./power-menu.js";
const metrics = Widget.Box({
className: "metrics",
vertical: true,
children: [cpuMetric, memoryMetric, diskMetric, batteryMetric],
});
const sliders = Widget.Box({
className: "sliders",
vertical: true,
children: [Audio(), Backlight()],
});
const settingsCol = Widget.CenterBox({
className: "settings-col",
vertical: true,
spacing: 8,
startWidget: sliders,
centerWidget: Widget.CenterBox({
className: "settings-col-temps",
startWidget: Temperature(),
endWidget: Weather(),
}),
endWidget: PowerMenu(),
});
const settings = Widget.Box({
className: "settings-unwrapped",
children: [metrics, settingsCol],
});
export default (monitor = 0) => {
const { SETTINGS } = WindowNames;
const name = `${SETTINGS}-${monitor}`;
return Popup({
name,
className: SETTINGS,
monitor,
layout: Widget.Box({
children: [
Padding(name),
Widget.Box({
hexpand: false,
vertical: true,
children: [
Revealer({
name,
child: settings,
transition: "slide_down",
transitionDuration: 400,
}),
Padding(name),
],
}),
],
}),
});
};

View File

@@ -0,0 +1,104 @@
const battery = await Service.import("battery");
const divide = ([total, free]) => free / total;
const cpuValue = Variable(0, {
poll: [
2000,
"top -b -n 1",
(out) =>
divide([
100,
out
.split("\n")
.find((line) => line.includes("Cpu(s)"))
.split(/\s+/)[1]
.replace(",", "."),
]),
],
});
const memoryValue = Variable([0, 0], {
poll: [
2000,
"free",
(out) => {
const data = out
.split("\n")
.find((line) => line.includes("Mem:"))
.split(/\s+/)
.splice(1, 2);
return [(data[1] / (1024 * 1024)).toFixed(2), divide(data)];
},
],
});
const diskValue = Variable(["0G", "0%"], {
poll: [
120000,
"df -kh /",
(out) => out.split("\n")[1].split(/\s+/).splice(3, 2),
],
});
const mkMetric = ({
className,
tooltipText,
value,
label,
icon,
visible = true,
}) =>
Widget.Box(
{
spacing: 10,
className,
tooltipText,
visible,
},
Widget.CircularProgress({
className: "metric-progress",
child: Widget.Icon({
icon,
className: "metric-icon",
}),
value,
}),
Widget.Label({
wrap: true,
label,
}),
);
export const cpuMetric = mkMetric({
className: "cpu-metric",
tooltipText: cpuValue.bind().as((c) => `CPU: ${(c * 100).toFixed(2)}%`),
value: cpuValue.bind(),
label: cpuValue.bind().as((c) => `${(c * 100).toFixed(2)}%`),
icon: "cpu-symbolic",
});
export const memoryMetric = mkMetric({
className: "memory-metric",
tooltipText: memoryValue.bind().as((m) => `RAM :${m[0]}G`),
value: memoryValue.bind().as((m) => m[1]),
label: memoryValue.bind().as((m) => `${m[0]}G`),
icon: "memory-symbolic",
});
export const diskMetric = mkMetric({
className: "disk-metric",
tooltipText: diskValue.bind().as((d) => `Free Space :${d[0]}`),
value: diskValue.bind().as((d) => Number(d[1]) / 100),
label: diskValue.bind().as((d) => d[1]),
icon: "drive-harddisk-symbolic",
});
export const batteryMetric = mkMetric({
className: "battery-metric",
tooltipText: battery.bind("percent").as((p) => `Battery: ${p}%`),
value: battery.bind("percent").as((p) => (p > 0 ? p / 100 : 0)),
label: battery.bind("percent").as((p) => `${p}%`),
icon: battery.bind("icon_name"),
visible: battery.bind("available"),
});

View File

@@ -0,0 +1,94 @@
export default () => {
const isLocked = Variable(true);
const power = Variable("poweroff");
const suspend = Variable("sleep");
const cursor = "pointer";
const unlockButton = Widget.Button({
onPrimaryClick: () => {
isLocked.value = false;
},
tooltipText: "Unock power menu",
child: Widget.Icon("system-lock-screen-symbolic"),
cursor,
});
const lockButton = Widget.Button({
onPrimaryClick: () => {
isLocked.value = true;
},
tooltipText: "Lock power menu",
child: Widget.Icon("system-lock-screen-symbolic"),
cursor,
});
const poweroffButton = Widget.Button({
onPrimaryClick: () => {
Utils.exec("poweroff");
},
onSecondaryClick: () => (power.value = "reboot"),
tooltipText: "Shutdown",
child: Widget.Icon("system-shutdown-symbolic"),
cursor,
});
const rebootButton = Widget.Button({
onPrimaryClick: () => {
Utils.exec("reboot");
},
onSecondaryClick: () => (power.value = "poweroff"),
tooltipText: "Reboot",
child: Widget.Icon("system-reboot-symbolic"),
cursor,
});
const sleepButton = Widget.Button({
onPrimaryClick: () => {
Utils.exec("systemctl suspend");
},
onSecondaryClick: () => (suspend.value = "hibernate"),
tooltipText: "Sleep",
child: Widget.Icon("weather-clear-night-symbolic"),
cursor,
});
const hibernateButton = Widget.Button({
onPrimaryClick: () => {
Utils.exec("systemctl hibernate");
},
onSecondaryClick: () => (suspend.value = "sleep"),
tooltipText: "Hibernate",
child: Widget.Icon("computer-symbolic"),
cursor,
});
const powerStack = Widget.Stack({
children: {
poweroff: poweroffButton,
reboot: rebootButton,
},
shown: power.bind(),
});
const suspendStack = Widget.Stack({
children: {
sleep: sleepButton,
hibernate: hibernateButton,
},
shown: suspend.bind(),
});
return Widget.Stack({
className: "power-menu",
children: {
locked: Widget.CenterBox({ centerWidget: unlockButton }),
unlocked: Widget.CenterBox({
startWidget: powerStack,
centerWidget: lockButton,
endWidget: suspendStack,
}),
},
shown: isLocked.bind().as((l) => (l ? "locked" : "unlocked")),
});
};

View File

@@ -0,0 +1,62 @@
export default () => {
const getThermalZone = () => {
try {
return Utils.exec([
"bash",
"-c",
`awk '{print FILENAME ":" $0}' /sys/class/thermal/thermal_zone*/type`,
])
.split("\n")
.find((line) => line.includes("x86_pkg_temp"))
.split(":")[0]
.slice(0, -4);
} catch (e) {
console.error(e);
console.log("settings/temperature: cannot get thermal zone");
}
return undefined;
};
const thermalZone = Variable(getThermalZone());
const tempValue = thermalZone.value
? Variable(0, {
poll: [
5000,
() => {
try {
return (
Utils.readFile(`${thermalZone.value}/temp`) / 1000
).toFixed(2);
} catch {
console.log(
"settings/temperature: specified thermal zone does not exist",
);
}
return 0;
},
],
})
: Variable(undefined);
return Widget.CenterBox({
vertical: true,
visible: thermalZone.bind().as((t) => !!t),
centerWidget: Widget.Box(
{
vertical: true,
spacing: 8,
tooltipText: tempValue.bind().as((t) => `CPU Temperature: ${t}°C`),
className: tempValue
.bind()
.as((t) => `temperature${t > 65 ? "-hot" : ""}`),
},
Widget.Icon({
icon: "sensors-temperature-symbolic",
}),
Widget.Label({
label: tempValue.bind().as((t) => `${t}°C`),
}),
),
});
};

View File

@@ -0,0 +1,78 @@
const conditionIcons = {
Clear: "clear",
Sunny: "clear",
"Partly Cloudy": "few-clouds",
Cloudy: "overcast",
Overcast: "overcast",
"Light Rain": "showers-scattered",
Drizzle: "showers-scattered",
Rain: "showers",
"Heavy Rain": "showers",
Showers: "showers",
Thunderstorm: "storm",
Snow: "snow",
"Light Snow": "snow",
"Heavy Snow": "snow",
Mist: "fog",
Fog: "fog",
Haze: "fog",
Dust: "fog",
Smoke: "fog",
Sand: "fog",
Wind: "windy",
Tornado: "tornado",
undefined: "clear",
};
const fetchWeather = async () => {
return await Utils.fetch("http://wttr.in/?format=j1")
.then((res) => res.json())
.then((j) => j["current_condition"][0])
.catch((e) => {
console.error(e);
console.log("settings/weather: error fetching weather data");
});
};
export default () => {
const data = Variable(undefined, {
poll: [600000, async () => await fetchWeather()],
});
return Widget.Box(
{
vertical: true,
visible: data.bind().as((d) => !!d),
className: "weather",
},
Widget.Icon({
icon: data.bind().as((d) => {
const condition = d?.["weatherDesc"]?.[0]?.["value"];
return `weather-${conditionIcons[condition]}-symbolic`;
}),
}),
Widget.Label({
label: data.bind().as((d) => {
const conditions = d?.["weatherDesc"]?.map((w) => w["value"]) || [];
return conditions.join(" ");
}),
}),
Widget.Label({
label: data.bind().as((d) => {
const temperature = d?.["temp_C"];
const feelsLike = d?.["FeelsLikeC"];
return `${temperature}°C (${feelsLike}°C)`;
}),
}),
Widget.Label({
label: data.bind().as((d) => {
const humidity = d?.["humidity"];
const precipitation = d?.["precipMM"];
return `${humidity}%, ${precipitation}mm`;
}),
}),
);
};