testing ags

This commit is contained in:
cnst
2024-07-15 14:39:56 +02:00
parent 91e09c461b
commit 2f32e46601
55 changed files with 2218 additions and 22 deletions

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:
/home/cnst/.local/share/com.github.Aylur.ags/types

View File

@@ -0,0 +1,45 @@
import { App, Audio, Notifications, Utils } from "./imports.js";
import Bar from "./windows/bar/main.js";
import Music from "./windows/music/main.js";
import NotificationPopup from "./windows/notifications/popups.js";
import Osd from "./windows/osd/main.js";
import SystemMenu from "./windows/system-menu/main.js";
const scss = App.configDir + "/style.scss";
const css = App.configDir + "/style.css";
Utils.exec(`sass ${scss} ${css}`);
App.connect("config-parsed", () => print("config parsed"));
App.config({
style: css,
closeWindowDelay: {
"system-menu": 200,
},
});
Notifications.popupTimeout = 5000;
Notifications.forceTimeout = false;
Notifications.cacheActions = true;
Audio.maxStreamVolume = 1;
function reloadCss() {
console.log("scss change detected");
Utils.exec(`sass ${scss} ${css}`);
App.resetCss();
App.applyCss(css);
}
Utils.monitorFile(`${App.configDir}/style`, reloadCss);
/**
* @param {import("types/widgets/window.js").Window[]} windows
*/
function addWindows(windows) {
windows.forEach((win) => App.addWindow(win));
}
addWindows([Bar(), Music(), Osd(), SystemMenu(), NotificationPopup()]);
export {};

View File

@@ -1,22 +1,51 @@
{
inputs,
pkgs,
lib,
config,
...
}: {
# add the home manager module
imports = [inputs.ags.homeManagerModules.default];
}: let
requiredDeps = with pkgs; [
bash
coreutils
dart-sass
gawk
imagemagick
procps
ripgrep
util-linux
];
programs.ags = {
enable = true;
guiDeps = with pkgs; [
gnome.gnome-control-center
mission-center
overskride
wlogout
];
# null or path, leave as null if you don't want hm to manage the config
configDir = null;
dependencies = requiredDeps ++ guiDeps;
# additional packages to add to gjs's runtime
extraPackages = with pkgs; [
gtksourceview
webkitgtk
accountsservice
];
cfg = config.programs.ags;
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=/run/wrappers/bin:${lib.makeBinPath dependencies}";
ExecStart = "${cfg.package}/bin/ags";
Restart = "on-failure";
};
Install.WantedBy = ["graphical-session.target"];
};
}

View File

@@ -0,0 +1,5 @@
{
"typescript": {},
"excludes": ["**/node_modules"],
"plugins": ["https://plugins.dprint.dev/typescript-0.88.10.wasm"]
}

View File

@@ -0,0 +1,37 @@
// Required components
import GLib from "gi://GLib";
import App from "resource:///com/github/Aylur/ags/app.js";
import Service from "resource:///com/github/Aylur/ags/service.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Variable from "resource:///com/github/Aylur/ags/variable.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
// Services
import Audio from "resource:///com/github/Aylur/ags/service/audio.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
import Bluetooth from "resource:///com/github/Aylur/ags/service/bluetooth.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import Mpris from "resource:///com/github/Aylur/ags/service/mpris.js";
import Network from "resource:///com/github/Aylur/ags/service/network.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import SystemTray from "resource:///com/github/Aylur/ags/service/systemtray.js";
import Icons from "./utils/icons.js";
export {
App,
Audio,
Battery,
Bluetooth,
GLib,
Hyprland,
Icons,
Mpris,
Network,
Notifications,
Service,
SystemTray,
Utils,
Variable,
Widget,
};

View File

@@ -0,0 +1,79 @@
import { Service, Utils } from "../imports.js";
import Gio from "gi://Gio";
import GLib from "gi://GLib";
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
class BrightnessService extends Service {
static {
Service.register(
this,
{ "screen-changed": ["float"] },
{ "screen-value": ["float", "rw"] },
);
}
#screenValue = 0;
#interface = Utils.exec("sh -c 'ls -w1 /sys/class/backlight | head -1'");
#path = `/sys/class/backlight/${this.#interface}`;
#brightness = `${this.#path}/brightness`;
#max = Number(Utils.readFile(`${this.#path}/max_brightness`));
get screen_value() {
return this.#screenValue;
}
set screen_value(percent) {
percent = clamp(percent, 0, 1);
this.#screenValue = percent;
const file = Gio.File.new_for_path(this.#brightness);
const string = `${Math.round(percent * this.#max)}`;
new Promise((resolve, _) => {
file.replace_contents_bytes_async(
new GLib.Bytes(new TextEncoder().encode(string)),
null,
false,
Gio.FileCreateFlags.NONE,
null,
(self, res) => {
try {
self.replace_contents_finish(res);
resolve(self);
} catch (error) {
print(error);
}
},
);
});
}
constructor() {
super();
this.#updateScreenValue();
Utils.monitorFile(this.#brightness, () => this.#onChange());
}
#updateScreenValue() {
this.#screenValue = Number(Utils.readFile(this.#brightness)) / this.#max;
}
#onChange() {
this.#updateScreenValue();
this.notify("screen-value");
this.emit("screen-changed", this.#screenValue);
}
connectWidget(widget, callback, event = "screen-changed") {
super.connectWidget(widget, callback, event);
}
}
const service = new BrightnessService();
export default service;

View File

@@ -0,0 +1,65 @@
import { Audio, Icons, Service, Utils } from "../imports.js";
import { audioIcon, micIcon } from "../utils/audio.js";
import Brightness from "./brightness.js";
class Indicator extends Service {
static {
Service.register(this, {
popup: ["jsobject", "boolean"],
});
}
#delay = 1500;
#count = 0;
popup(value, label, icon, showProgress = true) {
const props = {
value,
label,
icon,
showProgress,
};
this.emit("popup", props, true);
this.#count++;
Utils.timeout(this.#delay, () => {
this.#count--;
if (this.#count === 0) {
this.emit("popup", props, false);
}
});
}
bluetooth(addr) {
this.popup(0, getBluetoothDevice(addr), Icons.bluetooth.active, false);
}
speaker() {
this.popup(
Audio.speaker?.volume ?? 0,
Audio.speaker?.description ?? "",
audioIcon(),
);
}
mic() {
this.popup(
Audio.microphone?.volume || 0,
Audio.microphone?.description || "",
micIcon(),
);
}
display() {
// brightness is async, so lets wait a bit
Utils.timeout(10, () =>
this.popup(Brightness.screenValue, "Brightness", Icons.brightness),
);
}
connect(event = "popup", callback) {
return super.connect(event, callback);
}
}
export default new Indicator();

View File

@@ -0,0 +1,18 @@
/* Error: expected "{".
* ,
* 1 | colors-dark.scss
* | ^
* '
* /home/cnst/.config/ags/style/colors.scss 1:17 @import
* /home/cnst/.config/ags/style.scss 5:9 root stylesheet */
body::before {
font-family: "Source Code Pro", "SF Mono", Monaco, Inconsolata, "Fira Mono",
"Droid Sans Mono", monospace, monospace;
white-space: pre;
display: block;
padding: 1em;
margin-bottom: 1em;
border-bottom: 2px solid black;
content: 'Error: expected "{".\a \2577 \a 1 \2502 colors-dark.scss\a \2502 ^\a \2575 \a /home/cnst/.config/ags/style/colors.scss 1:17 @import\a /home/cnst/.config/ags/style.scss 5:9 root stylesheet';
}

View File

@@ -0,0 +1,13 @@
/* style aggregator */
/* setup */
@import "style/prelude";
@import "style/colors";
@import "style/general";
/* modules & windows */
@import "style/bar";
@import "style/music";
@import "style/osd";
@import "style/system-menu";
@import "style/notifications";

View File

@@ -0,0 +1,134 @@
.bar {
background: $bar-bg;
min-height: 32px;
.module {
margin: 0 0.5rem;
}
}
/* workspaces */
.bar .workspaces {
margin: 0.2rem 0.5rem;
button {
background: rgba(0, 0, 0, 0.3);
border-radius: 2rem;
margin: 0.7rem 0.2rem;
min-width: 1rem;
transition: 100ms linear;
}
.focused {
min-width: 2rem;
}
.monitor0 {
background: $red;
}
.monitor1 {
background: $yellow;
}
.monitor2 {
background: $green;
}
.monitor3 {
background: $blue;
}
}
/* music */
.bar .music {
&>box {
@include animate;
border-radius: $round2;
margin: 0.4rem;
}
&.active>box {
background: $surface;
}
.cover {
background-size: cover;
background-position: center;
border-radius: 50%;
min-width: 2rem;
min-height: 2rem;
}
}
/* tray */
.tray button {
@include button;
background: none;
margin: 0.5rem 0;
&:not(:last-child) {
margin-right: 0.3rem;
}
&.active {
background: $surface;
}
}
menu {
background: $tooltip-bg;
border-radius: $round;
separator {
background-color: $surface;
}
menuitem {
@include button;
border-radius: 0;
padding: 0.4rem 0.7rem;
&:first-child {
border-radius: $round $round 0 0;
}
&:last-child {
border-radius: 0 0 $round $round;
}
&:only-child {
border-radius: $round;
}
}
}
/* system-info */
.bar .system-info {
margin: 0 0.2rem;
&>box {
margin: 0 0.3rem;
}
.type {
font-size: 0.55rem;
font-weight: 300;
}
.value {
font-size: 0.8rem;
}
}
.system-menu-toggler {
box {
@include animate;
margin: 0.4rem 0;
border-radius: $round2;
}
&.active box {
background: $surface;
}
}

View File

@@ -0,0 +1,32 @@
$red: #f38ba8;
$yellow: #f9e2af;
$green: #a6e3a1;
$blue: #89b4fa;
$tooltip-bg: #000000;
$fg: #ffffff;
$bg: rgba(0, 0, 0, 0.3);
$bar-bg: rgba(0, 0, 0, 0.21);
$surface: rgba(255, 255, 255, 0.15);
$overlay: rgba(255, 255, 255, 0.7);
$accent: #9d5b7a;
/* buttons */
$button-enabled: $accent;
$button-enabled-hover: adjust_color($button-enabled, $lightness: -10%);
$button-disabled: $surface;
$button-disabled-hover: adjust_color($button-disabled, $alpha: +0.1);
* {
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.2);
}
@mixin border {
// border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
// inset 0 0 0 1px rgba(255, 255, 255, 0.1),
0 3px 5px 1px rgba(0, 0, 0, 0.3);
}

View File

@@ -0,0 +1,32 @@
$red: #f38ba8;
$yellow: #f9e2af;
$green: #a6e3a1;
$blue: #89b4fa;
$tooltip-bg: #ffffff;
$fg: #000000;
$bg: rgba(255, 255, 255, 0.5);
$bar-bg: rgba(255, 255, 255, 0.3);
$surface: rgba(255, 255, 255, 0.3);
$overlay: rgba(0, 0, 0, 0.5);
$accent: #ddbaef;
/* buttons */
$button-enabled: $accent;
$button-enabled-hover: adjust_color($button-enabled, $lightness: -10%);
$button-disabled: $surface;
$button-disabled-hover: adjust_color($button-disabled, $alpha: +0.1);
* {
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.2);
}
@mixin border {
// border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
// inset 0 0 0 1px rgba(255, 255, 255, 0.1),
0 3px 5px 1px rgba(0, 0, 0, 0.3);
}

View File

@@ -0,0 +1 @@
colors-dark.scss

View File

@@ -0,0 +1,100 @@
/* general styles */
$round: 8px;
$round2: calc($round * 2);
$margin: 0.4rem;
$padding: 0.4rem;
$border-width: 2px;
$scale: 0.5rem;
@mixin animate {
transition: 200ms;
}
* {
color: $fg;
}
/* mixins */
@mixin window-rounding {
border-radius: $round2;
}
@mixin rounding {
border-radius: calc($round2 - $padding - $border-width);
}
@mixin window-box {
@include rounding;
background: $surface;
box-shadow: 0 1px 5px -5px rgba(0, 0, 0, 0.5);
margin: $margin;
padding: $padding;
}
@mixin window {
@include border;
@include window-rounding;
background: $bg;
margin: 5px 10px 15px;
padding: $padding;
}
tooltip {
background: $tooltip-bg;
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.1),
0 0 rgba(0, 0, 0, 0.4);
border-radius: $round;
}
/* scales & progress bars */
scale,
progressbar {
trough {
background-color: $surface;
border-radius: $scale;
min-width: calc($scale * 10);
padding: 0 calc($scale / 2);
}
highlight,
progress {
background: $overlay;
border-radius: $scale;
margin: 0 calc(0px - $scale / 2);
min-height: $scale;
}
}
@mixin button-active {
@include animate;
background: $button-enabled;
border-radius: 5rem;
padding: 0.4rem;
&:hover {
background: $button-enabled-hover;
}
}
@mixin button {
@include animate;
background: $button-disabled;
border-radius: 5rem;
padding: 0.4rem;
&:hover {
background: $button-disabled-hover;
}
}
.button {
@include button-active;
}
.button.disabled {
@include button;
}

View File

@@ -0,0 +1,54 @@
.music.window {
@include window;
.cover {
background-position: center;
background-size: cover;
border-radius: $round;
box-shadow: 0 1px 2px -1px $bg;
margin: 0.4rem;
min-height: 13rem;
min-width: 13rem;
}
}
.music.window .info {
margin: 0.5rem;
label,
scale {
margin: 0.3rem 0;
}
label.position,
label.length {
font-size: 0.8rem;
margin-bottom: 0;
}
scale {
margin-top: 0;
margin-bottom: 0;
}
.title {
font-size: 1.5rem;
font-weight: bold;
min-width: 14rem;
}
}
.music.window .controls {
button {
margin: 0 0.2rem;
font-size: 1.5rem;
}
}
.music.window .player-info {
margin-bottom: 0;
.player-icon {
font-size: 1.2rem;
}
}

View File

@@ -0,0 +1,57 @@
.notification {
@include window;
margin: 5px 5px 5px 10px;
min-width: 25rem;
border-radius: $round2;
background-color: $bg;
&.critical {
border: 1px solid red;
}
}
.notifications widget:last-child .notification {
margin-bottom: 15px;
}
.notification .icon {
image {
font-size: 5rem;
margin: 0.5rem;
min-height: 5rem;
min-width: 5rem;
}
>box {
border-radius: $round;
margin: 0.5rem;
min-height: 5rem;
min-width: 5rem;
}
}
.notification .actions .action-button {
@include window-box;
@include animate;
padding: 0.5rem 0;
&:hover {
background: $button-disabled-hover;
}
}
.notification .text {
margin: 0.5rem;
.title {
margin-bottom: 0.2rem;
font-weight: 500;
}
.body {
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
}
}

View File

@@ -0,0 +1,23 @@
.osd {
@include window;
padding: 0;
margin-bottom: 2rem;
image {
margin-left: 1rem;
color: rgba(0, 0, 0, 0.6);
}
progressbar trough {
border-radius: 16px;
background: none;
min-width: 12.5rem;
min-height: 2.5rem;
}
progressbar progress {
border-radius: 0;
border-radius: 16px;
min-height: 2.5rem;
}
}

View File

@@ -0,0 +1,5 @@
/* get rid of GTK theme's styles and set defaults */
* {
all: unset;
font-family: Inter, Roboto, sans-serif;
}

View File

@@ -0,0 +1,77 @@
/* general */
.system-menu {
@include window;
margin-top: 4px;
margin-right: 4px;
&>box {
@include window-box;
}
}
/* toggles */
.system-menu .toggle {
min-width: 20rem;
&:not(:last-child) {
margin-bottom: 0.3rem;
}
.button {
margin-right: 0.5rem;
}
}
/* power profiles */
.system-menu .power-profiles {
padding: 0;
.current-profile {
padding: 0.3rem;
}
image,
label {
margin: 0.3rem;
}
.options {
padding: 0;
widget {
@include button;
border-radius: 0;
&:last-child {
border-radius: 0 0 $round $round;
}
box {
padding: 0.3rem;
}
}
}
}
/* sliders */
.system-menu .sliders {
image {
margin: 0.3rem;
}
scale {
margin: 0 0.5rem;
}
}
.system-menu .battery-box {
image,
label {
margin: 0 0.3rem;
}
.time {
color: rgba(255, 255, 255, 0.7);
}
}

View File

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

1
home/core/gui/ags/types Symbolic link
View File

@@ -0,0 +1 @@
/home/cnst/.local/share/com.github.Aylur.ags/types

View File

@@ -0,0 +1,30 @@
import { Audio, Icons } from "../imports.js";
export const audioIcon = () => {
if (Audio.speaker?.stream.isMuted) return Icons.volume.muted;
const vol = Audio.speaker?.volume * 100;
const icon = [
[101, "overamplified"],
[67, "high"],
[34, "medium"],
[1, "low"],
[0, "muted"],
].find(([threshold]) => threshold <= vol)[1];
return Icons.volume[icon];
};
export const micIcon = () => {
if (Audio.microphone?.stream.isMuted) return Icons.microphone.muted;
const vol = Audio.microphone?.volume * 100;
const icon = [
[67, "high"],
[34, "medium"],
[1, "low"],
[0, "muted"],
].find(([threshold]) => threshold <= vol)[1];
return Icons.microphone[icon];
};

View File

@@ -0,0 +1,22 @@
import { Battery } from "../imports.js";
export const toTime = (time) => {
const MINUTE = 60;
const HOUR = MINUTE * 60;
if (time > 24 * HOUR) return "";
const hours = Math.round(time / HOUR);
const minutes = Math.round((time - hours * HOUR) / MINUTE);
const hoursDisplay = hours > 0 ? `${hours}h ` : "";
const minutesDisplay = minutes > 0 ? `${minutes}m ` : "";
return `${hoursDisplay}${minutesDisplay}`;
};
export const batteryTime = () => {
return Battery.timeRemaining > 0 && toTime(Battery.timeRemaining) != ""
? `${toTime(Battery.timeRemaining)}remaining`
: "";
};

View File

