Will change this commit message to mean something later
Some checks failed
dev release / build (push) Failing after 0s

This commit is contained in:
Breadway 2026-07-22 19:52:16 +08:00
parent f3905d8114
commit 8d3f55b607
14 changed files with 1491 additions and 40 deletions

View file

@ -21,6 +21,11 @@ bread reload
| `low-battery-warning.lua` | Critical notification once when the battery runs low; resets on AC. | none |
| `pause-media-on-headphone-unplug.lua` | Runs `playerctl pause` when a headphone/earbud device disconnects. | none (needs `playerctl`) |
| `dock-monitors.lua` | Applies a multi-monitor layout when an external display connects, reverts when removed. | edit output names/resolutions |
| `active-window-widget.lua` | Shows the focused window next to the workspace pills in breadbar, via `bread.widget` + `bread.state.watch`. | none |
| `cpu-temp-widget.lua` | Live CPU temperature readout in breadbar's stats area, via `bread.widget` + `bread.fs.read` on a timer. | edit `TEMP_PATH` for your hwmon layout |
| `bluetooth-toggle-widget.lua` | One-click Bluetooth power toggle in breadbar's tray, via `bread.widget` + a click handler. | none |
| `focus-mode-widget.lua` | Click-to-toggle "Focus" profile that mutes audio; a widget as an action launcher, not just a readout, and stays in sync with profile changes triggered elsewhere. | none (needs `wpctl`) |
| `workflow-status-widget.lua` | Surfaces `bread.workflow.list()` in breadbar's tray — shows whichever workflow (e.g. `dock-workflow.lua`, below) is currently running or failed, hidden otherwise. | none |
Each module is the standard skeleton — `bread.module{...}`, an `on_load` that
registers subscriptions, `return M` — so they double as references for writing

View file

@ -0,0 +1,46 @@
-- active-window-widget — shows the focused app right next to the
-- workspace pills, live-updated via bread.state.watch (no polling, no
-- bread.on needed).
--
-- Demonstrates: WidgetPlacement "right_of_workspaces", a state-watch-driven
-- widget (as opposed to a timer or event handler), and calling
-- bread.widget.update from inside the watch callback. breadbar's own bar
-- doesn't show the focused window anywhere today — this adds that for free.
--
-- Drop-in: copy into ~/.config/bread/modules/. Zero configuration.
local M = bread.module({ name = "active-window-widget", version = "1.0.0" })
local function label_for(window)
-- A JSON null (no window focused) doesn't necessarily arrive as Lua nil
-- through bread.state.* — mlua's serde bridge can hand back a distinct
-- null sentinel instead, which `not window` won't catch. Guard on the
-- type directly so any non-string value (nil, the sentinel, ...) falls
-- through to the placeholder rather than crashing on #window below.
if type(window) ~= "string" or window == "" then
return ""
end
if #window > 24 then
return window:sub(1, 24) .. ""
end
return window
end
local function widget_root(window)
return { type = "label", text = label_for(window), style = { color = "dim" } }
end
function M.on_load()
bread.widget.register({
id = "active-window",
placement = "right_of_workspaces",
tooltip = "Currently focused window",
root = widget_root(bread.state.active_window()),
})
bread.state.watch("active_window", function(new_val)
bread.widget.update("active-window", { root = widget_root(new_val) })
end)
end
return M

View file

