Skip to content

Commit

Permalink
config: add support for status commands
Browse files Browse the repository at this point in the history
  • Loading branch information
mahkoh committed Mar 7, 2024
1 parent 44b19cb commit 7eb4510
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 3 deletions.
32 changes: 32 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions jay-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ futures-util = { version = "0.3.30", features = ["io"] }
uapi = "0.2.12"
thiserror = "1.0.57"
backtrace = "0.3.69"
error_reporter = "1.0.0"
serde_json = "1.0.114"
bstr = { version = "1.9.0", default-features = false, features = ["std"] }
20 changes: 19 additions & 1 deletion jay-config/src/_private/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use {
input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat},
keyboard::Keymap,
logging::LogLevel,
tasks::JoinSlot,
tasks::{JoinHandle, JoinSlot},
theme::{colors::Colorable, sized::Resizable, Color},
timer::Timer,
video::{
Expand Down Expand Up @@ -84,6 +84,8 @@ pub(crate) struct Client {
read_interests: RefCell<HashMap<PollableId, Interest>>,
write_interests: RefCell<HashMap<PollableId, Interest>>,
tasks: Tasks,
status_task: Cell<Vec<JoinHandle<()>>>,
i3bar_separator: RefCell<Option<Rc<String>>>,
}

struct Interest {
Expand Down Expand Up @@ -206,6 +208,8 @@ pub unsafe extern "C" fn init(
read_interests: Default::default(),
write_interests: Default::default(),
tasks: Default::default(),
status_task: Default::default(),
i3bar_separator: Default::default(),
});
let init = slice::from_raw_parts(init, size);
client.handle_init_msg(init);
Expand Down Expand Up @@ -506,6 +510,20 @@ impl Client {
self.send(&ClientMessage::SetStatus { status });
}

pub fn set_status_tasks(&self, tasks: Vec<JoinHandle<()>>) {
for old in self.status_task.replace(tasks) {
old.abort();
}
}

pub fn set_i3bar_separator(&self, separator: &str) {
*self.i3bar_separator.borrow_mut() = Some(Rc::new(separator.to_string()));
}

pub fn get_i3bar_separator(&self) -> Option<Rc<String>> {
self.i3bar_separator.borrow().clone()
}

pub fn set_split(&self, seat: Seat, axis: Axis) {
self.send(&ClientMessage::SetSplit { seat, axis });
}
Expand Down
5 changes: 4 additions & 1 deletion jay-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
#![allow(
clippy::zero_prefixed_literal,
clippy::manual_range_contains,
clippy::uninlined_format_args
clippy::uninlined_format_args,
clippy::len_zero,
clippy::single_char_pattern,
clippy::single_char_add_str
)]

use {
Expand Down
244 changes: 244 additions & 0 deletions jay-config/src/status.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
//! Knobs for changing the status text.
use {
crate::{exec::Command, io::Async, tasks::spawn},
bstr::ByteSlice,
error_reporter::Report,
futures_util::{io::BufReader, AsyncBufReadExt},
serde::Deserialize,
std::borrow::BorrowMut,
uapi::{c, OwnedFd},
};

/// Sets the status text.
///
/// The status text is displayed at the right end of the bar.
Expand All @@ -10,3 +20,237 @@
pub fn set_status(status: &str) {
get!().set_status(status);
}

/// The format of a status command output.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum MessageFormat {
/// The output is plain text.
///
/// The command should output one line every time it wants to change the status.
/// The content of the line will be interpreted as plain text.
Plain,
/// The output uses [pango][pango] markup.
///
/// The command should output one line every time it wants to change the status.
/// The content of the line will be interpreted as pango markup.
///
/// [pango]: https://docs.gtk.org/Pango/pango_markup.html
Pango,
/// The output uses the [i3bar][i3bar] protocol.
///
/// The separator between individual components can be set using [`set_i3bar_separator`].
///
/// [i3bar]: https://github.com/i3/i3/blob/next/docs/i3bar-protocol
I3Bar,
}

