use std::env;
use std::iter::{Cycle, Peekable};
use std::process::Command;
use std::thread;
use std::time::{Duration, Instant};
use std::vec;
use crate::blocks::{Block, ConfigBlock, Update};
use crate::config::SharedConfig;
use crate::de::deserialize_update;
use crate::errors::*;
use crate::protocol::i3bar_event::I3BarEvent;
use crate::scheduler::Task;
use crate::subprocess::spawn_child_async;
use crate::util::expand_string;
use crate::widgets::text::TextWidget;
use crate::widgets::{I3BarWidget, State};
use crossbeam_channel::Sender;
use inotify::{EventMask, Inotify, WatchMask};
use serde_derive::Deserialize;
pub struct Custom {
id: usize,
update_interval: Update,
output: TextWidget,
command: Option<String>,
on_click: Option<String>,
cycle: Option<Peekable<Cycle<vec::IntoIter<String>>>>,
tx_update_request: Sender<Task>,
pub json: bool,
hide_when_empty: bool,
is_empty: bool,
shell: String,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, default)]
pub struct CustomConfig {
#[serde(deserialize_with = "deserialize_update")]
pub interval: Update,
pub command: Option<String>,
pub cycle: Option<Vec<String>>,
pub watch_files: Option<Vec<String>>,
pub json: bool,
pub hide_when_empty: bool,
pub shell: String,
}
impl Default for CustomConfig {
fn default() -> Self {
Self {
interval: Update::Every(Duration::from_secs(10)),
command: None,
cycle: None,
watch_files: None,
json: false,
hide_when_empty: false,
shell: env::var("SHELL").unwrap_or_else(|_| "sh".to_owned()),
}
}
}
impl ConfigBlock for Custom {
type Config = CustomConfig;
fn new(
id: usize,
block_config: Self::Config,
shared_config: SharedConfig,
tx: Sender<Task>,
) -> Result<Self> {
let mut custom = Custom {
id,
update_interval: block_config.interval,
output: TextWidget::new(id, 0, shared_config),
command: None,
on_click: None,
cycle: None,
tx_update_request: tx,
json: block_config.json,
hide_when_empty: block_config.hide_when_empty,
is_empty: true,
shell: block_config.shell,
};
if let Some(paths) = block_config.watch_files {
let tx_inotify = custom.tx_update_request.clone();
let mut notify = Inotify::init().expect("Failed to start inotify");
for path in paths {
let path_expanded = expand_string(&path)?;
notify
.add_watch(path_expanded, WatchMask::MODIFY)
.map_error_msg(|_| format!("Failed to watch file {path}"))?;
}
thread::Builder::new()
.name("custom".into())
.spawn(move || {
let mut buffer = [0; 1024];
loop {
let mut events = notify
.read_events_blocking(&mut buffer)
.expect("Error while reading inotify events");
if events.any(|event| event.mask.contains(EventMask::MODIFY)) {
tx_inotify
.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
thread::sleep(Duration::from_millis(250))
}
})
.unwrap();
};
if block_config.cycle.is_some() && block_config.command.is_some() {
return Err(Error::new("`command` and `cycle` are mutually exclusive"));
}
if let Some(cycle) = block_config.cycle {
custom.cycle = Some(cycle.into_iter().cycle().peekable());
return Ok(custom);
};
if let Some(command) = block_config.command {
custom.command = Some(command)
};
Ok(custom)
}
fn override_on_click(&mut self) -> Option<&mut Option<String>> {
Some(&mut self.on_click)
}
}
fn default_icon() -> String {
String::from("")
}
fn default_state() -> State {
State::Idle
}
#[derive(Deserialize)]
struct Output {
#[serde(default = "default_icon")]
icon: String,
#[serde(default = "default_state")]
state: State,
text: String,
}
impl Block for Custom {
fn name(&self) -> &'static str {
"custom"
}
fn update(&mut self) -> Result<Option<Update>> {
let command_str = self
.cycle
.as_mut()
.map(|c| c.peek().cloned().unwrap_or_else(|| "".to_owned()))
.or_else(|| self.command.clone())
.unwrap_or_else(|| "".to_owned());
let raw_output = Command::new(&self.shell)
.args(&["-c", &command_str])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_owned())
.error_msg("failed to read output")?;
if self.json {
let output: Output =
serde_json::from_str(&*raw_output).error_msg("error parsing JSON")?;
if output.icon.is_empty() {
self.output.unset_icon();
} else {
self.output.set_icon(&output.icon)?;
}
self.output.set_state(output.state);
self.is_empty = output.text.is_empty();
self.output.set_text(output.text);
} else {
self.is_empty = raw_output.is_empty();
self.output.set_text(raw_output);
}
Ok(Some(self.update_interval.clone()))
}
fn view(&self) -> Vec<&dyn I3BarWidget> {
if self.is_empty && self.hide_when_empty {
vec![]
} else {
vec![&self.output]
}
}
fn click(&mut self, _e: &I3BarEvent) -> Result<()> {
let mut update = false;
if let Some(ref on_click) = self.on_click {
spawn_child_async(&self.shell, &["-c", on_click]).ok();
update = true;
}
if let Some(ref mut cycle) = self.cycle {
cycle.next();
update = true;
}
if update {
self.tx_update_request
.send(Task {
id: self.id,
update_time: Instant::now(),
})
.error_msg("send error")?;
}
Ok(())
}
}