@@ -0,0 +1,27 @@
import { Bluetooth, Icons } from "../imports.js";
export const getBluetoothDevice = (addr) =>
Bluetooth.getDevice(addr).alias ?? Bluetooth.getDevice(addr).name;
export const getBluetoothIcon = (connected) => {
if (!Bluetooth.enabled) return Icons.bluetooth.disabled;
if (connected.length > 0) return Icons.bluetooth.active;
return Icons.bluetooth.disconnected;
};
export const getBluetoothText = (connected) => {
if (!Bluetooth.enabled) return "Bluetooth off";
if (connected.length > 0) {
const dev = Bluetooth.getDevice(connected[0].address);
let battery_str = "";
if (dev.battery_percentage > 0) {
battery_str += ` ${dev.battery_percentage}%`;
}
return dev.name + battery_str;
}
return "Bluetooth on";
};

View File

@@ -0,0 +1,107 @@
import { Hyprland } from "../imports.js";
export let DEFAULT_MONITOR;
const connID = Hyprland.connect("notify::workspaces", () => {
Hyprland.disconnect(connID);
DEFAULT_MONITOR = {
name: Hyprland.monitors[0].name,
id: Hyprland.monitors[0].id,
};
});
export const changeWorkspace = (ws) =>
Hyprland.messageAsync(`dispatch workspace ${ws}`);
export const focusedSwitch = (self) => {
const id = Hyprland.active.workspace.id;
if (self.lastFocused == id) return;
self.children[self.lastFocused - 1].toggleClassName("focused", false);
self.children[id - 1].toggleClassName("focused", true);
self.lastFocused = id;
};
export const added = (self, name) => {
if (!name) return;
const ws = Hyprland.workspaces.find((e) => e.name == name);
const id = ws?.id ?? Number(name);
const child = self.children[id - 1];
child.monitor = {
name: ws?.monitor ?? DEFAULT_MONITOR.name,
id: ws?.monitorID ?? DEFAULT_MONITOR.id,
};
child.active = true;
child.toggleClassName(`monitor${child.monitor.id}`, true);
// if this id is bigger than the last biggest id, visibilise all other ws before it
if (id > self.biggestId) {
for (let i = self.biggestId; i <= id; i++) {
self.children[i - 1].visible = true;
}
self.biggestId = id;
}
};
const makeInvisible = (self, id) => {
if (id <= 1) return;
const child = self.children[id - 1];
if (child.active) {
self.biggestId = id;
return;
}
child.visible = false;
makeInvisible(self, id - 1);
};
export const removed = (self, name) => {
if (!name) return;
const id = Number(name);
const child = self.children[id - 1];
child.toggleClassName(`monitor${child.monitor.id}`, false);
child.active = false;
// if this id is the biggest id, unvisibilise it and all other inactives until the next active before it
if (id == self.biggestId) {
makeInvisible(self, id);
}
};
export const moveWorkspace = (self, data) => {
const [id, name] = data.split(",");
const child = self.children[id - 1];
// remove previous monitor class
child.toggleClassName(`monitor${child.monitor.id}`, false);
// add new monitor and class
const monitor = Hyprland.monitors.find((e) => e.name == name);
child.monitor = {
name,
id: monitor?.id ?? DEFAULT_MONITOR.id,
};
print(`child ${id}: monitor ${name} ${child.monitor.id}`);
child.toggleClassName(`monitor${child.monitor.id}`, true);
};
export const sortWorkspaces = () => {
return Hyprland.workspaces
.sort((x, y) => {
return x.id - y.id;
})
.filter((x) => {
return x.name.indexOf("special") == -1;
});
};
export const getLastWorkspaceId = () => sortWorkspaces().slice(-1)[0].id;
export const workspaceActive = (id) => sortWorkspaces().some((e) => e.id == id);

View File

@@ -0,0 +1,35 @@
export default {
bluetooth: {
active: "bluetooth-active-symbolic",
disabled: "bluetooth-disabled-symbolic",
disconnected: "bluetooth-disconnected-symbolic",
},
brightness: "display-brightness-symbolic",
media: {
play: "media-playback-start-symbolic",
pause: "media-playback-pause-symbolic",
next: "media-skip-forward-symbolic",
previous: "media-skip-backward-symbolic",
player: "multimedia-player-symbolic",
},
volume: {
muted: "audio-volume-muted-symbolic",
low: "audio-volume-low-symbolic",
medium: "audio-volume-medium-symbolic",
high: "audio-volume-high-symbolic",
overamplified: "audio-volume-overamplified-symbolic",
},
microphone: {
muted: "microphone-sensitivity-muted-symbolic",
low: "microphone-sensitivity-low-symbolic",
medium: "microphone-sensitivity-medium-symbolic",
high: "microphone-sensitivity-high-symbolic",
},
powerButton: "system-shutdown-symbolic",
};

View File

@@ -0,0 +1,46 @@
import { Icons, Utils } from "../imports.js";
import GLib from "gi://GLib";
export const findPlayer = (players) => {
// try to get the first active player
const activePlayer = players.find((p) => p.playBackStatus == "Playing");
if (activePlayer != null) return activePlayer;
// otherwise get the first "working" player
for (const p of players) {
if (p.title != "undefined") return p;
}
};
export const mprisStateIcon = (status) => {
const state = status == "Playing" ? "pause" : "play";
return Icons.media[state];
};
export const MEDIA_CACHE_PATH = Utils.CACHE_DIR + "/media";
export const blurredPath = MEDIA_CACHE_PATH + "/blurred";
export const generateBackground = (cover_path) => {
const url = cover_path;
if (!url) return "";
const makeBg = (bg) => `background: center/cover url('${bg}')`;
const blurred = blurredPath + url.substring(MEDIA_CACHE_PATH.length);
if (GLib.file_test(blurred, GLib.FileTest.EXISTS)) {
return makeBg(blurred);
}
Utils.ensureDirectory(blurredPath);
Utils.exec(`convert ${url} -blur 0x22 ${blurred}`);
return makeBg(blurred);
};
export function lengthStr(length) {
const min = Math.floor(length / 60);
const sec = Math.floor(length % 60);
const sec0 = sec < 10 ? "0" : "";
return `${min}:${sec0}${sec}`;
}

View File

@@ -0,0 +1,27 @@
import { Network } from "../imports.js";
export const getNetIcon = () => {
if (Network.connectivity == "none") return "";
if (Network.primary == "wired") return "network-wired";
return Network.wifi.icon_name;
};
export const getNetText = () => {
// no connection
if (Network.connectivity == "none") return "No connection";
// wired
if (Network.primary == "wired") return "Wired";
// wifi
const wifi = Network.wifi;
switch (wifi.internet) {
case "connected":
return wifi.ssid;
case "connecting":
return "Connecting";
case "disconnected":
return "Disconnected";
}
};

