Skip to content

Commit

Permalink
IPC: Accept multiple actions from stdin
Browse files Browse the repository at this point in the history
  • Loading branch information
bbb651 committed Jan 19, 2025
1 parent bd559a2 commit a8d9709
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 11 deletions.
6 changes: 4 additions & 2 deletions niri-ipc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ use serde::{Deserialize, Serialize};
pub mod socket;
pub mod state;

mod utils;

/// Request from client to niri.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
Expand All @@ -63,8 +65,8 @@ pub enum Request {
FocusedOutput,
/// Request information about the focused window.
FocusedWindow,
/// Perform an action.
Action(Action),
/// Perform actions.
Action(#[serde(with = "utils::one_or_many")] Vec<Action>),
/// Change output configuration temporarily.
///
/// The configuration is changed temporarily and not saved into the config file. If the output
Expand Down
116 changes: 116 additions & 0 deletions niri-ipc/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
pub(crate) mod one_or_many {
use serde::{Deserialize, Deserializer, Serialize, Serializer};

pub(crate) fn serialize<T: Serialize, S: Serializer>(
value: &Vec<T>,
serializer: S,
) -> Result<S::Ok, S::Error> {
if value.len() == 1 {
value[0].serialize(serializer)
} else {
value.serialize(serializer)
}
}

pub(crate) fn deserialize<'de, T: Deserialize<'de>, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<T>, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany<T> {
Many(Vec<T>),
One(T),
}

match OneOrMany::deserialize(deserializer)? {
OneOrMany::Many(v) => Ok(v),
OneOrMany::One(v) => Ok(vec![v]),
}
}

#[cfg(test)]
mod tests {
use serde_json::de::SliceRead;
use serde_json::{Deserializer, Serializer, Value};

use super::*;

#[test]
fn serialize_one() {
let mut result = Vec::new();
let mut serializer = Serializer::new(&mut result);
serialize(&vec![Value::Null], &mut serializer).expect("failed to serialize");
assert_eq!(String::from_utf8_lossy(&result), "null");
}

#[test]
fn deserialize_one() {
let mut deserailier = Deserializer::new(SliceRead::new("null".as_bytes()));
let result: Vec<Value> = deserialize(&mut deserailier).expect("failed to deserialize");
assert_eq!(result, vec![Value::Null]);
}

#[test]
fn serialize_many() {
let mut result = Vec::new();
let mut serializer = Serializer::new(&mut result);
serialize(&vec![Value::Null, Value::Null], &mut serializer)
.expect("failed to serialize");
assert_eq!(String::from_utf8_lossy(&result), "[null,null]");
}

#[test]
fn deserialize_many() {
let mut deserailier = Deserializer::new(SliceRead::new("[null,null]".as_bytes()));
let result: Vec<Value> = deserialize(&mut deserailier).expect("failed to deserialize");
assert_eq!(result, vec![Value::Null, Value::Null]);
}

#[test]
fn serialize_none() {
let mut result = Vec::new();
let mut serializer = Serializer::new(&mut result);
serialize(&Vec::<Value>::new(), &mut serializer).expect("failed to serialize");
assert_eq!(String::from_utf8_lossy(&result), "[]");
}

#[test]
fn deserialize_none() {
let mut deserailier = Deserializer::new(SliceRead::new("[]".as_bytes()));
let result: Vec<Value> = deserialize(&mut deserailier).expect("failed to deserialize");
assert_eq!(result, Vec::<Value>::new());
}

#[test]
fn serialize_derive() {
#[derive(Debug, Serialize, PartialEq)]
enum Request {
Action(#[serde(with = "self")] Vec<String>),
}
let request = serde_json::to_string(&Request::Action(vec!["foo".to_string()]))
.expect("failed to serialize");
assert_eq!(request, r#"{"Action":"foo"}"#);
let request =
serde_json::to_string(&Request::Action(vec!["foo".to_string(), "bar".to_string()]))
.expect("failed to serialize");
assert_eq!(request, r#"{"Action":["foo","bar"]}"#);
}

#[test]
fn deserialize_derive() {
#[derive(Debug, Deserialize, PartialEq)]
enum Request {
Action(#[serde(with = "self")] Vec<String>),
}
let request: Request =
serde_json::from_str(r#"{"Action":"foo"}"#).expect("failed to deserialize");
assert_eq!(request, Request::Action(vec!["foo".to_string()]));
let request: Request =
serde_json::from_str(r#"{"Action":["foo","bar"]}"#).expect("failed to deserialize");
assert_eq!(
request,
Request::Action(vec!["foo".to_string(), "bar".to_string()])
);
}
}
}
6 changes: 5 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,13 @@ pub enum Msg {
FocusedWindow,
/// Perform an action.
Action {
// NOTE: This is only optional because of clap_derive limitations and will never be `None`.
// If an action is not provided it reads actions from stdin and turns to [`Msg::Actions`].
#[command(subcommand)]
action: Action,
action: Option<Action>,
},
#[clap(skip)]
Actions { actions: Vec<Action> },
/// Change output configuration temporarily.
///
/// The configuration is changed temporarily and not saved into the config file. If the output
Expand Down
8 changes: 6 additions & 2 deletions src/ipc/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Msg::Outputs => Request::Outputs,
Msg::FocusedWindow => Request::FocusedWindow,
Msg::FocusedOutput => Request::FocusedOutput,
Msg::Action { action } => Request::Action(action.clone()),
Msg::Action {
action: Some(action),
} => Request::Action(vec![action.clone()]),
Msg::Action { action: None } => unreachable!(),
Msg::Actions { actions } => Request::Action(actions.clone()),
Msg::Output { output, action } => Request::Output {
output: output.clone(),
action: action.clone(),
Expand Down Expand Up @@ -252,7 +256,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!("No output is focused.");
}
}
Msg::Action { .. } => {
Msg::Action { .. } | Msg::Actions { .. } => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
};
Expand Down
7 changes: 4 additions & 3 deletions src/ipc/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,15 +309,16 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let window = windows.values().find(|win| win.is_focused).cloned();
Response::FocusedWindow(window)
}
Request::Action(action) => {
Request::Action(actions) => {
let (tx, rx) = async_channel::bounded(1);

let action = niri_config::Action::from(action);
ctx.event_loop.insert_idle(move |state| {
// Make sure some logic like workspace clean-up has a chance to run before doing
// actions.
state.niri.advance_animations();
state.do_action(action, false);
for action in actions.into_iter().map(niri_config::Action::from) {
state.do_action(action, false);
}
let _ = tx.send_blocking(());
});

Expand Down
22 changes: 19 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ use std::io::{self, Write};
use std::os::fd::FromRawFd;
use std::path::PathBuf;
use std::process::Command;
use std::{env, mem};
use std::{env, iter, mem};

use clap::Parser;
use directories::ProjectDirs;
use niri::cli::{Cli, Sub};
use niri::cli::{Cli, Msg, Sub};
#[cfg(feature = "dbus")]
use niri::dbus;
use niri::ipc::client::handle_msg;
Expand All @@ -24,6 +24,7 @@ use niri::utils::watcher::Watcher;
use niri::utils::{cause_panic, version, IS_SYSTEMD_SERVICE};
use niri_config::Config;
use niri_ipc::socket::SOCKET_PATH_ENV;
use niri_ipc::Action;
use portable_atomic::Ordering;
use sd_notify::NotifyState;
use smithay::reexports::calloop::EventLoop;
Expand Down Expand Up @@ -104,7 +105,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
info!("config is valid");
return Ok(());
}
Sub::Msg { msg, json } => {
Sub::Msg { mut msg, json } => {
if let Msg::Action { action: None } = msg {
let actions = io::stdin()
.lines()
.map(|line| {
line.map(|line| {
Action::parse_from(iter::once("action").chain(line.split(' ')))
})
})
.collect::<Result<Vec<_>, _>>()?;
if actions.is_empty() {
warn!("read actions from stdin but no actions were provided");
return Ok(());
}
msg = Msg::Actions { actions }
}
handle_msg(msg, json)?;
return Ok(());
}
Expand Down

0 comments on commit a8d9709

Please sign in to comment.