bread/Examples.md

9.5 KiB

Bread Examples

These examples show how to translate existing Hyprland automation into Bread's event-driven Lua runtime.

Each snippet is designed to be drop-in friendly for a ~/.config/bread/modules/*.lua file. Start with a new module file and require it from ~/.config/bread/init.lua.

Example 1: Porting keyboard_and_display_watcher.sh (system script)

Source inspiration: ~/.config/hypr/scripts/system/keyboard_and_display_watcher.sh.

This example covers two parts that port cleanly to Bread:

  • Start/stop the Redox layout viewer when the keyboard appears
  • Start/stop a display sync service when an external monitor appears
-- ~/.config/bread/modules/redox_and_display.lua
local M = bread.module({ name = "redox_and_display", version = "1.0.0" })

local PREVIEW_CMD = "/home/breadway/redox-layout-viewer/target/release/redox-layout-viewer"
local APP_NAME = "redox-layout-vi"

local function start_viewer()
    bread.exec("pgrep -f '" .. APP_NAME .. "' >/dev/null || " .. PREVIEW_CMD .. " >/dev/null 2>&1 &")
end

local function stop_viewer()
    bread.exec("pkill -f '" .. APP_NAME .. "' >/dev/null 2>&1 || true")
end

local function is_redox(event)
    -- Inspect event.data.raw once to find stable identifiers in your environment.
    -- Typical udev fields include id_vendor, id_model, id_vendor_id, id_model_id, and name.
    local raw = event.data and event.data.raw or {}
    local name = tostring(raw.name or "")
    local vendor = tostring(raw.id_vendor or "")
    local model = tostring(raw.id_model or "")

    return name:lower():find("redox", 1, true)
        or vendor:lower():find("redox", 1, true)
        or model:lower():find("redox", 1, true)
end

local external_monitors = 0

local function update_display_service()
    if external_monitors > 0 then
        bread.exec("systemctl --user start hypr-display-sync.service")
    else
        bread.exec("systemctl --user stop hypr-display-sync.service")
    end
end

function M.on_load()
    bread.on("bread.device.keyboard.connected", function(event)
        if is_redox(event) then
            start_viewer()
        end
    end)

    bread.on("bread.device.keyboard.disconnected", function(event)
        if is_redox(event) then
            stop_viewer()
        end
    end)

    bread.on("bread.monitor.connected", function(event)
        local name = event.data and (event.data.name or event.data.raw) or ""
        -- ignore internal panel (eDP-1) and count only externals
        if not tostring(name):match("eDP%-1") then
            external_monitors = external_monitors + 1
            update_display_service()
        end
    end)

    bread.on("bread.monitor.disconnected", function(event)
        local name = event.data and (event.data.name or event.data.raw) or ""
        if not tostring(name):match("eDP%-1") then
            external_monitors = math.max(0, external_monitors - 1)
            update_display_service()
        end
    end)
end

return M

Notes:

  • Use bread.log(event.data.raw) once to see your exact udev fields for matching.
  • This drops polling and relies on udev/Hyprland events.

Example 2: Porting autostart.lua

Source inspiration: ~/.config/hypr/scripts/system/autostart.lua.

-- ~/.config/bread/modules/autostart.lua
local M = bread.module({ name = "autostart", version = "1.0.0" })

local home = os.getenv("HOME") or "/home/breadway"
local startup_commands = {
    "wal -R",
    home .. "/colorshell/build/colorshell",
    "awww-daemon",
    "awww restore",
    home .. "/.config/hypr/scripts/system/keyboard_and_display_watcher.sh",
    home .. "/.config/hypr/watch_hypr_scripts.sh",
    "systemctl --user daemon-reload",
    "systemctl --user start hypr-display-sync.service",
    "systemctl --user start hyprpolkitagent",
    "dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP",
    "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
    "flatpak run dev.deedles.Trayscale",
    "wificonf init",
    "pkill -f hyprpaper",
}

function M.on_load()
    bread.once("bread.system.startup", function()
        for _, cmd in ipairs(startup_commands) do
            bread.exec(cmd)
        end
    end)
end

return M

Example 3: Porting display/monitors.lua

Source inspiration: ~/.config/hypr/scripts/display/monitors.lua.

This uses Bread events and Hyprland keywords to update monitor layout when external displays change.

-- ~/.config/bread/modules/monitors.lua
local M = bread.module({ name = "monitors", version = "1.0.0" })

local function apply_internal_mode(has_external)
    local mode = has_external and "1920x1080@60" or "1920x1200@60"
    bread.hyprland.keyword("monitor", "eDP-1, " .. mode .. ", 0x0, 1")
end

local function apply_external()
    bread.hyprland.keyword("monitor", "DP-3, 1920x1080@60, auto, 1, mirror, eDP-1")
end

local externals = 0
local function update()
    apply_internal_mode(externals > 0)
    if externals > 0 then
        apply_external()
    end
end

function M.on_load()
    bread.on("bread.monitor.connected", function(event)
        local name = tostring((event.data and (event.data.name or event.data.raw)) or "")
        if not name:match("eDP%-1") then
            externals = externals + 1
            update()
        end
    end)

    bread.on("bread.monitor.disconnected", function(event)
        local name = tostring((event.data and (event.data.name or event.data.raw)) or "")
        if not name:match("eDP%-1") then
            externals = math.max(0, externals - 1)
            update()
        end
    end)

    bread.once("bread.system.startup", function()
        update()
    end)
end

return M

Example 4: Multi-step automation workflows

The examples above are all single-event reactions: one trigger, one handler. bread.wait/bread.spawn (coroutine-based waits) and the bread.workflow table build on those to let a module walk through several ordered steps — with a timeout on any wait, and an overall deadline for the whole thing — rather than each step having to re-derive "am I still in the middle of handling the last event."

Full source: examples/modules/dock-workflow.lua.

-- ~/.config/bread/modules/dock-workflow.lua
local M = bread.module({ name = "dock-workflow", version = "1.0.0" })

bread.workflow.define("dock-connected", function()
    bread.workflow.step("applying layout")
    bread.hyprland.keyword("monitor", "HDMI-A-1, preferred, 1920x0, 1")

    bread.workflow.step("waiting for monitor")
    -- wait_any resolves on whichever of these patterns fires first, or
    -- returns nil after the timeout — it does not block forever.
    local event = bread.wait_any(
        { "bread.monitor.connected", "bread.hyprland.event" },
        { timeout = 5000 }
    )
    if not event then
        error("monitor did not appear in time")
    end

    bread.workflow.step("activating profile")
    bread.profile.activate("docked")

    bread.workflow.step("waiting for workspace")
    bread.wait("bread.workspace.changed", { timeout = 3000 })

    bread.workflow.step("notifying")
    bread.notify("Dock connected", { title = "bread" })
end)

function M.on_load()
    bread.on("bread.device.dock.connected", function()
        -- The deadline covers the whole run, independent of each step's
        -- own wait timeout — a safety net against something hanging.
        bread.workflow.start("dock-connected", { deadline = 15000 })
    end)
end

return M

Walking through what each piece buys you over a plain bread.on handler:

  • bread.workflow.define(name, fn) / .start(name, opts) — registers the body under a name, then runs it as a bread.spawned coroutine. opts.deadline (ms) marks the run timed_out in the registry if it hasn't reached a terminal state in time — this is independent of, and layered on top of, any per-step timeout inside the body. opts.args is passed through as the single argument to the body function, if you need to parameterize a run.
  • bread.workflow.step(label) — call it from inside the running body to record "currently here." It doesn't change control flow at all; it exists purely so bread.workflow.status("dock-connected") (or the CLI/dashboard) can answer "is this stuck, and where?" instead of a black box between start and finish.
  • bread.wait_any(patterns, opts) — like bread.wait, but resolves on the first of several patterns (useful when you're not sure which specific event a compositor/adapter will actually emit for a given transition — see the two candidate patterns above). bread.wait_all(patterns, opts) is the complementary primitive: it resolves once every listed pattern has fired at least once (or the timeout elapses), returning a table keyed by pattern.
  • Errors are captured, not lost. If the body raises (as it does above when the monitor never appears), the workflow's registry entry moves to failed with the message attached — visible via bread.workflow.status(name) or the workflows.list IPC method — rather than only ever showing up as a one-line log the moment it happened.

Check on a running (or finished) workflow via the IPC method directly (there's no dedicated bread subcommand for this yet — see workflows.list in the IPC protocol dictionary):

echo '{"id":"1","method":"workflows.list","params":{}}' | nc -U -q0 "$XDG_RUNTIME_DIR/bread/breadd.sock"

Tips for porting your own scripts

  • Start by logging the event payload: bread.log(event.data.raw)
  • Replace polling loops with event subscriptions
  • Use bread.exec for shell commands and systemd operations
  • Use bread.state.watch for data that already lives in the runtime state