- README: quick-links bar, compositor-support table, module list grouped by
theme with links to the wiki, getting-started section, collapsible
dependency blocks, updated badges (CI, release), and a note that module
docs live in man/ (auto-synced to the wiki)
- add CONTRIBUTING.md (dev build, code style, docs workflow, PR checklist)
- add issue templates (bug report, feature request) and a PR template
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The name-vanished playerctl callback called event_box_.set_visible(false)
directly. On resume from suspend this runs in a re-entrant / torn-down
state and crashes in Gtk::Widget::set_visible. Only clear the player and
dp.emit() from the callback; update() (on the main thread) computes
visibility from player state and hides the module when there is no player,
matching the other hardened handlers.
Fixes#5124.
The WirePlumber module registers three async callbacks (onDefaultNodesApiLoaded,
onMixerApiLoaded, onPluginActivated) that receive a raw self pointer with a NULL
GCancellable. WirePlumber cannot withdraw an in-flight callback, so if the module
is destroyed before a queued callback fires (e.g. a temporary output/bar is removed
while a component load is still pending, or during an audio route transition), the
callback dereferences the freed self, causing heap corruption / a crash.
Guard each of these callbacks with isModuleAlive(), which checks the existing static
modules registry. The destructor already removes this from the registry before any
teardown, so a missing entry means self is dangling and the callback bails out
without touching it.
A GCancellable cannot fix this cleanly here: every callback dereferences self on its
first line, and wp_core_load_component completes via a WpTransition (not a GTask), so
the cancellable is not recoverable from the GAsyncResult either. The liveness check
must not touch self at all.
Fixes#3974.
On reload the GApplication is recreated but the default main context (and any
queued PRIORITY_HIGH_IDLE createBarsBatch source) survives. pending_outputs_
was left holding dangling waybar_output* into the just-cleared outputs_ list;
createBarsBatch filters by address, which can mis-match once a freed slot is
reused. Clear pending_outputs_ and reset bars_scheduled_ in bindInterfaces so
the next run batches from a clean state. Mitigates the dangling-pointer path of
#4129 (the cross-process app-id race in #4117 is separate).
Some pandoc versions (incl. the pinned 3.5) render scdoc bullet lists as a
series of `> ·` blockquotes instead of a Markdown list, which made the STYLE
sections look broken. Post-process the generated Markdown to collapse that
artifact back into real `- item` bullets, independent of the pandoc version.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MPD state machine drives all connection attempts from Glib::signal_timeout
callbacks, which run on the GTK main loop. tryConnect() called
mpd_connection_new() with the user-facing timeout_ (up to 30s by default), so an
unreachable server blocked the whole bar for the full connect timeout.
Bound the connect attempt to a short fixed timeout (2000 ms) so a dead server
fails fast, then restore the configured timeout_ for subsequent command reads so
slow-but-alive servers are unaffected.
Fixes#1186.
wlr/taskbar reads on-click* config values (close, minimize, maximize,
fullscreen, minimize-raise, activate) directly as internal actions in
Task::handle_clicked, but never adopted the eventActionMap_/doAction
mechanism. As a result AModule::handleUserEvent additionally forkExec-ed
the same value as a shell command, e.g. on-click-middle: "close" ran the
action and then failed with "sh: line 1: close: command not found".
Register the taskbar built-in action names in eventActionMap_ so they are
recognized as module actions, and skip the shell forkExec in
handleUserEvent when the configured value is a recognized module action.
Non-action values are still run as user shell commands.
Fixes#3284.
When a tray item exports menu accelerators (e.g. Mattermost), libdbusmenu-gtk
calls gtk_widget_set_accel_path() with a NULL accel group because the
DbusmenuGtkClient never had one assigned. This raises a Gtk-CRITICAL that
corrupts menu state, and aborts Waybar when running under
G_DEBUG=fatal-criticals.
Assign a fresh GtkAccelGroup to the client right after the menu is created,
before it is populated or shown.
Fixes#5142.
Improve the generated wiki pages:
- normalize definition-style option blocks (*name*: / typeof: / default:)
into scdoc tables, so every module renders options as a clean table like
the table-style pages (Bluetooth, Network, ... no longer a wall of text)
- drop pandoc's empty leading table-header row so the real header shows
- pin pandoc to 3.5 in CI (the distro 2.x man reader mangled lists into
blockquotes); matches local output
- prepend a visible "auto-generated from master, do not edit here" note to
every page (was an invisible HTML comment)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When no explicit device-path is configured, a multi-node keyboard can
expose several event devices where only one actually toggles the lock
LEDs. Previously update() read state from a single arbitrary device
(libinput_devices_.begin()), which often picked a node whose EV_LED
values never change, leaving the indicator stuck. Iterate all devices
and OR their NUML/CAPSL/SCROLLL values together so a lock is reported
on if any device reports it on. The single-device path is preserved
when device-path is set.
Fixes#2215.
For a module configured node-type "Audio/Source", node_id_ and
source_node_id_ resolve to the same source node, so its primary mute
state is stored in muted_. update() unconditionally mapped muted_ to the
muted/sink-muted classes and source-muted only to the secondary
source_muted_ flag, so a source module could never receive source-muted
-- only the sink classes.
Gate the mute-class selection on the configured node-type: a source-type
module drives source-muted from its primary mute state, while a sink-type
module keeps muted/sink-muted for its sink and source-muted for the
secondary default source it tracks for {format_source}. The primary node
still feeds {volume} via updateVolume, so source-widget volume rendering
is unaffected.
Fixes#4523.
The mixer-api is configured with the linear scale (0), so volume_ holds
the raw linear gain. The perceptual "cubic" value shown by wpctl and
exposed as {volume} is cbrt(linear), but update() computed pow(volume_, 3)
instead. Cubing under-reads every volume below max and collapses small
linear gains to 0% -- which is why the default Bluetooth sink (whose
normal levels map to low linear gains, e.g. wpctl 0.55 -> linear 0.166)
displayed 0% while wpctl reported a normal, unmuted volume.
Replace the inverted conversions with the correct cube-root/cube pair in
the display path, the scroll-scale conversions (cubic / cubic_percent),
and the max-volume ceiling mapping so scrolling and the cap stay
consistent with the corrected {volume}.
Fixes#5159.
An unsupported specifier (e.g. the %-I / %OI no-leading-zero padding
modifiers, which the date/std::chrono formatter does not implement) threw out
of update() and the whole clock module failed to load. Catch it, warn once,
and fall back to {:%H:%M} so the bar still comes up. Addresses #1469.
Addresses review: a zero interval_ must stay reserved for modules whose
default interval is already 0 (event-driven). Periodic modules (clock,
simpleclock, pollers) would otherwise do % interval_ (modulo by zero) or
sleep_for(0) in a tight loop. interval:0 on a periodic module now falls back
to its default interval.
The niri IPC worker slept 1ms per event and never reconnected. Under an
event burst the per-event cap back-pressures the socket, niri fills its
send buffer and drops the stream; read_line then returns false, the
detached thread exits and the module freezes permanently.
Remove the per-event sleep so events drain as fast as they arrive, and
wrap the socket setup and read loop in a reconnect loop that backs off
and re-establishes the stream on drop. A running_ flag lets the thread
exit cleanly on teardown.
Fixes#5117.
connectContext() throws std::runtime_error when pa_context_connect() fails.
It was called directly from contextStateCb (the libpulse mainloop thread,
running pure-C callback frames) on the PA_CONTEXT_FAILED reconnect path, so on
a pipewire/pulse restart the exception unwound across the C callback boundary
and triggered std::terminate/SIGABRT.
Add reconnectContext() noexcept which wraps connectContext() and logs failures
instead of throwing, and use it from the callback. Guard against the
FAILED -> connect -> FAILED recursion/busy loop with a reentrancy flag. The
constructor-time connectContext() still throws as before.
Fixes#5141.
On device unplug the inotify IN_DELETE handler removed the libinput
device and unref'd it before erasing the entry from libinput_devices_.
A repeated IN_DELETE event for the same path (observed as the
"has been removed" log line printed twice) could reach an
already-unlinked device and trigger a libinput list_remove assertion
abort.
Erase the map entry first (under devices_mutex_) so a second delete for
the same path is a no-op, then call libinput_path_remove_device() and
libinput_device_unref() exactly once per device pointer.
Fixes#5143, #4443, #4566.
onCmd() runs on the sway IPC worker thread and called updateAppIconName(),
which touches the global Gtk::IconTheme cache. Concurrent access with the
main thread's draw (propagate_draw -> gtk_icon_theme_has_icon ->
g_hash_table_lookup) races and can segfault, notably on multi-monitor and
focus changes.
Move the icon-theme lookup into Window::update(), which runs on the main
thread via dp.emit(), and only store app_id_/app_class_ in onCmd().
Fixes#4108.
set_current_layout() mutated label_'s GTK style context (remove_class/
add_class) while being called from the sway IPC worker thread via
onEvent(). Off-main-thread GTK widget mutation caused a SIGSEGV.
Record only the target layout in set_current_layout() and apply the
matching CSS class in update(), which the dispatcher runs on the GTK
main thread. A new applied_class_ member tracks the currently applied
class so update() can swap it. The shared layout_/applied_class_ state
is guarded by the existing mutex_.
Fixes#3702.
The Portal constructor synchronously auto-starts org.freedesktop.portal.Desktop
via a Gio::DBus::Proxy. If that service fails or crashes on start it throws a
Glib::Error, which was previously uncaught and terminated Waybar. Wrap the
construction in a try/catch, log a warning and leave portal as nullptr on
failure, and null-guard every dereference so a missing portal simply disables
light/dark appearance detection instead of crashing.
Fixes#3140, #3601.
Building a zoned_time/zoned_seconds from a local_time throws
ambiguous_local_time during the DST fall-back hour and nonexistent_local_time
across the spring-forward gap. update() runs this every minute with the
tooltip enabled by default and has no try/catch, so Waybar aborts every
minute during a DST transition. Pass choose::earliest at each construction to
resolve deterministically instead of throwing.
Fixes#2615; resolves the recurring DST-crash duplicates #5006, #5018, #5063,
#5096, #3024.
- parseOutputJson() passed script text/alt/tooltip straight to Pango/GTK; an
invalid-UTF-8 byte aborted the bar in g_utf8_collate. Validate/make_valid
like parseOutputRaw already does. Fixes#2829.
- restart-interval:0 was floored to 1ms, respawning the script ~1000x/s and
starving the main loop; a non-positive restart-interval now stops instead.
Part of #4842.
std::max(1L, interval*1000) turned a user's explicit "interval": 0 into a
1ms periodic refresh, i.e. a ~1000x/s busy loop that starves the GTK main
loop and leaks memory (mpris RSS growth, missing tooltips, frozen updates).
An explicit 0 now stays the 'no periodic refresh' sentinel. Fixes#4987,
#4842; helps #4864, #4917, #4998, #5145.
The format-<short_description>[-<variant>] override branches only passed a
positional arg, so a format using {short}/{long}/{variant} threw 'argument
not found', which disabled the whole module. Now supply the same named args
as the fallback/tooltip branches. Fixes#5120.
refresh() called config_["signal"].asInt() unconditionally on every RT
signal; a non-integer "signal" value throws Json::LogicError and aborts
Waybar. Matches the guard already present in custom/image/idle_inhibitor.
Fixes#3514.
generate.py now keeps _Sidebar.md in sync: any Module:-* page from the
mapping that is not yet linked is inserted alphabetically into the
Modules list. Existing entries (custom labels, nested sub-entries,
hand-written non-module links) are left untouched, so adding a module no
longer requires editing the sidebar by hand.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tooling that makes the scdoc man pages the single source of truth and
regenerates the wiki module pages from them:
- .github/wiki/mapping.json: man-page -> wiki-page mapping (aggregation-aware)
- .github/wiki/generate.py: scdoc -> pandoc -> gfm, strips man-only sections,
concatenates aggregated pages, appends optional extras/<Page>.md
- .github/wiki/extras/: hand-kept appendices (screenshots) with no man equivalent
- .github/workflows/wiki.yml: on push to man/** (or the tooling), regenerate
and push the wiki; other wiki pages are left untouched
Covers all 65 man pages -> 45 wiki pages (5 new: GPS, Inhibitor, Mango, Menu,
WWAN). Non-module and hand-written wiki pages are never modified.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port information that previously lived only in the GitHub wiki into the
authoritative scdoc man pages, so the man pages become the single source
of truth for module documentation.
33 man pages enriched (options, format replacements, actions, style
selectors, troubleshooting and implementation notes). Additions were
verified against the source; stale/incorrect wiki entries were
deliberately not ported. All pages still compile with scdoc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A full man<->code consistency audit surfaced options, format placeholders
and CSS classes that were implemented but undocumented, documented but not
implemented (some causing fmt crashes when copied from examples), and
defaults that disagreed with the code. This aligns the docs with the code
and fixes a few genuine code gaps.
Docs:
- Add the missing waybar-user(5) man page (and register it in meson.build)
- Document previously-undocumented options/placeholders/CSS across many
modules (custom image-path/image-name/icon-size, graph_type/width/
datapoints; battery smooth-power; wireplumber format-source/only-physical;
mpris {position}/prefer-album-artist; network {signalStrengthApp}/compact
bandwidth; pulseaudio {source_volume}/{source_desc}; upower {temperature}/
{model}/{native-path}; wwan {power_state}/{imei}; tray ignore-list; and
many CSS state classes: .sink-muted, .source-muted, .workspace-hover, etc.)
- Correct documented defaults to match the code (hyprland format {name},
gamemode {count}, cpu-graph interval 5, cava input_delay 4, niri taskbar
icon-size 16, disk/gps/wayfire formats, menu-actions object type, ...)
- Remove placeholders/options that do not apply (custom-graph {icon}/format/
format-icons/rotate) and fix crashing examples (wwan {mode}, gps
format-no-fix); note cava background/foreground/continuous_rendering are
cava-config-file options
Code:
- bluetooth: accept the documented `controller` key as a synonym of
`controller-alias` (the option was silently ignored)
- mango/workspaces: supply the documented `{name}` fmt arg (was missing ->
fmt::format threw)
- privacy: read `tooltip` as a bool (was guarded on isString(), so the
documented `tooltip: false` was silently ignored)
All man pages validated with scdoc 1.11.4. Not compiled locally (no gtkmm);
C++ build relies on CI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Debian sid, `dependency('systemd')` needs systemd.pc, which is shipped by
the `systemd-dev` package, not `libsystemd-dev`. Add it so the forced
-Dsystemd=enabled configure step succeeds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The linux matrix ran every distro with features in `auto` mode, so any
module whose dependency was missing from the image was silently skipped
and never compiled. Three modules (privacy/pipewire, gps, wwan) were built
by no CI job at all, and several others only on one or two distros.
Add a dedicated `build-full` job (Debian image) that installs the missing
dependencies inline and force-enables every optional feature, turning a
broken include or a missing dep into a hard build error instead of a silent
skip. Drop `debian` from the auto matrix since `build-full` uses the same
image and builds a strict superset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-0.15.0 review of the 0.15.0..HEAD range surfaced regressions and
bugs. This restores backward compatibility for existing configs/CSS,
fixes confirmed defects, and repairs the scdoc man-page build break on
master. Pango-markup tooltips are intentional and were kept.
Backward-compat restorations:
- AModule: honor legacy numeric Gdk::CursorType cursor values (int overload)
- memory: correct GiB divisor (was ~2.3% low); round bare {} placeholders
- wireplumber: scale max-volume into the linear domain so the cap works again
- idle_inhibitor: gate right/middle-click deactivate & scroll on dynamic-timeouts;
accept both dynamic-timeout(s); widen timeout to double (no fractional truncation)
- custom: keep #custom-<name>.<class> CSS selectors working (classes on box_)
- image: don't wordexp-split a single path; fall back to the literal path
- niri/window: restore hide-when-empty (new show-empty opt-in); escape tooltip
- wlr/taskbar: plain-text tooltip when markup is disabled
Bug fixes:
- tray: fix use-after-free in onAdd; guard the watcher retry timeout
- hyprland: clamp max-windows iterator (OOB); drop duplicate language tooltip block
- niri/window: supply {col}/{max_col} args in the empty branch (fmt::format_error)
- mpris: escape {dynamic}/{player} tooltip; fix dangling player; albumArtist source
- mango: fix use-after-free race (dispatch under callback_mutex_)
- mpd: contain throwing checkErrors in noexcept idle paths (no std::terminate/UAF)
- keyboard_state: always render every lock label, with guarded defaults
- bluetooth: bound GATT ReadValue timeout, opt-in + services-resolved gating,
preserve authoritative Battery1 percentage
- wireplumber: fix WpDevice reference leak / NULL handling
- battery, clock, dwl, wayfire, graph, custom_graph, transform, river: assorted
crash/logic fixes
Man page / build:
- niri-workspaces: fix scdoc "indented by an amount greater than 1"
(workspace-taskbar sub-options were mis-indented; breaks man-page build)
- document new show-empty (niri/window); correct network {txBitrate}/{rxBitrate}
Not compiled locally (no gtkmm on this host); C++ build relies on CI.
Man pages validated with scdoc 1.11.4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an ethernet link speed ({linkSpeed}) read from
/sys/class/net/<iface>/speed, plus a skip-decimal option on pow_format
to drop a trailing .0. Rebased onto master's store-based network
formatting; existing compact bandwidth args updated for the new
pow_format signature.
Adds a per-workspace app-icon taskbar (Workspace class) to the
niri/workspaces module. Rebased onto master: integrated with the
existing window-rewrite feature so the {windows} and {total} format
replacements continue to work alongside the new taskbar.