mirror of
https://github.com/outbackdingo/hyprcosmic.git
synced 2026-08-25 07:10:09 +00:00
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.
This commit is contained in:
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/bin/sh
|
||||
# Report fan speeds for waybar's custom/fan module, as JSON on stdout.
|
||||
#
|
||||
# waybar has no fan module, and its temperature module cannot be borrowed for
|
||||
# this: it divides by 1000 to turn millidegrees into degrees, which would render
|
||||
# 2700 rpm as 2 C.
|
||||
#
|
||||
# Nor can the sensor be named the way "temperature" names its own, with
|
||||
# hwmon-path-abs. That option points at a *parent* directory and takes the one
|
||||
# hwmonN inside it, which works for k10temp and amdgpu because each has exactly
|
||||
# one. The asus platform device has two --
|
||||
#
|
||||
# /sys/devices/platform/asus-nb-wmi/hwmon/hwmon9 name=asus
|
||||
# /sys/devices/platform/asus-nb-wmi/hwmon/hwmon10 name=asus_custom_fan_curve
|
||||
#
|
||||
# -- and only the first has fan*_input; the second holds the curve's set points.
|
||||
# Which of the two waybar picked would be down to readdir order.
|
||||
#
|
||||
# So this resolves by content instead of by path: any hwmon with a fan*_input.
|
||||
# That also makes it portable off this laptop, which a hardcoded asus path would
|
||||
# not be. Fans on a desktop's it87 or a thinkpad's thinkpad_hwmon are found the
|
||||
# same way.
|
||||
#
|
||||
# Prints nothing at all when the machine has no readable fan -- a VM, or a
|
||||
# passively cooled box. waybar renders an empty custom module as nothing, so the
|
||||
# bar loses the item rather than showing a dead zero.
|
||||
|
||||
set -eu
|
||||
|
||||
max=0
|
||||
tooltip=''
|
||||
|
||||
for hwmon in /sys/class/hwmon/hwmon*; do
|
||||
[ -d "$hwmon" ] || continue
|
||||
|
||||
# Not every hwmon has a name, and a chip is worth naming in the tooltip
|
||||
# when it does: "cpu_fan" alone does not say which controller reported it.
|
||||
chip=$(cat "$hwmon/name" 2>/dev/null) || chip=''
|
||||
[ -n "$chip" ] || chip=$(basename "$hwmon")
|
||||
|
||||
for input in "$hwmon"/fan*_input; do
|
||||
# The glob is literal when nothing matches, which is the common case:
|
||||
# most hwmons here are temperature-only.
|
||||
[ -e "$input" ] || continue
|
||||
|
||||
rpm=$(cat "$input" 2>/dev/null) || continue
|
||||
# A fan that is stopped reads 0, and a fan that has been unbound reads
|
||||
# nothing. Neither is an error, but neither belongs in the tooltip.
|
||||
case $rpm in
|
||||
'' | *[!0-9]*) continue ;;
|
||||
esac
|
||||
|
||||
# fanN_label when the driver supplies one -- "cpu_fan", "gpu_fan" --
|
||||
# and fanN otherwise.
|
||||
label=$(cat "${input%_input}_label" 2>/dev/null) || label=''
|
||||
[ -n "$label" ] || label=$(basename "${input%_input}")
|
||||
|
||||
[ "$rpm" -gt "$max" ] && max=$rpm
|
||||
|
||||
line="$chip $label: $rpm rpm"
|
||||
if [ -n "$tooltip" ]; then
|
||||
tooltip="$tooltip\\n$line"
|
||||
else
|
||||
tooltip=$line
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# No fan anywhere. Say nothing rather than reporting a confident 0 rpm.
|
||||
[ -n "$tooltip" ] || exit 0
|
||||
|
||||
# The bar shows the fastest fan, because that is the one you can hear and the
|
||||
# one that says whether the machine is working. The rest are in the tooltip.
|
||||
#
|
||||
# The class drives the colour in rules.css. 4000 rpm is where this laptop's fans
|
||||
# become audible over a quiet room; below 1 rpm every fan is stopped, which is
|
||||
# worth showing differently from a slow one.
|
||||
if [ "$max" -ge 4000 ]; then
|
||||
class=high
|
||||
elif [ "$max" -eq 0 ]; then
|
||||
class=idle
|
||||
else
|
||||
class=normal
|
||||
fi
|
||||
|
||||
printf '{"text":"%s","tooltip":"%s","class":"%s"}\n' "$max" "$tooltip" "$class"
|
||||
Executable
+205
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# Show every keyboard shortcut in the session, and what each one runs.
|
||||
#
|
||||
# WHY THIS EXISTS
|
||||
# ---------------
|
||||
# COSMIC has no keyboard reference. cosmic-settings lists shortcuts in a
|
||||
# scrolling pane behind three clicks, which is where you go to *change* one, not
|
||||
# where you go to remember one mid-task. Hyprland desktops all ship a cheatsheet
|
||||
# on a key and a bar click for exactly that reason, and this is that.
|
||||
#
|
||||
# WHERE THE BINDINGS COME FROM, AND WHY NOT cosmic.conf
|
||||
# -----------------------------------------------------
|
||||
# The obvious implementation reads cosmic.conf, and it would be wrong. This
|
||||
# machine's cosmic.conf declares six bindings. The session answers to 122. The
|
||||
# other 116 are COSMIC's own 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 cheatsheet showing six entries would not look broken; it would look
|
||||
# complete, and it would be missing every window, workspace and media key on the
|
||||
# machine. That is a worse failure than not having the feature.
|
||||
#
|
||||
# So the sources are the three RON files the compositor actually reads:
|
||||
#
|
||||
# /usr/share/cosmic/.../Shortcuts/v1/defaults 116 COSMIC built-ins
|
||||
# ~/.config/cosmic/.../Shortcuts/v1/custom 6 ours, projected from
|
||||
# cosmic.conf by cosmic-conf
|
||||
# /usr/share/cosmic/.../Shortcuts/v1/system_actions
|
||||
# what System(X) actually runs
|
||||
#
|
||||
# custom wins over defaults on a collision, matching the order the compositor
|
||||
# merges them, so a rebound key is listed once with the binding in force.
|
||||
#
|
||||
# system_actions is what makes this answer the question that was asked. A
|
||||
# default reads `System(Screenshot)`, which names an action and not a program;
|
||||
# system_actions maps that to `cosmic-screenshot`. Without it a third of the
|
||||
# list would name internal actions rather than the applications they launch.
|
||||
#
|
||||
# THE PARSER
|
||||
# ----------
|
||||
# These are RON files, and this reads them with a regex, which deserves a
|
||||
# defence. The alternative is a `cosmic-conf binds` subcommand, which would be
|
||||
# the principled home for this -- cosmic-conf already has the schema and the
|
||||
# bind grammar. That is a compiled change and a full package build, and this
|
||||
# feature does not need one: the files are one binding per line, and the shape
|
||||
# has not moved. The cost of being wrong here is a mangled row in a reference
|
||||
# window, not a broken keybinding, because nothing downstream consumes this.
|
||||
#
|
||||
# If the format does move, the failure is visible immediately and loudly: the
|
||||
# count check below refuses to show a list it could not parse.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SYS_DIR=/usr/share/cosmic/com.system76.CosmicSettings.Shortcuts/v1
|
||||
USER_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/cosmic/com.system76.CosmicSettings.Shortcuts/v1"
|
||||
|
||||
command -v rofi >/dev/null 2>&1 || {
|
||||
printf 'hyprcosmic-keybinds: rofi is not installed\n' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Errors go to a rofi dialog as well as stderr: the two ways in are a keybinding
|
||||
# and a bar click, and neither has a terminal to read stderr from. Same reasoning
|
||||
# as hyprcosmic-powermenu.
|
||||
fail() {
|
||||
printf 'hyprcosmic-keybinds: %s\n' "$*" >&2
|
||||
rofi -e "$*" >/dev/null 2>&1 || :
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -r "$SYS_DIR/defaults" ]] ||
|
||||
fail "cannot read $SYS_DIR/defaults, so the built-in shortcuts are unknown"
|
||||
|
||||
# Resolve System(X) to the command it runs, as "X<tab>command" pairs.
|
||||
#
|
||||
# system_actions carries /// doc comments and commented-out entries for actions
|
||||
# that exist in the enum but are not wired up (KeyboardBrightnessUp is one), so
|
||||
# this matches only lines that are a bare name followed by a quoted string.
|
||||
system_actions() {
|
||||
[[ -r "$SYS_DIR/system_actions" ]] || return 0
|
||||
sed -nE 's/^[[:space:]]*([A-Za-z]+):[[:space:]]*"(.*)",?[[:space:]]*$/\1\t\2/p' \
|
||||
"$SYS_DIR/system_actions"
|
||||
}
|
||||
|
||||
# One binding per line, as "modifiers<tab>key<tab>action".
|
||||
#
|
||||
# Two shapes appear. The usual one carries a key:
|
||||
# (modifiers: [Super, Shift], key: "Escape"): System(LogOut),
|
||||
# and a modifier-only binding has none at all:
|
||||
# (modifiers: [Super]): Spawn("rofi -show drun"),
|
||||
# The second is how Super alone opens the launcher, so dropping it would lose
|
||||
# the single most used binding on the machine.
|
||||
bindings() {
|
||||
sed -nE \
|
||||
-e 's/^[[:space:]]*\(modifiers:[[:space:]]*\[([^]]*)\],[[:space:]]*key:[[:space:]]*"([^"]*)"\):[[:space:]]*(.*),[[:space:]]*$/\1\t\2\t\3/p' \
|
||||
-e 's/^[[:space:]]*\(modifiers:[[:space:]]*\[([^]]*)\]\):[[:space:]]*(.*),[[:space:]]*$/\1\t\t\2/p' \
|
||||
"$1"
|
||||
}
|
||||
|
||||
actions_tsv="$(system_actions)"
|
||||
|
||||
# custom is read second so that its rows overwrite defaults in the map below.
|
||||
# Missing is normal, not an error: a session with no custom bindings has no such
|
||||
# file, and every default still applies.
|
||||
raw="$(bindings "$SYS_DIR/defaults")"
|
||||
if [[ -r "$USER_DIR/custom" ]]; then
|
||||
raw+=$'\n'"$(bindings "$USER_DIR/custom")"
|
||||
fi
|
||||
|
||||
[[ -n "${raw//[[:space:]]/}" ]] ||
|
||||
fail "parsed no shortcuts at all from $SYS_DIR/defaults; the file format has changed"
|
||||
|
||||
rendered="$(
|
||||
LC_ALL=C awk -F'\t' -v actions="$actions_tsv" '
|
||||
BEGIN {
|
||||
# System(X) -> command, from system_actions.
|
||||
n = split(actions, lines, "\n")
|
||||
for (i = 1; i <= n; i++) {
|
||||
if (split(lines[i], kv, "\t") == 2) cmd[kv[1]] = kv[2]
|
||||
}
|
||||
}
|
||||
|
||||
# Turn CamelCase into words so an action reads as a description rather than
|
||||
# an identifier: MoveToWorkspace -> "move to workspace". Applied only to the
|
||||
# action name, never to a command, which must stay verbatim to be typed.
|
||||
function words(s, out) {
|
||||
out = s
|
||||
gsub(/([a-z0-9])([A-Z])/, "\\1 \\2", out)
|
||||
return tolower(out)
|
||||
}
|
||||
|
||||
# Super is written first and Shift last, so that chords sort and read the
|
||||
# way they are pressed rather than the way the RON happened to list them.
|
||||
function order(m) {
|
||||
if (m == "Super") return 1
|
||||
if (m == "Ctrl") return 2
|
||||
if (m == "Alt") return 3
|
||||
if (m == "Shift") return 4
|
||||
return 5
|
||||
}
|
||||
|
||||
{
|
||||
mods = $1; key = $2; action = $3
|
||||
if (action == "") next
|
||||
|
||||
# "Super, Alt" -> "Super + Alt", in a fixed order.
|
||||
cnt = split(mods, m, /,[[:space:]]*/)
|
||||
for (i = 1; i <= cnt; i++) gsub(/^[[:space:]]+|[[:space:]]+$/, "", m[i])
|
||||
for (i = 1; i < cnt; i++)
|
||||
for (j = i + 1; j <= cnt; j++)
|
||||
if (order(m[j]) < order(m[i])) { t = m[i]; m[i] = m[j]; m[j] = t }
|
||||
|
||||
chord = ""
|
||||
for (i = 1; i <= cnt; i++) chord = chord (chord == "" ? "" : " + ") m[i]
|
||||
if (key != "") chord = chord (chord == "" ? "" : " + ") key
|
||||
|
||||
# Spawn("cmd") is a command as written in cosmic.conf.
|
||||
# System(X) is an action name that system_actions turns into a command.
|
||||
# Everything else is internal to the compositor and has no command; the
|
||||
# action itself is the honest answer, and inventing a program name for
|
||||
# Focus(Left) would be a lie.
|
||||
what = ""
|
||||
if (action ~ /^Spawn\(".*"\)$/) {
|
||||
what = substr(action, 8, length(action) - 9)
|
||||
} else if (action ~ /^System\(/) {
|
||||
name = action
|
||||
sub(/^System\(/, "", name); sub(/\)$/, "", name)
|
||||
what = (name in cmd) ? cmd[name] : words(name)
|
||||
} else if (action ~ /\(/) {
|
||||
name = action; arg = action
|
||||
sub(/\(.*$/, "", name)
|
||||
sub(/^[^(]*\(/, "", arg); sub(/\)$/, "", arg)
|
||||
what = words(name) " " arg
|
||||
} else {
|
||||
what = words(action)
|
||||
}
|
||||
|
||||
# Last write wins, which is why custom is appended after defaults.
|
||||
seen[chord] = what
|
||||
if (!(chord in orderkey)) orderkey[chord] = ++seq
|
||||
}
|
||||
|
||||
END {
|
||||
for (c in seen) printf "%d\t%s\t%s\n", orderkey[c], c, seen[c]
|
||||
}
|
||||
' <<<"$raw" | sort -n | cut -f2-
|
||||
)"
|
||||
|
||||
count="$(grep -c . <<<"$rendered")"
|
||||
((count > 0)) || fail "every shortcut line failed to parse; the RON format has changed"
|
||||
|
||||
# Column-aligned so the chords form a readable left edge. Done here rather than
|
||||
# in awk because the width has to be measured across the whole set first.
|
||||
width="$(cut -f1 <<<"$rendered" | LC_ALL=C awk '{ if (length($0) > w) w = length($0) } END { print w }')"
|
||||
|
||||
# -dmenu rather than a static window, so the list is filterable: 122 bindings is
|
||||
# more than fits on a screen, and typing "workspace" is how you actually use
|
||||
# this. No theme arguments, so ~/.config/rofi/config.rasi applies and it matches
|
||||
# the launcher and the power menu.
|
||||
#
|
||||
# The selection is discarded. This is a reference, not a menu -- there is no
|
||||
# sensible action for "you picked Super + Q", and running the bound command on
|
||||
# Return would make an accidental keypress close a window from a help screen.
|
||||
LC_ALL=C awk -F'\t' -v w="$width" '{ printf "%-*s %s\n", w, $1, $2 }' <<<"$rendered" |
|
||||
rofi -dmenu -i -no-custom -no-show-icons -p "keys" \
|
||||
-mesg "$count shortcuts" >/dev/null 2>&1 || :
|
||||
@@ -111,3 +111,22 @@ bind = $mainMod, Return, exec, cosmic-term
|
||||
# Super+Shift+E is Hyprland's own spelling for "exit". The menu also offers
|
||||
# lock and suspend, which is why the binding is not named after logout.
|
||||
bind = $mainMod SHIFT, E, exec, hyprcosmic-powermenu
|
||||
|
||||
# --- Help ---------------------------------------------------------------
|
||||
#
|
||||
# Every shortcut in the session, in one searchable window, with the command
|
||||
# each one runs. cosmic-settings can show these, but it is where you go to
|
||||
# change a binding, not where you go to remember one -- and the hyprcosmic
|
||||
# profile does not put it one click away.
|
||||
#
|
||||
# The script reads COSMIC's own Shortcuts files rather than this one. That is
|
||||
# deliberate and it matters: this file declares six bindings and the session
|
||||
# answers to 122, the rest being COSMIC defaults there is no reason to restate
|
||||
# here. A reference built from this file alone would look complete and be
|
||||
# missing every window, workspace and media key on the machine.
|
||||
#
|
||||
# Super+Shift+/ is Super+? on this keyboard, which is the usual spelling for
|
||||
# help, and it is one of the few chords in that corner COSMIC leaves free --
|
||||
# Super+K and Super+I are both focus actions in the defaults. The same script
|
||||
# backs waybar's keyboard button.
|
||||
bind = $mainMod SHIFT, slash, exec, hyprcosmic-keybinds
|
||||
|
||||
+139
-1
@@ -50,15 +50,19 @@
|
||||
"height": 34,
|
||||
"spacing": 4,
|
||||
|
||||
"modules-left": ["ext/workspaces", "wlr/taskbar"],
|
||||
"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",
|
||||
@@ -67,6 +71,23 @@
|
||||
"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"
|
||||
@@ -80,6 +101,56 @@
|
||||
"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.
|
||||
@@ -120,6 +191,23 @@
|
||||
]
|
||||
},
|
||||
|
||||
// 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": {
|
||||
@@ -205,6 +293,19 @@
|
||||
"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.
|
||||
//
|
||||
@@ -220,6 +321,43 @@
|
||||
"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
|
||||
|
||||
@@ -50,15 +50,19 @@
|
||||
"height": 34,
|
||||
"spacing": 4,
|
||||
|
||||
"modules-left": ["ext/workspaces", "wlr/taskbar"],
|
||||
"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",
|
||||
@@ -67,6 +71,23 @@
|
||||
"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": "@@KEYBOARD@@",
|
||||
"tooltip": true,
|
||||
"tooltip-format": "keyboard shortcuts\nSuper + Shift + /",
|
||||
"on-click": "hyprcosmic-keybinds"
|
||||
},
|
||||
|
||||
"ext/workspaces": {
|
||||
"format": "{name}",
|
||||
"on-click": "activate"
|
||||
@@ -80,6 +101,56 @@
|
||||
"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": "@@WINDOW@@ {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.
|
||||
@@ -120,6 +191,23 @@
|
||||
]
|
||||
},
|
||||
|
||||
// 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": "@@ALERT@@ {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": {
|
||||
@@ -205,6 +293,19 @@
|
||||
"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": "@@DISK@@ {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.
|
||||
//
|
||||
@@ -220,6 +321,43 @@
|
||||
"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": "@@GPU@@ {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": "@@FAN@@ {} rpm",
|
||||
"interval": 5
|
||||
},
|
||||
|
||||
"cpu": {
|
||||
"format": "@@CPU@@ {usage}%",
|
||||
"interval": 5
|
||||
|
||||
@@ -55,6 +55,12 @@ ICONS = {
|
||||
"BAT_80": 0xF0081,
|
||||
"BAT_90": 0xF0082,
|
||||
"BAT_100": 0xF0079,
|
||||
"WINDOW": 0xF2D0,
|
||||
"ALERT": 0xF0026,
|
||||
"DISK": 0xF02CA,
|
||||
"GPU": 0xF08AE,
|
||||
"FAN": 0xF0210,
|
||||
"KEYBOARD": 0xF030C,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -77,15 +77,20 @@ window#waybar {
|
||||
/* 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. */
|
||||
#custom-keybinds,
|
||||
#window,
|
||||
#mpris,
|
||||
#clock,
|
||||
#privacy,
|
||||
#systemd-failed-units,
|
||||
#idle_inhibitor,
|
||||
#custom-swaync,
|
||||
#bluetooth,
|
||||
#pulseaudio,
|
||||
#network,
|
||||
#disk,
|
||||
#temperature,
|
||||
#custom-fan,
|
||||
#cpu,
|
||||
#memory,
|
||||
#power-profiles-daemon,
|
||||
@@ -106,6 +111,12 @@ window#waybar {
|
||||
color: @critical;
|
||||
}
|
||||
|
||||
/* The other button on the bar. Accent rather than critical, because this one is
|
||||
* safe to press. */
|
||||
#custom-keybinds:hover {
|
||||
color: @accent;
|
||||
}
|
||||
|
||||
#clock {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -137,12 +148,35 @@ window#waybar {
|
||||
color: @warning;
|
||||
}
|
||||
|
||||
/* Same reasoning as mpris, but the class is not on the widget. waybar puts
|
||||
* `empty` on the whole bar rather than on #window, so the selector has to
|
||||
* descend into it -- waybar-hyprland-window(5) gives it as exactly this:
|
||||
*
|
||||
* window#waybar.empty #window When no windows are in the workspace
|
||||
*
|
||||
* The first `window` is the GTK window, the second is our module; they are
|
||||
* unrelated names that collide, which is why this reads so oddly. */
|
||||
window#waybar.empty #window {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 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; }
|
||||
|
||||
/* No state class needed: hide-on-ok removes the module entirely at zero, so it
|
||||
* is on screen only when there is something to report and can be coloured
|
||||
* unconditionally. */
|
||||
#systemd-failed-units { color: @critical; }
|
||||
|
||||
/* Set by hyprcosmic-fan: `high` above 4000 rpm, `idle` when every fan has
|
||||
* stopped. `normal` is left alone. */
|
||||
#custom-fan.high { color: @warning; }
|
||||
#custom-fan.idle { color: @muted; }
|
||||
#pulseaudio.muted { color: @muted; }
|
||||
#idle_inhibitor.activated { color: @accent; }
|
||||
#custom-swaync.notification { color: @accent; }
|
||||
|
||||
@@ -76,6 +76,8 @@ SHARED=(
|
||||
"config/rofi/palette.rasi:share/hyprcosmic/rofi/palette.rasi:644"
|
||||
"config/rofi/rules.rasi:share/hyprcosmic/rofi/rules.rasi:644"
|
||||
"config/bin/hyprcosmic-powermenu:bin/hyprcosmic-powermenu:755"
|
||||
"config/bin/hyprcosmic-fan:bin/hyprcosmic-fan:755"
|
||||
"config/bin/hyprcosmic-keybinds:bin/hyprcosmic-keybinds:755"
|
||||
)
|
||||
|
||||
# The session entry point. Kept apart from SHARED because it is versioned in the
|
||||
@@ -116,7 +118,13 @@ audit_config_tree() {
|
||||
while IFS= read -r -d '' f; do
|
||||
rel="${f#"$REPO"/}"
|
||||
[[ "$known" == *" $rel "* ]] || unclassified+=("$rel")
|
||||
done < <(find "$REPO/config" -type f -print0 | sort -z)
|
||||
# Dot-directories are pruned. Nothing shipped lives in one, and tooling
|
||||
# drops state inside the tree without asking -- .omc/ appeared under
|
||||
# config/waybar/ and failed this audit with six files that are gitignored
|
||||
# and are not assets. Failing on those trains you to ignore the one message
|
||||
# that catches a genuinely unclassified file. Dot *files* are still walked;
|
||||
# only directories are pruned.
|
||||
done < <(find "$REPO/config" -name '.?*' -type d -prune -o -type f -print0 | sort -z)
|
||||
|
||||
((${#unclassified[@]} == 0)) || die "not listed as shared or per-user: ${unclassified[*]}
|
||||
Add each to SHARED or PER_USER in $(basename "${BASH_SOURCE[0]}") and say which."
|
||||
|
||||
Reference in New Issue
Block a user