Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ words:
- XKCD
- xrandr
- xresources
- xsct
- xtask
- YMDE
- YMDET
Expand Down
16 changes: 5 additions & 11 deletions src/blocks/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
//! - Use `shellexpand`

use crate::formatting::Format;
use crate::subprocess::{CommandExt as _, get_output, get_shell};

use super::prelude::*;
use inotify::{Inotify, WatchMask};
Expand Down Expand Up @@ -190,11 +191,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
}
};

let shell = config
.shell
.clone()
.or_else(|| std::env::var("SHELL").ok())
.unwrap_or_else(|| "sh".to_string());
let shell = config.shell.clone().unwrap_or_else(get_shell);

if config.persistent {
let mut process = Command::new(&shell)
Expand All @@ -208,6 +205,8 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
.stdout(Stdio::piped())
.stdin(Stdio::null())
.kill_on_drop(true)
.with_environment()
.error("Could not add environment to child process")?
.spawn()
.error("failed to run command")?;

Expand Down Expand Up @@ -250,12 +249,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {

loop {
// Run command
let output = Command::new(&shell)
.args(["-c", &cmd])
.stdin(Stdio::null())
.output()
.await
.error("failed to run command")?;
let output = get_output(&cmd).await.error("failed to run command")?;
let stdout = std::str::from_utf8(&output.stdout)
.error("the output of command is invalid UTF-8")?
.trim();
Expand Down
16 changes: 11 additions & 5 deletions src/blocks/hueshift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
//! ---------------------|---------
//! `"redshift"` | X11
//! `"sct"` | X11
//! `"xsct"` | X11
//! `"gammastep"` | X11 and Wayland
//! `"wl_gammarelay"` | Wayland
//! `"wl_gammarelay_rs"` | Wayland
Expand Down Expand Up @@ -108,6 +109,8 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
HueShifter::Redshift
} else if has_command("sct").await? {
HueShifter::Sct
} else if has_command("xsct").await? {
HueShifter::Xsct
} else if has_command("gammastep").await? {
HueShifter::Gammastep
} else if has_command("wlsunset").await? {
Expand All @@ -120,7 +123,8 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {

let mut driver: Box<dyn HueShiftDriver> = match hue_shifter {
HueShifter::Redshift => Box::new(Redshift::new(config.interval)),
HueShifter::Sct => Box::new(Sct::new(config.interval)),
HueShifter::Sct => Box::new(Sct::new("sct", config.interval)),
HueShifter::Xsct => Box::new(Sct::new("xsct", config.interval)),
HueShifter::Gammastep => Box::new(Gammastep::new(config.interval)),
HueShifter::Wlsunset => Box::new(Wlsunset::new(config.interval)),
HueShifter::WlGammarelay => Box::new(WlGammarelayRs::new("wl-gammarelay").await?),
Expand Down Expand Up @@ -179,6 +183,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
pub enum HueShifter {
Redshift,
Sct,
Xsct,
Gammastep,
Wlsunset,
WlGammarelay,
Expand Down Expand Up @@ -225,12 +230,13 @@ impl HueShiftDriver for Redshift {
}

struct Sct {
cmd: &'static str,
interval: Seconds,
}

impl Sct {
fn new(interval: Seconds) -> Self {
Self { interval }
fn new(cmd: &'static str, interval: Seconds) -> Self {
Self { cmd, interval }
}
}

Expand All @@ -241,11 +247,11 @@ impl HueShiftDriver for Sct {
Ok(None)
}
async fn update(&mut self, temp: u16) -> Result<()> {
spawn_shell(&format!("sct {temp} >/dev/null 2>&1"))
spawn_shell(&format!("{0} {temp} >/dev/null 2>&1", self.cmd))
.error("Failed to set new color temperature using sct.")
}
async fn reset(&mut self) -> Result<()> {
spawn_process("sct", &[]).error("Failed to set new color temperature using sct.")
spawn_process(self.cmd, &["0"]).error("Failed to set new color temperature using sct.")
}
async fn receive_update(&mut self) -> Result<u16> {
sleep(self.interval.0).await;
Expand Down
10 changes: 4 additions & 6 deletions src/blocks/packages/pacman.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ use std::env;
use std::path::PathBuf;
use std::process::Stdio;

use tokio::fs::{create_dir_all, symlink};
use tokio::process::Command;

use super::*;
use crate::subprocess::get_output;
use crate::util::has_command;
use tokio::fs::{create_dir_all, symlink};
use tokio::process::Command;

make_log_macro!(debug, "pacman");

Expand Down Expand Up @@ -136,9 +136,7 @@ impl Backend for Aur {
}

async fn get_updates_list(&self) -> Result<Vec<String>> {
let stdout = Command::new("sh")
.args(["-c", &self.aur_command])
.output()
let stdout = get_output(&self.aur_command)
.await
.or_error(|| format!("aur command: {} failed", self.aur_command))?
.stdout;
Expand Down
13 changes: 3 additions & 10 deletions src/blocks/toggle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@
//! - `toggle_on`

use super::prelude::*;
use std::env;
use tokio::process::Command;
use crate::subprocess::get_output;

#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -90,13 +89,9 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let icon_on = config.icon_on.as_deref().unwrap_or("toggle_on");
let icon_off = config.icon_off.as_deref().unwrap_or("toggle_off");

let shell = env::var("SHELL").unwrap_or_else(|_| "sh".to_string());

loop {
// Check state
let output = Command::new(&shell)
.args(["-c", &config.command_state])
.output()
let output = get_output(&config.command_state)
.await
.error("Failed to run command_state")?;
let is_on = !std::str::from_utf8(&output.stdout)
Expand Down Expand Up @@ -129,9 +124,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
} else {
&config.command_on
};
let output = Command::new(&shell)
.args(["-c", cmd])
.output()
let output = get_output(cmd)
.await
.error("Failed to run command")?;
if output.status.success() {
Expand Down
2 changes: 1 addition & 1 deletion src/blocks/weather/met_no.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ impl WeatherProvider for Service<'_> {
"Unable to fetch the specified number of forecast_hours specified {}, only {} hours available",
forecast_hours,
data.properties.timeseries.len()
)))?;
)));
}

let data_agg: Vec<ForecastAggregateSegment> = data
Expand Down
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::errors::*;
use crate::formatting::config::Config as FormatConfig;
use crate::geolocator::Geolocator;
use crate::icons::{Icon, Icons};
use crate::subprocess::SubprocessConfig;
use crate::themes::{Theme, ThemeOverrides, ThemeUserConfig};

#[derive(Deserialize, Debug)]
Expand All @@ -33,6 +34,9 @@ pub struct Config {
#[serde(default = "default_error_fullscreen")]
pub error_fullscreen_format: FormatConfig,

#[serde(default)]
pub subprocess: SubprocessConfig,

#[serde(default)]
#[serde(rename = "block")]
pub blocks: Vec<BlockConfigEntry>,
Expand Down
8 changes: 3 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ mod netlink;
pub mod pipewire;
pub mod protocol;
mod signals;
mod subprocess;
pub mod subprocess;
pub mod themes;
pub mod widget;
mod wrappers;
Expand All @@ -36,7 +36,6 @@ use std::time::Duration;

use futures::Stream;
use futures::stream::{FuturesUnordered, StreamExt as _};
use tokio::process::Command;
use tokio::sync::{Notify, mpsc};

use crate::blocks::{BlockAction, BlockError, CommonApi, RESTART_BLOCK_BTN};
Expand All @@ -48,6 +47,7 @@ use crate::formatting::value::Value;
use crate::protocol::i3bar_block::I3BarBlock;
use crate::protocol::i3bar_event::{self, I3BarEvent};
use crate::signals::Signal;
use crate::subprocess::get_output;
use crate::widget::{State, Widget};

const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
Expand Down Expand Up @@ -244,9 +244,7 @@ impl BarState {
pub async fn spawn_block(&mut self, block_config: BlockConfigEntry) -> Result<()> {
if let Some(cmd) = &block_config.common.if_command {
// TODO: async
if !Command::new("sh")
.args(["-c", cmd])
.output()
if !get_output(cmd)
.await
.error("failed to run if_command")?
.status
Expand Down
6 changes: 6 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use clap::Parser;
use std::path;

use i3status_rs::blocks::BlockError;
use i3status_rs::config::Config;
use i3status_rs::errors::*;
use i3status_rs::escape::Escaped;
use i3status_rs::subprocess::subprocess_init;
use i3status_rs::widget::{State, Widget};
use i3status_rs::{BarState, protocol, util};

Expand Down Expand Up @@ -34,6 +36,10 @@ fn main() {
let config_path = util::find_file(&args.config, None, Some("toml"))?
.or_error(|| format!("Configuration file '{}' not found", args.config))?;
let mut config: Config = util::deserialize_toml_file(&config_path)?;
subprocess_init(
&config.subprocess,
path::absolute(config_path).error("Could not resolve config path")?,
)?;
let blocks = std::mem::take(&mut config.blocks);
let mut bar = BarState::new(config);
for block_config in blocks {
Expand Down
106 changes: 102 additions & 4 deletions src/subprocess.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,97 @@
use std::io;
use crate::errors::{Error, Result};
use serde::Deserialize;
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::os::unix::process::CommandExt as _;
use std::process::{Command, Stdio};
use std::process::{Command, Output, Stdio, id};
use std::sync::OnceLock;
use std::{env, io};

pub const ENV_VAR_PID: &str = "I3STATUS_RS_PID";
pub const ENV_VAR_CONFIG: &str = "I3STATUS_RS_CONFIG";

#[derive(Deserialize, Debug, Default)]
#[serde(deny_unknown_fields)]
pub struct SubprocessConfig {
#[serde(default)]
pub add_pid: Option<bool>,
#[serde(default)]
pub add_config_file_path: Option<bool>,
#[serde(default)]
pub environment: HashMap<String, String>,
}

static SUBPROCESS_ENV: OnceLock<HashMap<OsString, OsString>> = OnceLock::new();

pub fn subprocess_init(config: &SubprocessConfig, config_file: impl AsRef<OsStr>) -> Result<()> {
let mut env: HashMap<OsString, OsString> = HashMap::with_capacity(2 + config.environment.len());

env.extend(config.environment.iter().map(|(k, v)| (k.into(), v.into())));

fn insert_env(
env: &mut HashMap<OsString, OsString>,
key: &str,
value: OsString,
option_name: &str,
) -> Result<()> {
if env.insert(key.into(), value).is_some() {
Err(Error::new(format!(
"Cannot specify {key} in subprocess environment when subprocess.{option_name} is set"
)))
} else {
Ok(())
}
}

if config.add_pid.unwrap_or(true) {
insert_env(&mut env, ENV_VAR_PID, id().to_string().into(), "add_pid")?;
}

if config.add_config_file_path.unwrap_or(true) {
insert_env(
&mut env,
ENV_VAR_CONFIG,
config_file.as_ref().into(),
"add_config_file_path",
)?;
}

SUBPROCESS_ENV
.set(env)
.map_err(|_| Error::new("Subprocess environment already initialized"))
}

fn get_subprocess_env() -> io::Result<&'static HashMap<OsString, OsString>> {
SUBPROCESS_ENV
.get()
.ok_or_else(|| io::Error::other("Subprocess environment not initialized"))
}

pub trait CommandExt {
fn with_environment(&mut self) -> io::Result<&mut Self>;
}

impl CommandExt for Command {
fn with_environment(&mut self) -> io::Result<&mut Self> {
self.envs(get_subprocess_env()?);
Ok(self)
}
}

impl CommandExt for tokio::process::Command {
fn with_environment(&mut self) -> io::Result<&mut Self> {
self.envs(get_subprocess_env()?);
Ok(self)
}
}

/// Spawn a new detached process
pub fn spawn_process(cmd: &str, args: &[&str]) -> io::Result<()> {
let mut proc = Command::new(cmd);
proc.args(args);
proc.stdin(Stdio::null());
proc.stdout(Stdio::null());
proc.with_environment()?;
// Safety: libc::daemon() is async-signal-safe
unsafe {
proc.pre_exec(|| match libc::daemon(0, 0) {
Expand All @@ -21,16 +105,30 @@ pub fn spawn_process(cmd: &str, args: &[&str]) -> io::Result<()> {

/// Spawn a new detached shell
pub fn spawn_shell(cmd: &str) -> io::Result<()> {
spawn_process("sh", &["-c", cmd])
spawn_process(&get_shell(), &["-c", cmd])
}

pub async fn spawn_shell_sync(cmd: &str) -> io::Result<()> {
tokio::process::Command::new("sh")
tokio::process::Command::new(get_shell())
.args(["-c", cmd])
.stdin(Stdio::null())
.stdout(Stdio::null())
.with_environment()?
.spawn()?
.wait()
.await?;
Ok(())
}

pub fn get_shell() -> String {
env::var("SHELL").unwrap_or_else(|_| "sh".to_string())
}

pub async fn get_output(shell_command: &str) -> io::Result<Output> {
tokio::process::Command::new(get_shell())
.args(["-c", shell_command])
.stdin(Stdio::null())
.with_environment()?
.output()
.await
}
Loading
Loading