View File

@@ -0,0 +1,46 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import { Widget } from "../imports.js";
const { Box, Revealer, Window } = Widget;
export default ({
name,
child,
revealerSetup = null,
transition = "crossfade",
transitionDuration = 200,
...props
}) => {
const window = Window({
name,
popup: false,
focusable: false,
visible: false,
...props,
setup: (self) => (self.getChild = () => child),
child: Box({
css: `
min-height: 1px;
min-width: 1px;
padding: 1px;
`,
child: Revealer({
transition,
transitionDuration,
child: child,
setup:
revealerSetup ??
((self) =>
self.hook(App, (self, currentName, visible) => {
if (currentName === name) {
self.reveal_child = visible;
}
})),
}),
}),
});
return window;
};

View File

@@ -0,0 +1,62 @@
import { App, Widget } from "../../imports.js";
import Battery from "./modules/battery.js";
import Bluetooth from "./modules/bluetooth.js";
import Date from "./modules/date.js";
import Music from "./modules/music.js";
import Net from "./modules/net.js";
import CpuRam from "./modules/cpu_ram.js";
import Tray from "./modules/tray.js";
import Workspaces from "./modules/workspaces.js";
const SystemInfo = () =>
Widget.EventBox({
className: "system-menu-toggler",
onPrimaryClick: () => App.toggleWindow("system-menu"),
child: Widget.Box({
children: [Net(), Bluetooth(), Battery()],
}),
}).hook(App, (self, window, visible) => {
if (window === "system-menu") {
self.toggleClassName("active", visible);
}
});
const Start = () =>
Widget.Box({
hexpand: true,
hpack: "start",
children: [
Workspaces(),
// Indicators
],
});
const Center = () =>
Widget.Box({
children: [Music()],
});
const End = () =>
Widget.Box({
hexpand: true,
hpack: "end",
children: [Tray(), CpuRam(), SystemInfo(), Date()],
});
export default () =>
Widget.Window({
monitor: 0,
name: `bar`,
anchor: ["bottom", "left", "right"],
exclusivity: "exclusive",
child: Widget.CenterBox({
className: "bar",
startWidget: Start(),
centerWidget: Center(),
endWidget: End(),
}),
});

View File

@@ -0,0 +1,6 @@
import { Battery, Widget } from "../../../imports.js";
export default () =>
Widget.Icon({ className: "battery module" })
.bind("icon", Battery, "icon-name")
.bind("tooltip-text", Battery, "percent", (p) => `Battery on ${p}%`);

View File

@@ -0,0 +1,10 @@
import { Bluetooth, Widget } from "../../../imports.js";
import {
getBluetoothIcon,
getBluetoothText,
} from "../../../utils/bluetooth.js";
export default () =>
Widget.Icon({ className: "bluetooth module" })
.bind("icon", Bluetooth, "connected-devices", getBluetoothIcon)
.bind("tooltip-text", Bluetooth, "connected-devices", getBluetoothText);

View File

@@ -0,0 +1,70 @@
import { Utils, Widget } from "../../../imports.js";
const Indicator = (props) =>
Widget.Box({
vertical: true,
vexpand: true,
vpack: "center",
children: [
Widget.Label({
className: "type",
label: props.type,
}),
Widget.Label({ className: "value" }).poll(2000, props.poll),
],
}).poll(2000, props.boxpoll);
const cpu = {
type: "CPU",
poll: (self) =>
Utils.execAsync([
"sh",
"-c",
`top -bn1 | rg '%Cpu' | tail -1 | awk '{print 100-$8}'`,
])
.then((r) => (self.label = Math.round(Number(r)) + "%"))
.catch((err) => print(err)),
boxpoll: (self) =>
Utils.execAsync(["sh", "-c", "lscpu --parse=MHZ"])
.then((r) => {
const mhz = r.split("\n").slice(4);
const freq = mhz.reduce((acc, e) => acc + Number(e), 0) / mhz.length;
self.tooltipText = Math.round(freq) + " MHz";
})
.catch((err) => print(err)),
};
const ram = {
type: "MEM",
poll: (self) =>
Utils.execAsync([
"sh",
"-c",
`free | tail -2 | head -1 | awk '{print $3/$2*100}'`,
])
.then((r) => (self.label = Math.round(Number(r)) + "%"))
.catch((err) => print(err)),
boxpoll: (self) =>
Utils.execAsync([
"sh",
"-c",
"free --si -h | tail -2 | head -1 | awk '{print $3}'",
])
.then((r) => (self.tooltipText = r))
.catch((err) => print(err)),
};
export default () =>
Widget.EventBox({
onPrimaryClick: () => Utils.execAsync(["missioncenter"]),
child: Widget.Box({
className: "system-info module",
children: [Indicator(cpu), Indicator(ram)],
}),
});

View File

@@ -0,0 +1,10 @@
import { Utils, Widget } from "../../../imports.js";
export default () =>
Widget.EventBox({
child: Widget.Label({ className: "date module" }).poll(1000, (self) =>
Utils.execAsync(["date", "+%a %b %d %H:%M"]).then(
(r) => (self.label = r),
),
),
});

View File

@@ -0,0 +1,40 @@
import { Mpris, Widget } from "../../../imports.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import { findPlayer } from "../../../utils/mpris.js";
const Cover = (player) =>
Widget.Box({ className: "cover" }).bind(
"css",
player,
"cover-path",
(cover) => `background-image: url('${cover ?? ""}');`,
);
const Title = (player) =>
Widget.Label({ className: "title module" }).bind(
"label",
player,
"track-title",
(title) => ((title ?? "") == "Unknown title" ? "" : title),
);
export const MusicBox = (player) =>
Widget.Box({
children: [Cover(player), Title(player)],
});
export default () =>
Widget.EventBox({
className: "music",
onPrimaryClick: () => App.toggleWindow("music"),
})
.hook(App, (self, window, visible) => {
if (window === "music") {
self.toggleClassName("active", visible);
}
})
.bind("visible", Mpris, "players", (p) => p.length > 0)
.bind("child", Mpris, "players", (players) => {
if (players.length == 0) return Widget.Box();
return MusicBox(findPlayer(players));
});

View File

