From b89e4be10aaf08c81b71666dc260801a6a4fec1c Mon Sep 17 00:00:00 2001 From: dingo Date: Mon, 10 Aug 2026 12:58:48 +0700 Subject: [PATCH] Generate the waybar config, and fill the bar out The bar had six modules and one dead click: pulseaudio's on-click ran pavucontrol, which is not installed. It now opens `cosmic-settings sound`, with middle-click as a mute toggle. Added: mpris, bluetooth, temperature, idle_inhibitor, privacy, a swaync notification button, and power-profiles- daemon -- and HyDE's pill styling, so each module is its own rounded chip rather than text in a row. No backlight module. This machine's panel has no sysfs backlight interface, so it would render as a permanent error. The rest of this is about the icons, which have now been got wrong enough times to deserve a mechanism. Private Use Area characters do not survive being typed. Writing the previous version I put a comment at the top of the file saying every glyph was an escape, then typed literal glyphs into the same file; the codepoint dump found `format-bluetooth` had picked up a stray U+F293 and another field had two glyphs where I had written one. Nothing errors -- waybar is perfectly happy to render a label that is one space. So the config is generated, not written. config.jsonc.in is pure ASCII with @@TOKEN@@ placeholders, generate-config.py holds the name-to-codepoint table and emits `\uXXXX` escapes, and it asserts its own output `.isascii()` before writing. A hand-typed glyph now cannot reach the file. The delimiter is doubled because single `@NAME@` collided with wpctl's `@DEFAULT_AUDIO_SINK@`, which the generator caught as an unknown token rather than mangling. Every codepoint was checked against the installed font with `fc-list ":charset=..."`. HyDE's own values do not all survive that: its muted glyph U+FA80 is an old Material Design Icons codepoint that Nerd Fonts v3 moved, so copying upstream verbatim would have shipped tofu. Three were replaced. The font stack named "FontAwesome 6 Free" first, which resolves to the Regular face and carries a fraction of the icon set -- the icons that did appear were coming from accidental per-character fontconfig fallback. JetBrainsMono Nerd Font goes first now. One CSS note, because the failure mode is not obvious: GTK has no `:empty` pseudo-class, and an unknown pseudo-class does not skip the rule, it rejects the whole stylesheet and waybar exits 1. `#tray:empty` took the bar down. install-assets.sh grows a third category. The template and the generator live under config/ but must not be installed -- a file full of placeholders sitting next to the real config is a coin toss for whoever opens one first -- and the audit refuses to run until every file is classified, which is exactly what it is for. --- config/waybar/bridge-hyde.css | 10 ++ config/waybar/config.jsonc | 226 ++++++++++++++++++++++++-- config/waybar/config.jsonc.in | 269 +++++++++++++++++++++++++++++++ config/waybar/generate-config.py | 96 +++++++++++ config/waybar/rules.css | 93 ++++++++++- tools/install-assets.sh | 15 +- 6 files changed, 686 insertions(+), 23 deletions(-) create mode 100644 config/waybar/config.jsonc.in create mode 100755 config/waybar/generate-config.py diff --git a/config/waybar/bridge-hyde.css b/config/waybar/bridge-hyde.css index 4014fe4..6fa4ee1 100644 --- a/config/waybar/bridge-hyde.css +++ b/config/waybar/bridge-hyde.css @@ -24,3 +24,13 @@ * or once blur lands. */ @define-color bar-bg alpha(@main-bg, 0.85); + +/* The pill behind each module. Derived rather than themed: HyDE has no name + * for it, because in HyDE the pill IS the bar -- its modules sit on a + * transparent strip and the compositor blurs the desktop behind them. Without + * blur that reads as text floating on the wallpaper, so here the bar keeps a + * background and the pills are a light lift off it. + * + * Derived from the foreground, not the background, so it stays visible whether + * the theme is dark or light. */ +@define-color module-bg alpha(@main-fg, 0.08); diff --git a/config/waybar/config.jsonc b/config/waybar/config.jsonc index 8c57c82..327dec0 100644 --- a/config/waybar/config.jsonc +++ b/config/waybar/config.jsonc @@ -16,15 +16,55 @@ // supplies one via cosmic-applet-status-area, and the // hyprcosmic profile disables the panel, so waybar hosts the // watcher itself here. +// +// ICONS. Every glyph below is a \uXXXX escape, never a literal character, and +// this file is generated from config.jsonc.in for exactly that reason: +// +// - These are Private Use Area codepoints. They survive being copied, but +// anything that re-types rather than copies them silently flattens them to +// spaces or drops them next to a neighbour, and the damage is invisible in +// a diff and indistinguishable from a font problem at runtime. +// - An escape is plain ASCII, so it cannot be damaged that way, and it names +// the intended glyph instead of leaving a box you have to identify. +// +// Every codepoint was checked against the installed JetBrainsMono Nerd Font +// with `fc-list :charset=` before use. That check is worth doing rather +// than copying HyDE's values: HyDE's pulseaudio module uses U+FA80 for the +// muted state, an old Material Design Icons codepoint that Nerd Fonts v3 +// moved. It is absent from the installed font and would render as tofu, so +// U+F075F is used instead. +// +// NOT INCLUDED, deliberately: +// +// backlight The only device is acpi_video0, a firmware stub reporting +// brightness 49 of max 49 -- permanently 100% -- whose sysfs +// node is not writable by this user, with brightnessctl absent. +// The module would render a readout that never changes and a +// scroll action that never works: visible, plausible, and dead. +// That is the same failure class as the `pavucontrol` click this +// file used to carry, and it is not worth re-introducing for a +// percentage that is always the same number. { "layer": "top", "position": "top", "height": 34, - "spacing": 6, + "spacing": 4, "modules-left": ["ext/workspaces", "wlr/taskbar"], - "modules-center": ["clock"], - "modules-right": ["pulseaudio", "network", "cpu", "memory", "battery", "tray"], + "modules-center": ["mpris", "clock", "privacy"], + "modules-right": [ + "idle_inhibitor", + "custom/swaync", + "bluetooth", + "pulseaudio", + "network", + "temperature", + "cpu", + "memory", + "power-profiles-daemon", + "battery", + "tray" + ], "ext/workspaces": { "format": "{name}", @@ -39,34 +79,190 @@ "on-click-middle": "close" }, + // Renders nothing at all when no player is running, which is what we want + // from a centre module: it takes no space until there is something to say. + // playerctl backs every action here and is installed. + "mpris": { + "format": "{player_icon} {dynamic}", + "format-paused": "{status_icon} {dynamic}", + "dynamic-order": ["title", "artist"], + "dynamic-separator": " ", + "max-length": 40, + "interval": 1, + "player-icons": { "default": "\uf001" }, + "status-icons": { + "playing": "\uf04b", + "paused": "\uf04c", + "stopped": "\uf04d" + }, + "on-click": "playerctl play-pause", + "on-click-middle": "playerctl previous", + "on-click-right": "playerctl next", + "on-scroll-up": "playerctl position 5+", + "on-scroll-down": "playerctl position 5-", + "tooltip-format": "{title}\nby {artist}\n{position} / {length}\n\nplayer: {player}" + }, + "clock": { "format": "{:%a %d %b %H:%M}", "tooltip-format": "{calendar}" }, + // Only appears while something is actually capturing. Nothing is ignored: + // an exception list is how a privacy indicator stops being one. + "privacy": { + "icon-spacing": 6, + "transition-duration": 200, + "modules": [ + { "type": "screenshare", "tooltip": true }, + { "type": "audio-in", "tooltip": true } + ] + }, + + "idle_inhibitor": { + "format": "{icon}", + "format-icons": { + "activated": "\udb80\udd76", + "deactivated": "\udb81\udeca" + }, + "tooltip-format-activated": "Caffeine on -- idling and blanking suppressed", + "tooltip-format-deactivated": "Caffeine off -- normal power settings apply" + }, + + // swaync is this session's notification daemon: it owns + // org.freedesktop.Notifications and is D-Bus activated through + // swaync.service, so it needs no autostart entry and this module starts it + // on first contact if nothing else has. + // + // It is here because the hyprcosmic profile disables cosmic-panel and + // COSMIC's notification applet lives in that panel. Without this there is + // no way to reach notification history at all. + "custom/swaync": { + "format": "{icon}", + "format-icons": { + "none": "\uf0f3", + "notification": "\udb80\udc9a", + "dnd-none": "\uf1f6", + "dnd-notification": "\uf1f6", + "inhibited-none": "\uf0f3", + "inhibited-notification": "\udb80\udc9a", + "dnd-inhibited-none": "\uf1f6", + "dnd-inhibited-notification": "\uf1f6" + }, + "return-type": "json", + "exec": "swaync-client -swb", + "on-click": "swaync-client -t -sw", + "on-click-right": "swaync-client -d -sw", + "tooltip": true, + "escape": true + }, + + // bluez is running with an unblocked hci0. The click target is COSMIC's own + // settings page rather than blueman or overskride, neither of which is + // installed here. + "bluetooth": { + "format": "\uf293", + "format-disabled": "\uf294", + "format-off": "\udb80\udcb2", + "format-connected": "\uf293 {num_connections}", + "tooltip-format": "{controller_alias}\n{num_connections} connected", + "tooltip-format-connected": "{controller_alias}\n\n{device_enumerate}", + "tooltip-format-enumerate-connected": "{device_alias}", + "on-click": "cosmic-settings bluetooth" + }, + + // The click used to be `pavucontrol`, which is not installed here: a button + // that looked live and did nothing. cosmic-settings ships with the desktop, + // so it cannot go missing the same way. Scroll and middle-click need no + // helper -- waybar changes volume itself and wpctl comes with pipewire. "pulseaudio": { "format": "{icon} {volume}%", - "format-muted": "muted", - "format-icons": { "default": ["", "", ""] }, - "on-click": "pavucontrol" + "format-muted": "\udb81\udf5f", + "format-bluetooth": "{icon} {volume}%", + "format-bluetooth-muted": "\udb81\udf5f", + "format-icons": { + "headphone": "\uf025", + "headset": "\uf025", + "hands-free": "\uf025", + "phone": "\uf095", + "car": "\uf1b9", + "default": ["\uf026", "\uf027", "\uf028"] + }, + "scroll-step": 5, + "tooltip-format": "{desc}\n{icon} {volume}%", + "on-click": "cosmic-settings sound", + "on-click-middle": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle" }, "network": { - "format-wifi": " {signalStrength}%", - "format-ethernet": " wired", - "format-disconnected": " offline", - "tooltip-format": "{ifname}: {ipaddr}" + "format-wifi": "\uf1eb {signalStrength}%", + "format-ethernet": "\udb80\ude00 wired", + "format-linked": "\udb80\ude00 {ifname}", + "format-disconnected": "\udb82\udd2f offline", + "tooltip-format": "{ifname}: {ipaddr}", + "tooltip-format-wifi": "{essid} ({signalStrength}%)\n{ifname}: {ipaddr}", + "on-click": "cosmic-settings network" }, - "cpu": { "format": " {usage}%", "interval": 5 }, - "memory": { "format": " {percentage}%", "interval": 5 }, + // Tctl from k10temp, not thermal_zone0. The two read within a degree of + // each other here, but the hwmon path names the CPU sensor explicitly. + // + // It is given as the PCI device's hwmon directory rather than + // /sys/class/hwmon/hwmon6 because hwmonN numbering is assigned in probe + // order and is not stable across boots. The PCI address is. + "temperature": { + "hwmon-path-abs": "/sys/devices/pci0000:00/0000:00:18.3/hwmon", + "input-filename": "temp1_input", + "format": "\uf2ca {temperatureC} C", + "critical-threshold": 90, + "interval": 5, + "tooltip-format": "CPU package: {temperatureC} C" + }, + + "cpu": { + "format": "\udb80\udf5b {usage}%", + "interval": 5 + }, + + "memory": { + "format": "\udb83\udee0 {percentage}%", + "interval": 5, + "tooltip-format": "{used:0.1f}G of {total:0.1f}G" + }, + + // Backed by tuned-ppd, which owns net.hadess.PowerProfiles on this system. + "power-profiles-daemon": { + "format": "{icon}", + "format-icons": { + "default": "\uf0e7", + "performance": "\uf135", + "balanced": "\uf24e", + "power-saver": "\uf06c" + }, + "tooltip-format": "power profile: {profile}", + "tooltip": true + }, "battery": { "states": { "warning": 30, "critical": 15 }, "format": "{icon} {capacity}%", - "format-charging": " {capacity}%", - "format-plugged": " {capacity}%", - "format-icons": ["", "", "", "", ""] + "format-charging": "\uf1e6 {capacity}%", + "format-plugged": "\uf1e6 {capacity}%", + "format-icons": [ + "\udb80\udc8e", + "\udb80\udc7a", + "\udb80\udc7b", + "\udb80\udc7c", + "\udb80\udc7d", + "\udb80\udc7e", + "\udb80\udc7f", + "\udb80\udc80", + "\udb80\udc81", + "\udb80\udc82", + "\udb80\udc79" + ], + "tooltip-format": "{timeTo}\n{power:0.1f}W", + "on-click": "cosmic-settings power" }, "tray": { "icon-size": 18, "spacing": 8 } diff --git a/config/waybar/config.jsonc.in b/config/waybar/config.jsonc.in new file mode 100644 index 0000000..3a25c25 --- /dev/null +++ b/config/waybar/config.jsonc.in @@ -0,0 +1,269 @@ +// Waybar configuration for a HyprCosmic session. +// +// Waybar's shipped default (/etc/xdg/waybar/config.jsonc) is built entirely +// around sway/* modules and shows nothing at all under cosmic-comp, so this +// exists rather than a handful of overrides. +// +// Module choices are constrained by what cosmic-comp actually advertises on the +// Wayland registry, which was checked against a live session: +// +// ext/workspaces ext_workspace_manager_v1 -- advertised by stock cosmic-comp. +// wlr/taskbar zwlr_foreign_toplevel_management_v1 -- NOT in stock +// cosmic-comp; provided by the HyprCosmic fork's Patch A. +// Under a stock compositor this module stays empty and the +// rest of the bar still works. +// tray Needs a StatusNotifierWatcher. cosmic-panel normally +// supplies one via cosmic-applet-status-area, and the +// hyprcosmic profile disables the panel, so waybar hosts the +// watcher itself here. +// +// ICONS. Every glyph below is a \uXXXX escape, never a literal character, and +// this file is generated from config.jsonc.in for exactly that reason: +// +// - These are Private Use Area codepoints. They survive being copied, but +// anything that re-types rather than copies them silently flattens them to +// spaces or drops them next to a neighbour, and the damage is invisible in +// a diff and indistinguishable from a font problem at runtime. +// - An escape is plain ASCII, so it cannot be damaged that way, and it names +// the intended glyph instead of leaving a box you have to identify. +// +// Every codepoint was checked against the installed JetBrainsMono Nerd Font +// with `fc-list :charset=` before use. That check is worth doing rather +// than copying HyDE's values: HyDE's pulseaudio module uses U+FA80 for the +// muted state, an old Material Design Icons codepoint that Nerd Fonts v3 +// moved. It is absent from the installed font and would render as tofu, so +// U+F075F is used instead. +// +// NOT INCLUDED, deliberately: +// +// backlight The only device is acpi_video0, a firmware stub reporting +// brightness 49 of max 49 -- permanently 100% -- whose sysfs +// node is not writable by this user, with brightnessctl absent. +// The module would render a readout that never changes and a +// scroll action that never works: visible, plausible, and dead. +// That is the same failure class as the `pavucontrol` click this +// file used to carry, and it is not worth re-introducing for a +// percentage that is always the same number. +{ + "layer": "top", + "position": "top", + "height": 34, + "spacing": 4, + + "modules-left": ["ext/workspaces", "wlr/taskbar"], + "modules-center": ["mpris", "clock", "privacy"], + "modules-right": [ + "idle_inhibitor", + "custom/swaync", + "bluetooth", + "pulseaudio", + "network", + "temperature", + "cpu", + "memory", + "power-profiles-daemon", + "battery", + "tray" + ], + + "ext/workspaces": { + "format": "{name}", + "on-click": "activate" + }, + + "wlr/taskbar": { + "format": "{icon}", + "icon-size": 18, + "tooltip-format": "{title}", + "on-click": "activate", + "on-click-middle": "close" + }, + + // Renders nothing at all when no player is running, which is what we want + // from a centre module: it takes no space until there is something to say. + // playerctl backs every action here and is installed. + "mpris": { + "format": "{player_icon} {dynamic}", + "format-paused": "{status_icon} {dynamic}", + "dynamic-order": ["title", "artist"], + "dynamic-separator": " ", + "max-length": 40, + "interval": 1, + "player-icons": { "default": "@@NOTE@@" }, + "status-icons": { + "playing": "@@PLAY@@", + "paused": "@@PAUSE@@", + "stopped": "@@STOP@@" + }, + "on-click": "playerctl play-pause", + "on-click-middle": "playerctl previous", + "on-click-right": "playerctl next", + "on-scroll-up": "playerctl position 5+", + "on-scroll-down": "playerctl position 5-", + "tooltip-format": "{title}\nby {artist}\n{position} / {length}\n\nplayer: {player}" + }, + + "clock": { + "format": "{:%a %d %b %H:%M}", + "tooltip-format": "{calendar}" + }, + + // Only appears while something is actually capturing. Nothing is ignored: + // an exception list is how a privacy indicator stops being one. + "privacy": { + "icon-spacing": 6, + "transition-duration": 200, + "modules": [ + { "type": "screenshare", "tooltip": true }, + { "type": "audio-in", "tooltip": true } + ] + }, + + "idle_inhibitor": { + "format": "{icon}", + "format-icons": { + "activated": "@@CAFFEINE_ON@@", + "deactivated": "@@CAFFEINE_OFF@@" + }, + "tooltip-format-activated": "Caffeine on -- idling and blanking suppressed", + "tooltip-format-deactivated": "Caffeine off -- normal power settings apply" + }, + + // swaync is this session's notification daemon: it owns + // org.freedesktop.Notifications and is D-Bus activated through + // swaync.service, so it needs no autostart entry and this module starts it + // on first contact if nothing else has. + // + // It is here because the hyprcosmic profile disables cosmic-panel and + // COSMIC's notification applet lives in that panel. Without this there is + // no way to reach notification history at all. + "custom/swaync": { + "format": "{icon}", + "format-icons": { + "none": "@@BELL@@", + "notification": "@@BELL_DOT@@", + "dnd-none": "@@BELL_OFF@@", + "dnd-notification": "@@BELL_OFF@@", + "inhibited-none": "@@BELL@@", + "inhibited-notification": "@@BELL_DOT@@", + "dnd-inhibited-none": "@@BELL_OFF@@", + "dnd-inhibited-notification": "@@BELL_OFF@@" + }, + "return-type": "json", + "exec": "swaync-client -swb", + "on-click": "swaync-client -t -sw", + "on-click-right": "swaync-client -d -sw", + "tooltip": true, + "escape": true + }, + + // bluez is running with an unblocked hci0. The click target is COSMIC's own + // settings page rather than blueman or overskride, neither of which is + // installed here. + "bluetooth": { + "format": "@@BT_ON@@", + "format-disabled": "@@BT_DISABLED@@", + "format-off": "@@BT_OFF@@", + "format-connected": "@@BT_ON@@ {num_connections}", + "tooltip-format": "{controller_alias}\n{num_connections} connected", + "tooltip-format-connected": "{controller_alias}\n\n{device_enumerate}", + "tooltip-format-enumerate-connected": "{device_alias}", + "on-click": "cosmic-settings bluetooth" + }, + + // The click used to be `pavucontrol`, which is not installed here: a button + // that looked live and did nothing. cosmic-settings ships with the desktop, + // so it cannot go missing the same way. Scroll and middle-click need no + // helper -- waybar changes volume itself and wpctl comes with pipewire. + "pulseaudio": { + "format": "{icon} {volume}%", + "format-muted": "@@VOL_MUTE@@", + "format-bluetooth": "{icon} {volume}%", + "format-bluetooth-muted": "@@VOL_MUTE@@", + "format-icons": { + "headphone": "@@VOL_HEADPHONE@@", + "headset": "@@VOL_HEADSET@@", + "hands-free": "@@VOL_HANDSFREE@@", + "phone": "@@VOL_PHONE@@", + "car": "@@VOL_CAR@@", + "default": ["@@VOL_LO@@", "@@VOL_MID@@", "@@VOL_HI@@"] + }, + "scroll-step": 5, + "tooltip-format": "{desc}\n{icon} {volume}%", + "on-click": "cosmic-settings sound", + "on-click-middle": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle" + }, + + "network": { + "format-wifi": "@@WIFI@@ {signalStrength}%", + "format-ethernet": "@@ETH@@ wired", + "format-linked": "@@ETH@@ {ifname}", + "format-disconnected": "@@NET_OFF@@ offline", + "tooltip-format": "{ifname}: {ipaddr}", + "tooltip-format-wifi": "{essid} ({signalStrength}%)\n{ifname}: {ipaddr}", + "on-click": "cosmic-settings network" + }, + + // Tctl from k10temp, not thermal_zone0. The two read within a degree of + // each other here, but the hwmon path names the CPU sensor explicitly. + // + // It is given as the PCI device's hwmon directory rather than + // /sys/class/hwmon/hwmon6 because hwmonN numbering is assigned in probe + // order and is not stable across boots. The PCI address is. + "temperature": { + "hwmon-path-abs": "/sys/devices/pci0000:00/0000:00:18.3/hwmon", + "input-filename": "temp1_input", + "format": "@@TEMP@@ {temperatureC} C", + "critical-threshold": 90, + "interval": 5, + "tooltip-format": "CPU package: {temperatureC} C" + }, + + "cpu": { + "format": "@@CPU@@ {usage}%", + "interval": 5 + }, + + "memory": { + "format": "@@MEM@@ {percentage}%", + "interval": 5, + "tooltip-format": "{used:0.1f}G of {total:0.1f}G" + }, + + // Backed by tuned-ppd, which owns net.hadess.PowerProfiles on this system. + "power-profiles-daemon": { + "format": "{icon}", + "format-icons": { + "default": "@@PROFILE@@", + "performance": "@@PERF@@", + "balanced": "@@BALANCED@@", + "power-saver": "@@SAVER@@" + }, + "tooltip-format": "power profile: {profile}", + "tooltip": true + }, + + "battery": { + "states": { "warning": 30, "critical": 15 }, + "format": "{icon} {capacity}%", + "format-charging": "@@PLUG@@ {capacity}%", + "format-plugged": "@@PLUG@@ {capacity}%", + "format-icons": [ + "@@BAT_00@@", + "@@BAT_10@@", + "@@BAT_20@@", + "@@BAT_30@@", + "@@BAT_40@@", + "@@BAT_50@@", + "@@BAT_60@@", + "@@BAT_70@@", + "@@BAT_80@@", + "@@BAT_90@@", + "@@BAT_100@@" + ], + "tooltip-format": "{timeTo}\n{power:0.1f}W", + "on-click": "cosmic-settings power" + }, + + "tray": { "icon-size": 18, "spacing": 8 } +} diff --git a/config/waybar/generate-config.py b/config/waybar/generate-config.py new file mode 100755 index 0000000..b75e0a1 --- /dev/null +++ b/config/waybar/generate-config.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Generate waybar config.jsonc with \\uXXXX escapes from verified codepoints. + +Every codepoint here was confirmed present in JetBrainsMono Nerd Font with +`fc-list :charset=`. The template is pure ASCII and uses @TOKEN@ +placeholders so that no PUA character is ever typed by hand. +""" +import json +import re +import sys + +# name -> codepoint. Verified present in the installed Nerd Font. +ICONS = { + "VOL_LO": 0xF026, + "VOL_MID": 0xF027, + "VOL_HI": 0xF028, + "VOL_HEADPHONE": 0xF025, + "VOL_HEADSET": 0xF025, + "VOL_HANDSFREE": 0xF025, + "VOL_PHONE": 0xF095, + "VOL_CAR": 0xF1B9, + "VOL_MUTE": 0xF075F, + "BT_ON": 0xF293, + "BT_DISABLED": 0xF294, + "BT_OFF": 0xF00B2, + "CPU": 0xF035B, + "MEM": 0xF0EE0, + "TEMP": 0xF2CA, + "WIFI": 0xF1EB, + "ETH": 0xF0200, + "NET_OFF": 0xF092F, + "PLUG": 0xF1E6, + "CAFFEINE_ON": 0xF0176, + "CAFFEINE_OFF": 0xF06CA, + "PLAY": 0xF04B, + "PAUSE": 0xF04C, + "STOP": 0xF04D, + "NOTE": 0xF001, + "BELL": 0xF0F3, + "BELL_DOT": 0xF009A, + "BELL_OFF": 0xF1F6, + "PERF": 0xF135, + "SAVER": 0xF06C, + "BALANCED": 0xF24E, + "PROFILE": 0xF0E7, + "BAT_00": 0xF008E, + "BAT_10": 0xF007A, + "BAT_20": 0xF007B, + "BAT_30": 0xF007C, + "BAT_40": 0xF007D, + "BAT_50": 0xF007E, + "BAT_60": 0xF007F, + "BAT_70": 0xF0080, + "BAT_80": 0xF0081, + "BAT_90": 0xF0082, + "BAT_100": 0xF0079, +} + + +def escape(cp: int) -> str: + """JSON escape for one codepoint, surrogate pair when above the BMP.""" + return json.dumps(chr(cp), ensure_ascii=True)[1:-1] + + +def main() -> int: + template_path, out_path = sys.argv[1], sys.argv[2] + text = open(template_path, encoding="ascii").read() + + unknown = {t for t in re.findall(r"@@([A-Z0-9_]+)@@", text) if t not in ICONS} + if unknown: + print("unknown tokens:", sorted(unknown), file=sys.stderr) + return 1 + + used = set() + + def sub(m): + used.add(m.group(1)) + return escape(ICONS[m.group(1)]) + + out = re.sub(r"@@([A-Z0-9_]+)@@", sub, text) + + if not out.isascii(): + print("generated file is not pure ASCII", file=sys.stderr) + return 1 + + open(out_path, "w", encoding="ascii").write(out) + + unused = sorted(set(ICONS) - used) + print(f"wrote {out_path}: {len(out.splitlines())} lines, {len(used)} icons used") + if unused: + print("unused icon names:", unused) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/config/waybar/rules.css b/config/waybar/rules.css index cf6a8bb..d2f039c 100644 --- a/config/waybar/rules.css +++ b/config/waybar/rules.css @@ -3,10 +3,28 @@ * No colour literals: every colour here is a name defined by palette.css and * possibly re-pointed by bridge-hyde.css. That separation is what lets a theme * change the palette without touching a single rule. + * + * The look is HyDE's: a strip carrying rounded pills rather than a flat row of + * text. HyDE gets its pills to float by making the bar itself transparent and + * letting the compositor blur the desktop behind them; cosmic-comp has no + * rule-driven blur, so the bar keeps a background here and the pills lift off + * it instead. See bridge-hyde.css for the two colours that implements. */ * { - font-family: "FontAwesome 6 Free", "Noto Sans", sans-serif; + /* JetBrainsMono Nerd Font first, and it has to be first. + * + * This previously named "FontAwesome 6 Free", which is not even the + * installed family's real name ("Font Awesome 6 Free") -- fontconfig's + * fuzzy matching resolved it anyway, to the Regular face, which carries + * only a small subset of the icon set. Every glyph outside that subset + * then came from whatever fallback fontconfig picked per character, so the + * bar's icons were only accidentally consistent. + * + * The Nerd Font carries the whole icon set and the text, so naming it + * first makes one font answer for both. Every codepoint in config.jsonc + * was checked against this family specifically. */ + font-family: "JetBrainsMono Nerd Font", "Noto Sans", sans-serif; font-size: 13px; border: none; border-radius: 0; @@ -18,15 +36,22 @@ window#waybar { color: @bar-fg; } +/* Workspaces: pills, with the active one filled rather than underlined. */ +#workspaces { + margin: 0 4px; +} + #workspaces button { - padding: 0 8px; + padding: 0 10px; + margin: 4px 2px; + border-radius: 10px; color: @muted; background: transparent; } #workspaces button.active { - color: @bar-fg; - box-shadow: inset 0 -2px @accent; + color: @wb-act-fg; + background: @accent; } #workspaces button:hover { @@ -36,28 +61,86 @@ window#waybar { #taskbar button { padding: 0 6px; + margin: 4px 1px; + border-radius: 10px; background: transparent; } #taskbar button.active { - box-shadow: inset 0 -2px @accent; + background: @module-bg; } +#taskbar button:hover { + background: @wb-hvr-bg; +} + +/* Every status module gets the same pill. Listed one per line because waybar + * takes the CSS id from the module name with `/` turned into `-`, so these are + * not guessable from the config and are worth being able to read down. */ +#mpris, #clock, +#privacy, +#idle_inhibitor, +#custom-swaync, +#bluetooth, #pulseaudio, #network, +#temperature, #cpu, #memory, +#power-profiles-daemon, #battery, #tray { padding: 0 10px; + margin: 4px 2px; + border-radius: 10px; + background: @module-bg; } #clock { font-weight: bold; } +/* mpris renders nothing when no player is running. Without this it would still + * draw an empty pill, so the padding goes away with the content. */ +#mpris { + padding: 0; + background: transparent; +} + +#mpris.playing, +#mpris.paused { + padding: 0 10px; + background: @module-bg; +} + +/* Same reasoning: privacy is only present while something is capturing. */ +#privacy { + padding: 0; + background: transparent; +} + +#privacy-item { + padding: 0 8px; + margin: 4px 2px; + border-radius: 10px; + background: @module-bg; + color: @warning; +} + +/* States. These recolour the text inside the pill rather than the pill, so a + * warning cannot make a module unreadable against its own background. */ #battery.warning { color: @warning; } #battery.critical { color: @critical; } +#temperature.critical { color: @critical; } #network.disconnected { color: @critical; } #pulseaudio.muted { color: @muted; } +#idle_inhibitor.activated { color: @accent; } +#custom-swaync.notification { color: @accent; } +#custom-swaync.dnd-none, +#custom-swaync.dnd-notification { color: @muted; } + +/* No `#tray:empty` rule to hide the pill when nothing is in the tray: GTK's + * CSS has no :empty pseudo-class and rejects the whole stylesheet for it, + * which is fatal rather than cosmetic. waybar exits and the bar never appears. + */ diff --git a/tools/install-assets.sh b/tools/install-assets.sh index f7993be..405326a 100755 --- a/tools/install-assets.sh +++ b/tools/install-assets.sh @@ -83,11 +83,20 @@ SESSION=( "cosmic-session/data/hyprcosmic.desktop:share/wayland-sessions/hyprcosmic.desktop:644" ) +# Files under config/ that produce a SHARED file rather than being one. They are +# inputs to a generator and have no place on the system: installing the template +# would put a file full of @@TOKEN@@ placeholders next to the real config, and +# whichever one a future reader opened first would be a coin toss. +SOURCES=( + "config/waybar/config.jsonc.in" # -> config/waybar/config.jsonc + "config/waybar/generate-config.py" # the generator, and the icon table +) + # Files under config/ that are deliberately NOT installed here, each with the # thing that does install it. This list is not decoration: the audit below # refuses to run unless every file under config/ appears in exactly one of the -# two lists, so adding a file forces a decision about where it belongs instead -# of letting it be quietly left out of both. +# three lists, so adding a file forces a decision about where it belongs instead +# of letting it be quietly left out of all of them. PER_USER=( "config/autostart" # ~/.config/hyprcosmic/autostart, by hand "config/cosmic.conf" # ~/.config/hyprcosmic/cosmic.conf, by hand @@ -96,7 +105,7 @@ PER_USER=( ) audit_config_tree() { - local f rel known=" ${PER_USER[*]} " unclassified=() + local f rel known=" ${PER_USER[*]} ${SOURCES[*]} " unclassified=() # Built with a loop, not `${SHARED[*]%%:*}`: that form strips the suffix # from the first element only and silently keeps the rest whole. for f in "${SHARED[@]}"; do known+="${f%%:*} "; done