Namespace Hyprland events under bread.hyprland.*, keep legacy names via [compat]
normalize_hyprland() dual-emits each of its 10 flat event names (bread.workspace.changed, bread.monitor.connected, bread.window.opened, etc.) alongside a bread.hyprland.<rest> equivalent, so portable automation can subscribe to bread.hyprland.* and be guaranteed compositor-specific coverage without also matching genuinely cross-backend events like bread.power.*. The bread.hyprland.event fallback was already namespaced and is unaffected. Gated behind [compat] legacy_hyprland_event_names (default true during the deprecation window) so the old names keep firing until modules migrate; set to false to emit only the namespaced names. EventNormalizer::with_legacy_hyprland_event_names(bool) is a builder on top of the existing ::new(dedup_window_ms), threaded through from config in main.rs. Bumps API_VERSION to 1.5.0 (additive), updates Documentation.md's Hyprland event reference with Deprecated/Since markers and the new [compat] option, refreshes the README config example and the dock-workflow/git-branch-widget examples to the namespaced names, and adds DEPRECATIONS.md tracking the deferred full removal once the window closes.
This commit is contained in:
parent
96639516b1
commit
d270ac6ff7
9 changed files with 535 additions and 95 deletions
40
DEPRECATIONS.md
Normal file
40
DEPRECATIONS.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Deprecations
|
||||||
|
|
||||||
|
Tracks API surface currently in its deprecation window per
|
||||||
|
[`Documentation.md`'s API Stability & Versioning](Documentation.md#api-stability--versioning)
|
||||||
|
policy — marked `Deprecated` and still functioning, pending removal in a
|
||||||
|
future major version.
|
||||||
|
|
||||||
|
## Hyprland legacy flat event names (since v1.5)
|
||||||
|
|
||||||
|
**What's deprecated:** the 10 pre-namespace Hyprland event names emitted by
|
||||||
|
`normalize_hyprland()` in `breadd/src/core/normalizer.rs`:
|
||||||
|
|
||||||
|
- `bread.workspace.changed`, `bread.workspace.created`, `bread.workspace.destroyed`
|
||||||
|
- `bread.monitor.connected`, `bread.monitor.disconnected`
|
||||||
|
- `bread.window.focus.changed`, `bread.window.focused`, `bread.window.opened`,
|
||||||
|
`bread.window.closed`, `bread.window.moved`
|
||||||
|
|
||||||
|
**Recommended form going forward:** their `bread.hyprland.<rest>` equivalents
|
||||||
|
(e.g. `bread.hyprland.workspace.changed`), which make explicit that these
|
||||||
|
events are Hyprland-specific rather than portable across a future second
|
||||||
|
compositor backend — see `Documentation.md`'s
|
||||||
|
[Hyprland event reference](Documentation.md#hyprland) for the full mapping.
|
||||||
|
|
||||||
|
**Current behavior:** both names fire by default (`[compat]
|
||||||
|
legacy_hyprland_event_names = true`). Setting that flag to `false` suppresses
|
||||||
|
the legacy names; only `bread.hyprland.*` fires.
|
||||||
|
|
||||||
|
**Deferred follow-up (not yet scheduled):**
|
||||||
|
|
||||||
|
1. Flip `[compat] legacy_hyprland_event_names`'s *default* to `false` in a
|
||||||
|
later minor release, once downstream modules have had a full deprecation
|
||||||
|
window to migrate.
|
||||||
|
2. Remove the legacy flat names and the `[compat]` flag entirely in the next
|
||||||
|
major version (v2) — at that point `normalize_hyprland()` only ever
|
||||||
|
produces `bread.hyprland.*` names and the dual-emit machinery
|
||||||
|
(`emit_hyprland_dual` in `normalizer.rs`) can be deleted.
|
||||||
|
|
||||||
|
Neither step is scheduled yet; this file exists so the removal isn't
|
||||||
|
forgotten once the window closes. No issue tracker is wired up to this repo,
|
||||||
|
so this note is the tracking mechanism until one exists.
|
||||||
|
|
@ -909,19 +909,42 @@ Both USB/udev devices and Bluetooth devices emit `bread.device.connected` / `bre
|
||||||
|
|
||||||
#### Hyprland
|
#### Hyprland
|
||||||
|
|
||||||
|
*Since: v1.5 — the `bread.hyprland.*` namespaced forms below. Bread's event vocabulary is meant to be portable across a future second compositor backend; a flat `bread.workspace.*`/`bread.monitor.*`/`bread.window.*` name gave no way to tell a genuinely cross-backend event (like `bread.power.*`) apart from one that is Hyprland-specific. The 10 rows marked `Deprecated: v1.5` are unaffected functionally — they keep firing — but new automation should subscribe to their `bread.hyprland.*` sibling instead.*
|
||||||
|
|
||||||
|
Every Hyprland-sourced event below is dual-emitted: the daemon fires both the legacy flat name and its `bread.hyprland.<rest>` equivalent with identical `data`/`timestamp`/`source`, unless `[compat] legacy_hyprland_event_names = false` is set (see below), in which case only the namespaced name fires. A module that subscribes only to `bread.hyprland.*` always gets full workspace/monitor/window coverage regardless of that setting.
|
||||||
|
|
||||||
| Event | Data |
|
| Event | Data |
|
||||||
|-------|------|
|
|-------|------|
|
||||||
| `bread.workspace.changed` | raw payload |
|
| `bread.workspace.changed` *(Deprecated: v1.5 — use `bread.hyprland.workspace.changed`)* | raw payload |
|
||||||
| `bread.workspace.created` | `{ workspace }` |
|
| `bread.hyprland.workspace.changed` *(Since: v1.5)* | raw payload |
|
||||||
| `bread.workspace.destroyed` | `{ workspace }` |
|
| `bread.workspace.created` *(Deprecated: v1.5 — use `bread.hyprland.workspace.created`)* | `{ workspace }` |
|
||||||
| `bread.monitor.connected` | raw payload |
|
| `bread.hyprland.workspace.created` *(Since: v1.5)* | `{ workspace }` |
|
||||||
| `bread.monitor.disconnected` | raw payload |
|
| `bread.workspace.destroyed` *(Deprecated: v1.5 — use `bread.hyprland.workspace.destroyed`)* | `{ workspace }` |
|
||||||
| `bread.window.focus.changed` | raw payload |
|
| `bread.hyprland.workspace.destroyed` *(Since: v1.5)* | `{ workspace }` |
|
||||||
| `bread.window.focused` | `{ address }` |
|
| `bread.monitor.connected` *(Deprecated: v1.5 — use `bread.hyprland.monitor.connected`)* | raw payload |
|
||||||
| `bread.window.opened` | `{ address, workspace, class, title }` |
|
| `bread.hyprland.monitor.connected` *(Since: v1.5)* | raw payload |
|
||||||
| `bread.window.closed` | `{ address }` |
|
| `bread.monitor.disconnected` *(Deprecated: v1.5 — use `bread.hyprland.monitor.disconnected`)* | raw payload |
|
||||||
| `bread.window.moved` | `{ address, workspace }` |
|
| `bread.hyprland.monitor.disconnected` *(Since: v1.5)* | raw payload |
|
||||||
| `bread.hyprland.event` | `{ kind, raw, data }` (unhandled kinds) |
|
| `bread.window.focus.changed` *(Deprecated: v1.5 — use `bread.hyprland.window.focus.changed`)* | raw payload |
|
||||||
|
| `bread.hyprland.window.focus.changed` *(Since: v1.5)* | raw payload |
|
||||||
|
| `bread.window.focused` *(Deprecated: v1.5 — use `bread.hyprland.window.focused`)* | `{ address }` |
|
||||||
|
| `bread.hyprland.window.focused` *(Since: v1.5)* | `{ address }` |
|
||||||
|
| `bread.window.opened` *(Deprecated: v1.5 — use `bread.hyprland.window.opened`)* | `{ address, workspace, class, title }` |
|
||||||
|
| `bread.hyprland.window.opened` *(Since: v1.5)* | `{ address, workspace, class, title }` |
|
||||||
|
| `bread.window.closed` *(Deprecated: v1.5 — use `bread.hyprland.window.closed`)* | `{ address }` |
|
||||||
|
| `bread.hyprland.window.closed` *(Since: v1.5)* | `{ address }` |
|
||||||
|
| `bread.window.moved` *(Deprecated: v1.5 — use `bread.hyprland.window.moved`)* | `{ address, workspace }` |
|
||||||
|
| `bread.hyprland.window.moved` *(Since: v1.5)* | `{ address, workspace }` |
|
||||||
|
| `bread.hyprland.event` | `{ kind, raw, data }` (unhandled kinds — already namespaced, not part of this migration) |
|
||||||
|
|
||||||
|
##### Compatibility: `[compat]` config
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[compat]
|
||||||
|
legacy_hyprland_event_names = true # default during the deprecation window
|
||||||
|
```
|
||||||
|
|
||||||
|
Set to `false` to suppress the 10 legacy flat names above and emit only their `bread.hyprland.*` equivalents. This defaults to `true` for now; per the [API Stability & Versioning](#api-stability--versioning) deprecation-window policy, the default will flip to `false` in a later release once the window closes. Removing the legacy names entirely is a further, separate follow-up — see the note in `DEPRECATIONS.md`.
|
||||||
|
|
||||||
#### Power
|
#### Power
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Instead of scattering behavior across shell scripts, compositor configs, udev ru
|
||||||
Bread runs a long-lived daemon (`breadd`) that:
|
Bread runs a long-lived daemon (`breadd`) that:
|
||||||
|
|
||||||
1. Ingests raw signals from your compositor, hardware, and OS
|
1. Ingests raw signals from your compositor, hardware, and OS
|
||||||
2. Normalizes them into stable, semantic events (`bread.device.dock.connected`, `bread.monitor.connected`, etc.)
|
2. Normalizes them into stable, semantic events (`bread.device.dock.connected`, `bread.hyprland.monitor.connected`, etc.)
|
||||||
3. Maintains a live model of your desktop state
|
3. Maintains a live model of your desktop state
|
||||||
4. Delivers those events to Lua modules that implement your automation
|
4. Delivers those events to Lua modules that implement your automation
|
||||||
|
|
||||||
|
|
@ -144,6 +144,9 @@ enabled = true
|
||||||
[events]
|
[events]
|
||||||
dedup_window_ms = 100
|
dedup_window_ms = 100
|
||||||
|
|
||||||
|
[compat]
|
||||||
|
legacy_hyprland_event_names = true # dual-emits bread.hyprland.* alongside legacy flat names; see Documentation.md
|
||||||
|
|
||||||
[notifications]
|
[notifications]
|
||||||
default_timeout_ms = 5000
|
default_timeout_ms = 5000
|
||||||
default_urgency = "normal"
|
default_urgency = "normal"
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ pub struct Config {
|
||||||
pub notifications: NotificationsConfig,
|
pub notifications: NotificationsConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub events: EventsConfig,
|
pub events: EventsConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub compat: CompatConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
|
@ -119,6 +121,22 @@ pub struct EventsConfig {
|
||||||
pub dedup_window_ms: u64,
|
pub dedup_window_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deprecation-window toggles for backwards compatibility with pre-namespace
|
||||||
|
/// event names. See `[compat]` in `breadd.toml` / `Documentation.md`'s
|
||||||
|
/// Hyprland event reference for the migration this gates.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct CompatConfig {
|
||||||
|
/// When `true` (the default during the deprecation window), the Hyprland
|
||||||
|
/// adapter dual-emits both its legacy flat event names (e.g.
|
||||||
|
/// `bread.workspace.changed`) and their namespaced `bread.hyprland.*`
|
||||||
|
/// equivalents (e.g. `bread.hyprland.workspace.changed`). Set to `false`
|
||||||
|
/// to suppress the legacy names and emit only the namespaced ones — this
|
||||||
|
/// will become the default in a later release once the deprecation
|
||||||
|
/// window closes.
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub legacy_hyprland_event_names: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct NotificationsConfig {
|
pub struct NotificationsConfig {
|
||||||
#[serde(default = "default_notify_timeout")]
|
#[serde(default = "default_notify_timeout")]
|
||||||
|
|
@ -218,6 +236,14 @@ impl Default for NotificationsConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for CompatConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
legacy_hyprland_event_names: default_true(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load() -> Result<Self> {
|
pub fn load() -> Result<Self> {
|
||||||
let path = config_path();
|
let path = config_path();
|
||||||
|
|
@ -379,6 +405,7 @@ mod tests {
|
||||||
assert_eq!(cfg.notifications.notify_send_path, "notify-send");
|
assert_eq!(cfg.notifications.notify_send_path, "notify-send");
|
||||||
assert!(cfg.modules.builtin);
|
assert!(cfg.modules.builtin);
|
||||||
assert!(cfg.modules.disable.is_empty());
|
assert!(cfg.modules.disable.is_empty());
|
||||||
|
assert!(cfg.compat.legacy_hyprland_event_names);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -394,6 +421,7 @@ mod tests {
|
||||||
let cfg: Config = toml::from_str("").unwrap();
|
let cfg: Config = toml::from_str("").unwrap();
|
||||||
assert_eq!(cfg.daemon.log_level, "info");
|
assert_eq!(cfg.daemon.log_level, "info");
|
||||||
assert!(cfg.adapters.hyprland.enabled);
|
assert!(cfg.adapters.hyprland.enabled);
|
||||||
|
assert!(cfg.compat.legacy_hyprland_event_names);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -435,6 +463,9 @@ dedup_window_ms = 250
|
||||||
default_timeout_ms = 1000
|
default_timeout_ms = 1000
|
||||||
default_urgency = "critical"
|
default_urgency = "critical"
|
||||||
notify_send_path = "/usr/local/bin/notify-send"
|
notify_send_path = "/usr/local/bin/notify-send"
|
||||||
|
|
||||||
|
[compat]
|
||||||
|
legacy_hyprland_event_names = false
|
||||||
"#;
|
"#;
|
||||||
let cfg: Config = toml::from_str(raw).unwrap();
|
let cfg: Config = toml::from_str(raw).unwrap();
|
||||||
assert_eq!(cfg.daemon.log_level, "debug");
|
assert_eq!(cfg.daemon.log_level, "debug");
|
||||||
|
|
@ -453,6 +484,7 @@ notify_send_path = "/usr/local/bin/notify-send"
|
||||||
assert_eq!(cfg.events.dedup_window_ms, 250);
|
assert_eq!(cfg.events.dedup_window_ms, 250);
|
||||||
assert_eq!(cfg.notifications.default_timeout_ms, 1000);
|
assert_eq!(cfg.notifications.default_timeout_ms, 1000);
|
||||||
assert_eq!(cfg.notifications.default_urgency, "critical");
|
assert_eq!(cfg.notifications.default_urgency, "critical");
|
||||||
|
assert!(!cfg.compat.legacy_hyprland_event_names);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -466,6 +498,23 @@ log_level = "trace"
|
||||||
// Untouched sections still get their defaults.
|
// Untouched sections still get their defaults.
|
||||||
assert!(cfg.adapters.hyprland.enabled);
|
assert!(cfg.adapters.hyprland.enabled);
|
||||||
assert_eq!(cfg.events.dedup_window_ms, 100);
|
assert_eq!(cfg.events.dedup_window_ms, 100);
|
||||||
|
assert!(cfg.compat.legacy_hyprland_event_names);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compat_section_defaults_legacy_hyprland_names_to_true() {
|
||||||
|
let cfg = Config::default();
|
||||||
|
assert!(cfg.compat.legacy_hyprland_event_names);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compat_section_can_disable_legacy_hyprland_names() {
|
||||||
|
let raw = r#"
|
||||||
|
[compat]
|
||||||
|
legacy_hyprland_event_names = false
|
||||||
|
"#;
|
||||||
|
let cfg: Config = toml::from_str(raw).unwrap();
|
||||||
|
assert!(!cfg.compat.legacy_hyprland_event_names);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ pub struct EventNormalizer {
|
||||||
/// fired within the current window, so subsequent child-node events from the
|
/// fired within the current window, so subsequent child-node events from the
|
||||||
/// same plug-in are suppressed at the normalizer level.
|
/// same plug-in are suppressed at the normalizer level.
|
||||||
seen_devices: RwLock<HashMap<String, u64>>,
|
seen_devices: RwLock<HashMap<String, u64>>,
|
||||||
|
/// Mirrors `[compat] legacy_hyprland_event_names` in `breadd.toml`. When
|
||||||
|
/// `true` (the default during the deprecation window), `normalize_hyprland`
|
||||||
|
/// dual-emits both its legacy flat event names and their namespaced
|
||||||
|
/// `bread.hyprland.*` equivalents. See `with_legacy_hyprland_event_names`.
|
||||||
|
legacy_hyprland_event_names: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventNormalizer {
|
impl EventNormalizer {
|
||||||
|
|
@ -22,9 +27,21 @@ impl EventNormalizer {
|
||||||
dedup_window_ms,
|
dedup_window_ms,
|
||||||
recent: RwLock::new(HashMap::new()),
|
recent: RwLock::new(HashMap::new()),
|
||||||
seen_devices: RwLock::new(HashMap::new()),
|
seen_devices: RwLock::new(HashMap::new()),
|
||||||
|
legacy_hyprland_event_names: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Overrides whether the Hyprland adapter's legacy flat event names
|
||||||
|
/// (`bread.workspace.changed` etc.) keep firing alongside their
|
||||||
|
/// `bread.hyprland.*` equivalents. Defaults to `true` via `new`, mirroring
|
||||||
|
/// `[compat] legacy_hyprland_event_names`'s documented default during the
|
||||||
|
/// deprecation window; `main.rs` overrides this from
|
||||||
|
/// `config.compat.legacy_hyprland_event_names`.
|
||||||
|
pub fn with_legacy_hyprland_event_names(mut self, enabled: bool) -> Self {
|
||||||
|
self.legacy_hyprland_event_names = enabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn normalize(&self, raw: &RawEvent) -> Vec<BreadEvent> {
|
pub fn normalize(&self, raw: &RawEvent) -> Vec<BreadEvent> {
|
||||||
let mut out = match &raw.source {
|
let mut out = match &raw.source {
|
||||||
AdapterSource::Udev => self.normalize_udev(raw),
|
AdapterSource::Udev => self.normalize_udev(raw),
|
||||||
|
|
@ -169,87 +186,67 @@ impl EventNormalizer {
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
"workspace" | "workspacev2" => vec![BreadEvent {
|
"workspace" | "workspacev2" => {
|
||||||
event: "bread.workspace.changed".to_string(),
|
self.emit_hyprland_dual("bread.workspace.changed", raw.payload.clone(), raw)
|
||||||
timestamp: raw.timestamp,
|
}
|
||||||
source: AdapterSource::Hyprland,
|
"createworkspace" => {
|
||||||
data: raw.payload.clone(),
|
self.emit_hyprland_dual("bread.workspace.created", json!({ "workspace": data }), raw)
|
||||||
}],
|
}
|
||||||
"createworkspace" => vec![BreadEvent {
|
"destroyworkspace" => self.emit_hyprland_dual(
|
||||||
event: "bread.workspace.created".to_string(),
|
"bread.workspace.destroyed",
|
||||||
timestamp: raw.timestamp,
|
json!({ "workspace": data }),
|
||||||
source: AdapterSource::Hyprland,
|
raw,
|
||||||
data: json!({ "workspace": data }),
|
),
|
||||||
}],
|
"monitoradded" => {
|
||||||
"destroyworkspace" => vec![BreadEvent {
|
self.emit_hyprland_dual("bread.monitor.connected", json!({ "name": data }), raw)
|
||||||
event: "bread.workspace.destroyed".to_string(),
|
}
|
||||||
timestamp: raw.timestamp,
|
"monitorremoved" => {
|
||||||
source: AdapterSource::Hyprland,
|
self.emit_hyprland_dual("bread.monitor.disconnected", json!({ "name": data }), raw)
|
||||||
data: json!({ "workspace": data }),
|
}
|
||||||
}],
|
"activewindow" => {
|
||||||
"monitoradded" => vec![BreadEvent {
|
self.emit_hyprland_dual("bread.window.focus.changed", raw.payload.clone(), raw)
|
||||||
event: "bread.monitor.connected".to_string(),
|
}
|
||||||
timestamp: raw.timestamp,
|
|
||||||
source: AdapterSource::Hyprland,
|
|
||||||
data: json!({ "name": data }),
|
|
||||||
}],
|
|
||||||
"monitorremoved" => vec![BreadEvent {
|
|
||||||
event: "bread.monitor.disconnected".to_string(),
|
|
||||||
timestamp: raw.timestamp,
|
|
||||||
source: AdapterSource::Hyprland,
|
|
||||||
data: json!({ "name": data }),
|
|
||||||
}],
|
|
||||||
"activewindow" => vec![BreadEvent {
|
|
||||||
event: "bread.window.focus.changed".to_string(),
|
|
||||||
timestamp: raw.timestamp,
|
|
||||||
source: AdapterSource::Hyprland,
|
|
||||||
data: raw.payload.clone(),
|
|
||||||
}],
|
|
||||||
"activewindowv2" => {
|
"activewindowv2" => {
|
||||||
let fields = split_hyprland_fields(data);
|
let fields = split_hyprland_fields(data);
|
||||||
vec![BreadEvent {
|
self.emit_hyprland_dual(
|
||||||
event: "bread.window.focused".to_string(),
|
"bread.window.focused",
|
||||||
timestamp: raw.timestamp,
|
json!({
|
||||||
source: AdapterSource::Hyprland,
|
|
||||||
data: json!({
|
|
||||||
"address": fields.first().unwrap_or(&"")
|
"address": fields.first().unwrap_or(&"")
|
||||||
}),
|
}),
|
||||||
}]
|
raw,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
"openwindow" => {
|
"openwindow" => {
|
||||||
let fields = split_hyprland_fields(data);
|
let fields = split_hyprland_fields(data);
|
||||||
vec![BreadEvent {
|
self.emit_hyprland_dual(
|
||||||
event: "bread.window.opened".to_string(),
|
"bread.window.opened",
|
||||||
timestamp: raw.timestamp,
|
json!({
|
||||||
source: AdapterSource::Hyprland,
|
|
||||||
data: json!({
|
|
||||||
"address": fields.first().unwrap_or(&""),
|
"address": fields.first().unwrap_or(&""),
|
||||||
"workspace": fields.get(1).unwrap_or(&""),
|
"workspace": fields.get(1).unwrap_or(&""),
|
||||||
"class": fields.get(2).unwrap_or(&""),
|
"class": fields.get(2).unwrap_or(&""),
|
||||||
"title": fields.get(3).unwrap_or(&""),
|
"title": fields.get(3).unwrap_or(&""),
|
||||||
}),
|
}),
|
||||||
}]
|
raw,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
"closewindow" => {
|
"closewindow" => {
|
||||||
let fields = split_hyprland_fields(data);
|
let fields = split_hyprland_fields(data);
|
||||||
vec![BreadEvent {
|
self.emit_hyprland_dual(
|
||||||
event: "bread.window.closed".to_string(),
|
"bread.window.closed",
|
||||||
timestamp: raw.timestamp,
|
json!({ "address": fields.first().unwrap_or(&"") }),
|
||||||
source: AdapterSource::Hyprland,
|
raw,
|
||||||
data: json!({ "address": fields.first().unwrap_or(&"") }),
|
)
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
"movewindow" => {
|
"movewindow" => {
|
||||||
let fields = split_hyprland_fields(data);
|
let fields = split_hyprland_fields(data);
|
||||||
vec![BreadEvent {
|
self.emit_hyprland_dual(
|
||||||
event: "bread.window.moved".to_string(),
|
"bread.window.moved",
|
||||||
timestamp: raw.timestamp,
|
json!({
|
||||||
source: AdapterSource::Hyprland,
|
|
||||||
data: json!({
|
|
||||||
"address": fields.first().unwrap_or(&""),
|
"address": fields.first().unwrap_or(&""),
|
||||||
"workspace": fields.get(1).unwrap_or(&""),
|
"workspace": fields.get(1).unwrap_or(&""),
|
||||||
}),
|
}),
|
||||||
}]
|
raw,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
_ => vec![BreadEvent {
|
_ => vec![BreadEvent {
|
||||||
event: "bread.hyprland.event".to_string(),
|
event: "bread.hyprland.event".to_string(),
|
||||||
|
|
@ -260,6 +257,40 @@ impl EventNormalizer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emits a Hyprland event under its namespaced `bread.hyprland.<rest>`
|
||||||
|
/// name — always — plus, when `legacy_hyprland_event_names` is enabled
|
||||||
|
/// (the default during the deprecation window), a second `BreadEvent`
|
||||||
|
/// under the pre-namespace flat name (e.g. `bread.workspace.changed`).
|
||||||
|
/// This is the single mechanism behind Workstream C: the two names carry
|
||||||
|
/// identical `data`/`timestamp`/`source`, so a module can migrate to
|
||||||
|
/// `bread.hyprland.*` at its own pace without missing events either way.
|
||||||
|
fn emit_hyprland_dual(
|
||||||
|
&self,
|
||||||
|
legacy_event: &str,
|
||||||
|
data: Value,
|
||||||
|
raw: &RawEvent,
|
||||||
|
) -> Vec<BreadEvent> {
|
||||||
|
let rest = legacy_event.strip_prefix("bread.").unwrap_or(legacy_event);
|
||||||
|
let namespaced_event = format!("bread.hyprland.{rest}");
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(2);
|
||||||
|
if self.legacy_hyprland_event_names {
|
||||||
|
out.push(BreadEvent {
|
||||||
|
event: legacy_event.to_string(),
|
||||||
|
timestamp: raw.timestamp,
|
||||||
|
source: AdapterSource::Hyprland,
|
||||||
|
data: data.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out.push(BreadEvent {
|
||||||
|
event: namespaced_event,
|
||||||
|
timestamp: raw.timestamp,
|
||||||
|
source: AdapterSource::Hyprland,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_power(&self, raw: &RawEvent) -> Vec<BreadEvent> {
|
fn normalize_power(&self, raw: &RawEvent) -> Vec<BreadEvent> {
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
|
@ -710,6 +741,11 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Hyprland ─────────────────────────────────────────────────────────
|
// ─── Hyprland ─────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// `EventNormalizer::new` defaults `legacy_hyprland_event_names` to `true`
|
||||||
|
// (mirrors `[compat]`'s documented default), so by default every mapped
|
||||||
|
// Hyprland kind dual-emits: the legacy flat name plus its namespaced
|
||||||
|
// `bread.hyprland.*` sibling. See `emit_hyprland_dual`.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hyprland_workspace_change() {
|
fn hyprland_workspace_change() {
|
||||||
|
|
@ -721,8 +757,11 @@ mod tests {
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
let out = n.normalize(&ev);
|
let out = n.normalize(&ev);
|
||||||
assert_eq!(out.len(), 1);
|
assert_eq!(out.len(), 2);
|
||||||
assert_eq!(out[0].event, "bread.workspace.changed");
|
assert!(out.iter().any(|e| e.event == "bread.workspace.changed"));
|
||||||
|
assert!(out
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.event == "bread.hyprland.workspace.changed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -735,9 +774,14 @@ mod tests {
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
let out = n.normalize(&ev);
|
let out = n.normalize(&ev);
|
||||||
assert_eq!(out.len(), 1);
|
assert_eq!(out.len(), 2);
|
||||||
assert_eq!(out[0].event, "bread.window.focused");
|
assert!(out.iter().any(|e| e.event == "bread.window.focused"));
|
||||||
assert_eq!(out[0].data.get("address").unwrap(), "0xdeadbeef");
|
assert!(out
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.event == "bread.hyprland.window.focused"));
|
||||||
|
for ev in &out {
|
||||||
|
assert_eq!(ev.data.get("address").unwrap(), "0xdeadbeef");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -750,17 +794,25 @@ mod tests {
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
let out = n.normalize(&ev);
|
let out = n.normalize(&ev);
|
||||||
assert_eq!(out.len(), 1);
|
assert_eq!(out.len(), 2);
|
||||||
assert_eq!(out[0].event, "bread.window.opened");
|
assert!(out.iter().any(|e| e.event == "bread.window.opened"));
|
||||||
let d = &out[0].data;
|
assert!(out
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.event == "bread.hyprland.window.opened"));
|
||||||
|
for ev in &out {
|
||||||
|
let d = &ev.data;
|
||||||
assert_eq!(d.get("address").unwrap(), "0xabc");
|
assert_eq!(d.get("address").unwrap(), "0xabc");
|
||||||
assert_eq!(d.get("workspace").unwrap(), "2");
|
assert_eq!(d.get("workspace").unwrap(), "2");
|
||||||
assert_eq!(d.get("class").unwrap(), "firefox");
|
assert_eq!(d.get("class").unwrap(), "firefox");
|
||||||
assert_eq!(d.get("title").unwrap(), "Mozilla Firefox");
|
assert_eq!(d.get("title").unwrap(), "Mozilla Firefox");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hyprland_unknown_kind_falls_through_to_generic_event() {
|
fn hyprland_unknown_kind_falls_through_to_generic_event() {
|
||||||
|
// The `bread.hyprland.event` fallback is already namespaced, so it's
|
||||||
|
// exempt from dual-emit — it never had a legacy flat name to begin
|
||||||
|
// with.
|
||||||
let n = EventNormalizer::new(0);
|
let n = EventNormalizer::new(0);
|
||||||
let ev = raw(
|
let ev = raw(
|
||||||
AdapterSource::Hyprland,
|
AdapterSource::Hyprland,
|
||||||
|
|
@ -788,9 +840,274 @@ mod tests {
|
||||||
json!({"kind": "monitorremoved", "data": "HDMI-A-1"}),
|
json!({"kind": "monitorremoved", "data": "HDMI-A-1"}),
|
||||||
2,
|
2,
|
||||||
));
|
));
|
||||||
assert_eq!(added[0].event, "bread.monitor.connected");
|
assert_eq!(added.len(), 2);
|
||||||
assert_eq!(added[0].data.get("name").unwrap(), "HDMI-A-1");
|
assert!(added.iter().any(|e| e.event == "bread.monitor.connected"));
|
||||||
assert_eq!(removed[0].event, "bread.monitor.disconnected");
|
assert!(added
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.event == "bread.hyprland.monitor.connected"));
|
||||||
|
for ev in &added {
|
||||||
|
assert_eq!(ev.data.get("name").unwrap(), "HDMI-A-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(removed.len(), 2);
|
||||||
|
assert!(removed
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.event == "bread.monitor.disconnected"));
|
||||||
|
assert!(removed
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.event == "bread.hyprland.monitor.disconnected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (a) With the default config (`legacy_hyprland_event_names = true`),
|
||||||
|
/// every one of the 10 dual-emit mappings fires both its legacy flat
|
||||||
|
/// name and its namespaced `bread.hyprland.*` sibling, with identical
|
||||||
|
/// data on each.
|
||||||
|
#[test]
|
||||||
|
fn hyprland_dual_emit_covers_all_ten_mappings_by_default() {
|
||||||
|
let n = EventNormalizer::new(0);
|
||||||
|
let cases: &[(&str, &str, &str, &str)] = &[
|
||||||
|
(
|
||||||
|
"workspace",
|
||||||
|
"2",
|
||||||
|
"bread.workspace.changed",
|
||||||
|
"bread.hyprland.workspace.changed",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"workspacev2",
|
||||||
|
"2,name",
|
||||||
|
"bread.workspace.changed",
|
||||||
|
"bread.hyprland.workspace.changed",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"createworkspace",
|
||||||
|
"3",
|
||||||
|
"bread.workspace.created",
|
||||||
|
"bread.hyprland.workspace.created",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"destroyworkspace",
|
||||||
|
"3",
|
||||||
|
"bread.workspace.destroyed",
|
||||||
|
"bread.hyprland.workspace.destroyed",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"monitoradded",
|
||||||
|
"HDMI-A-1",
|
||||||
|
"bread.monitor.connected",
|
||||||
|
"bread.hyprland.monitor.connected",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"monitorremoved",
|
||||||
|
"HDMI-A-1",
|
||||||
|
"bread.monitor.disconnected",
|
||||||
|
"bread.hyprland.monitor.disconnected",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"activewindow",
|
||||||
|
"firefox,Mozilla Firefox",
|
||||||
|
"bread.window.focus.changed",
|
||||||
|
"bread.hyprland.window.focus.changed",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"activewindowv2",
|
||||||
|
"0xdead",
|
||||||
|
"bread.window.focused",
|
||||||
|
"bread.hyprland.window.focused",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"openwindow",
|
||||||
|
"0xabc>>2>>firefox>>Mozilla Firefox",
|
||||||
|
"bread.window.opened",
|
||||||
|
"bread.hyprland.window.opened",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"closewindow",
|
||||||
|
"0xabc",
|
||||||
|
"bread.window.closed",
|
||||||
|
"bread.hyprland.window.closed",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"movewindow",
|
||||||
|
"0xabc,2",
|
||||||
|
"bread.window.moved",
|
||||||
|
"bread.hyprland.window.moved",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (kind, data, legacy_event, namespaced_event) in cases {
|
||||||
|
let ev = raw(
|
||||||
|
AdapterSource::Hyprland,
|
||||||
|
"hypr",
|
||||||
|
json!({"kind": kind, "data": data}),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
let out = n.normalize(&ev);
|
||||||
|
assert_eq!(out.len(), 2, "kind {kind} should dual-emit exactly 2 events");
|
||||||
|
assert!(
|
||||||
|
out.iter().any(|e| &e.event == legacy_event),
|
||||||
|
"kind {kind} missing legacy event {legacy_event}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
out.iter().any(|e| &e.event == namespaced_event),
|
||||||
|
"kind {kind} missing namespaced event {namespaced_event}"
|
||||||
|
);
|
||||||
|
let legacy_data = out.iter().find(|e| &e.event == legacy_event).unwrap().data.clone();
|
||||||
|
let namespaced_data = out
|
||||||
|
.iter()
|
||||||
|
.find(|e| &e.event == namespaced_event)
|
||||||
|
.unwrap()
|
||||||
|
.data
|
||||||
|
.clone();
|
||||||
|
assert_eq!(
|
||||||
|
legacy_data, namespaced_data,
|
||||||
|
"kind {kind}: legacy and namespaced events should carry identical data"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (b) With `legacy_hyprland_event_names = false`, only the namespaced
|
||||||
|
/// `bread.hyprland.*` names fire — the legacy flat names are fully
|
||||||
|
/// suppressed, not just relegated to a secondary slot.
|
||||||
|
#[test]
|
||||||
|
fn hyprland_legacy_names_suppressed_when_compat_disabled() {
|
||||||
|
let n = EventNormalizer::new(0).with_legacy_hyprland_event_names(false);
|
||||||
|
let cases: &[(&str, &str, &str, &str)] = &[
|
||||||
|
(
|
||||||
|
"workspace",
|
||||||
|
"2",
|
||||||
|
"bread.workspace.changed",
|
||||||
|
"bread.hyprland.workspace.changed",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"createworkspace",
|
||||||
|
"3",
|
||||||
|
"bread.workspace.created",
|
||||||
|
"bread.hyprland.workspace.created",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"destroyworkspace",
|
||||||
|
"3",
|
||||||
|
"bread.workspace.destroyed",
|
||||||
|
"bread.hyprland.workspace.destroyed",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"monitoradded",
|
||||||
|
"HDMI-A-1",
|
||||||
|
"bread.monitor.connected",
|
||||||
|
"bread.hyprland.monitor.connected",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"monitorremoved",
|
||||||
|
"HDMI-A-1",
|
||||||
|
"bread.monitor.disconnected",
|
||||||
|
"bread.hyprland.monitor.disconnected",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"activewindow",
|
||||||
|
"firefox,Mozilla Firefox",
|
||||||
|
"bread.window.focus.changed",
|
||||||
|
"bread.hyprland.window.focus.changed",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"activewindowv2",
|
||||||
|
"0xdead",
|
||||||
|
"bread.window.focused",
|
||||||
|
"bread.hyprland.window.focused",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"openwindow",
|
||||||
|
"0xabc>>2>>firefox>>Mozilla Firefox",
|
||||||
|
"bread.window.opened",
|
||||||
|
"bread.hyprland.window.opened",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"closewindow",
|
||||||
|
"0xabc",
|
||||||
|
"bread.window.closed",
|
||||||
|
"bread.hyprland.window.closed",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"movewindow",
|
||||||
|
"0xabc,2",
|
||||||
|
"bread.window.moved",
|
||||||
|
"bread.hyprland.window.moved",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (kind, data, legacy_event, namespaced_event) in cases {
|
||||||
|
let ev = raw(
|
||||||
|
AdapterSource::Hyprland,
|
||||||
|
"hypr",
|
||||||
|
json!({"kind": kind, "data": data}),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
let out = n.normalize(&ev);
|
||||||
|
assert_eq!(
|
||||||
|
out.len(),
|
||||||
|
1,
|
||||||
|
"kind {kind} should emit exactly 1 event when legacy names are disabled"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
out[0].event, *namespaced_event,
|
||||||
|
"kind {kind} should emit only the namespaced event"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!out.iter().any(|e| &e.event == legacy_event),
|
||||||
|
"kind {kind} leaked legacy event {legacy_event} despite being disabled"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The already-namespaced fallback is unaffected either way.
|
||||||
|
let fallback = n.normalize(&raw(
|
||||||
|
AdapterSource::Hyprland,
|
||||||
|
"hypr",
|
||||||
|
json!({"kind": "submap", "data": "resize"}),
|
||||||
|
1,
|
||||||
|
));
|
||||||
|
assert_eq!(fallback.len(), 1);
|
||||||
|
assert_eq!(fallback[0].event, "bread.hyprland.event");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (c) A module that subscribes only to `bread.hyprland.*` gets full
|
||||||
|
/// coverage of workspace, monitor, and window activity — regardless of
|
||||||
|
/// the `[compat]` setting — since the namespaced name is unconditional.
|
||||||
|
/// This is the actual promise Workstream C makes: "portable automation"
|
||||||
|
/// only means something if the namespaced form alone is a complete feed.
|
||||||
|
#[test]
|
||||||
|
fn hyprland_namespace_only_subscriber_gets_full_coverage_regardless_of_compat() {
|
||||||
|
let kinds_and_data: &[(&str, &str)] = &[
|
||||||
|
("workspace", "2"),
|
||||||
|
("createworkspace", "3"),
|
||||||
|
("destroyworkspace", "3"),
|
||||||
|
("monitoradded", "HDMI-A-1"),
|
||||||
|
("monitorremoved", "HDMI-A-1"),
|
||||||
|
("activewindow", "firefox,Mozilla Firefox"),
|
||||||
|
("activewindowv2", "0xdead"),
|
||||||
|
("openwindow", "0xabc>>2>>firefox>>Mozilla Firefox"),
|
||||||
|
("closewindow", "0xabc"),
|
||||||
|
("movewindow", "0xabc,2"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for legacy_enabled in [true, false] {
|
||||||
|
let n = EventNormalizer::new(0).with_legacy_hyprland_event_names(legacy_enabled);
|
||||||
|
for (kind, data) in kinds_and_data {
|
||||||
|
let out = n.normalize(&raw(
|
||||||
|
AdapterSource::Hyprland,
|
||||||
|
"hypr",
|
||||||
|
json!({"kind": kind, "data": data}),
|
||||||
|
1,
|
||||||
|
));
|
||||||
|
let namespaced_hits = out
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.event.starts_with("bread.hyprland."))
|
||||||
|
.count();
|
||||||
|
assert_eq!(
|
||||||
|
namespaced_hits, 1,
|
||||||
|
"kind {kind} (legacy_enabled={legacy_enabled}) should always emit exactly \
|
||||||
|
one bread.hyprland.* event, matching a bread.hyprland.* subscriber's view"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Power ─────────────────────────────────────────────────────────────
|
// ─── Power ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -1053,9 +1370,14 @@ mod tests {
|
||||||
json!({"kind": "workspace", "data": "2"}),
|
json!({"kind": "workspace", "data": "2"}),
|
||||||
1100,
|
1100,
|
||||||
);
|
);
|
||||||
assert_eq!(n.normalize(&a).len(), 1);
|
// Hyprland's "workspace" kind dual-emits (legacy + namespaced name,
|
||||||
|
// see the Hyprland test section below), so each distinct payload
|
||||||
|
// produces 2 events rather than 1 — but the point of this test is
|
||||||
|
// that neither call is suppressed by dedup, since the payloads
|
||||||
|
// differ and thus so does the dedup key.
|
||||||
|
assert_eq!(n.normalize(&a).len(), 2);
|
||||||
// Different payloads = different dedup key
|
// Different payloads = different dedup key
|
||||||
assert_eq!(n.normalize(&b).len(), 1);
|
assert_eq!(n.normalize(&b).len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ use crate::lua::RuntimeHandle;
|
||||||
/// something new-but-additive (a binding, an event, an IPC param); bump the
|
/// something new-but-additive (a binding, an event, an IPC param); bump the
|
||||||
/// major version only for a breaking change, which should not happen inside
|
/// major version only for a breaking change, which should not happen inside
|
||||||
/// this daemon's v1 lifetime per that section's stated policy.
|
/// this daemon's v1 lifetime per that section's stated policy.
|
||||||
const API_VERSION: &str = "1.4.0";
|
const API_VERSION: &str = "1.5.0";
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,10 @@ async fn main() -> Result<()> {
|
||||||
shutdown_rx.clone(),
|
shutdown_rx.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
let normalizer = Arc::new(EventNormalizer::new(config.events.dedup_window_ms));
|
let normalizer = Arc::new(
|
||||||
|
EventNormalizer::new(config.events.dedup_window_ms)
|
||||||
|
.with_legacy_hyprland_event_names(config.compat.legacy_hyprland_event_names),
|
||||||
|
);
|
||||||
{
|
{
|
||||||
let normalizer = normalizer.clone();
|
let normalizer = normalizer.clone();
|
||||||
let normalized_tx = normalized_tx.clone();
|
let normalized_tx = normalized_tx.clone();
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ bread.workflow.define("dock-connected", function()
|
||||||
|
|
||||||
bread.workflow.step("waiting for monitor")
|
bread.workflow.step("waiting for monitor")
|
||||||
local event = bread.wait_any(
|
local event = bread.wait_any(
|
||||||
{ "bread.monitor.connected", "bread.hyprland.event" },
|
{ "bread.hyprland.monitor.connected", "bread.hyprland.event" },
|
||||||
{ timeout = 5000 }
|
{ timeout = 5000 }
|
||||||
)
|
)
|
||||||
if not event then
|
if not event then
|
||||||
|
|
@ -35,7 +35,7 @@ bread.workflow.define("dock-connected", function()
|
||||||
bread.profile.activate("docked")
|
bread.profile.activate("docked")
|
||||||
|
|
||||||
bread.workflow.step("waiting for workspace")
|
bread.workflow.step("waiting for workspace")
|
||||||
bread.wait("bread.workspace.changed", { timeout = 3000 })
|
bread.wait("bread.hyprland.workspace.changed", { timeout = 3000 })
|
||||||
|
|
||||||
bread.workflow.step("notifying")
|
bread.workflow.step("notifying")
|
||||||
bread.notify("Dock connected", { title = "bread" })
|
bread.notify("Dock connected", { title = "bread" })
|
||||||
|
|
|
||||||
|
|
@ -146,8 +146,8 @@ function M.on_load()
|
||||||
-- catch a branch switch inside the same still-focused tab (e.g. `git
|
-- catch a branch switch inside the same still-focused tab (e.g. `git
|
||||||
-- checkout` run without ever changing window focus), which produces no
|
-- checkout` run without ever changing window focus), which produces no
|
||||||
-- focus event at all.
|
-- focus event at all.
|
||||||
bread.on("bread.window.focused", update)
|
bread.on("bread.hyprland.window.focused", update)
|
||||||
bread.on("bread.window.focus.changed", update)
|
bread.on("bread.hyprland.window.focus.changed", update)
|
||||||
bread.every(2000, update)
|
bread.every(2000, update)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue