mur_common/hub/
trigger.rs1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq)]
8pub enum DwellSpec {
9 Seconds(f64),
10 UntilDone,
11 UntilAck,
12 UntilActive,
13 Lipsync,
14 UntilFocusOff,
15}
16
17impl<'de> Deserialize<'de> for DwellSpec {
18 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
19 struct Visitor;
20 impl<'de> serde::de::Visitor<'de> for Visitor {
21 type Value = DwellSpec;
22 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 write!(
24 f,
25 "a number or one of until_done/until_ack/until_active/lipsync/until_focus_off"
26 )
27 }
28 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
29 Ok(DwellSpec::Seconds(v as f64))
30 }
31 fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Self::Value, E> {
32 Ok(DwellSpec::Seconds(v))
33 }
34 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
35 Ok(DwellSpec::Seconds(v as f64))
36 }
37 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
38 match v {
39 "until_done" => Ok(DwellSpec::UntilDone),
40 "until_ack" => Ok(DwellSpec::UntilAck),
41 "until_active" => Ok(DwellSpec::UntilActive),
42 "lipsync" => Ok(DwellSpec::Lipsync),
43 "until_focus_off" => Ok(DwellSpec::UntilFocusOff),
44 _ => Err(E::unknown_variant(
45 v,
46 &[
47 "until_done",
48 "until_ack",
49 "until_active",
50 "lipsync",
51 "until_focus_off",
52 ],
53 )),
54 }
55 }
56 }
57 d.deserialize_any(Visitor)
58 }
59}
60
61impl Serialize for DwellSpec {
62 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
63 match self {
64 DwellSpec::Seconds(n) => s.serialize_f64(*n),
65 DwellSpec::UntilDone => s.serialize_str("until_done"),
66 DwellSpec::UntilAck => s.serialize_str("until_ack"),
67 DwellSpec::UntilActive => s.serialize_str("until_active"),
68 DwellSpec::Lipsync => s.serialize_str("lipsync"),
69 DwellSpec::UntilFocusOff => s.serialize_str("until_focus_off"),
70 }
71 }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct ExpressionTrigger {
78 pub on: String,
80 pub expression: String,
82 pub dwell_s: DwellSpec,
83 #[serde(default)]
84 pub bubble: bool,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub message: Option<String>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct TriggerSet {
93 pub triggers: Vec<ExpressionTrigger>,
94}
95
96const DEFAULT_YAML: &str = include_str!("triggers/default.yaml");
99
100pub fn default_triggers() -> Vec<ExpressionTrigger> {
101 serde_yaml_ng::from_str::<TriggerSet>(DEFAULT_YAML)
102 .expect("built-in default.yaml is always valid")
103 .triggers
104}
105
106pub fn load_triggers(agent_dir: &Path) -> Vec<ExpressionTrigger> {
108 let path = agent_dir.join("triggers.yaml");
109 if let Ok(text) = std::fs::read_to_string(&path)
110 && let Ok(ts) = serde_yaml_ng::from_str::<TriggerSet>(&text)
111 {
112 return ts.triggers;
113 }
114 default_triggers()
115}
116
117#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn default_triggers_count() {
125 assert_eq!(default_triggers().len(), 10);
126 }
127
128 #[test]
129 fn dwell_spec_numeric() {
130 let t: TriggerSet =
131 serde_yaml_ng::from_str("triggers:\n - { on: x, expression: idle, dwell_s: 3.5 }")
132 .unwrap();
133 assert_eq!(t.triggers[0].dwell_s, DwellSpec::Seconds(3.5));
134 }
135
136 #[test]
137 fn dwell_spec_until_ack() {
138 let t: TriggerSet = serde_yaml_ng::from_str(
139 "triggers:\n - { on: x, expression: error, dwell_s: until_ack }",
140 )
141 .unwrap();
142 assert_eq!(t.triggers[0].dwell_s, DwellSpec::UntilAck);
143 }
144
145 #[test]
146 fn all_known_dwells_parse() {
147 let yaml = "triggers:
148 - { on: a, expression: idle, dwell_s: 2 }
149 - { on: b, expression: idle, dwell_s: until_done }
150 - { on: c, expression: idle, dwell_s: until_ack }
151 - { on: d, expression: idle, dwell_s: until_active }
152 - { on: e, expression: idle, dwell_s: lipsync }
153 - { on: f, expression: idle, dwell_s: until_focus_off }";
154 let ts: TriggerSet = serde_yaml_ng::from_str(yaml).unwrap();
155 assert_eq!(ts.triggers.len(), 6);
156 }
157}