@@ -0,0 +1,7 @@
import { Network, Widget } from "../../../imports.js";
import { getNetIcon, getNetText } from "../../../utils/net.js";
export default () =>
Widget.Icon({ className: "net module" })
.bind("icon", Network, "connectivity", getNetIcon)
.bind("tooltip-text", Network, "connectivity", getNetText);

View File

@@ -0,0 +1,47 @@
import { SystemTray, Widget } from "../../../imports.js";
import Gdk from "gi://Gdk?version=3.0";
const Item = (item) =>
Widget.Button({
child: Widget.Icon().bind("icon", item, "icon"),
onPrimaryClick: (_, ev) => {
try {
item.activate(ev);
} catch (err) {
print(err);
}
},
setup: (self) => {
const id = item.menu?.connect("popped-up", (menu) => {
self.toggleClassName("active");
menu.connect("notify::visible", (menu) => {
self.toggleClassName("active", menu.visible);
});
menu.disconnect(id);
});
if (id) {
self.connect("destroy", () => item.menu?.disconnect(id));
}
self.bind("tooltip-markup", item, "tooltip-markup");
},
onSecondaryClick: (btn) =>
item.menu?.popup_at_widget(
btn,
Gdk.Gravity.SOUTH,
Gdk.Gravity.NORTH,
null,
),
});
export default () =>
Widget.Box({ className: "tray module" }).bind(
"children",
SystemTray,
"items",
(items) => items.map(Item),
);

View File

@@ -0,0 +1,72 @@
import { Hyprland, Widget } from "../../../imports.js";
import {
added,
changeWorkspace,
DEFAULT_MONITOR,
focusedSwitch,
getLastWorkspaceId,
moveWorkspace,
removed,
workspaceActive,
} from "../../../utils/hyprland.js";
globalThis.hyprland = Hyprland;
const makeWorkspaces = () =>
[...Array(10)].map((_, i) => {
const id = i + 1;
return Widget.Button({
onPrimaryClick: () => changeWorkspace(id),
visible: getLastWorkspaceId() >= id,
setup: (self) => {
const ws = Hyprland.getWorkspace(id);
self.id = id;
self.active = workspaceActive(id);
self.monitor = DEFAULT_MONITOR;
if (self.active) {
self.monitor = {
name: ws?.monitor ?? DEFAULT_MONITOR.name,
id: ws?.monitorID ?? DEFAULT_MONITOR.id,
};
self.toggleClassName(`monitor${self.monitor.id}`, true);
}
},
});
});
export default () =>
Widget.EventBox({
onScrollUp: () => changeWorkspace("+1"),
onScrollDown: () => changeWorkspace("-1"),
child: Widget.Box({
className: "workspaces module",
// The Hyprland service is ready later than ags is done parsing the config,
// so only build the widget when we receive a signal from it.
setup: (self) => {
const connID = Hyprland.connect("notify::workspaces", () => {
Hyprland.disconnect(connID);
self.children = makeWorkspaces();
self.lastFocused = Hyprland.active.workspace.id;
self.biggestId = getLastWorkspaceId();
self
.hook(Hyprland.active.workspace, focusedSwitch)
.hook(Hyprland, added, "workspace-added")
.hook(Hyprland, removed, "workspace-removed")
.hook(
Hyprland,
(self, name, data) => {
if (name === "moveworkspace") moveWorkspace(self, data);
},
"event",
);
});
},
}),
});

View File

@@ -0,0 +1,29 @@
import { Icons, Widget } from "../../imports.js";
import { mprisStateIcon } from "../../utils/mpris.js";
export default (player) =>
Widget.CenterBox({
className: "controls",
hpack: "center",
startWidget: Widget.Button({
onClicked: () => player.previous(),
child: Widget.Icon(Icons.media.previous),
}),
centerWidget: Widget.Button({
onClicked: () => player.playPause(),
child: Widget.Icon().bind(
"icon",
player,
"play-back-status",
mprisStateIcon,
),
}),
endWidget: Widget.Button({
onClicked: () => player.next(),
child: Widget.Icon(Icons.media.next),
}),
});

View File

@@ -0,0 +1,9 @@
import { Widget } from "../../imports.js";
export default (player) =>
Widget.Box({ className: "cover" }).bind(
"css",
player,
"cover-path",
(cover) => `background-image: url('${cover ?? ""}')`,
);

View File

@@ -0,0 +1,43 @@
import { Mpris, Widget } from "../../imports.js";
import { findPlayer, generateBackground } from "../../utils/mpris.js";
import PopupWindow from "../../utils/popup_window.js";
import Cover from "./cover.js";
import { Artists, Title } from "./title_artists.js";
import TimeInfo from "./time_info.js";
import Controls from "./controls.js";
import PlayerInfo from "./player_info.js";
const Info = (player) =>
Widget.Box({
className: "info",
vertical: true,
vexpand: false,
hexpand: false,
homogeneous: true,
children: [
PlayerInfo(player),
Title(player),
Artists(player),
Controls(player),
TimeInfo(player),
],
});
const MusicBox = (player) =>
Widget.Box({
className: "music window",
children: [Cover(player), Info(player)],
}).bind("css", player, "cover-path", generateBackground);
export default () =>
PopupWindow({
monitor: 0,
anchor: ["top"],
name: "music",
child: Widget.Box(),
}).bind("child", Mpris, "players", (players) => {
if (players.length == 0) return Widget.Box();
return MusicBox(findPlayer(players));
});

View File

@@ -0,0 +1,21 @@
import { Icons, Utils, Widget } from "../../imports.js";
export default (player) =>
Widget.Box({
className: "player-info",
vexpand: true,
vpack: "start",
children: [
Widget.Icon({
hexpand: true,
hpack: "end",
className: "player-icon",
tooltipText: player.identity ?? "",
}).bind("icon", player, "entry", (entry) => {
// the Spotify icon is called spotify-client
if (entry == "spotify") entry = "spotify-client";
return Utils.lookUpIcon(entry ?? "") ? entry : Icons.media.player;
}),
],
});

View File

