diff --git a/misc/.config/ags/config.js b/misc/.config/ags/config.js new file mode 100644 index 0000000..19144b7 --- /dev/null +++ b/misc/.config/ags/config.js @@ -0,0 +1,33 @@ +import { execAsync, timeout } from 'resource:///com/github/Aylur/ags/utils.js'; +import { forMonitors } from "./utils.js"; +import { Bar } from "./modules/bar/bar.js"; +import { Popups } from './modules/popups/popups.js'; + +timeout(100, () => execAsync([ + 'notify-send', + 'Notification Popup Example', + 'Lorem ipsum dolor sit amet, qui minim labore adipisicing ' + + 'minim sint cillum sint consectetur cupidatat.', + '-A', 'Cool!', + '-i', 'info-symbolic', +])); + +// main scss file +const scss = `${App.configDir}/style.scss` + +// target css file +const css = `${App.configDir}/style.css` + +// make sure sassc is installed on your system +Utils.exec(`rm ${css} >/dev/null 2>&1`) +Utils.exec(`sassc ${scss} ${css}`) + +// Main config +export default { + style: `${App.configDir}/style.css`, + windows: [ + ...forMonitors(Bar), + Popups(0), + ], +}; + diff --git a/misc/.config/ags/modules/bar/bar.js b/misc/.config/ags/modules/bar/bar.js new file mode 100644 index 0000000..62ea9ef --- /dev/null +++ b/misc/.config/ags/modules/bar/bar.js @@ -0,0 +1,50 @@ +const Network = await Service.import("network"); +const Bluetooth = await Service.import("bluetooth"); + +// Widgets +import { Workspaces, ClientTitle } from "./workspaces.js"; +import { Battery } from "./battery.js"; +import { Date, Time } from "./clock.js"; + +const Left = (monitor) => Widget.Box({ + children: [ + Workspaces(monitor), + ClientTitle(), + ], +}); + +const Center = () => Widget.Box({ + children: [ ], +}); + +const Right = () => Widget.Box({ + hpack: "end", + children: [ + Battery(), + Date(), + Time(), + ], +}); + +const hyprlandMonitors = Utils.exec(`/bin/sh -c "hyprctl monitors | grep -o '(ID [0-9]*' | awk '{print $2}' | sort"`).split('\n').map(Number); + +const getHyprlandMonitor = monitor => { + if (monitor >= hyprlandMonitors.length) { + return 0; + } + return hyprlandMonitors[monitor]; +}; + +export const Bar = (monitor = 0) => Widget.Window({ + name: `bar-${monitor}`, // name has to be unique + class_name: 'bar', + monitor, + anchor: ['top', 'left', 'right'], + exclusivity: 'exclusive', + child: Widget.CenterBox({ + start_widget: Left(getHyprlandMonitor(monitor)), + center_widget: Center(), + end_widget: Right(), + }), +}); + diff --git a/misc/.config/ags/modules/bar/battery.js b/misc/.config/ags/modules/bar/battery.js new file mode 100644 index 0000000..2e98dff --- /dev/null +++ b/misc/.config/ags/modules/bar/battery.js @@ -0,0 +1,24 @@ +const { exec, execAsync } = Utils; + +export const Battery = () => Widget.Button({ + class_name: "battery", + child: Widget.Label({ + setup: (self) => {self.poll(1000, (self) => + execAsync("block_battery") + .then((out) => { + self.label = out; + let num = Number(out.substring(2, out.length - 1)); + + // Turn red at low battery + if (num < 20) { + self.class_name = "module, critical"; + } else { + self.class_name = "module"; + }; + }) + .catch(console.error), + )}, + }), + on_clicked: () => {}, +}); + diff --git a/misc/.config/ags/modules/bar/clock.js b/misc/.config/ags/modules/bar/clock.js new file mode 100644 index 0000000..ee26d24 --- /dev/null +++ b/misc/.config/ags/modules/bar/clock.js @@ -0,0 +1,32 @@ +const { exec, execAsync } = Utils; + +export const Date = () => Widget.Button({ + child: Widget.Label({ + class_name: "module", + setup: (self) => { + self.poll(1000, (self) => + execAsync(["date", "+ %a %b %e"]) + .then((time) => (self.label = time)) + .catch(console.error), + ); + }, + }), + on_clicked: () => { + }, +}); + +export const Time = () => Widget.Button({ + child: Widget.Label({ + class_name: "module", + setup: (self) => { + self.poll(1000, (self) => + execAsync(["date", "+ %R"]) + .then((time) => (self.label = time)) + .catch(console.error), + ); + }, + }), + on_clicked: () => { + }, +}); + diff --git a/misc/.config/ags/modules/bar/workspaces.js b/misc/.config/ags/modules/bar/workspaces.js new file mode 100644 index 0000000..0024295 --- /dev/null +++ b/misc/.config/ags/modules/bar/workspaces.js @@ -0,0 +1,53 @@ +const Hyprland = await Service.import("hyprland"); + +var monitorsActiveWorkspaceCache = new Array(10).fill(-1); + +const is_active = (workspaceID) => { + let monitorID = Hyprland.getWorkspace(workspaceID).monitorID; + var monitor = Hyprland.getMonitor(monitorID); + + // Somewhat of a hack going on here. For some reason, the + // Hyprland.monitors array only contains the monitorIDs up to the + // currently focused one. That means if I am currently focused on + // monitor 0, then I cannot check what the focused workspace is on + // monitor 3. Thus, I had to create a global cache on top to reference + // for when the result of Hyprland.getMonitor(monitorID) is undefined. + let activeID = -1; + if (monitor == undefined) { + activeID = monitorsActiveWorkspaceCache[monitorID]; + } else { + monitorsActiveWorkspaceCache[monitorID] = monitor.activeWorkspace.id; + activeID = monitor.activeWorkspace.id; + } + return workspaceID == activeID; +} + +export const Workspaces = (monitorID) => Widget.Box({ + class_name: 'workspaces', + children: Hyprland.bind('workspaces').transform(ws => { + return ws + .filter(({ id, name }) => { + if (name == "special") { + // Hyprland sometimes uses a special workspace, which + // wouldn't appear on the bar. + return false; + } + + // Only show the workspaces belonging to the monitor + return Hyprland.getWorkspace(id).monitorID == monitorID; + }) + .sort((a, b) => a.id - b.id) // Show in order of ID + .map(({ id, name }) => Widget.Button({ + on_clicked: () => Hyprland.sendMessage(`dispatch workspace ${id}`), + child: Widget.Label(`${name}`), + class_name: Hyprland.active.workspace.bind('id') + .transform(i => `${is_active(id) ? 'focused' : ''}`), + })); + }), +}); + +export const ClientTitle = () => Widget.Label({ + class_name: 'module', + label: Hyprland.active.client.bind('title'), +}); + diff --git a/misc/.config/ags/modules/popups/popups.js b/misc/.config/ags/modules/popups/popups.js new file mode 100644 index 0000000..133e9ee --- /dev/null +++ b/misc/.config/ags/modules/popups/popups.js @@ -0,0 +1,105 @@ +import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js'; +import Widget from 'resource:///com/github/Aylur/ags/widget.js'; +import { lookUpIcon } from 'resource:///com/github/Aylur/ags/utils.js'; + +/** @param {import('resource:///com/github/Aylur/ags/service/notifications.js').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; + `, + }); + } + + let icon = 'add'; + if (lookUpIcon(app_icon)) + icon = app_icon; + + if (app_entry && lookUpIcon(app_entry)) + icon = app_entry; + + return Widget.Icon(icon); +}; + +/** @param {import('resource:///com/github/Aylur/ags/service/notifications.js').Notification} n */ +export const Notification = n => { + const icon = Widget.Box({ + vpack: 'start', + class_name: 'icon', + child: NotificationIcon(n), + }); + + 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', + label: n.body, + wrap: true, + }); + + const actions = Widget.Box({ + class_name: 'actions', + children: n.actions.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: () => n.dismiss(), + child: Widget.Box({ + class_name: `notification ${n.urgency}`, + vertical: true, + children: [ + Widget.Box({ + children: [ + icon, + Widget.Box({ + vertical: true, + children: [ + title, + body, + ], + }), + ], + }), + actions, + ], + }), + }); +}; + +export const Popups = monitor => Widget.Window({ + name: `notifications`, + monitor, + layer: 'overlay', + anchor: ['top', 'right'], + child: Widget.Box({ + class_name: 'notifications', + vertical: true, + children: Notifications.bind('popups').transform(popups => { + return popups.map(Notification); + }), + }), +}); + diff --git a/misc/.config/ags/style.scss b/misc/.config/ags/style.scss new file mode 100644 index 0000000..a0575d5 --- /dev/null +++ b/misc/.config/ags/style.scss @@ -0,0 +1,107 @@ +$fg: #FFFFFF; +$bg: #000000; + +* { + font-family: Fira Code, Symbols Nerd Font Mono; + font-size: 15px; +} + +window.bar { + background-color: $bg; + color: $fg; +} + +// .module { +// margin-left: 5px; +// margin-right: 5px; +// } + +.bar button { + padding-left: 5px; + padding-right: 5px; + background: $bg; + border-radius: 0px; + border-color: $bg; +} + +.bar button:hover{ + background: #666666; +} + +.bar button.focused{ + // When the workspace is being shown, and the monitor is focused. + box-shadow: inset 0 -3px $fg; +} + +.bar button.active{ + // When the workspace is being shown, and the monitor is focused. + box-shadow: inset 0 -3px $fg; +} + +.battery .critical { + color: #FF0000; +} + + +window#notifications { + all: unset; +} + +window#notifications box.notifications { + padding: .5em; +} + +.icon { + min-width: 68px; + min-height: 68px; + margin-right: 1em; +} + +.icon image { + font-size: 58px; + /* to center the icon */ + margin: 5px; + color: $fg +} + +.icon box { + min-width: 68px; + min-height: 68px; + border-radius: 7px; +} + +.notification { + min-width: 350px; + border-radius: 11px; + padding: 1em; + margin: .5em; + border: 1px solid $fg; + background-color: $bg; +} + +.notification.critical { + border: 1px solid lightcoral; +} + +.title { + color: $fg; + font-size: 1.4em; +} + +.body { + color: $fg; +} + +.actions .action-button { + margin: 0 .4em; + margin-top: .8em; +} + +.actions .action-button:first-child { + margin-left: 0; +} + +.actions .action-button:last-child { + margin-right: 0; +} + diff --git a/misc/.config/ags/utils.js b/misc/.config/ags/utils.js new file mode 100644 index 0000000..4f1ee8b --- /dev/null +++ b/misc/.config/ags/utils.js @@ -0,0 +1,11 @@ +import Gdk from "gi://Gdk" + +export function forMonitors(widget) { + const n = Gdk.Display.get_default()?.get_n_monitors() || 1; + return range(n, 0).map(widget).flat(1); +} + +export function range(length, start = 1) { + return Array.from({ length }, (_, i) => i + start) +} +