i3status_rs/protocol/
i3bar_event.rs

1use std::os::unix::io::FromRawFd as _;
2use std::time::Duration;
3
4use serde::Deserialize;
5
6use futures::StreamExt as _;
7use tokio::fs::File;
8use tokio::io::{AsyncBufReadExt as _, BufReader};
9
10use crate::BoxedStream;
11use crate::click::MouseButton;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct I3BarEvent {
15    pub id: usize,
16    pub instance: Option<String>,
17    pub button: MouseButton,
18}
19
20fn unprocessed_events_stream(invert_scrolling: bool) -> BoxedStream<I3BarEvent> {
21    // Avoid spawning a blocking therad (why doesn't tokio do this too?)
22    // This should be safe given that this function is called only once
23    let stdin = unsafe { File::from_raw_fd(0) };
24    let lines = BufReader::new(stdin).lines();
25
26    futures::stream::unfold(lines, move |mut lines| async move {
27        loop {
28            // Take only the valid JSON object between curly braces (cut off leading bracket, commas and whitespace)
29            let line = lines.next_line().await.ok().flatten()?;
30            let line = line.trim_start_matches(|c| c != '{');
31            let line = line.trim_end_matches(|c| c != '}');
32
33            if line.is_empty() {
34                continue;
35            }
36
37            #[derive(Deserialize)]
38            struct I3BarEventRaw {
39                instance: Option<String>,
40                button: MouseButton,
41            }
42
43            let event: I3BarEventRaw = match serde_json::from_str(line) {
44                Ok(event) => event,
45                Err(err) => {
46                    eprintln!("Failed to deserialize click event.\nData: {line}\nError: {err}");
47                    continue;
48                }
49            };
50
51            let (id, instance) = match event.instance {
52                Some(name) => {
53                    let (id, instance) = name.split_once(':').unwrap();
54                    let instance = if instance.is_empty() {
55                        None
56                    } else {
57                        Some(instance.to_owned())
58                    };
59                    (id.parse().unwrap(), instance)
60                }
61                None => continue,
62            };
63
64            use MouseButton::*;
65            let button = match (event.button, invert_scrolling) {
66                (WheelUp, false) | (WheelDown, true) => WheelUp,
67                (WheelUp, true) | (WheelDown, false) => WheelDown,
68                (other, _) => other,
69            };
70
71            let event = I3BarEvent {
72                id,
73                instance,
74                button,
75            };
76
77            break Some((event, lines));
78        }
79    })
80    .boxed_local()
81}
82
83pub fn events_stream(
84    invert_scrolling: bool,
85    double_click_delay: Duration,
86) -> BoxedStream<I3BarEvent> {
87    let events = unprocessed_events_stream(invert_scrolling);
88    futures::stream::unfold((events, None), move |(mut events, pending)| async move {
89        if let Some(pending) = pending {
90            return Some((pending, (events, None)));
91        }
92
93        let mut event = events.next().await?;
94
95        // Handle double clicks (for now only left)
96        if event.button == MouseButton::Left && !double_click_delay.is_zero() {
97            if let Ok(new_event) = tokio::time::timeout(double_click_delay, events.next()).await {
98                let new_event = new_event?;
99                if event == new_event {
100                    event.button = MouseButton::DoubleLeft;
101                } else {
102                    return Some((event, (events, Some(new_event))));
103                }
104            }
105        }
106
107        Some((event, (events, None)))
108    })
109    .fuse()
110    .boxed_local()
111}