@@ -0,0 +1,68 @@
import { Widget } from "../../imports.js";
import { lengthStr } from "../../utils/mpris.js";
export const PositionLabel = (player) =>
Widget.Label({
className: "position",
hexpand: true,
xalign: 0,
setup: (self) => {
const update = (_, time) => {
player.length > 0
? (self.label = lengthStr(time || player.position))
: (self.visible = !!player);
};
self.hook(player, update, "position").poll(1000, update);
},
});
export const LengthLabel = (player) =>
Widget.Label({
className: "length",
hexpand: true,
xalign: 1,
})
.bind("visible", player, "length", (length) => length > 0)
.bind("label", player, "length", (length) => lengthStr(length));
export const Position = (player) =>
Widget.Slider({
className: "position",
draw_value: false,
onChange: ({ value }) => (player.position = player.length * value),
setup: (self) => {
const update = () => {
if (self.dragging) return;
self.visible = player.length > 0;
if (player.length > 0) {
self.value = player.position / player.length;
}
};
self
.hook(player, update)
.hook(player, update, "position")
.poll(1000, update);
},
});
export default (player) =>
Widget.Box({
vertical: true,
vexpand: true,
vpack: "end",
children: [
Widget.Box({
hexpand: true,
children: [PositionLabel(player), LengthLabel(player)],
}),
Position(player),
],
});

View File

@@ -0,0 +1,32 @@
import { Widget } from "../../imports.js";
export const Title = (player) =>
Widget.Scrollable({
className: "title",
vscroll: "never",
hscroll: "automatic",
child: Widget.Label({
className: "title",
label: "Nothing playing",
}).bind(
"label",
player,
"track-title",
(title) => title ?? "Nothing playing",
),
});
export const Artists = (player) =>
Widget.Scrollable({
className: "artists",
vscroll: "never",
hscroll: "automatic",
child: Widget.Label({ className: "artists" }).bind(
"label",
player,
"track-artists",
(artists) => artists.join(", ") ?? "",
),
});

View File

@@ -0,0 +1,139 @@
import { Hyprland, Notifications, Utils, Widget } from "../../imports.js";
const closeAll = () => {
Notifications.popups.map((n) => n.dismiss());
};
/** @param {import("types/service/notifications").Notification} n */
const NotificationIcon = ({ app_entry, app_icon, image }) => {
if (image) {
return Widget.Box({
css: `
background-image: url("${image}");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
`,
});
}
if (Utils.lookUpIcon(app_icon)) {
return Widget.Icon(app_icon);
}
if (app_entry && Utils.lookUpIcon(app_entry)) {
return Widget.Icon(app_entry);
}
return null;
};
/** @param {import('types/service/notifications').Notification} n */
export const Notification = (n) => {
const icon = Widget.Box({
vpack: "start",
class_name: "icon",
// @ts-ignore
setup: (self) => {
let icon = NotificationIcon(n);
if (icon !== null) {
self.child = icon;
}
},
});
const title = Widget.Label({
class_name: "title",
xalign: 0,
justification: "left",
hexpand: true,
max_width_chars: 24,
truncate: "end",
wrap: true,
label: n.summary,
use_markup: true,
});
const body = Widget.Label({
class_name: "body",
hexpand: true,
use_markup: true,
xalign: 0,
justification: "left",
max_width_chars: 100,
wrap: true,
label: n.body,
});
const actions = Widget.Box({
class_name: "actions",
children: n.actions
.filter(({ id }) => id != "default")
.map(({ id, label }) =>
Widget.Button({
class_name: "action-button",
on_clicked: () => n.invoke(id),
hexpand: true,
child: Widget.Label(label),
}),
),
});
return Widget.EventBox({
on_primary_click: () => {
if (n.actions.length > 0) n.invoke(n.actions[0].id);
},
on_middle_click: closeAll,
on_secondary_click: () => n.dismiss(),
child: Widget.Box({
class_name: `notification ${n.urgency}`,
vertical: true,
children: [
Widget.Box({
class_name: "info",
children: [
icon,
Widget.Box({
vertical: true,
class_name: "text",
vpack: "center",
setup: (self) => {
if (n.body.length > 0) {
self.children = [title, body];
} else {
self.children = [title];
}
},
}),
],
}),
actions,
],
}),
});
};
let lastMonitor;
export const notificationPopup = () =>
Widget.Window({
name: "notifications",
anchor: ["top", "right"],
child: Widget.Box({
css: "padding: 1px;",
class_name: "notifications",
vertical: true,
// @ts-ignore
children: Notifications.bind("popups").transform((popups) => {
return popups.map(Notification);
}),
}),
}).hook(Hyprland.active, (self) => {
// prevent useless resets
if (lastMonitor === Hyprland.active.monitor) return;
self.monitor = Hyprland.active.monitor.id;
});
export default notificationPopup;

View File

@@ -0,0 +1,86 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import { Audio, Hyprland, Widget } from "../../imports.js";
import Brightness from "../../services/brightness.js";
import Indicators from "../../services/osd.js";
import PopupWindow from "../../utils/popup_window.js";
// connections
Audio.connect("speaker-changed", () =>
Audio.speaker.connect("changed", () => {
if (!App.getWindow("system-menu")?.visible) {
Indicators.speaker();
}
}),
);
Audio.connect("microphone-changed", () =>
Audio.microphone.connect("changed", () => Indicators.mic()),
);
Brightness.connect("screen-changed", () => {
if (!App.getWindow("system-menu")?.visible) {
Indicators.display();
}
});
let lastMonitor;
const child = () =>
Widget.Box({
className: "osd",
children: [
Widget.Overlay({
hexpand: true,
visible: false,
passThrough: true,
child: Widget.ProgressBar({
hexpand: true,
vertical: false,
}).hook(Indicators, (self, props) => {
self.value = props?.value ?? 0;
self.visible = props?.showProgress ?? false;
}),
overlays: [
Widget.Box({
hexpand: true,
children: [
Widget.Icon().hook(
Indicators,
(self, props) => (self.icon = props?.icon ?? ""),
),
Widget.Box({
hexpand: true,
}),
],
}),
],
}),
],
});
export default () =>
PopupWindow({
name: "osd",
monitor: 0,
layer: "overlay",
child: child(),
click_through: true,
anchor: ["bottom"],
revealerSetup: (self) =>
self.hook(Indicators, (revealer, _, visible) => {
revealer.reveal_child = visible;
}),
})
.hook(Hyprland.active, (self) => {
// prevent useless resets
if (lastMonitor === Hyprland.active.monitor) return;
self.monitor = Hyprland.active.monitor.id;
})
.hook(Indicators, (win, _, visible) => {
win.visible = visible;
});

