1use 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
15pub fn set_status(status: &str) {
23 get!().set_status(status);
24}
25
26#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
28pub enum MessageFormat {
29 Plain,
34 Pango,
41 I3Bar,
47}
48
49pub 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
128pub fn unset_status_command() {
130 get!().set_status_tasks(vec![]);
131}
132
133pub 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("&"),
250 '<' => dst.push_str("<"),
251 '>' => dst.push_str(">"),
252 '\'' => dst.push_str("'"),
253 '"' => dst.push_str("""),
254 _ => dst.push(c),
255 }
256 }
257 true
258 } else {
259 false
260 }
261}