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,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`;
}),
}),
);
};