View File

@@ -0,0 +1,52 @@
import { App, Battery, Icons, Utils, Widget } from "../../imports.js";
import { batteryTime } from "../../utils/battery.js";
const batteryEnergy = () => {
return Battery.energyRate > 0.1 ? `${Battery.energyRate.toFixed(1)} W ` : "";
};
const BatteryIcon = () =>
Widget.Icon()
.bind("icon", Battery, "percent", () => Battery.iconName)
.bind("tooltip-text", Battery, "energy-rate", batteryEnergy);
const BatteryPercent = () =>
Widget.Label().bind("label", Battery, "percent", (percent) => `${percent}%`);
const BatteryTime = () =>
Widget.Label({
className: "time",
vexpand: true,
vpack: "center",
})
.bind("label", Battery, "charging", batteryTime)
.bind("label", Battery, "energy-rate", batteryTime);
const BatteryBox = () =>
Widget.Box({
className: "battery-box",
visible: Battery.available,
children: [BatteryIcon(), BatteryPercent(), BatteryTime()],
});
const PowerButton = () =>
Widget.Button({
className: "button disabled",
hexpand: true,
hpack: "end",
onPrimaryClick: () => {
App.toggleWindow("system-menu");
Utils.exec("wlogout");
},
child: Widget.Icon(Icons.powerButton),
});
export default () =>
Widget.Box({
className: "battery-info",
children: [BatteryBox(), PowerButton()],
});

View File

@@ -0,0 +1,22 @@
import { Widget } from "../../imports.js";
import PopupWindow from "../../utils/popup_window.js";
import Toggles from "./toggles.js";
import Sliders from "./sliders.js";
import BatteryInfo from "./battery_info.js";
const SystemMenuBox = () =>
Widget.Box({
className: "system-menu",
vertical: true,
children: [Toggles(), Sliders(), BatteryInfo()],
});
export default () =>
PopupWindow({
monitor: 0,
anchor: ["top", "right"],
name: "system-menu",
child: SystemMenuBox(),
});

View File

@@ -0,0 +1,74 @@
import { App, Audio, Icons, Utils, Widget } from "../../imports.js";
import Brightness from "../../services/brightness.js";
import { audioIcon } from "../../utils/audio.js";
const Slider = (args) =>
Widget.Box({
...(args.props ?? {}),
className: args.name,
children: [
Widget.Button({
onPrimaryClick: args.icon.action ?? null,
child: Widget.Icon({
icon: args.icon.icon ?? "",
setup: args.icon.setup,
}),
}),
Widget.Slider({
drawValue: false,
hexpand: true,
setup: args.slider.setup,
onChange: args.slider.onChange ?? null,
}),
],
});
const vol = () => {
return {
name: "volume",
icon: {
icon: "",
action: () => {
App.toggleWindow("system-menu");
Utils.execAsync("pwvucontrol");
},
setup: (self) =>
self
.bind("icon", Audio.speaker, "volume", audioIcon)
.bind("icon", Audio.speaker.stream, "is-muted", audioIcon),
},
slider: {
setup: (self) => self.bind("value", Audio.speaker, "volume"),
onChange: ({ value }) => (Audio.speaker.volume = value),
},
};
};
const brightness = () => {
return {
name: "brightness",
icon: {
icon: Icons.brightness,
},
slider: {
setup: (self) => self.bind("value", Brightness, "screen-value"),
onChange: ({ value }) => (Brightness.screenValue = value),
},
};
};
export default () =>
Widget.Box({
className: "sliders",
vertical: true,
// The Audio service is ready later than ags is done parsing the config,
// so only build the widget when we receive a signal from it.
setup: (self) => {
const connID = Audio.connect("notify::speaker", () => {
Audio.disconnect(connID);
self.children = [Slider(vol()), Slider(brightness())];
});
},
});

View File

@@ -0,0 +1,102 @@
import { App, Bluetooth, Network, Utils, Widget } from "../../imports.js";
import { getNetIcon, getNetText } from "../../utils/net.js";
import { getBluetoothIcon, getBluetoothText } from "../../utils/bluetooth.js";
const Toggle = (args) =>
Widget.Box({
...(args.props ?? {}),
className: `toggle ${args.name}`,
hexpand: true,
hpack: "start",
children: [
Widget.Button({
className: "button",
child: Widget.Icon({
setup: args.icon.setup,
}),
setup: args.icon.buttonSetup,
}),
Widget.Button({
hexpand: true,
child: Widget.Label({
hpack: "start",
setup: args.label.setup,
}),
setup: args.label.buttonSetup,
}),
],
});
const net = {
name: "net",
icon: {
setup: (self) =>
self
.bind("icon", Network, "connectivity", getNetIcon)
.bind("icon", Network.wifi, "icon-name", getNetIcon),
buttonSetup: (self) => {
self.onPrimaryClick = () => Network.toggleWifi();
self.hook(
Network,
(btn) =>
btn.toggleClassName("disabled", Network.connectivity != "full"),
"notify::connectivity",
);
},
},
label: {
setup: (self) =>
self
.bind("label", Network, "connectivity", () => getNetText())
.bind("label", Network.wifi, "ssid", () => getNetText()),
buttonSetup: (self) => {
self.onPrimaryClick = () => {
App.toggleWindow("system-menu");
Utils.execAsync([
"sh",
"-c",
"XDG_CURRENT_DESKTOP=GNOME gnome-control-center",
]);
};
},
},
};
const bt = {
name: "bluetooth",
icon: {
setup: (self) =>
self.bind("icon", Bluetooth, "connected-devices", getBluetoothIcon),
buttonSetup: (self) => {
self.onPrimaryClick = () => Bluetooth.toggle();
self.hook(
Bluetooth,
(btn) => btn.toggleClassName("disabled", !Bluetooth.enabled),
"notify::enabled",
);
},
},
label: {
setup: (self) =>
self.bind("label", Bluetooth, "connected-devices", getBluetoothText),
buttonSetup: (self) => {
self.onPrimaryClick = () => {
App.toggleWindow("system-menu");
Utils.execAsync("overskride");
};
},
},
};
export default () =>
Widget.Box({
className: "toggles",
vertical: true,
children: [Toggle(net), Toggle(bt)],
});