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

15
home/natto/ags/README.md Normal file
View File

@@ -0,0 +1,15 @@
# Starter Config
if suggestions don't work, first make sure
you have TypeScript LSP working in your editor
if you do not want typechecking only suggestions
```json
// tsconfig.json
"checkJs": false
```
types are symlinked to:
/nix/store/4rpg1hbvvfb8wpxf1a6ljbm390wfcwcd-ags-1.8.2/share/com.github.Aylur.ags/types

31
home/natto/ags/config.js Normal file
View File

@@ -0,0 +1,31 @@
import Bar from "./windows/bar/index.js";
import Settings from "./windows/settings/index.js";
import MusicBox from "./windows/music-box/index.js";
import Calendar from "./windows/calendar.js";
const configDir = App.configDir;
const scssStyle = `${configDir}/style.scss`;
const cssStyle = `${configDir}/style.css`;
const compileSass = () => {
Utils.exec(`scss ${scssStyle} ${cssStyle}`);
console.log("sass compiled to css");
};
compileSass();
Utils.monitorFile(`${configDir}/styles`, () => {
console.log("change detected in style");
compileSass();
App.resetCss();
App.applyCss(cssStyle);
console.log("new style applied");
});
App.config({
style: "./style.css",
windows: [Bar(), MusicBox(), Settings(), Calendar()],
});
export {};

View File

@@ -0,0 +1,6 @@
export const WindowNames = {
BAR: "bar",
SETTINGS: "settings",
MUSIC_BOX: "music-box",
CALENDAR: "calendar",
};

View File

@@ -0,0 +1,37 @@
{ pkgs, lib, config, inputs, ... }:
let
cfg = config.programs.ags;
deps = with pkgs; [
sass
gawk
bash
procps
coreutils
imagemagick
config.wayland.windowManager.hyprland.package
] ++ lib.optional config.isLaptop brightnessctl;
in
{
imports = [
inputs.ags.homeManagerModules.default
];
programs.ags.enable = true;
systemd.user.services.ags = {
Unit = {
Description = "Aylur's Gtk Shell";
PartOf = [
"tray.target"
"graphical-session.target"
];
};
Service = {
Environment = "PATH=${lib.makeBinPath deps}";
ExecStart = "${cfg.package}/bin/ags";
Restart = "on-failure";
};
Install.WantedBy = [ "graphical-session.target" ];
};
}

View File

@@ -0,0 +1,5 @@
@import "styles/global.scss";
@import "styles/bar.scss";
@import "styles/music-box.scss";
@import "styles/settings.scss";
@import "styles/calendar.scss";

View File

@@ -0,0 +1,40 @@
.bar {
background: $background;
.hyprland {
background: none;
button.focused {
transition-duration: 0;
color: $mauve;
box-shadow: 0 0 0 9999px rgba($mauve, 0.08) inset;
}
button.unfocused {
background: none;
color: $flamingo;
}
}
.music {
.music-title {
color: $sapphire;
}
.music-controls button {
color: $yellow;
}
}
.network .network-icon {
color: $lavender;
}
.date-wrapper {
color: $text;
}
.tray-button {
color: $flamingo;
}
}

View File

@@ -0,0 +1,8 @@
.calendar {
.calendar-unwrapped {
margin-top: 10px;
border: 2px solid $mauve;
border-radius: 4px;
background: rgba($base, 0.9);
}
}

View File

@@ -0,0 +1,57 @@
$rosewater: #f5e0dc;
$flamingo: #f2cdcd;
$pink: #f5c2e7;
$mauve: #cba6f7;
$red: #f38ba8;
$maroon: #eba0ac;
$peach: #fab387;
$yellow: #f9e2af;
$green: #a6e3a1;
$teal: #94e2d5;
$sky: #89dceb;
$sapphire: #74c7ec;
$blue: #89b4fa;
$lavender: #b4befe;
$text: #cdd6f4;
$subtext1: #bac2de;
$subtext0: #a6adc8;
$overlay2: #9399b2;
$overlay1: #7f849c;
$overlay0: #6c7086;
$surface2: #585b70;
$surface1: #45475a;
$surface0: #313244;
$base: #1e1e2e;
$mantle: #181825;
$crust: #11111b;
$background: $base;
$foreground: $text;
* {
font-weight: bold;
}
button {
background: none;
border-radius: 0px;
}
icon {
font-size: 24px;
}
.bar,
.music-box,
.calendar,
.settings {
font-size: 16px;
font-family: "Fira Mono";
}
.bar {
.hyprland {
font-size: 18px;
font-family: "Lohit Gurmukhi";
}
}

View File

