home/natto/ags: init
Signed-off-by: Amneesh Singh <natto@weirdnatto.in>
This commit is contained in:
69
home/natto/ags/windows/settings/audio.js
Normal file
69
home/natto/ags/windows/settings/audio.js
Normal 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")),
|
||||
}),
|
||||
});
|
||||
};
|
48
home/natto/ags/windows/settings/backlight.js
Normal file
48
home/natto/ags/windows/settings/backlight.js
Normal 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],
|
||||
});
|
||||
};
|
72
home/natto/ags/windows/settings/index.js
Normal file
72
home/natto/ags/windows/settings/index.js
Normal 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),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
};
|
104
home/natto/ags/windows/settings/metrics.js
Normal file
104
home/natto/ags/windows/settings/metrics.js
Normal 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"),
|
||||
});
|
94
home/natto/ags/windows/settings/power-menu.js
Normal file
94
home/natto/ags/windows/settings/power-menu.js
Normal 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")),
|
||||
});
|
||||
};
|
62
home/natto/ags/windows/settings/temperature.js
Normal file
62
home/natto/ags/windows/settings/temperature.js
Normal 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`),
|
||||
}),
|
||||
),
|
||||
});
|
||||
};
|
78
home/natto/ags/windows/settings/weather.js
Normal file
78
home/natto/ags/windows/settings/weather.js
Normal 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`;
|
||||
}),
|
||||
}),
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user