use std::ffi::OsStr;
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use clap::builder::PossibleValue;
use clap::{ColorChoice, Parser, Subcommand, ValueEnum};
use istat::bail;
use istat::error::Result;
use istat::i3::{I3Button, I3ClickEvent, I3Modifier};
use istat::ipc::get_socket_path;
use istat::ipc::protocol::{encode_ipc_msg, IpcBarEvent, IpcMessage, IpcReply, IPC_HEADER_LEN};
use serde_json::Value;
#[derive(Debug, Parser)]
#[clap(name = "istat-ipc", color = ColorChoice::Always)]
struct Cli {
#[command(subcommand)]
cmd: CliCommand,
#[clap(long)]
socket: Option<PathBuf>,
}
#[derive(Debug, Subcommand)]
enum CliCommand {
Info,
RefreshAll,
GetBar,
GetConfig {
pointer: Option<String>,
},
GetTheme {
pointer: Option<String>,
},
SetTheme {
pointer: String,
json_value: String,
},
Click {
target: String,
button: Button,
#[clap(long, short)]
modifiers: Vec<Modifier>,
#[clap(long, short)]
x: Option<usize>,
#[clap(long, short)]
y: Option<usize>,
#[clap(long)]
relative_x: Option<usize>,
#[clap(long)]
relative_y: Option<usize>,
#[clap(long)]
output_x: Option<usize>,
#[clap(long)]
output_y: Option<usize>,
#[clap(long, short = 'W')]
width: Option<usize>,
#[clap(long, short = 'H')]
height: Option<usize>,
},
Signal {
target: String,
},
Custom {
target: String,
#[clap(trailing_var_arg = true)]
args: Vec<String>,
},
Shutdown,
}
#[derive(Debug, Clone)]
struct Button(I3Button);
impl ValueEnum for Button {
fn value_variants<'a>() -> &'a [Self] {
&[
Self(I3Button::Left),
Self(I3Button::Middle),
Self(I3Button::Right),
Self(I3Button::ScrollUp),
Self(I3Button::ScrollDown),
]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
match self.0 {
I3Button::Left => Some(PossibleValue::new("left")),
I3Button::Middle => Some(PossibleValue::new("middle")),
I3Button::Right => Some(PossibleValue::new("right")),
I3Button::ScrollUp => Some(PossibleValue::new("scroll_up")),
I3Button::ScrollDown => Some(PossibleValue::new("scroll_down")),
I3Button::ScrollRight => Some(PossibleValue::new("scroll_right")),
I3Button::ScrollLeft => Some(PossibleValue::new("scroll_left")),
_ => None,
}
}
}
#[derive(Debug, Clone)]
struct Modifier(I3Modifier);
impl ValueEnum for Modifier {
fn value_variants<'a>() -> &'a [Self] {
&[
Self(I3Modifier::Control),
Self(I3Modifier::Mod1),
Self(I3Modifier::Mod2),
Self(I3Modifier::Mod3),
Self(I3Modifier::Mod4),
Self(I3Modifier::Mod5),
Self(I3Modifier::Shift),
]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
match self.0 {
I3Modifier::Control => Some(PossibleValue::new("control")),
I3Modifier::Mod1 => Some(PossibleValue::new("mod1")),
I3Modifier::Mod2 => Some(PossibleValue::new("mod2")),
I3Modifier::Mod3 => Some(PossibleValue::new("mod3")),
I3Modifier::Mod4 => Some(PossibleValue::new("mod4")),
I3Modifier::Mod5 => Some(PossibleValue::new("mod5")),
I3Modifier::Shift => Some(PossibleValue::new("shift")),
}
}
}
fn send_message(socket_path: impl AsRef<OsStr>, msg: IpcMessage) -> Result<IpcReply> {
let mut stream = UnixStream::connect(socket_path.as_ref())?;
let msg = encode_ipc_msg(msg)?;
if let Err(e) = stream.write_all(&msg) {
bail!("Error writing to socket: {}", e);
}
let mut buf = vec![];
let n = match stream.read_to_end(&mut buf) {
Ok(n) => n,
Err(e) => {
bail!("Error reading from socket: {}", e);
}
};
Ok(serde_json::from_slice(&buf[IPC_HEADER_LEN..n])?)
}
fn send_and_print_response(socket_path: impl AsRef<OsStr>, msg: IpcMessage) -> Result<()> {
let resp = match send_message(&socket_path, msg) {
Ok(resp) => resp,
Err(e) => bail!("failed to send ipc message: {}", e),
};
println!(
"{}",
match resp {
IpcReply::Help(help) => help,
IpcReply::Value(value) => value.to_string(),
x => serde_json::to_string(&x)?,
}
);
Ok(())
}
fn get_json_response(socket_path: &PathBuf, msg: IpcMessage) -> Result<Value> {
Ok(match send_message(socket_path, msg)? {
IpcReply::Value(json) => json,
_ => unreachable!(),
})
}
fn main() -> Result<()> {
let args = Cli::parse();
let socket_path = get_socket_path(args.socket.as_ref())?;
match args.cmd {
CliCommand::Shutdown => send_and_print_response(&socket_path, IpcMessage::Shutdown)?,
CliCommand::Info => send_and_print_response(&socket_path, IpcMessage::Info)?,
CliCommand::GetBar => send_and_print_response(&socket_path, IpcMessage::GetBar)?,
CliCommand::RefreshAll => send_and_print_response(&socket_path, IpcMessage::RefreshAll)?,
CliCommand::GetConfig { pointer: None } => {
send_and_print_response(&socket_path, IpcMessage::GetConfig)?
}
CliCommand::GetTheme { pointer: None } => {
send_and_print_response(&socket_path, IpcMessage::GetTheme)?
}
CliCommand::GetConfig {
pointer: Some(pointer),
} => {
let config = get_json_response(&socket_path, IpcMessage::GetConfig)?;
match config.pointer(&pointer) {
Some(value) => println!("{}", value),
None => bail!("No value found at: {}", pointer),
}
}
CliCommand::GetTheme {
pointer: Some(pointer),
} => {
let theme = get_json_response(&socket_path, IpcMessage::GetTheme)?;
match theme.pointer(&pointer) {
Some(value) => println!("{}", value),
None => bail!("No value found at: {}", pointer),
}
}
CliCommand::SetTheme {
pointer,
json_value,
} => {
let mut theme = get_json_response(&socket_path, IpcMessage::GetTheme)?;
match theme.pointer_mut(&pointer) {
Some(value) => {
let trimmed = json_value.trim();
let new_value = match serde_json::from_str::<Value>(&trimmed) {
Ok(value) => Ok(value),
Err(_) if !trimmed.starts_with(&['[', '{', '\'', '"']) => {
Ok(Value::String(trimmed.into()))
}
err => err,
}?;
*value = new_value;
send_and_print_response(&socket_path, IpcMessage::SetTheme(theme))?;
}
None => bail!("No value found at: {}", pointer),
}
}
CliCommand::Click {
target,
button,
modifiers,
x,
y,
relative_x,
relative_y,
output_x,
output_y,
width,
height,
} => {
let mut click = I3ClickEvent::default();
click.button = button.0;
click.instance = Some(target.clone());
click.modifiers = modifiers.into_iter().map(|m| m.0).collect();
x.map(|x| click.x = x);
y.map(|y| click.y = y);
relative_x.map(|relative_x| click.relative_x = relative_x);
relative_y.map(|relative_y| click.relative_y = relative_y);
output_x.map(|output_x| click.output_x = output_x);
output_y.map(|output_y| click.output_y = output_y);
width.map(|width| click.width = width);
height.map(|height| click.height = height);
let event = IpcBarEvent::Click(click);
send_and_print_response(
&socket_path,
IpcMessage::BarEvent {
instance: target,
event,
},
)?;
}
CliCommand::Signal { target } => send_and_print_response(
&socket_path,
IpcMessage::BarEvent {
instance: target,
event: IpcBarEvent::Signal,
},
)?,
CliCommand::Custom { target, args } => send_and_print_response(
&socket_path,
IpcMessage::BarEvent {
instance: target,
event: IpcBarEvent::Custom(args),
},
)?,
}
Ok(())
}
#[cfg(test)]
#[path = "../src/test_utils.rs"]
mod test_utils;
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use super::test_utils::generate_manpage;
use super::*;
#[test]
fn manpage() {
generate_manpage(Cli::command());
}
}