From 36ea0b0e39eaf66c33f5e1f37d0ba8346116cb0b Mon Sep 17 00:00:00 2001 From: caem Date: Thu, 30 May 2024 19:45:14 +0200 Subject: [PATCH] Update the ags configuration There is now a bargain bin gnome-shell quicksettings menu with basically only media controls. This also takes care of notifications now. TODO: Add the code for shutting down and restarting the computer with the buttons in the quicksettings menu. --- packages/wm/hyprland.nix | 1 - users/hu/packages/ags/config/assets/nixos.svg | 1 + users/hu/packages/ags/config/bar.js | 210 ++++++++++++++++++ users/hu/packages/ags/config/config.js | 139 +----------- users/hu/packages/ags/config/style.css | 176 ++++++++++++++- users/hu/packages/ags/config/vendor/Media.js | 158 +++++++++++++ .../ags/config/vendor/notificationPopups.js | 148 ++++++++++++ users/hu/packages/dunst.nix | 7 - 8 files changed, 693 insertions(+), 147 deletions(-) create mode 100644 users/hu/packages/ags/config/assets/nixos.svg create mode 100644 users/hu/packages/ags/config/bar.js create mode 100644 users/hu/packages/ags/config/vendor/Media.js create mode 100644 users/hu/packages/ags/config/vendor/notificationPopups.js delete mode 100644 users/hu/packages/dunst.nix diff --git a/packages/wm/hyprland.nix b/packages/wm/hyprland.nix index 3037a9c..7827ad1 100644 --- a/packages/wm/hyprland.nix +++ b/packages/wm/hyprland.nix @@ -7,7 +7,6 @@ environment.systemPackages = with pkgs; [ hyprpaper - dunst rofi-wayland foot wl-clipboard diff --git a/users/hu/packages/ags/config/assets/nixos.svg b/users/hu/packages/ags/config/assets/nixos.svg new file mode 100644 index 0000000..fb26b80 --- /dev/null +++ b/users/hu/packages/ags/config/assets/nixos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/users/hu/packages/ags/config/bar.js b/users/hu/packages/ags/config/bar.js new file mode 100644 index 0000000..e9087d9 --- /dev/null +++ b/users/hu/packages/ags/config/bar.js @@ -0,0 +1,210 @@ +const hyprland = await Service.import("hyprland"); +const audio = await Service.import("audio"); + +import { Media } from "./vendor/Media.js"; + +const VolumeWidget = (properties) => { + return Widget.Box({ + class_name: "app-volume", + children: [ + Widget.Box({ + class_name: "app-mixer", + vertical: true, + hexpand: true, + children: [ + Widget.Label({ + class_name: "app-mixer-label", + label: properties.description, + truncate: "end", + maxWidthChars: 60, + wrap: false, + justification: "left", + }), + Widget.Box({ + class_name: "app-volume-slider", + children: [ + Widget.Icon({ + class_name: "volume-icon", + icon: "audio-volume-high-symbolic", + }), + Widget.Slider({ + hexpand: true, + draw_value: false, + on_change: ({ value }) => properties.volume = value, + setup: self => self.hook(properties, () => { + self.value = properties.volume || 0 + }), + }), + ], + }), + // Widget.Label(properties.stream.name), + ], + }), + ], + }); +}; + +const VolumeWidgets = () => { + if (!audio || !audio.apps) { + return []; + } + + let widgets = []; + for (let i = 0; i < audio.apps.length; i++) { + widgets.push(VolumeWidget(audio.apps[i])); + } + + return widgets; +}; + +const Qsd = () => Widget.Box({ + name: "qsd", + class_name: "qsd", + vpack: "start", + vertical: true, + children: [ + Widget.Box({ + name: "qsd-power", + class_name: "qsd-power", + hpack: "end", + children: [ + Widget.Button({ + child: Widget.Icon({ + icon: "system-reboot-symbolic" + }), + }), + Widget.Button({ + on_clicked: () => { + console.log(audio.apps) + }, + child: Widget.Icon({ + icon: "system-shutdown-symbolic" + }), + }), + ], + }), + Widget.Box({ + name: "qsd-volume-mixer", + class_name: "qsd-volume-mixer", + children: [ + Widget.Box({ + class_name: "qsd-volume-slider-container", + vertical: true, + vpack: "start", + }).hook(audio, self => { + self.children = [ + Widget.Box({ + class_name: "master-volume", + children: [ + Widget.Icon({ + class_name: "volume-icon", + icon: "audio-volume-high-symbolic", + }), + Widget.Label("master"), + Widget.Slider({ + hexpand: true, + draw_value: false, + on_change: ({ value }) => audio.speaker.volume = value, + setup: self => self.hook(audio.speaker, () => { + self.value = audio.speaker.volume || 0 + }), + }), + ] + }) + ].concat(VolumeWidgets()); + }), + ], + }), + Media(), + ], +}); + +const Menu = () => { + return Widget.Window({ + name: "shell-menu", + class_name: "shell-menu", + exclusivity: "normal", + anchor: ["top", "right"], + margins: [5, 5], + // keymode: "on-demand", + layer: "top", + monitor: 0, + child: Qsd(), + }); +} + +App.addWindow(Menu()); +App.toggleWindow("shell-menu"); + +const MenuButton = () => { + return Widget.Button({ + on_clicked: () => App.toggleWindow("shell-menu"), + child: Widget.Icon({ + icon: "nixos" + }), + class_name: "menu_button", + }); +} + +const data = Variable("", { + poll: [1000, 'date "+%H:%M:%S"'], +}); + +const Clock = () => { + return Widget.Label({ + class_name: "clock", + label: data.bind(), + }); +} + +const End = () => { + return Widget.Box({ + hpack: "end", + spacing: 8, + children: [ + Clock(), + MenuButton(), + ], + }); +} + +const Workspaces = () => { + 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({ + on_clicked: () => hyprland.messageAsync(`dispatch workspace ${id}`), + child: Widget.Label(`${id}`), + class_name: activeId.as((i) => `${i === id ? "focused" : ""}`), + }) + ) + ); + + return Widget.Box({ + class_name: "workspaces", + children: workspaces, + }); +} + +const Start = () => { + return Widget.Box({ + children: [ + Workspaces(), + ], + }); +} + +export const Bar = () => { + return Widget.Window({ + name: "bar", + class_name: "bar", + monitor: 0, + anchor: ["top", "left", "right"], + exclusivity: "exclusive", + child: Widget.CenterBox({ + start_widget: Start(), + end_widget: End(), + }), + }); +} + diff --git a/users/hu/packages/ags/config/config.js b/users/hu/packages/ags/config/config.js index 1aac540..7d59086 100644 --- a/users/hu/packages/ags/config/config.js +++ b/users/hu/packages/ags/config/config.js @@ -1,141 +1,12 @@ -/* TODO: Make this more useful */ - -const hyprland = await Service.import("hyprland") -const notifications = await Service.import("notifications") -const mpris = await Service.import("mpris") -const audio = await Service.import("audio") -const systemtray = await Service.import("systemtray") - -const date = Variable("", { - poll: [1000, 'date "+%H:%M:%S"'], -}) - -function Workspaces() { - 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({ - on_clicked: () => hyprland.messageAsync(`dispatch workspace ${id}`), - child: Widget.Label(`${id}`), - class_name: activeId.as((i) => `${i === id ? "focused" : ""}`), - }) - ) - ); - - return Widget.Box({ - class_name: "workspaces", - children: workspaces, - }) -} - -function Clock() { - return Widget.Label({ - class_name: "clock", - label: date.bind(), - }) -} - -function Notification() { - const popups = notifications.bind("popups") - return Widget.Box({ - class_name: "notification", - visible: popups.as(p => p.length > 0), - children: [ - Widget.Icon({ - icon: "preferences-system-notifications-symbolic", - }), - Widget.Label({ - label: popups.as(p => p[0]?.summary || ""), - }), - ], - }) -} - - -function Media() { - const label = Utils.watch("", mpris, "player-changed", () => { - if (mpris.players[0]) { - const { track_artists, track_title } = mpris.players[0] - return `${track_artists.join(", ")} - ${track_title}` - } else { - return "Nothing is playing" - } - }) - - return Widget.Button({ - class_name: "media", - on_primary_click: () => mpris.getPlayer("")?.playPause(), - on_scroll_up: () => mpris.getPlayer("")?.next(), - on_scroll_down: () => mpris.getPlayer("")?.previous(), - child: Widget.Label({ label }), - }) -} - -function SysTray() { - const items = systemtray.bind("items") - .as(items => items.map(item => Widget.Button({ - child: Widget.Icon({ icon: item.bind("icon") }), - on_primary_click: (_, event) => item.activate(event), - on_secondary_click: (_, event) => item.openMenu(event), - tooltip_markup: item.bind("tooltip_markup"), - }))) - - return Widget.Box({ - children: items, - }) -} - -function Left() { - return Widget.Box({ - spacing: 8, - children: [ - Workspaces(), - ], - }) -} - -function Center() { - return Widget.Box({ - spacing: 8, - children: [ - Media(), - Notification(), - ], - }) -} - -function Right() { - return Widget.Box({ - hpack: "end", - spacing: 8, - children: [ - SysTray(), - Clock(), - ], - }) -} - -function Bar(monitor = 0) { - return Widget.Window({ - name: `bar-${monitor}`, - class_name: "bar", - monitor, - anchor: ["top", "left", "right"], - exclusivity: "exclusive", - child: Widget.CenterBox({ - start_widget: Left(), - center_widget: Center(), - end_widget: Right(), - }), - }) -} +import { Bar } from "./bar.js"; +import { NotificationPopups } from "./vendor/notificationPopups.js"; +App.addIcons(`${App.configDir}/assets`); App.config({ style: "./style.css", windows: [ Bar(), + NotificationPopups(), ], -}) +}); -export { } diff --git a/users/hu/packages/ags/config/style.css b/users/hu/packages/ags/config/style.css index 9ac7355..93c8198 100644 --- a/users/hu/packages/ags/config/style.css +++ b/users/hu/packages/ags/config/style.css @@ -1,21 +1,29 @@ +* { + font-family: 'Go Mono Nerd Font'; + font-size: 12pt; +} + window.bar { background-color: @theme_bg_color; color: @theme_fg_color; + border-bottom: 1px solid @theme_selected_bg_color; } button { min-width: 0; - padding-top: 0; - padding-bottom: 0; + padding: 0 6pt 0 6pt; background-color: transparent; + margin: 3pt 0 3pt 0; } button:active { background-color: @theme_selected_bg_color; + color: @theme_bg_color; } button:hover { - border-bottom: 3px solid @theme_fg_color; + border-bottom: 3pt solid @theme_fg_color; + margin-bottom: 0pt; /* Prevent bar from resizing from added bottom length from the border */ } label { @@ -23,7 +31,8 @@ label { } .workspaces button.focused { - border-bottom: 3px solid @theme_selected_bg_color; + border-bottom: 3pt solid @theme_selected_bg_color; + margin-bottom: 0pt ; /* Prevent bar from resizing from added bottom length from the border */ } .client-title { @@ -34,7 +43,164 @@ label { color: yellow; } +/* levelbar block, highlight { - min-height: 10px; + min-height: 40pt; +} +*/ + +window.notification-popups box.notifications { + padding: .5em; +} + +window.notification-popups * { + font-size: 10pt; +} + +.icon { + min-width: 38px; + min-height: 38px; + margin-right: 1em; +} + +.icon image { + font-size: 38px; + /* to center the icon */ + margin: 5px; + color: @theme_fg_color; +} + +.icon box { + min-width: 38px; + min-height: 38px; + border-radius: 7px; +} + +.notification { + min-width: 350px; + border-radius: 11px; + padding: 1em; + margin: .5em; + border: 1px solid @theme_selected_bg_color; + background-color: @theme_bg_color; +} + +.notification.critical { + border: 1px solid lightcoral; +} + +.title { + color: @theme_fg_color; +} + +.body { + color: @theme_unfocused_fg_color; +} + +.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; +} + +.qsd { + border: 1px solid @theme_selected_bg_color; + background-color: @theme_bg_color; + border-radius: 10pt; + padding: 12pt; + color: @theme_fg_color; +} + +.qsd-power { + border: 1px solid @theme_selected_bg_color; + border-radius: 10pt; + padding: 6pt; + margin-bottom: 6pt; +} + +.qsd-volume-mixer { + border: 1px solid @theme_selected_bg_color; + padding: 12pt; + border-radius: 10pt; +} + +.qsd-volume-slider-container { + min-width: 250pt; +} + +.volume-icon { + padding-right: 8pt; +} + +.app-mixer { + border: 1px solid @theme_selected_bg_color; + padding: 12pt; + border-radius: 10pt; + margin-top: 6pt; +} + +.app-mixer-label { + font-size: 10pt; +} + +.player { + border: 1px solid @theme_selected_bg_color; + padding: 12pt; + border-radius: 10pt; + margin-top: 8pt; + padding: 10px; + min-width: 350px; +} + +.player .img { + min-width: 100px; + min-height: 100px; + background-size: cover; + background-position: center; + border-radius: 13px; + margin-right: 1em; +} + +.player .title { + font-size: 1.2em; +} + +.player .artist { + font-size: 1.1em; + color: @insensitive_fg_color; +} + +.player scale.position { + padding: 0; + margin-bottom: .3em; +} + +.player scale.position trough { + min-height: 8px; +} + +.player scale.position highlight { + background-color: @theme_fg_color; +} + +.player scale.position slider { + all: unset; +} + +.player button { + min-height: 1em; + min-width: 1em; + padding: .3em; +} + +.player button.play-pause { + margin: 0 .3em; } diff --git a/users/hu/packages/ags/config/vendor/Media.js b/users/hu/packages/ags/config/vendor/Media.js new file mode 100644 index 0000000..d6871b4 --- /dev/null +++ b/users/hu/packages/ags/config/vendor/Media.js @@ -0,0 +1,158 @@ +/* + * This snippet is taken from the ags examples directory. It is licensed under GPLv3. + * More information available here: https://github.com/Aylur/ags/blob/main/LICENSE + * */ + +const mpris = await Service.import("mpris") +const players = mpris.bind("players") + +const FALLBACK_ICON = "audio-x-generic-symbolic" +const PLAY_ICON = "media-playback-start-symbolic" +const PAUSE_ICON = "media-playback-pause-symbolic" +const PREV_ICON = "media-skip-backward-symbolic" +const NEXT_ICON = "media-skip-forward-symbolic" + +/** @param {number} length */ +function lengthStr(length) { + const min = Math.floor(length / 60) + const sec = Math.floor(length % 60) + const sec0 = sec < 10 ? "0" : "" + return `${min}:${sec0}${sec}` +} + +/** @param {import('types/service/mpris').MprisPlayer} player */ +function Player(player) { + const img = Widget.Box({ + class_name: "img", + vpack: "start", + css: player.bind("cover_path").transform(p => ` + background-image: url('${p}'); + `), + }) + + const title = Widget.Label({ + class_name: "title", + wrap: true, + hpack: "start", + label: player.bind("track_title"), + }) + + const artist = Widget.Label({ + class_name: "artist", + wrap: true, + hpack: "start", + label: player.bind("track_artists").transform(a => a.join(", ")), + }) + + const positionSlider = Widget.Slider({ + class_name: "position", + draw_value: false, + on_change: ({ value }) => player.position = value * player.length, + visible: player.bind("length").as(l => l > 0), + setup: self => { + function update() { + const value = player.position / player.length + self.value = value > 0 ? value : 0 + } + self.hook(player, update) + self.hook(player, update, "position") + self.poll(1000, update) + }, + }) + + const positionLabel = Widget.Label({ + class_name: "position", + hpack: "start", + 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({ + class_name: "length", + hpack: "end", + visible: player.bind("length").transform(l => l > 0), + label: player.bind("length").transform(lengthStr), + }) + + const icon = Widget.Icon({ + class_name: "icon", + hexpand: true, + hpack: "end", + vpack: "start", + tooltip_text: player.identity || "", + icon: player.bind("entry").transform(entry => { + const name = `${entry}-symbolic` + return Utils.lookUpIcon(name) ? name : FALLBACK_ICON + }), + }) + + const playPause = Widget.Button({ + class_name: "play-pause", + on_clicked: () => player.playPause(), + visible: player.bind("can_play"), + 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({ + on_clicked: () => player.previous(), + visible: player.bind("can_go_prev"), + child: Widget.Icon(PREV_ICON), + }) + + const next = Widget.Button({ + on_clicked: () => player.next(), + visible: player.bind("can_go_next"), + child: Widget.Icon(NEXT_ICON), + }) + + return Widget.Box( + { class_name: "player" }, + img, + Widget.Box( + { + vertical: true, + hexpand: true, + }, + Widget.Box([ + title, + icon, + ]), + artist, + Widget.Box({ vexpand: true }), + positionSlider, + Widget.CenterBox({ + start_widget: positionLabel, + center_widget: Widget.Box([ + prev, + playPause, + next, + ]), + end_widget: lengthLabel, + }), + ), + ) +} + +export function Media() { + return Widget.Box({ + vertical: true, + css: "min-height: 2px; min-width: 2px;", // small hack to make it visible + visible: players.as(p => p.length > 0), + children: players.as(p => p.map(Player)), + }) +} diff --git a/users/hu/packages/ags/config/vendor/notificationPopups.js b/users/hu/packages/ags/config/vendor/notificationPopups.js new file mode 100644 index 0000000..781c3eb --- /dev/null +++ b/users/hu/packages/ags/config/vendor/notificationPopups.js @@ -0,0 +1,148 @@ +/* + * This snippet is taken from the ags examples directory. It is licensed under GPLv3. + * More information available here: https://github.com/Aylur/ags/blob/main/LICENSE + * + * Modifications have been made. + * */ + +const notifications = await Service.import("notifications") + +/** @param {import('resource:///com/github/Aylur/ags/service/notifications.js').Notification} n */ +function 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 = "dialog-information-symbolic" + if (Utils.lookUpIcon(app_icon)) + icon = app_icon + + if (app_entry && Utils.lookUpIcon(app_entry)) + icon = app_entry + + return Widget.Box({ + child: Widget.Icon(icon), + }) +} + +/** @param {import('resource:///com/github/Aylur/ags/service/notifications.js').Notification} n */ +function Notification(n) { + /* Setting the max width in gtk css is not supported for some reason + * so we have to split the length of the lines here by inserting newline + * characters where appropriate. */ + let body_text = n.body; + for (let i = 0; i < body_text.length; i += 40) { + let left = body_text.substring(0, i); + let right = body_text.substring(i); + body_text = left + "\n" + right; + } + + 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: body_text, + 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) + n.dismiss() + }, + hexpand: true, + child: Widget.Label(label), + })), + }) + + return Widget.EventBox( + { + attribute: { id: n.id }, + on_primary_click: n.dismiss, + }, + Widget.Box( + { + class_name: `notification ${n.urgency}`, + vertical: true, + }, + Widget.Box([ + icon, + Widget.Box( + { vertical: true }, + title, + body, + ), + ]), + actions, + ), + ) +} + +export function NotificationPopups(monitor = 0) { + const list = Widget.Box({ + vertical: true, + children: notifications.popups.map(Notification), + }) + + function onNotified(_, /** @type {number} */ id) { + const n = notifications.getNotification(id) + if (n) + list.children = [Notification(n), ...list.children] + } + + function onDismissed(_, /** @type {number} */ id) { + list.children.find(n => n.attribute.id === id)?.destroy() + } + + list.hook(notifications, onNotified, "notified") + .hook(notifications, onDismissed, "dismissed") + + return Widget.Window({ + monitor, + name: `notifications${monitor}`, + class_name: "notification-popups", + anchor: ["top", "right"], + child: Widget.Box({ + css: "min-width: 2px; min-height: 2px;", + class_name: "notifications", + vertical: true, + child: list, + + /** this is a simple one liner that could be used instead of + hooking into the 'notified' and 'dismissed' signals. + but its not very optimized becuase it will recreate + the whole list everytime a notification is added or dismissed */ + // children: notifications.bind('popups') + // .as(popups => popups.map(Notification)) + }), + }) +} + diff --git a/users/hu/packages/dunst.nix b/users/hu/packages/dunst.nix deleted file mode 100644 index 91d51a4..0000000 --- a/users/hu/packages/dunst.nix +++ /dev/null @@ -1,7 +0,0 @@ -{ ... }: - -{ - # Todo: Make pretty - services.dunst.enable = true; -} -