1use {
4 crate::{exec::Command, io::Async, tasks::spawn},
5 bstr::ByteSlice,
6 error_reporter::Report,
7 futures_util::{AsyncBufReadExt, io::BufReader},
8 serde::Deserialize,
9 std::borrow::BorrowMut,
10 uapi::{OwnedFd, c},
11};
12
13pub fn set_status(status: &str) {
21 get!().set_status(status);
22}
23
24#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
26pub enum MessageFormat {
27 Plain,
32 Pango,
39 I3Bar,
45}
46
47pub 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
126pub fn unset_status_command() {
128 get!().set_status_tasks(vec![]);
129}
130
131pub 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.trim();
185 if let Some(l) = line.strip_prefix(",") {
186 line = l;
187 }
188 if let Some(l) = line.strip_suffix(",") {
189 line = l;
190 }
191 let components = match serde_json::from_str::<Vec<Component>>(line) {
192 Ok(c) => c,
193 Err(e) => {
194 log::warn!(
195 "Could not deserialize i3bar status message: {}",
196 Report::new(e)
197 );
198 continue;
199 }
200 };
201 let separator = get!().get_i3bar_separator();
202 let separator = match &separator {
203 Some(s) => s.as_str(),
204 _ => r##" <span color="#333333">|</span> "##,
205 };
206 status.clear();
207 let mut first = true;
208 for component in &components {
209 if component.full_text.is_empty() {
210 continue;
211 }
212 if !first {
213 status.push_str(separator);
214 }
215 first = false;
216 let have_span = component.color.is_some() || component.background.is_some();
217 if have_span {
218 status.push_str("<span");
219 if let Some(color) = &component.color {
220 let _ = write!(status, r#" color="{color}""#);
221 }
222 if let Some(color) = &component.background {
223 let _ = write!(status, r#" bgcolor="{color}""#);
224 }
225 status.push_str(">");
226 }
227 if component.markup.as_deref() == Some("pango")
228 || !escape_pango(&component.full_text, &mut status)
229 {
230 status.push_str(&component.full_text);
231 }
232 if have_span {
233 status.push_str("</span>");
234 }
235 }
236 set_status(&status);
237 }
238}
239
240fn escape_pango(src: &str, dst: &mut String) -> bool {
241 if src
242 .bytes()
243 .any(|b| matches!(b, b'&' | b'<' | b'>' | b'\'' | b'"'))
244 {
245 for c in src.chars() {
246 match c {
247 '&' => dst.push_str("&"),
248 '<' => dst.push_str("<"),
249 '>' => dst.push_str(">"),
250 '\'' => dst.push_str("'"),
251 '"' => dst.push_str("""),
252 _ => dst.push(c),
253 }
254 }
255 true
256 } else {
257 false
258 }
259}