Files
hyprcosmic/config/waybar/config.jsonc
T
gitops fc8c2a727d waybar: six more modules, and a keyboard reference the session had none of
The bar showed which windows existed but not which one had the keyboard,
and nothing at all about the GPU, the fans, the disk, or a unit that had
failed at boot. Six modules, two of which needed a script because waybar
has no module that fits.

hyprland/window, and no compositor change

Reading the active window needs `j/activewindow` on startup and
`activewindow>>` on .socket2.sock, and hypr_ipc already serves both. A
retitle of the focused window propagates too, which is the case that
usually needs a separate `windowtitle` event: this fork diffs a snapshot
on a 150 ms tick rather than emitting from call sites, so src/hypr_ipc/
sync.rs sees a (class, title) change and cannot miss it. There is already
a test for exactly that -- title_change_alone_emits_activewindow.

The rewrite rules trim the app suffix from titles written for a title bar
rather than a 60-column slot. All five now use the same [-<em dash>]
class. Vivaldi had a bare hyphen because it was written from the live
title -- j/clients reports "Inbox - ... - Gmail - Vivaldi" -- and the
others got the class in a later pass, which left the one rule that would
break if Vivaldi ever switched separators. COSMIC's own applications use
an em dash and not a hyphen, so a rule written with a hyphen matches
nothing there.

custom/fan, because neither of the obvious options can name this sensor

waybar has no fan module. Its temperature module cannot be borrowed: it
divides by 1000 to turn millidegrees into degrees, which renders 2700 rpm
as 2 C. Nor can hwmon-path-abs point at the fans. That option names a
parent directory and takes the one hwmonN inside it, which works for
k10temp and amdgpu because each has exactly one, but asus-nb-wmi has two:

    hwmon9   name=asus                  fan1_input, fan2_input
    hwmon10  name=asus_custom_fan_curve  the curve's set points

Which of the two waybar picked would be down to readdir order. So the
script resolves by content instead -- any hwmon with a fan*_input -- which
also makes it work on hardware that is not this laptop. It prints nothing
where there is no readable fan, and waybar draws an empty custom module as
nothing, so a VM loses the item rather than showing a dead 0 rpm.

hyprcosmic-keybinds, and why it does not read cosmic.conf

The obvious implementation reads cosmic.conf and would be wrong. This
machine's cosmic.conf declares six bindings. The session answers to 122.
The other 116 are COSMIC's defaults, which the fork does not restate
because it has no reason to -- Super+Q closes a window whether or not
anybody wrote it down. A reference showing six entries would not look
broken, it would look complete, while missing every window, workspace and
media key on the machine.

So it reads the three RON files the compositor reads: Shortcuts/v1/
defaults for the built-ins, the user's Shortcuts/v1/custom for what
cosmic-conf projected out of cosmic.conf, and Shortcuts/v1/system_actions
to turn System(Screenshot) into cosmic-screenshot. Without the third, a
third of the list would name internal actions rather than the programs
they launch, which is the half of the question that was being asked.

Parsing RON with sed deserves a defence. The principled home for this is a
`cosmic-conf binds` subcommand, which already has the schema and the bind
grammar -- and is a compiled change and a full package build. The files
are one binding per line and nothing downstream consumes this, so the cost
of being wrong is a mangled row in a help window, not a broken keybinding.
The script refuses to show a list it could not parse rather than showing a
short one.

The binding for it was chosen with the script: Super+K and Super+I are
both focus actions in COSMIC's defaults, and Super+Shift+/ is free.

install-assets: prune dot-directories from the audit

audit_config_tree refuses to run unless every file under config/ is
classified, and it started failing on six gitignored .omc/ state files
that tooling had dropped into config/waybar/. Failing on those trains you
to ignore the one message that catches a genuinely unclassified asset.
Dot files are still walked; only directories are pruned.

Verified before committing

