jay_config/
status.rs

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