@ -0,0 +1,66 @@
-- bluetooth-toggle-widget — a real one-click Bluetooth power toggle in the
-- hamburger popover's tray section, showing power state and connected
-- device count. breadbar's native bar only shows a passive BT icon; this
-- adds an actual control surface for it.
--
-- Demonstrates: WidgetPlacement "tray", a bread.every-polled read of
-- bread.bluetooth.devices()/.powered(), and bread.bar.widget_clicked
-- driving a real action (bread.bluetooth.power) rather than just display.
--
-- Drop-in: copy into ~/.config/bread/modules/. Zero configuration.
local M = bread.module({ name = "bluetooth-toggle-widget", version = "1.0.0" })
local function widget_root()
local powered = bread.bluetooth.powered()
local devices = bread.bluetooth.devices() or {}
local connected = 0
for _, d in ipairs(devices) do
if d.connected then
connected = connected + 1
end
end
local text, style
if powered == nil then
text, style = "BT n/a", { color = "dim" }
elseif not powered then
text, style = "BT off", { color = "dim" }
elseif connected > 0 then
text, style = "BT (" .. connected .. ")", { color = "accent", weight = "bold" }
else
text, style = "BT on", { color = "fg" }
end
return {
type = "label",
text = text,
style = style,
on_click = "toggle",
}
end
function M.on_load()
bread.widget.register({
id = "toggle",
placement = "tray",
tooltip = "Click to toggle Bluetooth power",
root = widget_root(),
})
bread.every(5000, function()
bread.widget.update("toggle", { root = widget_root() })
end)
bread.on("bread.bar.widget_clicked", function(e)
if e.data.widget_id == "bluetooth-toggle-widget.toggle" and e.data.action == "toggle" then
bread.bluetooth.power(not bread.bluetooth.powered())
-- Give BlueZ a moment to apply before refreshing the label.
bread.after(500, function()
bread.widget.update("toggle", { root = widget_root() })
end)
end
end)
end
return M

View file

@ -0,0 +1,64 @@
-- cpu-temp-widget — live CPU package temperature, read straight from the
-- k10temp hwmon sysfs node via bread.fs.read.
--
-- Demonstrates: WidgetPlacement "left_of_stats", a bread.every-polled
-- widget reading real hardware state (the same category of readout
-- breadbar's native CPU%/RAM stats already do in Rust — this shows it's
-- just as easy from a drop-in Lua module), and the typed `style` vocabulary
-- (color + weight) swapping based on a threshold so the widget visually
-- flags when something's hot — no CSS, no guessing which class names the
-- rendering app happens to define.
--
-- Drop-in: copy into ~/.config/bread/modules/. TEMP_PATH is specific to
-- this machine (AMD, k10temp) — find yours with:
-- grep -l k10temp /sys/class/hwmon/hwmon*/name
-- and adjust below; a missing/unreadable path just shows "—" rather than
-- erroring, since bread.fs.read returns nil (not an error) for that case.
local M = bread.module({ name = "cpu-temp-widget", version = "1.0.0" })
local TEMP_PATH = "/sys/class/hwmon/hwmon6/temp1_input"
local HOT_THRESHOLD_C = 80
local function read_temp_c()
local raw = bread.fs.read(TEMP_PATH)
if not raw then
return nil
end
return tonumber(raw) / 1000
end
local function widget_root(temp_c)
local text = temp_c and string.format("%.0f°C", temp_c) or ""
local hot = temp_c ~= nil and temp_c >= HOT_THRESHOLD_C
return {
type = "box",
children = {
{
type = "label",
text = text,
style = hot and { color = "red", weight = "bold" } or { color = "dim" },
},
{
type = "progress",
value = temp_c and math.min(temp_c / 100, 1.0) or 0,
style = hot and { color = "red" } or nil,
},
},
}
end
function M.on_load()
bread.widget.register({
id = "cpu-temp",
placement = "left_of_stats",
tooltip = "CPU package temperature (Tctl)",
root = widget_root(read_temp_c()),
})
bread.every(5000, function()
bread.widget.update("cpu-temp", { root = widget_root(read_temp_c()) })
end)
end
return M

View file