The generated config parses as JSON with all 23 modules defined and none
defined but unplaced. The three stylesheets concatenate and parse clean
through GTK's own CssProvider, which is the parser waybar uses, and every
@colour they name is defined. Both scripts pass a syntax check, and
hyprcosmic-keybinds was run with rofi stubbed -- nothing appeared on the
desktop -- producing 118 rows, which is 116 defaults plus 6 ours less the
4 that override. System() actions resolved to real commands, chords sorted
Super first, and the modifier-only Super binding survived, which a parser
requiring a key field would have dropped.

Every sensor was read before being wired to a module rather than after:
amdgpu edge 55 C, cpu_fan 2700 and gpu_fan 2500 rpm, zero failed units in
both scopes, / at 3 percent.

Not verified: how any of it looks. Rendering needs waybar restarted
against a session that is not at the lock screen.
2026-08-11 07:46:29 +07:00

428 lines
18 KiB
JSON

// 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": ["custom/keybinds", "ext/workspaces", "wlr/taskbar", "hyprland/window"],
"modules-center": ["mpris", "clock", "privacy"],
"modules-right": [
"systemd-failed-units",
"idle_inhibitor",
"custom/swaync",
"bluetooth",
"pulseaudio",
"network",
"disk",
"temperature",
"temperature#gpu",
"custom/fan",
"cpu",
"memory",
"power-profiles-daemon",
"battery",
"tray",
"custom/power"
],
// Opens the keyboard reference. First on the bar because it is the thing
// you look for when you do not yet know where anything is.
//
// No "exec": this is a button, not a readout. A custom module with only a
// format and an on-click runs nothing on an interval, so it costs a static
// glyph and nothing else.
//
// The same script is on Super + Shift + / -- the binding is named in the
// tooltip because a help button you have to reach for with the mouse has
// not finished helping.
"custom/keybinds": {
"format": "\udb80\udf0c",
"tooltip": true,
"tooltip-format": "keyboard shortcuts\nSuper + Shift + /",
"on-click": "hyprcosmic-keybinds"
},
"ext/workspaces": {
"format": "{name}",
"on-click": "activate"
},
"wlr/taskbar": {
"format": "{icon}",
"icon-size": 18,
"tooltip-format": "{title}",
"on-click": "activate",
"on-click-middle": "close"
},
// The taskbar above shows which windows exist; this shows which one has the
// keyboard. Nothing else on the bar answered that.
//
// This is the one module here that talks to the fork's Hyprland IPC rather
// than to a Wayland protocol, and it needs no compositor change: waybar
// reads `j/activewindow` on startup and then follows `activewindow>>` on
// .socket2.sock, both of which hypr_ipc already serves. A retitle of the
// focused window propagates too -- src/hypr_ipc/sync.rs snapshots the
// focused window as (class, title) and emits on any difference, which is
// why upstream's separate `windowtitle` event is not needed here. Upstream
// Hyprland emits from call sites and so must announce that case
// separately; this fork diffs state on a 150 ms tick and cannot miss it.
//
// `separate-outputs` is left off deliberately: there is one bar on one
// output, and with it on, the module reports that monitor's active window
// rather than the focused one, which on a single head is the same answer by
// a longer route.
"hyprland/window": {
"format": "\uf2d0 {title}",
"max-length": 60,
"tooltip": true,
"tooltip-format": "{title}\n\nclass: {class}",
// Titles are written for a window's own title bar, not for a 60-column
// slot on a bar, so the worst offenders are trimmed to the part that
// identifies the window. ECMAScript regex; the value is a replacement
// string where $1 is the first capture.
//
// The separator is an em dash, and it is written as an escape
// for the same reason every glyph in this file is: the template is read
// as ASCII and the generator refuses to emit anything that is not, so a
// literal em dash pasted here fails the build rather than reaching the
// bar. COSMIC's applications really do use an em dash and not a hyphen
// -- `j/clients` reports "dingo@fedora:~ <em dash> COSMIC Terminal" -- so
// a rule written with a hyphen matches nothing.
//
// There is deliberately no catch-all for the empty title. waybar's
// documentation does not say whether a rule is applied on a full match
// or a search, so an empty pattern is either "matches nothing but the
// empty title" or "matches every title", and the difference is the
// whole bar. The no-window case is handled in rules.css instead, the
// same way #mpris already handles having nothing to say.
"rewrite": {
"(.*) [-\u2014] Vivaldi": "$1",
"(.*) [-\u2014] COSMIC Terminal": "$1",
"(.*) [-\u2014] COSMIC Text Editor": "$1",
"(.*) [-\u2014] COSMIC Files": "$1",
"(.*) [-\u2014] Mozilla Firefox": "$1"
}
},
// 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": {
"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 }
]
},
// First on the right, and silent unless something is wrong. A unit that
// failed at boot otherwise stays failed until the day you happen to run
// `systemctl --failed` -- there is no other surface for it in a COSMIC
// session, which has no equivalent of GNOME's abrt notification.
//
// Both scopes are watched. The user scope is where a session's own units
// fail, which is the half that matters here and the half a system-only
// check misses.
"systemd-failed-units": {
"format": "\udb80\udc26 {nr_failed}",
"format-ok": "",
"system": true,
"user": true,
"hide-on-ok": true,
"on-click": "cosmic-term -e sh -c 'systemctl --failed; systemctl --user --failed; exec $SHELL'"
},
"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": "\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": "\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"
},
// Root only. /home is on the same filesystem here, and a second entry for
// it would be the same number twice.
//
// No warning threshold is set: waybar's disk module has no `states`, so the
// colour comes from rules.css, which cannot see the percentage. The number
// is the warning.
"disk": {
"path": "/",
"format": "\udb80\udeca {percentage_used}%",
"interval": 60,
"tooltip-format": "{used} used of {total} on {path}, {free} free"
},
// 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"
},
// A second instance of the same module. `temperature#gpu` is waybar's alias
// syntax: everything before the # selects the module, everything after is
// just a name, and it becomes #temperature.gpu in CSS.
//
// The dGPU's own sensor, not the CPU's. On this machine the two diverge by
// 30 degrees under load, so one reading cannot stand for both.
//
// temp1_label reads "edge" -- the die edge, which is the sensor amdgpu
// exposes on this card. There is no junction or memory sensor here to
// prefer over it.
//
// hwmon-path-abs is safe for this one: the PCI device has exactly one
// hwmonN inside it, unlike the asus platform device that custom/fan below
// has to work around.
"temperature#gpu": {
"hwmon-path-abs": "/sys/devices/pci0000:00/0000:00:08.1/0000:05:00.0/hwmon",
"input-filename": "temp1_input",
"format": "\udb82\udcae {temperatureC} C",
"critical-threshold": 95,
"interval": 5,
"tooltip-format": "GPU edge: {temperatureC} C"
},
// Speed of the fastest fan, with every fan in the tooltip. See the script
// for why this is not a temperature module with a different divisor and not
// an hwmon-path-abs: both were tried and neither can name this sensor.
//
// Emits nothing on a machine with no readable fan, and waybar draws an
// empty custom module as nothing, so this costs no space on hardware that
// has none.
"custom/fan": {
"exec": "hyprcosmic-fan",
"return-type": "json",
"format": "\udb80\ude10 {} rpm",
"interval": 5
},
"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": "\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 },
// Last on the bar, because it is the one button here that can end the
// session or the uptime and the far corner is the hardest place to hit by
// accident.
//
// It is here for the same reason "custom/swaync" is: the hyprcosmic profile
// disables cosmic-panel, and COSMIC's power applet lives in that panel.
// Without this there is no logout, reboot or shutdown anywhere in the
// session -- the only way out was `systemctl reboot` from a terminal.
//
// The click runs the same script as the $mainMod SHIFT E binding rather
// than calling systemctl here, so the confirmation step cannot be bypassed
// by using the mouse.
"custom/power": {
"format": "\uf011",
"tooltip": true,
"tooltip-format": "Lock, suspend, log out, reboot or shut down",
"on-click": "hyprcosmic-powermenu"
}
}