@@ -0,0 +1,79 @@
.music-box {
.music-box-unwrapped {
margin-top: 15px;
.music-player {
background: $base;
border: 2px solid $mauve;
border-radius: 13px;
.cover-art {
margin: 2em;
border-radius: 13px;
background-size: cover;
background-position: center;
}
.music-details {
$shadow: 0px 0px 10px rgba(black, 0.8);
padding: 2em;
.title {
font-size: 30px;
color: $pink;
text-shadow: $shadow;
}
.icon-wrapper {
.icon {
font-size: 24px;
color: $yellow;
}
.artist {
color: $yellow;
text-shadow: $shadow;
}
}
.length-label,
.position-label {
text-shadow: $shadow;
}
.music-controls {
border-radius: 20px 8px 20px 8px;
padding: 5px;
margin: 0px 10px;
background: rgba($base, 0.5);
button {
font-size: 30px;
&:hover {
box-shadow: none;
}
}
}
scale trough {
min-height: 10px;
margin: 0 15px;
highlight {
background-image: linear-gradient(to right, $sapphire, $blue);
}
slider {
border-radius: 4px;
background: $background;
margin: -10px -10px;
transition: 0.1s;
&:hover {
border: 2px $mauve solid;
box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.1);
}
&:active {
box-shadow:
0 0 0 1px inset,
0 0 0 8px rgba(255, 255, 255, 0.1);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,112 @@
.settings {
.settings-unwrapped {
margin-top: 10px;
border: 2px solid $mauve;
border-radius: 4px;
background: $base;
.metrics {
& > * {
background-color: rgba(255, 255, 255, 0.03);
border-radius: 4px;
margin: 10px;
padding: 10px;
.metric-progress {
min-width: 40px;
min-height: 40px;
font-size: 4px;
margin: 4px;
.metric-icon {
font-size: 15px;
}
}
}
.cpu-metric {
color: $mauve;
}
.memory-metric {
color: $flamingo;
}
.disk-metric {
color: $pink;
}
.battery-metric {
color: $yellow;
}
}
.settings-col {
.sliders {
$icon-size: 15px;
$scale-height: 20px;
margin: 10px;
scale trough {
min-height: $scale-height;
min-width: 160px;
border-radius: 3px;
highlight {
all: unset;
border-radius: 3px;
}
slider {
$slider-height: $scale-height + 5px;
min-width: $slider-height;
min-height: $slider-height;
border-radius: $slider-height;
background: $text;
box-shadow: 0 2px 0 0 rgba(255, 255, 255, 0.1);
}
}
.volume {
.volume-icon {
font-size: $icon-size;
color: $green;
}
scale trough highlight {
background-color: $green;
}
}
.brightness {
.brightness-icon {
font-size: $icon-size;
color: $yellow;
}
scale trough highlight {
background-color: $yellow;
}
}
}
.settings-col-temps {
.temperature {
color: $blue;
}
.temperature-hot {
color: $red;
}
.weather {
color: $rosewater;
}
}
.power-menu {
margin: 20px;
button {
&:hover {
color: $red;
box-shadow: none;
}
font-size: 24px;
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022"
],
"allowJs": true,
"checkJs": false,
"strict": true,
"noImplicitAny": false,
"baseUrl": ".",
"typeRoots": [
"./types"
],
"skipLibCheck": true
}
}

View File

@@ -0,0 +1,42 @@
import GLib from "gi://GLib";
export const lengthStr = (length) => {
if (length < 0) return "0:00";
const min = Math.floor(length / 60);
const sec = Math.floor(length % 60);
const sec0 = sec < 10 ? "0" : "";
return `${min}:${sec0}${sec}`;
};
export const blurBg = (cover) => {
if (!cover) return "";
const cachePath = Utils.CACHE_DIR + "/media";
const blurPath = cachePath + "/blur";
const bgPath = blurPath + cover.substring(cachePath.length);
if (!GLib.file_test(bgPath, GLib.FileTest.EXISTS)) {
Utils.ensureDirectory(blurPath);
Utils.exec(
`convert ${cover} -scale 10% -blur 0x2 -resize 1000% ${bgPath}`,
);
}
return `
background-image: url('${bgPath}');
background-repeat: no-repeat;
background-position: center;
background-size: cover;
`;
};
export const findPlayer = (players) => {
const active = players.find((p) => p.playBackStatus === "Playing");
if (active) return active;
for (const p of players) if (p) return p;
return undefined;
};

View File

@@ -0,0 +1,41 @@
export const Padding = (name, { hexpand = true, vexpand = true } = {}) =>
Widget.EventBox({
hexpand,
vexpand,
can_focus: false,
setup: (w) => w.on("button-press-event", () => App.closeWindow(name)),
});
export const Revealer = ({
name,
child,
transition = "slide_down",
transitionDuration = 200,
}) =>
Widget.Box({
css: "padding: 1px;",
child: Widget.Revealer({
transition,
transitionDuration,
child: Widget.Box({
child,
}),
setup: (self) =>
self.hook(App, (_, window, visible) => {
if (window === name) self.reveal_child = visible;
}),
}),
});
export default ({ name, layout, ...props }) =>
Widget.Window({
name,
setup: (w) => w.keybind("Escape", () => App.closeWindow(name)),
visible: false,
keymode: "on-demand",
exclusivity: "normal",
layer: "top",
anchor: ["top", "bottom", "right", "left"],
child: layout,
...props,
});

View File

@@ -0,0 +1,5 @@
export const shrinkText = (str, n) => {
let newStr = str.substring(0, n);
if (str.length > n) newStr = newStr + "...";
return newStr;
};

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

View File

@@ -1,6 +1,7 @@
{ pkgs, config, conf, inputs, ... }:
{
imports = [
./ags
# ./eww
];