"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.
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.
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.
- 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.
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.
Was a bare gtk4::Window with no titlebar — GTK4's own minimal CSD
fallback for that case is a flat bar with plain system-font title text
and square corners, which is what actually made this look like a stray
window from a different decade next to the rest of the (rounded,
borderless) ecosystem. Now sets a real HeaderBar (which picks up the
window's existing title automatically) and gives the dialog + headerbar
matching rounded corners and dark theming via the existing
.wifi-add-dialog CSS class.
Also switched the Connect button off "suggested-action": GTK4's bundled
theme special-cases that exact class name for a native OS-accent-colour
feature that isn't a normal CSS rule and doesn't yield to any
background-color override this stylesheet adds, confirmed empirically
(a much more specific selector had zero effect). Moved to
"confirm-button", the same accent-button convention breadman/breadpad
already use.
Known residual issue, not resolved here: the Connect button still
doesn't render the accent colour even via "confirm-button" — narrowed
down to *some* button-specific styling quirk in this one dialog (window-
level rules like the rounded corners and dark background apply
correctly; a maximally-obvious magenta test rule on the same selector
also failed to render, ruling out a color-value mistake specifically),
but not root-caused further. Left as a known follow-up rather than
blocking the CSD fix, which was the actual ask.
Also removed two `cursor: pointer` CSS properties (on .clickable and
.media-widget) that GTK4 doesn't support as a CSS property at all — was
logging "Theme parser error: No property named cursor" on every
startup, doing nothing functionally either way.
breadbar is "a bar + the notification daemon + the OSD" — the screenshot
mode only covered the bar and its control-panel popover, missing seven
more distinct surfaces: the WiFi/Bluetooth connectivity popover (both
tabs), the media-controls popover, the standalone notification window
(both normal and critical urgency), the standalone OSD window (volume
and brightness), and the wifi add-network dialog. All ten views are now
--screenshot targets.
Two real refactors needed to make the standalone notification/OSD
windows screenshot-able at all, not just bigger match arms:
- Both windows are built deep inside an async task (`run_osd`/
`popup::run`), only reachable after the real event loop starts — no
window handle ever existed for a caller to hook `connect_map` on before
that. Window construction is now synchronous in `osd::spawn`/
`notifications::spawn`, handed to the async loop as a parameter instead
of created inside it.
- Screenshot mode seeds each with one fixed sample event (SampleKind) via
the same channel the real pactl/backlight/D-Bus sources feed, instead
of waiting for real hardware/dbus activity. For notifications
specifically this also means skipping the real
org.freedesktop.Notifications D-Bus registration entirely in screenshot
mode — claiming that well-known name would just race the real breadbar
(if running) for it, for no benefit, since nothing external needs to
reach a screenshot-only instance.
show_add_network_dialog gained an `on_build` hook (called before
`.present()`, the only point `connect_map` can still catch the map) so
screenshot mode can capture it without changing its one real call site's
behavior — and its `anchor` parameter widened from `&Button` to
`&impl IsA<Widget>` since the screenshot path anchors off a
`ToggleButton`, not a `Button`.
Renders a single view ("bar" or "control-panel"), captures it via
bread-screenshots, then exits — driven by bread-ecosystem's bread-capture
orchestrator, or runnable standalone for one-off captures.
Waits on GTK's map signal (plus a short settle delay) before capturing
rather than guessing a fixed sleep upfront: the control-panel view in
particular needs the popover's autohide disabled (a programmatic popup()
has no input-event serial to grab the Wayland seat with) and a longer
settle window so the CPU/RAM/PWR/GPU/network stats — populated by a
2-second background poller, not the popover's own open — have had at
least one full cycle to fill in before the capture fires.
--width/--height match whatever canvas bread-capture's isolation sizes
the compositor to, so the "bar" view's geometry doesn't depend on
querying anything at capture time.
Needs RelmApp::with_args(vec![]) (GLib's own arg parser otherwise rejects
--screenshot/--output before clap ever sees them) and
allow_multiple_instances(true) for screenshot runs specifically, since
GApplication is single-instance by default and this machine typically
already has a real breadbar running.
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