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.
This commit is contained in:
2026-08-10 12:58:48 +07:00
parent bae7c5b0ff
commit b89e4be10a
6 changed files with 686 additions and 23 deletions
+10
View File
@@ -24,3 +24,13 @@
* or once blur lands. * or once blur lands.
*/ */
@define-color bar-bg alpha(@main-bg, 0.85); @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);
+211 -15
View File
@@ -16,15 +16,55 @@
// supplies one via cosmic-applet-status-area, and the // supplies one via cosmic-applet-status-area, and the
// hyprcosmic profile disables the panel, so waybar hosts the // hyprcosmic profile disables the panel, so waybar hosts the
// watcher itself here. // 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=<cp>` 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", "layer": "top",
"position": "top", "position": "top",
"height": 34, "height": 34,
"spacing": 6, "spacing": 4,
"modules-left": ["ext/workspaces", "wlr/taskbar"], "modules-left": ["ext/workspaces", "wlr/taskbar"],
"modules-center": ["clock"], "modules-center": ["mpris", "clock", "privacy"],
"modules-right": ["pulseaudio", "network", "cpu", "memory", "battery", "tray"], "modules-right": [
"idle_inhibitor",
"custom/swaync",
"bluetooth",
"pulseaudio",
"network",
"temperature",
"cpu",
"memory",
"power-profiles-daemon",
"battery",
"tray"
],
"ext/workspaces": { "ext/workspaces": {
"format": "{name}", "format": "{name}",
@@ -39,34 +79,190 @@
"on-click-middle": "close" "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} <i>{dynamic}</i>",
"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": { "clock": {
"format": "{:%a %d %b %H:%M}", "format": "{:%a %d %b %H:%M}",
"tooltip-format": "<tt><small>{calendar}</small></tt>" "tooltip-format": "<tt><small>{calendar}</small></tt>"
}, },
// 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": { "pulseaudio": {
"format": "{icon} {volume}%", "format": "{icon} {volume}%",
"format-muted": "muted", "format-muted": "\udb81\udf5f",
"format-icons": { "default": ["", "", ""] }, "format-bluetooth": "{icon} {volume}%",
"on-click": "pavucontrol" "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": { "network": {
"format-wifi": " {signalStrength}%", "format-wifi": "\uf1eb {signalStrength}%",
"format-ethernet": " wired", "format-ethernet": "\udb80\ude00 wired",
"format-disconnected": " offline", "format-linked": "\udb80\ude00 {ifname}",
"tooltip-format": "{ifname}: {ipaddr}" "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 }, // Tctl from k10temp, not thermal_zone0. The two read within a degree of
"memory": { "format": " {percentage}%", "interval": 5 }, // 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": { "battery": {
"states": { "warning": 30, "critical": 15 }, "states": { "warning": 30, "critical": 15 },
"format": "{icon} {capacity}%", "format": "{icon} {capacity}%",
"format-charging": " {capacity}%", "format-charging": "\uf1e6 {capacity}%",
"format-plugged": " {capacity}%", "format-plugged": "\uf1e6 {capacity}%",
"format-icons": ["", "", "", "", ""] "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 } "tray": { "icon-size": 18, "spacing": 8 }
+269
View File
@@ -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=<cp>` 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} <i>{dynamic}</i>",
"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": "<tt><small>{calendar}</small></tt>"
},
// 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 }
}
+96
View File
@@ -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=<cp>`. 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())
+88 -5
View File
@@ -3,10 +3,28 @@
* No colour literals: every colour here is a name defined by palette.css and * 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 * possibly re-pointed by bridge-hyde.css. That separation is what lets a theme
* change the palette without touching a single rule. * 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; font-size: 13px;
border: none; border: none;
border-radius: 0; border-radius: 0;
@@ -18,15 +36,22 @@ window#waybar {
color: @bar-fg; color: @bar-fg;
} }
/* Workspaces: pills, with the active one filled rather than underlined. */
#workspaces {
margin: 0 4px;
}
#workspaces button { #workspaces button {
padding: 0 8px; padding: 0 10px;
margin: 4px 2px;
border-radius: 10px;
color: @muted; color: @muted;
background: transparent; background: transparent;
} }
#workspaces button.active { #workspaces button.active {
color: @bar-fg; color: @wb-act-fg;
box-shadow: inset 0 -2px @accent; background: @accent;
} }
#workspaces button:hover { #workspaces button:hover {
@@ -36,28 +61,86 @@ window#waybar {
#taskbar button { #taskbar button {
padding: 0 6px; padding: 0 6px;
margin: 4px 1px;
border-radius: 10px;
background: transparent; background: transparent;
} }
#taskbar button.active { #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, #clock,
#privacy,
#idle_inhibitor,
#custom-swaync,
#bluetooth,
#pulseaudio, #pulseaudio,
#network, #network,
#temperature,
#cpu, #cpu,
#memory, #memory,
#power-profiles-daemon,
#battery, #battery,
#tray { #tray {
padding: 0 10px; padding: 0 10px;
margin: 4px 2px;
border-radius: 10px;
background: @module-bg;
} }
#clock { #clock {
font-weight: bold; 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.warning { color: @warning; }
#battery.critical { color: @critical; } #battery.critical { color: @critical; }
#temperature.critical { color: @critical; }
#network.disconnected { color: @critical; } #network.disconnected { color: @critical; }
#pulseaudio.muted { color: @muted; } #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.
*/
+12 -3
View File
@@ -83,11 +83,20 @@ SESSION=(
"cosmic-session/data/hyprcosmic.desktop:share/wayland-sessions/hyprcosmic.desktop:644" "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 # 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 # 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 # 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 # three lists, so adding a file forces a decision about where it belongs instead
# of letting it be quietly left out of both. # of letting it be quietly left out of all of them.
PER_USER=( PER_USER=(
"config/autostart" # ~/.config/hyprcosmic/autostart, by hand "config/autostart" # ~/.config/hyprcosmic/autostart, by hand
"config/cosmic.conf" # ~/.config/hyprcosmic/cosmic.conf, by hand "config/cosmic.conf" # ~/.config/hyprcosmic/cosmic.conf, by hand
@@ -96,7 +105,7 @@ PER_USER=(
) )
audit_config_tree() { 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 # Built with a loop, not `${SHARED[*]%%:*}`: that form strips the suffix
# from the first element only and silently keeps the rest whole. # from the first element only and silently keeps the rest whole.
for f in "${SHARED[@]}"; do known+="${f%%:*} "; done for f in "${SHARED[@]}"; do known+="${f%%:*} "; done