@ -0,0 +1,66 @@
-- focus-mode-widget — a widget that does something, not just shows
-- something: click to toggle a "focus" bread profile, which mutes audio
-- output as an observable effect. Stays in sync if the profile changes
-- from elsewhere too — the CLI (`bread profile-activate default`), another
-- module, another widget — not just from its own click, by reacting to
-- bread.profile.activated rather than tracking its own local state.
--
-- Demonstrates: a widget as an action launcher wired to bread's actual
-- profile primitive (bread.profile.activate), not just a passive readout;
-- staying in sync with state that can change from other sources; combining
-- bread.exec with a profile switch for a real, checkable effect.
--
-- Drop-in: copy into ~/.config/bread/modules/. Needs `wpctl` (pipewire —
-- already a dependency of breadbar's own volume slider, so if the bar's
-- volume control works, this will too).
local M = bread.module({ name = "focus-mode-widget", version = "1.0.0" })
local FOCUS_PROFILE = "focus"
local DEFAULT_PROFILE = "default"
local function is_focused()
return bread.state.profile().active == FOCUS_PROFILE
end
local function widget_root()
return {
type = "label",
text = "Focus",
style = is_focused() and { color = "accent", weight = "bold" } or { color = "dim" },
on_click = "toggle",
}
end
function M.on_load()
bread.widget.register({
id = "toggle",
placement = "left_of_clock",
tooltip = "Click to toggle Focus mode (mutes audio, activates the 'focus' profile)",
root = widget_root(),
})
-- Not just self.click -> self.update: any profile change, from any
-- source, is reflected here. Try `bread profile-activate default` from
-- a terminal while this is in "focus" state to see it flip on its own.
bread.on("bread.profile.activated", function()
bread.widget.update("toggle", { root = widget_root() })
end)
bread.on("bread.bar.widget_clicked", function(e)
if e.data.widget_id ~= "focus-mode-widget.toggle" or e.data.action ~= "toggle" then
return
end
if is_focused() then
bread.profile.activate(DEFAULT_PROFILE)
bread.exec("wpctl set-mute @DEFAULT_AUDIO_SINK@ 0")
bread.notify("Focus mode off", { title = "bread" })
else
bread.profile.activate(FOCUS_PROFILE)
bread.exec("wpctl set-mute @DEFAULT_AUDIO_SINK@ 1")
bread.notify("Focus mode on — audio muted", { title = "bread" })
end
end)
end
return M

View file

@ -0,0 +1,83 @@
-- workflow-status-widget — surfaces bread's workflow engine (bread.workflow,
-- see Examples.md's "Example 4" and dock-workflow.lua in this directory) in
-- the bar, which had zero visibility anywhere in the UI before this. Shows
-- whichever non-done workflow was most recently updated, with its current
-- step if it's set one via bread.workflow.step(); hides entirely when
-- nothing is running/failed/timed_out, so it stays out of the way until
-- there's actually something to look at.
--
-- Demonstrates: WidgetPlacement "tray", polling an existing bread subsystem
-- (bread.workflow.list()) instead of raw hardware or a single module's own
-- state, and a widget that disappears (visible = false) rather than
-- showing a stale or empty readout.
--
-- Drop-in: copy into ~/.config/bread/modules/. Zero configuration — it
-- reflects whatever workflow any other loaded module starts, including
-- dock-workflow.lua in this same directory.
local M = bread.module({ name = "workflow-status-widget", version = "1.0.0" })
local function most_relevant()
local workflows = bread.workflow.list()
local best = nil
for _, w in ipairs(workflows) do
if w.state ~= "done" and (not best or w.updated_at > best.updated_at) then
best = w
end
end
return best
end
local function widget_update()
local w = most_relevant()
if not w then
-- bread.widget.update leaves an omitted field unchanged, not
-- cleared — a Lua table can't distinguish "tooltip = nil" from
-- "no tooltip key at all", so an explicit "" is what actually wipes
-- the previous tooltip instead of leaving it stale under a hidden
-- widget (harmless in practice since GTK won't show a tooltip on
-- an invisible widget, but `bread state widgets` would otherwise
-- report it forever).
return { visible = false, tooltip = "", root = { type = "label", text = "" } }
end
local color = "dim"
if w.state == "failed" or w.state == "timed_out" then
color = "red"
elseif w.state == "running" then
color = "accent"
end
local text = w.name
if w.step then
text = text .. ": " .. w.step
end
return {
visible = true,
tooltip = "Workflow " .. w.name .. "" .. w.state,
root = { type = "label", text = text, style = { color = color } },
}
end
function M.on_load()
local u = widget_update()
bread.widget.register({
id = "status",
placement = "tray",
visible = u.visible,
tooltip = u.tooltip,
root = u.root,
})
bread.every(3000, function()
local next_u = widget_update()
bread.widget.update("status", {
visible = next_u.visible,
tooltip = next_u.tooltip,
root = next_u.root,
})
end)
end
return M