/// Sets a command whose output will be used as the status text.
///
/// The [`stdout`](Command::stdout) and [`stderr`](Command::stderr)` of the command will
/// be overwritten by this function. The stdout will be used for the status text and the
/// stderr will be appended to the compositor log.
///
/// The format of stdout is determined by the `format` parameter.
pub fn set_status_command(format: MessageFormat, mut command: impl BorrowMut<Command>) {
macro_rules! pipe {
() => {{
let (read, write) = match uapi::pipe2(c::O_CLOEXEC) {
Ok(p) => p,
Err(e) => {
log::error!("Could not create a pipe: {}", Report::new(e));
return;
}
};
let read = match Async::new(read) {
Ok(r) => BufReader::new(r),
Err(e) => {
log::error!("Could not create an Async object: {}", Report::new(e));
return;
}
};
(read, write)
}};
}
let (mut read, write) = pipe!();
let (mut stderr_read, stderr_write) = pipe!();
let command = command.borrow_mut();
command.stdout(write).stderr(stderr_write).spawn();
let name = command.prog.clone();
let name2 = command.prog.clone();
let stderr_handle = spawn(async move {
let mut line = vec![];
loop {
line.clear();
if let Err(e) = stderr_read.read_until(b'\n', &mut line).await {
log::warn!("Could not read from {name2} stderr: {}", Report::new(e));
return;
}
if line.len() == 0 {
return;
}
log::warn!(
"{name2} emitted a message on stderr: {}",
line.trim_with(|c| c == '\n').as_bstr()
);
}
});
let handle = spawn(async move {
if format == MessageFormat::I3Bar {
handle_i3bar(name, read).await;
return;
}
let mut line = String::new();
let mut cleaned = String::new();
loop {
line.clear();
if let Err(e) = read.read_line(&mut line).await {
log::error!("Could not read from `{name}`: {}", Report::new(e));
return;
}
if line.is_empty() {
log::info!("{name} closed stdout");
return;
}
let line = line.strip_suffix("\n").unwrap_or(&line);
cleaned.clear();
if format != MessageFormat::Pango && escape_pango(line, &mut cleaned) {
set_status(&cleaned);
} else {
set_status(line);
}
}
});
get!().set_status_tasks(vec![handle, stderr_handle]);
}

/// Unsets the previously set status command.
pub fn unset_status_command() {
get!().set_status_tasks(vec![]);
}

/// Sets the separator for i3bar status commands.
///
/// The separator should be specified in [pango][pango] markup language.
///
/// [pango]: https://docs.gtk.org/Pango/pango_markup.html
pub fn set_i3bar_separator(separator: &str) {
get!().set_i3bar_separator(separator);
}

async fn handle_i3bar(name: String, mut read: BufReader<Async<OwnedFd>>) {
use std::fmt::Write;

#[derive(Deserialize)]
struct Version {
version: i32,
}
#[derive(Deserialize)]
struct Component {
markup: Option<String>,
full_text: String,
color: Option<String>,
background: Option<String>,
}
let mut line = String::new();
macro_rules! read_line {
() => {{
line.clear();
if let Err(e) = read.read_line(&mut line).await {
log::error!("Could not read from `{name}`: {}", Report::new(e));
return;
}
if line.is_empty() {
log::info!("{name} closed stdout");
return;
}
}};
}
read_line!();
match serde_json::from_str::<Version>(&line) {
Ok(v) if v.version == 1 => {}
Ok(v) => log::warn!("Unexpected i3bar format version: {}", v.version),
Err(e) => {
log::warn!(
"Could not deserialize i3bar version message: {}",
Report::new(e)
);
return;
}
}
read_line!();
let mut status = String::new();
loop {
read_line!();
let mut line = line.as_str();
if let Some(l) = line.strip_prefix(",") {
line = l;
}
let components = match serde_json::from_str::<Vec<Component>>(line) {
Ok(c) => c,
Err(e) => {
log::warn!(
"Could not deserialize i3bar status message: {}",
Report::new(e)
);
continue;
}
};
let separator = get!().get_i3bar_separator();
let separator = match &separator {
Some(s) => s.as_str(),
_ => r##" <span color="#333333">|</span> "##,
};
status.clear();
let mut first = true;
for component in &components {
if component.full_text.is_empty() {
continue;
}
if !first {
status.push_str(separator);
}
first = false;
let have_span = component.color.is_some() || component.background.is_some();
if have_span {
status.push_str("<span");
if let Some(color) = &component.color {
let _ = write!(status, r#" color="{color}""#);
}
if let Some(color) = &component.background {
let _ = write!(status, r#" bgcolor="{color}""#);
}
status.push_str(">");
}
if component.markup.as_deref() == Some("pango")
|| !escape_pango(&component.full_text, &mut status)
{
status.push_str(&component.full_text);
}
if have_span {
status.push_str("</span>");
}
}
set_status(&status);
}
}

fn escape_pango(src: &str, dst: &mut String) -> bool {
if src
.bytes()
.any(|b| matches!(b, b'&' | b'<' | b'>' | b'\'' | b'"'))
{
for c in src.chars() {
match c {
'&' => dst.push_str("&amp;"),
'<' => dst.push_str("&lt;"),
'>' => dst.push_str("&gt;"),
'\'' => dst.push_str("&apos;"),
'"' => dst.push_str("&quot;"),
_ => dst.push(c),
}
}
true
} else {
false
}
}
2 changes: 1 addition & 1 deletion src/config/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ impl ConfigProxyHandler {
fn get_seat(&self, seat: Seat) -> Result<Rc<WlSeatGlobal>, CphError> {
let seats = self.state.globals.seats.lock();
for seat_global in seats.values() {
if seat_global.id().raw() == seat.0 as _ {
if seat_global.id().raw() == seat.0 as u32 {
return Ok(seat_global.clone());
}
}
Expand Down

0 comments on commit 7eb4510

Please sign in to comment.