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