config: add support for status commands
This commit is contained in:
parent
44b19cbc9a
commit
7eb4510eab
6 changed files with 303 additions and 3 deletions
32
Cargo.lock
generated
32
Cargo.lock
generated
|
|
@ -338,6 +338,12 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "error_reporter"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
|
|
@ -487,6 +493,12 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a7558cc96ddcaf0b4144d7149984ace2899bb29d4ee2999979d429efc305200"
|
checksum = "2a7558cc96ddcaf0b4144d7149984ace2899bb29d4ee2999979d429efc305200"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jay"
|
name = "jay"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -535,9 +547,12 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
"bstr",
|
||||||
|
"error_reporter",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uapi",
|
"uapi",
|
||||||
]
|
]
|
||||||
|
|
@ -873,6 +888,12 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
@ -899,6 +920,17 @@ dependencies = [
|
||||||
"syn 2.0.48",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shaderc"
|
name = "shaderc"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,6 @@ futures-util = { version = "0.3.30", features = ["io"] }
|
||||||
uapi = "0.2.12"
|
uapi = "0.2.12"
|
||||||
thiserror = "1.0.57"
|
thiserror = "1.0.57"
|
||||||
backtrace = "0.3.69"
|
backtrace = "0.3.69"
|
||||||
|
error_reporter = "1.0.0"
|
||||||
|
serde_json = "1.0.114"
|
||||||
|
bstr = { version = "1.9.0", default-features = false, features = ["std"] }
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use {
|
||||||
input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat},
|
input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat},
|
||||||
keyboard::Keymap,
|
keyboard::Keymap,
|
||||||
logging::LogLevel,
|
logging::LogLevel,
|
||||||
tasks::JoinSlot,
|
tasks::{JoinHandle, JoinSlot},
|
||||||
theme::{colors::Colorable, sized::Resizable, Color},
|
theme::{colors::Colorable, sized::Resizable, Color},
|
||||||
timer::Timer,
|
timer::Timer,
|
||||||
video::{
|
video::{
|
||||||
|
|
@ -84,6 +84,8 @@ pub(crate) struct Client {
|
||||||
read_interests: RefCell<HashMap<PollableId, Interest>>,
|
read_interests: RefCell<HashMap<PollableId, Interest>>,
|
||||||
write_interests: RefCell<HashMap<PollableId, Interest>>,
|
write_interests: RefCell<HashMap<PollableId, Interest>>,
|
||||||
tasks: Tasks,
|
tasks: Tasks,
|
||||||
|
status_task: Cell<Vec<JoinHandle<()>>>,
|
||||||
|
i3bar_separator: RefCell<Option<Rc<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Interest {
|
struct Interest {
|
||||||
|
|
@ -206,6 +208,8 @@ pub unsafe extern "C" fn init(
|
||||||
read_interests: Default::default(),
|
read_interests: Default::default(),
|
||||||
write_interests: Default::default(),
|
write_interests: Default::default(),
|
||||||
tasks: Default::default(),
|
tasks: Default::default(),
|
||||||
|
status_task: Default::default(),
|
||||||
|
i3bar_separator: Default::default(),
|
||||||
});
|
});
|
||||||
let init = slice::from_raw_parts(init, size);
|
let init = slice::from_raw_parts(init, size);
|
||||||
client.handle_init_msg(init);
|
client.handle_init_msg(init);
|
||||||
|
|
@ -506,6 +510,20 @@ impl Client {
|
||||||
self.send(&ClientMessage::SetStatus { status });
|
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) {
|
pub fn set_split(&self, seat: Seat, axis: Axis) {
|
||||||
self.send(&ClientMessage::SetSplit { seat, axis });
|
self.send(&ClientMessage::SetSplit { seat, axis });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,10 @@
|
||||||
#![allow(
|
#![allow(
|
||||||
clippy::zero_prefixed_literal,
|
clippy::zero_prefixed_literal,
|
||||||
clippy::manual_range_contains,
|
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 {
|
use {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
//! Knobs for changing the status text.
|
//! 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.
|
/// Sets the status text.
|
||||||
///
|
///
|
||||||
/// The status text is displayed at the right end of the bar.
|
/// The status text is displayed at the right end of the bar.
|
||||||
|
|
@ -10,3 +20,237 @@
|
||||||
pub fn set_status(status: &str) {
|
pub fn set_status(status: &str) {
|
||||||
get!().set_status(status);
|
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("&"),
|
||||||
|
'<' => dst.push_str("<"),
|
||||||
|
'>' => dst.push_str(">"),
|
||||||
|
'\'' => dst.push_str("'"),
|
||||||
|
'"' => dst.push_str("""),
|
||||||
|
_ => dst.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -453,7 +453,7 @@ impl ConfigProxyHandler {
|
||||||
fn get_seat(&self, seat: Seat) -> Result<Rc<WlSeatGlobal>, CphError> {
|
fn get_seat(&self, seat: Seat) -> Result<Rc<WlSeatGlobal>, CphError> {
|
||||||
let seats = self.state.globals.seats.lock();
|
let seats = self.state.globals.seats.lock();
|
||||||
for seat_global in seats.values() {
|
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());
|
return Ok(seat_global.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue