Decision #1 (one chip highlight height per bar, vertically centred): the
.stat-pair/.stat-pair.icon-only/.media-widget/workspace-btn CSS all sized
to their own content box before this (min-height: 0, or a stale 32/20px
figure), which is why battery sat high and wifi/menu ran taller than their
neighbours. Added a hardcoded approved_chip_height() (26/22/22 by
WorkspaceStyle, matching the demo spec) since bread-ecosystem's own
chip_height token (32/20/36) predates this pass and disagrees with it, and
that repo is a sibling agent's this cycle.
Also found and fixed a second copy of the hamburger-corner-mismatch bug:
.control-panel-btn (hamburger only) hardcoded its own border-radius/
min-width/min-height *later* in the cascade than .stat-pair, silently
winning over chip_radius/chip_height regardless of the icon-only fix
already in place. Dropped the four conflicting properties so .stat-pair
cascades through unchanged.
Spotlight workspace dots: bumped to the approved Option B spec (10px
tall, widths 8/13/17/22 by open-window count) — the prior pass landed
9px/[6,10,14,18], one generation behind. dot_widths is likewise
theme.toml-derived and stale, so overridden locally with a comment
rather than edited upstream.
Workspace row "sits low on first paint" bug: root cause was the ws-in
entrance animation (row-in keyframe, margin-top 8px -> 0) playing on
every button during the very first rebuild_buttons call, racing
WorkspaceTrail::place()'s single-shot geometry sample and freezing the
trail pill a few px low until the next real switch re-measured settled
layout. The demo never animates the initial row in at all — only
subsequently-added workspaces should. Suppressed ws-in specifically on
the first build (button_map starts empty exactly once).
Also fixed: make_button's set_size_request was still pinning the stale
tokens().chip_height() as a hard GTK minimum, which would have out-ranked
the CSS min-height fix above for Trail/Pill workspace pills.
04-spotlight.html's .dots button is 6px tall — reported as "too small
and hard to click." The click-target side of that is a separate,
non-visual fix elsewhere; this is the visual side only. 6px is also
genuinely hard to see on a real display, not just hard to hit, so
DOT_HEIGHT bumps it to 9px (bar::workspaces::make_dot_button) — enough
to read clearly without growing into a little chip that would fight
the capsule's minimal look. Widths are untouched: dot_widths is the
manifest's own per-occupancy encoding ([6, 10, 14, 18] in the builtin
spotlight theme.toml) and stays exactly as specified; only the height,
which has no manifest token of its own, is breadbar's call to make.
The CSS .workspace-dot min-height is updated to match (9px) so it
doesn't silently drift from the value that actually governs the
rendered size (a direct set_size_request, not normal CSS layout).
Not visually verified via capture — the isolated headless-Sway harness
has no Hyprland IPC, so workspace dots render empty there regardless
of theme (pre-existing harness limit, unrelated to this change).
Verified by reading the CSS/Rust against 04-spotlight.html's <style>
block instead, and by cargo build/clippy/test passing clean.
The isolated screenshot harness has no Hyprland IPC (bread-capture's
isolation.rs), so it can never exercise a nonzero window count and prove
dot widths visually. Pulled the count->dot_widths-index mapping out of
make_dot_button as dot_width_index(), a pure function with no GTK
dependency, and unit-tested it directly (0/1/2 pass through, 3+ collapses
onto index 3, negative counts clamp instead of underflowing the array
index) as the stand-in for that visual proof.
- New bread-launcher dep (gtk feature), pinned tag v0.7.4 with a dev-only
[patch] entry alongside bread-theme's.
- Wire the drawer slot (plan §2/§7): root becomes a vbox whose first row is
the existing CenterBox and whose second is drawer_box, appended via the
same ModuleRegistry::for_each_in_slot pattern left/centre/right already
use. Empty for liquid-motion/glass-workbench (zero children, zero size);
theme.rs's window.breadbar > centerbox selector becomes > box > centerbox
to follow the new nesting, byte-identical CSS for both existing themes.
- modules.workspaces.style = dots: bar::workspaces::make_dot_button, width
from modules.workspaces.dot_widths, wired as a third rebuild_buttons arm.
- New launcher_entry (plain GtkEntry) and launcher_results
(bread_launcher::gtk::ResultsList, sharing breadbox's cache/history via
bread_launcher::LAUNCHER_APP) modules, built unconditionally and placed
only when a theme's [bar.slots] names them (spotlight today).
- Capsule expand/collapse: typing/focus opens the drawer and animates its
height via bread_theme::anim::spring_to; Up/Down move the selection,
Enter launches (do_launch + record_launch) and collapses, Escape collapses
and releases keyboard focus (on_demand ties to GTK's own focus-widget
state, so releasing GTK focus hands the compositor keyboard back).
- window.width now wires Width::Px into set_default_width (previously
unconsumed — Fill-anchored themes never needed it, the capsule does).
- Two new --screenshot views: capsule-collapsed, capsule-expanded (the
latter focuses launcher_entry, types a query, and captures bar height +
the drawer's actual settled height rather than a guessed constant).
cargo check/clippy: no new warnings (same two pre-existing: dead_code on
system_stats_box/cpu_pair/mem_pair/pwr_pair, from_* on workspaces.rs).
cargo test: 18/18 unchanged.
Wires modules.workspaces.style/modules.clock.style into the bar for real
(Phase 3 shipped the schema but only ever consumed trail/flip), and adds
cpu/ram as bar modules, so glass-workbench (bread-theme) renders correctly
while liquid-motion's default path is untouched:
- workspaces: "pill" never calls WorkspaceTrail::place/stretch (the trail
overlay stays invisible) and honours modules.workspaces.show_empty for
real, rendering unoccupied non-active workspaces dimmed via CSS instead
of filtering them out of the row. "trail" keeps the exact pre-existing
filter/place/stretch behaviour regardless of show_empty.
- clock: "plain" registers a plain date+time label (date_lbl, built but
never parented until now, plus a new clock_plain_lbl) instead of the
per-digit flip box. "flip" is untouched.
- cpu/ram: two new bar chips (bar_cpu_pair/bar_ram_pair, bar_cpu_lbl/
bar_ram_lbl) fed by the same AppInput::StatsUpdate data as the control
panel's cpu_pair/mem_pair. Separate instances rather than reparenting
the panel's own pair — a GTK widget can only have one parent, and
reparenting would pull them out of the control panel's sys-grid, which
no theme asked to change.
theme.rs::load_css() now branches on tokens.bar_border() (full-border
island vs. flush bar's bottom-only hairline) and modules.workspaces.style
(dimmed/translucent trail-fill buttons vs. solid-accent-fill pills), and
unconditionally gains .clock-plain/.clock-plain-time rules. The trail/flip
branches reproduce today's CSS byte-for-byte.
Verification (bread-capture, isolated headless-Sway harness):
- Noise floor: same baseline binary against itself varies from AE=0 (same
capture minute) up to ~1900px (an in-flight digit-flip/clock-tick or a
live Wi-Fi scan straddling two captures) and ~39000px on control-panel
(live hardware sensors) — all pre-existing, not introduced by this change.
- Regression: this binary vs. the pre-Phase-5 baseline, captured in the
same clock-minute to remove the dominant noise source, is AE=0 (bit-for-
bit identical) on the bar view under the default liquid-motion theme.
- glass-workbench (BREAD_SHELL_THEME=glass-workbench): bar view is
1920x36 (flush, no margin) vs. liquid-motion's 1920x56 (12px margin +
44px island), square corners vs. liquid-motion's rounded island, plain
"Mon 24/08 23:24" clock, and cpu/ram/wifi/battery/control chips with no
media widget — matches demo 02. The isolated Sway capture harness has no
live Hyprland IPC, so it reports zero workspaces under both themes
(pre-existing, unrelated to this change) — the pill fill/dim CSS itself
is exercised by code review and the trail-untouched regression result,
not by a captured pixel with visible buttons.
cargo test: 18/18 passing (breadbar), no new clippy/cargo-check warnings
beyond the two pre-existing ones this task named up front.
Phase 3b of the shell theme system. A slot list entry can now be
`widget:<key>`, where `<key>` is either a WidgetPlacement alias
(right_of_workspaces, left_of_clock, right_of_clock, left_of_stats, tray)
or a Lua module name. ModuleRegistry::for_each_in_slot creates each
widget container on demand at its slot position; reconcile_widgets routes
each WidgetSpec by module name first, falling back to its placement
alias, and logs+drops (never panics) a spec with no matching container.
bread_shared::widget::WidgetPlacement stays untouched — it's still the
wire type breadd sends, unmodified and unshadowed; only the container
map that placement now resolves through is theme-driven.
Adds ModuleRegistry (src/bar/slots.rs), mapping each [bar.slots] module
name (workspaces/media/clock/volume/wifi/battery/control) to its
already-built widget. main.rs now registers the seven modules once
they're constructed, then walks ShellTheme::slots() to append them
into the left/centre/right containers in theme order instead of a
hardcoded sequence. An unknown module name in a theme manifest is
logged and skipped rather than panicking.
The Lua-declared widget_* containers keep their fixed interleave
(right-of-workspaces, left/right-of-clock, left-of-stats) — that's
Phase 3b, not this change.
breadbar no longer hardcodes BAR_HEIGHT/BAR_MARGIN_TOP/BAR_MARGIN_SIDES/
CHIP_HEIGHT/ICON_PX or the root window's layer-shell setup: it loads
bread_theme:🐚:ShellTheme once (theme::shell_theme(), cached in a
thread-local) and reads bar.window (anchors, margin, exclusive zone,
keyboard mode - now set explicitly instead of relying on the library
default) plus tokens.{icon_px,chip_height} from it everywhere those used
to be literals.
The four satellite surfaces (breadbar-osd, breadbar-notif, breadbar-panel,
breadbar-dismiss) now get their anchor/margin/width/layer from the
manifest's [surfaces.*] table via a small new surface::apply() helper,
narrowly scoped to the three anchor shapes those surfaces actually use.
The pre-existing 8px gap between the panel's top margin and the dismiss
scrim's is preserved exactly (and now commented) rather than "fixed".
theme.rs's load_css() keeps its ~250 lines of hand-written breadbar CSS
(notifications, wifi popover, control panel, media widget) but now reads
its five radius/pad locals and the two easing curves (spring vs
spring_settle - hover/settle transitions were previously miscategorized
as the overshoot curve in the constant audit; the actual code already
used the settle curve there, confirmed against src) from theme tokens
instead of hardcoding them.
Also wires bread_theme:🐚:watch() so editing the active theme's
theme.toml/extra.css hot-reloads CSS tokens without a restart, same as
a pywal palette change already does. Window-spec values still need a
restart per the plan (read once at window-construction time).
Verified pixel-identical: captured all 10 breadbar --screenshot views
(bar, control-panel, connectivity-wifi/bluetooth, media-popover,
notification/-critical, osd-volume/-brightness, wifi-add-dialog) via
bread-capture's isolated headless-Sway harness against both the
pre-change and post-change binaries; every view diffs byte-identical
at the decoded-pixel level.
The control panel lists PipeWire sinks and sets the default, moving
playing streams so the change is immediate. The workspace pill starts
on switch without a layout wait, measures against the Fixed host so it
stays centered on the digit, and ignores row-wide bounds that used to
stretch it across several chips.
One layer-shell window per Hyprland output now loads that output's
bread-theme palette. Notifications, history, and OSD follow the
monitor they appear on. Pin bread-theme to v0.7.4.
Adds real SVG icon assets replacing the icons-needed.txt checklist,
a bluetooth popover module, per-notification urgency (CSS-styled),
and app-name/summary dedup in notification cards. Rounds out the
control panel's stats section with RAM and power draw alongside the
existing CPU/GPU/net rows.
- src/bar/workspaces.rs: the Hyprland EventStream loop exited permanently
on the first Err/end-of-stream (Hyprland restart/reload, IPC hiccup),
freezing every workspace button for the bar's remaining life. Now wrapped
in a reconnect loop with capped exponential backoff, re-syncing workspace
state on every reconnect
- src/notifications/mod.rs + popup.rs: three spec deviations fixed —
expire_timeout=0 now means never-expire instead of being coerced to 5s
(and a critical-urgency notification with no explicit timeout also
persists by default); NotificationClosed is now emitted with the correct
reason code whenever a notification actually goes away (expiry or an
explicit CloseNotification call); replaces_id no longer races its
auto-dismiss timer against the replacement's, via a per-id generation
counter checked before a stale timer is allowed to dismiss anything
- src/main.rs + README.md + assets/icons-needed.txt: lock button (and its
docs) now invoke breadlock instead of hyprlock, the thing breadlock was
built to replace
- src/notifications/mod.rs: added 4 unit tests for the new expire_timeout/
urgency mapping (pulled into a pure compute_expire() for testability) —
this crate had zero test coverage before
Rows for scanned networks not yet in breadcrumbs' saved config were
both dimmed and disabled (set_sensitive(false)), so there was no way
to add them from the popover. They now stay visually dimmed via the
existing wifi-popover-row-unsaved style but remain clickable, opening
a small password dialog that saves + joins the network via
`breadcrumbs add` + `breadcrumbs join`.
GitHub Actions self-hosted runners need per-repo registration on a
personal account; Forgejo Actions' runner already serves every repo
with zero setup. Moves release publishing there (dl.breadway.dev stays
the primary bakery target; GitHub release upload is kept as the
fallback via an explicit token, since Forgejo Actions has no ambient
GITHUB_TOKEN) and adds a mirror workflow to keep GitHub in sync
automatically.
The packaged binary panicked on startup ("svg load: Unrecognized image file
format"): asset SVGs were referenced by their build-time CARGO_MANIFEST_DIR
path (absent on an installed system, so read_to_string returned empty bytes),
and gdk::Texture::from_bytes can no longer decode SVG since librsvg dropped its
gdk-pixbuf loader.
- include_str! the SVGs into the binary (no runtime asset files)
- rasterise via resvg/tiny-skia into a gdk::MemoryTexture (no system loader)
Implements org.kde.StatusNotifierWatcher as a D-Bus service so apps
like Nextcloud can register their tray icons. Icons are rendered from
SNI ARGB pixmaps (falling back to icon-name theme lookup), click calls
Activate(0,0), and NameOwnerChanged cleans up ghost icons when an app
exits. Styling follows the Bread Design System (4px tertiary radius,
xs/sm spacing, opacity transitions).
Also fixes a latent infinite-loop risk in osd.rs (.flatten → .map_while)
and syncs the notifications server version string to CARGO_PKG_VERSION.
- Cargo.toml: depend on bread-theme (path dep for local dev, git dep for
production) with gtk feature; remove local theme dependencies
- src/theme.rs: replace local pywal/Catppuccin impl with bread_theme::gtk
helpers; local bar-specific CSS is preserved
- bakery.toml: describes breadbar for bakery install
- release.yml: builds on hestia self-hosted runner, publishes binary to
dl.breadway.dev and GitHub Releases on v* tags