Skip to main content

taskers_runtime/
signals.rs

1use taskers_domain::{SignalEvent, SignalKind};
2
3const OSC_PREFIX: &str = "\u{1b}]777;taskers;";
4const BEL: char = '\u{7}';
5const ST: &str = "\u{1b}\\";
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct ParsedSignal {
9    pub kind: SignalKind,
10    pub message: Option<String>,
11    pub title: Option<String>,
12}
13
14#[derive(Debug, Default, Clone)]
15pub struct SignalStreamParser {
16    pending: String,
17}
18
19impl ParsedSignal {
20    pub fn into_event(self, source: impl Into<String>) -> SignalEvent {
21        SignalEvent::new(source, self.kind, self.message)
22    }
23}
24
25pub fn parse_signal_frames(buffer: &str) -> Vec<ParsedSignal> {
26    let mut parser = SignalStreamParser::default();
27    parser.push(buffer)
28}
29
30impl SignalStreamParser {
31    pub fn push(&mut self, chunk: &str) -> Vec<ParsedSignal> {
32        self.pending.push_str(chunk);
33
34        let mut frames = Vec::new();
35        let mut cursor = 0usize;
36        let mut keep_from = self.pending.len().saturating_sub(OSC_PREFIX.len());
37
38        while let Some(found) = self.pending[cursor..].find(OSC_PREFIX) {
39            let frame_start = cursor + found;
40            let content_start = frame_start + OSC_PREFIX.len();
41            let remainder = &self.pending[content_start..];
42
43            let Some((raw_frame, consumed)) = frame_slice(remainder) else {
44                keep_from = frame_start;
45                break;
46            };
47
48            if let Some(parsed) = parse_frame(raw_frame) {
49                frames.push(parsed);
50            }
51
52            cursor = content_start + consumed;
53            keep_from = cursor;
54        }
55
56        self.pending = self.pending[keep_from..].to_string();
57        frames
58    }
59}
60
61fn parse_frame(frame: &str) -> Option<ParsedSignal> {
62    let mut kind = None;
63    let mut message = None;
64    let mut title = None;
65
66    for part in frame.split(';') {
67        let (key, value) = part.split_once('=')?;
68        match key {
69            "kind" => {
70                kind = Some(match value {
71                    "started" => SignalKind::Started,
72                    "progress" => SignalKind::Progress,
73                    "completed" => SignalKind::Completed,
74                    "waiting_input" => SignalKind::WaitingInput,
75                    "error" => SignalKind::Error,
76                    "notification" => SignalKind::Notification,
77                    _ => return None,
78                });
79            }
80            "message" => message = Some(value.replace("%20", " ")),
81            "title" => title = Some(value.replace("%20", " ")),
82            _ => {}
83        }
84    }
85
86    Some(ParsedSignal {
87        kind: kind?,
88        message,
89        title,
90    })
91}
92
93fn frame_slice(remainder: &str) -> Option<(&str, usize)> {
94    if let Some(end) = remainder.find(BEL) {
95        return Some((&remainder[..end], end + BEL.len_utf8()));
96    }
97    if let Some(end) = remainder.find(ST) {
98        return Some((&remainder[..end], end + ST.len()));
99    }
100    None
101}
102
103#[cfg(test)]
104mod tests {
105    use taskers_domain::SignalKind;
106
107    use super::{SignalStreamParser, parse_signal_frames};
108
109    #[test]
110    fn parses_multiple_frames_with_different_terminators() {
111        let output = concat!(
112            "hello",
113            "\u{1b}]777;taskers;kind=waiting_input;message=Need%20approval\u{7}",
114            "world",
115            "\u{1b}]777;taskers;kind=completed;message=Done\u{1b}\\",
116        );
117
118        let frames = parse_signal_frames(output);
119
120        assert_eq!(frames.len(), 2);
121        assert_eq!(frames[0].kind, SignalKind::WaitingInput);
122        assert_eq!(frames[0].message.as_deref(), Some("Need approval"));
123        assert_eq!(frames[1].kind, SignalKind::Completed);
124    }
125
126    #[test]
127    fn ignores_unknown_frames() {
128        let output = "\u{1b}]777;taskers;kind=unknown;message=Bad\u{7}";
129        assert!(parse_signal_frames(output).is_empty());
130    }
131
132    #[test]
133    fn stream_parser_handles_split_frames() {
134        let mut parser = SignalStreamParser::default();
135
136        assert!(
137            parser
138                .push("\u{1b}]777;taskers;kind=waiting_input;message=Need")
139                .is_empty()
140        );
141
142        let frames = parser.push("%20approval\u{7}");
143        assert_eq!(frames.len(), 1);
144        assert_eq!(frames[0].kind, SignalKind::WaitingInput);
145        assert_eq!(frames[0].message.as_deref(), Some("Need approval"));
146    }
147}