Two fixes.
The trail pill still grew when spamming between workspace 1 and an empty 6.
Switching to an empty workspace makes Hyprland create and destroy it, which
rebuilds the button row mid-animation and leaves the source button detached —
button_geom then returned None and the fallback handed the wide mid-stretch
span straight back in, reintroducing exactly the accumulation the previous
commit removed. Width now falls back to the destination button, which is
always live, and no path returns the live width any more.
Separately, AppInput::OpenLauncher's local arm only called grab_focus(). Focus
stopped opening the capsule when connect_enter's open_fn() call was removed
(that call was what opened the capsule during window construction), so the
keybind focused the entry and left the drawer shut. It now invokes
launcher_open_fn, which was added for this and never read — the dead-code
warning on that field is what surfaced it.
Interrupting an in-flight stretch fed the pill's CURRENT geometry back in as
the next animation's starting point. Mid-stretch that geometry deliberately
spans both the old and new buttons, and ease_overshoot (c = 1.4) pushes it
wider still past the target — so each interrupted switch began wider than the
last and the pill grew until it hit MAX_CHIP_W. That cap was masking the
compounding rather than preventing it, which is why the bug survived an
earlier fix that only tightened the cap.
from_geom now takes POSITION from the live geometry, so rapid switches still
read as one continuous movement, and WIDTH from the source button's natural
size, so width can no longer compound across interruptions.
Three prior fixes failed because each neutered one path that called open_fn
while others remained. This inverts the approach: opening is now gated on the
input actually coming from a person.
- connect_enter no longer opens. Focus alone is not intent, and GTK grants
focus during window construction, which is what put the capsule in its open
state at startup.
- connect_changed skips when a programmatic_text_change flag is set, so
close_fn's set_text("") and initial buffer setup cannot reopen it.
- A click gesture and the OpenLauncher hotkey are the only paths that open.
- BREADBAR_CAPSULE_DEBUG=1 traces every open/close and why, so a future
accidental open is diagnosable instead of mysterious.
Compiles and tests pass, but NOT yet verified against a live compositor: the
agent that wrote this was interrupted before running it. The startup-collapsed
check still needs a real session.
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.
.workspace-trail/.workspace-btn hardcoded border-radius: 12px and
min-height: 28px — neither matches this theme's own radius_sm token
(9px) nor the 01-liquid-motion.html demo's .ws-btn/.trail rules (26px
tall, 9px radius). radius_sm happens to be an exact match for the
demo's 9px here. Reported: "the pills on liquid motion just look
off".
Not visually verified via capture — the isolated headless-Sway
harness has no Hyprland IPC, so workspace buttons render empty there
regardless of theme (pre-existing harness limit, unrelated to this
change). Verified by reading the CSS against 01-liquid-motion.html's
<style> block instead, and by `cargo build`/clippy/test passing clean.
.stat-pair's border-radius was a hardcoded 10px shared identically by
all three themes: coincidentally close for liquid-motion (radius_sm
9px, matching its demo's .chip radius almost exactly), flatly wrong
for glass-workbench (demo's .chip is 6px, exactly this theme's own
radius_sm), and disconnected from spotlight's much rounder capsule
language. Chips now round by chip_radius: radius_sm for liquid-motion/
glass-workbench, radius_pill for spotlight (its bar/dots are already
that round, so its lone .stat-pair occupant, the battery chip, now
reads as part of that same family instead of a stray rounded rect
inside a much-rounder capsule).
Also drops .stat-pair.icon-only's own 999px radius override. That
made wifi and the liquid-motion hamburger — the only two icon-only
chips — fully circular while their row neighbours (vol, battery)
stayed a rounded rect at .stat-pair's radius: a visible mismatch
inside one row, called out against the hamburger specifically.
Every demo's .chip class draws vol/wifi/battery/menu identically and
none of them circular, so dropping the override just lets the shared
radius cascade through unchanged.
Verified via bread-capture's isolated headless-Sway harness with a
temporary (not committed) debug outline on .stat-pair: liquid-motion's
four right-side chips now share one visible corner radius instead of
two circular + two rounded-rect, and spotlight's battery chip reads as
a rounded pill matching its capsule/dots instead of a sharper rect.
wifi_img.set_hexpand(false) meant the icon packed at the start of the
32px icon-only chip with no leftover space for halign:Center to work
with, sitting visibly left of center on glass-workbench/liquid-motion.
hexpand(true) fixes the centering, but that alone silently bubbles the
expand flag up through connectivity_pair into the shared right-hand
stats box and the centerbox's end slot, blowing the wifi/vol cluster's
layout apart (confirmed via a --screenshot bar capture: the whole
cluster jumped left against the clock with a huge gap before
battery/hamburger). connectivity_pair.set_hexpand(false) pins the
box's own expand explicitly so the fix stays contained to this chip.
Also drops the dead .wifi-pair { padding: 6px } rule: it can never
apply since .stat-pair.icon-only's two-class selector always beats
its one-class specificity regardless of source order, so the intended
padding never actually rendered.
"it only sometimes is dismissed when you click somewhere else": the
dismiss scrim's clickable region started at a fixed offset below the
screen top (the capsule row plus the drawer's max possible height,
kept generous on purpose so an animating drawer never has its result
rows swallowed) but via a plain layer-shell margin, which pushes the
scrim's *entire width* down by that much — not just the strip under
the capsule. That left a full-screen-wide dead band above it (near
470px tall on a 1200px-tall display) where a click neither dismissed
the capsule nor hit anything else, since breadbar-dismiss sits on the
overlay layer above the bar's own top layer and nothing else was
there to catch it either.
show_capsule_dismiss now also takes an optional (x, width) hole,
computed from the capsule's own real on-screen column — queried live
from `hyprctl layers -j` (ground truth; the layer-shell protocol never
hands a client its own assigned position back) rather than assumed —
and punches exactly that column out of the scrim's input region via a
custom cairo Region, leaving the same vertical safety margin in place
but no longer swallowing clicks beside the capsule. Falls back to the
old full-width margin if the geometry query fails for any reason.
Added capsule_dismiss_hole (pure coordinate math, unit tested) and
hypr_capsule_center_x (the live query) to main.rs.
GTK4 auto-assigns keyboard focus to the first can-focus widget in a
window as it's first mapped, and launcher_entry was that widget with
nothing to stop it — EventControllerFocus's connect_enter turned that
spurious focus into an unconditional open_fn() call, so the capsule
looked open (and "focused") the moment breadbar started, with no real
click ever having reached it. Since keyboard = "on_demand" only grants
the layer-shell surface real compositor keyboard focus in response to
genuine interaction, this also explains why typing and Escape did
nothing in that state: GTK believed the entry had focus, but the
compositor never actually handed the surface a keyboard grab.
launcher_entry now starts with can_focus(false), so nothing can land
focus on it implicitly. The two places that legitimately want it
focused now ask for it explicitly: a new GestureClick on the entry
(a real, compositor-visible click — the same kind of interaction
on-demand keyboard mode is meant to react to) and the OpenLauncher
command handler, both flip can_focus back on immediately before
calling grab_focus().
run_mode_action discarded spawn()'s Result with 'let _ =' for both
RunShell and OpenUrl, with no log line either way. If xdg-open is
missing, or the shell/spawn fails for any reason, pressing Enter on a
'>'-command or '.'-URL row did nothing at all with zero diagnostic —
worse than logging nothing being silent, there was no way to even
suspect what happened.
Log to stderr on Err for both variants. Also pull the URL
scheme-adding logic out into a pure url_open_target() helper, covered
by two new tests, so it's exercised without spawning a real process.
launcher_command::spawn() only runs for the primary App instance, and
AppInput::OpenLauncher always grabbed focus on that instance's own
capsule — but self.monitor is whichever output was focused ONCE, at
that instance's own init(), not re-resolved on every keybind press.
Start breadbar while eDP-1 is focused, move to DVI-I-1, press the
launcher keybind, and the capsule opened on eDP-1, off-screen from
where the user was looking.
Re-resolve the focused Hyprland monitor at fire-time via
primary_hypr_monitor() (already used for the same purpose at startup)
and route to the right instance: the primary's own capsule if the
focused monitor is its own, or forward to that monitor's tracked
Controller<App> in self.satellites otherwise. Falls back to the local
capsule when there's no focused monitor to resolve (screenshot mode,
hyprctl unavailable) or no tracked satellite for it yet (a very recent
hotplug reconcile hasn't caught up with).
Split the routing decision into a pure resolve_launcher_route()
helper so it's unit-testable without a live App/GTK/Hyprland stack;
covered by four new tests.
feature/theme-spotlight branched before main's 297207a ('notifications:
stop toast popups from stealing focus or blocking clicks') and the
notification popup was rewritten in this branch to use
surface::apply() for its layer-shell setup, which sets only
anchor/margin/width/layer — no input region — and left the window on
KeyboardMode::OnDemand. Merging this branch would have silently
reintroduced the original bug with no merge conflict to flag it.
Re-apply the fix on top of surface::apply(): add surface::click_through(),
an opt-in helper that sets an empty layer-shell input region on map so
every pointer event passes through to whatever's underneath, and call
it from the toast's create_window(). history.rs's window (the genuinely
interactive notification-history view) is untouched and correctly
keeps OnDemand and normal hit-testing.
Also switch the toast itself from KeyboardMode::OnDemand to
KeyboardMode::None. The toast's card layout does build action buttons
and, when a notification carries an inline-reply hint, a GtkEntry —
but with an empty input region nothing on the toast is ever clickable
or focusable regardless of keyboard mode, so OnDemand only offered a
focus capability with no way to trigger it. Those controls remain
reachable from the history window, which is opened deliberately and
keeps real hit-testing.
surface::apply()'s SurfaceWidth::Px handling only called
set_default_width(), which is advisory — a wide child (an unwrapped
app-name label, or a long summary/body with nothing narrower than its
natural width to wrap against) overrides it, so breadbar-notif (320px)
and breadbar-osd (180px) could render wider than their theme's
configured width instead of wrapping. This is the exact 'wide child
overrides set_default_width' trap main.rs's capsule Width::Px handling
already learned and documented; apply() predates that fix and never
got it.
Add the same set_size_request(px, -1) pin used there. history.rs's
360px override of the shared breadbar-notif namespace has to override
both calls now, not just set_default_width, since the pin from
apply() would otherwise win over a bare default-width override.
bread_theme:🐚:watch() now returns an opaque ThemeWatch (it re-arms
itself onto a new theme's directory when the active theme id changes,
rather than staying pinned to whichever directory was active at call
time) instead of a bare gio::FileMonitor. Update the stored handle's
type to match.
Same change as breadbox's fa85fc1. The [patch] block pointing bread-theme and
bread-launcher at ../bread-ecosystem shipped in the committed manifest, where
CI cannot resolve it — the workflow clones this repo alone and runs cargo
--locked with no sibling checkout. Moved to a gitignored .cargo/config.toml.
Hygiene, not a CI fix: bread-launcher does not exist at tag v0.7.4, so the pin
cannot resolve without the override until bread-ecosystem is tagged.
An app may only publish inside its own bread.<app_id>.* namespace, so
breadbox (app id 'box') cannot emit bread.command.box.open — bread-client
refuses it and the redirect did nothing but log a warning. The capsule now
also subscribes to bread.box.open_requested, which is what breadbox emits.
bread.command.box.open is kept as well: that is the addressed-to-an-app
command form an external trigger (the bread CLI, a keybind, another app)
would legitimately send, so the capsule opens whether it was asked directly
or told by breadbox.
Two rendering bugs, both from treating set_size_request as a maximum when it
is only a minimum.
The drawer was collapsed with set_size_request(-1, 0) but left visible. GTK
still allocates a visible box its natural height, and this layer-shell surface
has no fixed height, so the window grew to fit the entire results list: the
capsule rendered open at idle with a stray result row beneath it. It is now
hidden while collapsed, revealed in open_fn before the height animation, and
hidden again on the frame the collapse reaches zero so the close stays
animated rather than snapping shut.
The capsule's width came from set_default_width, which is only a preference a
wide child overrides. Combined with the results list propagating its natural
width, the pill stretched well past the theme's 480px. The width request is
now pinned, and the list no longer propagates width.
Completes the spotlight capsule (theme 04), THEME_SYSTEM_PLAN.md §7:
- Keyboard open (item A): a new launcher_command module subscribes to
bread.command.box.open — only when the active theme's launcher is
Embedded — and focuses launcher_entry on receipt, which the existing
EventControllerFocus::connect_enter already turns into an open_fn()
call, same path a mouse click into the entry takes. breadbox's own
keybind-triggered launch now redirects to this same event under an
embedded theme instead of stacking its overlay window on top (see the
breadbox commit).
- Click-away scrim (item B): open_fn/close_fn now show/hide panel.rs's
dismiss surface via the new show_capsule_dismiss/hide_dismiss, and
panels.set_on_dismiss(close_fn) wires a click-away into the capsule's
own close path. The scrim's clickable region starts at a fixed offset
(capsule row height + the drawer's own maximum content height,
DRAWER_MAX_HEIGHT_PX) rather than the drawer's live height, so it can
never geometrically overlap a rendered result row regardless of
breadbar-dismiss's overlay layer always rendering above the bar's own.
- Query modes (item C): connect_changed now runs bread_launcher::parse_query
against the entry text and, when the parsed prefix is listed in
[launcher].modes, swaps launcher_results.scroller for a new mode_list
populated by populate_mode_list (calc result / filtered commands / an
"open this URL" prompt). key_ctrl routes Up/Down/Return at whichever
list is active; a `>`/`.` row's action runs through the new ModeAction
enum (RunShell for a command's own fixed exec string, OpenUrl straight
to xdg-open) so arbitrary typed text is never passed through a shell.
- Sections (item D): ResultsList::new's new `sections` bool is threaded
from [launcher].sections.
- Search-state geometry (item E): root's own width now spring-animates
between [launcher].width and .search_width (animate_capsule_width, same
set_size_request-driven technique animate_drawer_height already uses),
and a `.searching` class on root switches border-radius to
.search_radius via a new CSS rule/transition in theme.rs.
Also fixes a real bug this surfaced: drawer_target_height was measuring
drawer_box itself, whose size_request is continuously overwritten by
animate_drawer_height's own tick callback — GTK clamps a widget's
measure() up to at least its own explicit size request, so switching from
the (tall) app list to a one-row calc result measured the PREVIOUS
frame's forced height instead of the new content's actual size. It now
sums each visible child's own natural height directly, which also fixes
the same latent issue for narrowing app-search results.
screenshot.rs gains two new views (capsule-sections, capsule-calc) to
exercise D and C's calc path.
PanelSet's shared breadbar-dismiss surface already handles click-away for
the wifi/control/media popovers; this adds the pieces the capsule needs to
reuse it rather than a second mechanism: an extra on_dismiss callback
invoked alongside hide_all() on every dismiss click, and
show_capsule_dismiss/hide_dismiss, which show the scrim with its
clickable region starting at a caller-given top margin instead of the
popover default.
That margin matters: breadbar-dismiss's layer (overlay) always renders
above the bar's own (top), so if its clickable region ever reached up
into where the drawer is actually drawn, it would swallow clicks meant
for a result row instead of the click-away. The capsule's own call site
(main.rs, not part of this commit) always passes a margin sized to the
drawer's maximum possible height, never its live one, so this can never
happen regardless of how tall the drawer currently is.
Not yet wired into the capsule's open/close — PanelSet's three new
methods are unused until that follow-up commit, hence the transient
dead_code warnings.
EventControllerKey defaults to PropagationPhase::Bubble. GtkEntry handles
Return in the target phase itself — emitting activate and returning TRUE —
which stops propagation before a bubble-phase controller runs. The capsule's
Enter-to-launch handler was therefore never reached: the selected app never
launched, with no error, because the keypress was consumed upstream.
Capture phase puts the controller ahead of the entry for the keys it claims
(Return/Up/Down/Escape); everything else still Proceeds to the entry so
normal text input is unaffected.
The drawer's spring easing overshoots past t=1.0 by design — the bounce is
the point on expand. On collapse (from=content height, to=0) that same
overshoot carried the interpolated value below zero, and set_size_request
hard-asserts height >= -1, so a GTK-CRITICAL fired once per frame for the
whole 360ms close. Clamp to 0 rather than -1: -1 is GTK's use-natural-height
sentinel, which is not what a closing drawer wants.
reconcile_widgets re-ran its undeliverable-widget warning on every reconcile,
and breadd re-pushes specs continuously for any timer-driven widget, so a
one-off diagnostic became unbounded log spam under a theme whose slots don't
name that widget's placement. Warn once per widget id instead.
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.
bread-theme was already pinned to v0.7.4 while bread-utils and
bread-screenshots trailed at v0.7.2, even though bread-ecosystem locks
all workspace packages together as of a9754d9. Bump the two lagging
pins so all three crates from that repo resolve to the same tag/commit.
(cherry picked from commit f0de82aafb547134e0edc9dac8d72c01586db57c)
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.
Parse FDO action pairs onto popup buttons, emit ActionInvoked on
click (default action on the body), and show an inline-reply field
when senders request it. Closing still emits NotificationClosed.
History persist is unchanged.
Keep a bounded in-memory history (last 50) and a layer-shell window
listing app, summary, truncated body, and time. Toggle with
`breadbar --history` or D-Bus `dev.breadway.Bar.ToggleHistory`.
Pin bread-theme, bread-utils, and bread-screenshots to tag v0.7.2.
bread-shared stays on bread v0.8.0-rc.1.
Delegates dev/rc/release build steps to bread-ecosystem's pinned Arch
container via ci/build.sh, pinned to bread-ecosystem commit
147cfbbf96ae4b171027defa1130d2caddb934b1. Adds a check.yml workflow
for fast clippy/test signal on feature/fix branches, matching the
pattern proven in breadpad. No ci/deps.txt needed — breadbar's
dependencies (zbus, resvg, gtk4-layer-shell, hyprland) are all pure
Rust or already covered by the shared image's package set.
bread-theme moves to git.breadway.dev v0.7.1. bread-utils and
bread-screenshots pin ecosystem origin/main (v0.7.1 has no
BreadClient::request and predates the screenshots crate). bread-shared
pins bread v0.8.0-rc.1 — latest existing tag with widget wire types
(v0.7.0 has none; v0.8.0 was never cut).
bakery.toml sets bread_deps = ["bread"] so the daemon is installed
alongside the bar. CLAUDE.md records the single-trunk workflow and
the bar / notifications / OSD / widgets split.
Replaces the dev/beta branch split with one trunk (main): dev-track
builds still publish on every push, but the beta track now publishes
from a vX.Y.Z-rc.N prerelease tag instead of a separately-maintained
beta branch. Removes the branch nobody reliably kept in sync.