Skip to main content

jay_config/
status.rs

1//! Knobs for changing the status text.
2
3use crate::exec::Command;
4use crate::io::Async;
5use crate::tasks::spawn;
6use bstr::ByteSlice;
7use error_reporter::Report;
8use futures_util::AsyncBufReadExt;
9use futures_util::io::BufReader;
10use serde::Deserialize;
11use std::borrow::BorrowMut;
12use uapi::OwnedFd;
13use uapi::c;
14
15/// Sets the status text.
16///
17/// The status text is displayed at the right end of the bar.
18///
19/// The status text should be specified in [pango][pango] markup language.
20///
21/// [pango]: https://docs.gtk.org/Pango/pango_markup.html
22pub fn set_status(status: &str) {
23    get!().set_status(status);
24}
25
26/// The format of a status command output.
27#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
28pub enum MessageFormat {
29    /// The output is plain text.
30    ///
31    /// The command should output one line every time it wants to change the status.
32    /// The content of the line will be interpreted as plain text.
33    Plain,
34    /// The output uses [pango][pango] markup.
35    ///
36    /// The command should output one line every time it wants to change the status.
37    /// The content of the line will be interpreted as pango markup.
38    ///
39    /// [pango]: https://docs.gtk.org/Pango/pango_markup.html
40    Pango,
41    /// The output uses the [i3bar][i3bar] protocol.
42    ///
43    /// The separator between individual components can be set using [`set_i3bar_separator`].
44    ///
45    /// [i3bar]: https://github.com/i3/i3/blob/next/docs/i3bar-protocol
46    I3Bar,
47}
48
49/// Sets a command whose output will be used as the status text.
50///
51/// The [`stdout`](Command::stdout) and [`stderr`](Command::stderr)` of the command will
52/// be overwritten by this function. The stdout will be used for the status text and the
53/// stderr will be appended to the compositor log.
54///
55/// The format of stdout is determined by the `format` parameter.
56pub fn set_status_command(format: MessageFormat, mut command: impl BorrowMut<Command>) {
57    macro_rules! pipe {
58        () => {{
59            let (read, write) = match uapi::pipe2(c::O_CLOEXEC) {
60                Ok(p) => p,
61                Err(e) => {
62                    log::error!("Could not create a pipe: {}", Report::new(e));
63                    return;
64                }
65            };
66            let read = match Async::new(read) {
67                Ok(r) => BufReader::new(r),
68                Err(e) => {
69                    log::error!("Could not create an Async object: {}", Report::new(e));
70                    return;
71                }
72            };
73            (read, write)
74        }};
75    }
76    let (mut read, write) = pipe!();
77    let (mut stderr_read, stderr_write) = pipe!();
78    let command = command.borrow_mut();
79    command.stdout(write).stderr(stderr_write).spawn();
80    let name = command.prog.clone();
81    let name2 = command.prog.clone();
82    let stderr_handle = spawn(async move {
83        let mut line = vec![];
84        loop {
85            line.clear();
86            if let Err(e) = stderr_read.read_until(b'\n', &mut line).await {
87                log::warn!("Could not read from {name2} stderr: {}", Report::new(e));
88                return;
89            }
90            if line.len() == 0 {
91                return;
92            }
93            log::warn!(
94                "{name2} emitted a message on stderr: {}",
95                line.trim_with(|c| c == '\n').as_bstr()
96            );
97        }
98    });
99    let handle = spawn(async move {
100        if format == MessageFormat::I3Bar {
101            handle_i3bar(name, read).await;
102            return;
103        }
104        let mut line = String::new();
105        let mut cleaned = String::new();
106        loop {
107            line.clear();
108            if let Err(e) = read.read_line(&mut line).await {
109                log::error!("Could not read from `{name}`: {}", Report::new(e));
110                return;
111            }
112            if line.is_empty() {
113                log::info!("{name} closed stdout");
114                return;
115            }
116            let line = line.strip_suffix("\n").unwrap_or(&line);
117            cleaned.clear();
118            if format != MessageFormat::Pango && escape_pango(line, &mut cleaned) {
119                set_status(&cleaned);
120            } else {
121                set_status(line);
122            }
123        }
124    });
125    get!().set_status_tasks(vec![handle, stderr_handle]);
126}
127
128/// Unsets the previously set status command.
129pub fn unset_status_command() {
130    get!().set_status_tasks(vec![]);
131}
132
133/// Sets the separator for i3bar status commands.
134///
135/// The separator should be specified in [pango][pango] markup language.
136///
137/// [pango]: https://docs.gtk.org/Pango/pango_markup.html
138pub fn set_i3bar_separator(separator: &str) {
139    get!().set_i3bar_separator(separator);
140}
141
142async fn handle_i3bar(name: String, mut read: BufReader<Async<OwnedFd>>) {
143    use std::fmt::Write;
144
145    #[derive(Deserialize)]
146    struct Version {
147        version: i32,
148    }
149    #[derive(Deserialize)]
150    struct Component {
151        markup: Option<String>,
152        full_text: String,
153        color: Option<String>,
154        background: Option<String>,
155    }
156    let mut line = String::new();
157    macro_rules! read_line {
158        () => {{
159            line.clear();
160            if let Err(e) = read.read_line(&mut line).await {
161                log::error!("Could not read from `{name}`: {}", Report::new(e));
162                return;
163            }
164            if line.is_empty() {
165                log::info!("{name} closed stdout");
166                return;
167            }
168        }};
169    }
170    read_line!();
171    match serde_json::from_str::<Version>(&line) {
172        Ok(v) if v.version == 1 => {}
173        Ok(v) => log::warn!("Unexpected i3bar format version: {}", v.version),
174        Err(e) => {
175            log::warn!(
176                "Could not deserialize i3bar version message: {}",
177                Report::new(e)
178            );
179            return;
180        }
181    }
182    read_line!();
183    let mut status = String::new();
184    loop {
185        read_line!();
186        let mut line = line.trim();
187        if let Some(l) = line.strip_prefix(",") {
188            line = l;
189        }
190        if let Some(l) = line.strip_suffix(",") {
191            line = l;
192        }
193        let components = match serde_json::from_str::<Vec<Component>>(line) {
194            Ok(c) => c,
195            Err(e) => {
196                log::warn!(
197                    "Could not deserialize i3bar status message: {}",
198                    Report::new(e)
199                );
200                continue;
201            }
202        };
203        let separator = get!().get_i3bar_separator();
204        let separator = match &separator {
205            Some(s) => s.as_str(),
206            _ => r##" <span color="#333333">|</span> "##,
207        };
208        status.clear();
209        let mut first = true;
210        for component in &components {
211            if component.full_text.is_empty() {
212                continue;
213            }
214            if !first {
215                status.push_str(separator);
216            }
217            first = false;
218            let have_span = component.color.is_some() || component.background.is_some();
219            if have_span {
220                status.push_str("<span");
221                if let Some(color) = &component.color {
222                    let _ = write!(status, r#" color="{color}""#);
223                }
224                if let Some(color) = &component.background {
225                    let _ = write!(status, r#" bgcolor="{color}""#);
226                }
227                status.push_str(">");
228            }
229            if component.markup.as_deref() == Some("pango")
230                || !escape_pango(&component.full_text, &mut status)
231            {
232                status.push_str(&component.full_text);
233            }
234            if have_span {
235                status.push_str("</span>");
236            }
237        }
238        set_status(&status);
239    }
240}
241
242fn escape_pango(src: &str, dst: &mut String) -> bool {
243    if src
244        .bytes()
245        .any(|b| matches!(b, b'&' | b'<' | b'>' | b'\'' | b'"'))
246    {
247        for c in src.chars() {
248            match c {
249                '&' => dst.push_str("&amp;"),
250                '<' => dst.push_str("&lt;"),
251                '>' => dst.push_str("&gt;"),
252                '\'' => dst.push_str("&apos;"),
253                '"' => dst.push_str("&quot;"),
254                _ => dst.push(c),
255            }
256        }
257        true
258    } else {
259        false
260    }
261}