diff --git a/Cargo.toml b/Cargo.toml index 18c2829..317cfd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,5 @@ members = [ "plugin-desktop", "plugin-audio", "plugin-network", + "plugin-window", ] diff --git a/Makefile b/Makefile index e5cd69e..a7f188c 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,7 @@ deploy: cp -vf ./target/release/run ${DEST}/rmenu-run cp -vf ./target/release/audio ${DEST}/rmenu-audio cp -vf ./target/release/network ${DEST}/rmenu-network + cp -vf ./target/release/window ${DEST}/rmenu-window cp -vf ./rmenu/public/config.yaml ${DEST}/config.yaml build: build-rmenu build-plugins @@ -34,3 +35,4 @@ build-plugins: ${CARGO} build -p desktop ${FLAGS} ${CARGO} build -p audio ${FLAGS} ${CARGO} build -p network ${FLAGS} + ${CARGO} build -p window ${FLAGS} diff --git a/plugin-window/Cargo.toml b/plugin-window/Cargo.toml new file mode 100644 index 0000000..13bf968 --- /dev/null +++ b/plugin-window/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "window" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["sway"] +sway = [] + +[dependencies] +anyhow = "1.0.72" +clap = { version = "4.3.21", features = ["derive"] } +rmenu-plugin = { version = "0.0.1", path = "../rmenu-plugin" } +serde = { version = "1.0.183", features = ["derive"] } +serde_json = "1.0.104" diff --git a/plugin-window/src/main.rs b/plugin-window/src/main.rs new file mode 100644 index 0000000..cdfcc99 --- /dev/null +++ b/plugin-window/src/main.rs @@ -0,0 +1,52 @@ +use std::fmt::Debug; + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use rmenu_plugin::Entry; + +#[cfg(feature = "sway")] +mod sway; + +/// Trait To Implement for Window Focus +pub trait WindowManager: Debug { + fn focus(&self, id: &str) -> Result<()>; + fn entries(&self) -> Result>; +} + +#[derive(Debug, Subcommand)] +pub enum Commands { + ListWindow, + Focus { id: String }, +} + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Cli { + #[clap(subcommand)] + command: Option, +} + +/// Retrieve WindowManager Implementation +#[allow(unreachable_code)] +fn get_impl() -> impl WindowManager { + #[cfg(feature = "sway")] + return sway::SwayManager {}; + // if no features are enabled for some reason? + panic!("No Implementations Available") +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + let windows = get_impl(); + let command = cli.command.unwrap_or(Commands::ListWindow); + match command { + Commands::Focus { id } => windows.focus(&id)?, + Commands::ListWindow => { + for entry in windows.entries()? { + println!("{}", serde_json::to_string(&entry).unwrap()); + } + } + } + Ok(()) +} diff --git a/plugin-window/src/sway.rs b/plugin-window/src/sway.rs new file mode 100644 index 0000000..6712bb4 --- /dev/null +++ b/plugin-window/src/sway.rs @@ -0,0 +1,93 @@ +//! Sway WindowMangager Window Selector +use std::process::{Command, Stdio}; + +use anyhow::{anyhow, Context, Result}; +use rmenu_plugin::Entry; +use serde::Deserialize; +use serde_json::Value; + +use crate::WindowManager; + +static SWAY_TYPE_KEY: &'static str = "type"; +static SWAY_NODES_KEY: &'static str = "nodes"; +static SWAY_WINDOW_TYPE: &'static str = "con"; +static SWAY_WINDOW_NAME: &'static str = "name"; + +#[derive(Debug, Deserialize)] +pub struct SwayWindow { + pub name: String, + pub pid: u64, + pub focused: bool, +} + +#[derive(Debug)] +pub struct SwayManager {} + +pub fn get_windows() -> Result> { + // retrieve output of swaymsg tree + let out = Command::new("swaymsg") + .args(["-t", "get_tree"]) + .stdout(Stdio::piped()) + .output() + .context("SwayMsg Failed to Execute")?; + if !out.status.success() { + return Err(anyhow!("Invalid SwayMsg Status: {:?}", out.status)); + } + // read output as string + let result: Value = + serde_json::from_slice(&out.stdout).context("Failed to Parse SwayMsg Output")?; + // recursively parse object for window definitions + let mut nodes = vec![result]; + let mut windows = vec![]; + while let Some(item) = nodes.pop() { + if !item.is_object() { + return Err(anyhow!("Unexpected Node Value: {:?}", item)); + } + // pass additional nodes if not a valid window object + let Some(ntype) = item.get(SWAY_TYPE_KEY) else { continue }; + let is_nulled = item + .get(SWAY_WINDOW_NAME) + .map(|v| v.is_null()) + .unwrap_or(false); + if ntype != SWAY_WINDOW_TYPE || is_nulled { + let Some(snodes) = item.get(SWAY_NODES_KEY) else { continue }; + match snodes { + Value::Array(array) => nodes.extend(array.clone().into_iter()), + _ => return Err(anyhow!("Unexpected NodeList Value: {:?}", snodes)), + } + continue; + } + let window: SwayWindow = + serde_json::from_value(item.clone()).context("Failed to Parse Window Object")?; + windows.push(window); + } + windows.sort_by_key(|w| w.focused); + Ok(windows) +} + +impl WindowManager for SwayManager { + /// Focus on Specified Window + fn focus(&self, id: &str) -> Result<()> { + let out = Command::new("swaymsg") + .arg(format!("[pid={}] focus", id)) + .output() + .context("Failed SwayMsg To Focus Window: {id:?}")?; + if !out.status.success() { + return Err(anyhow!("SwayMsg Exited with Error: {:?}", out.status)); + } + Ok(()) + } + /// Generate RMenu Entries + fn entries(&self) -> Result> { + let exe = std::env::current_exe()?.to_str().unwrap().to_string(); + let windows = get_windows()?; + let entries = windows + .into_iter() + .map(|w| { + let exec = format!("{exe} focus {:?}", w.pid); + Entry::new(&w.name, &exec, None) + }) + .collect(); + Ok(entries) + } +} diff --git a/rmenu/public/config.yaml b/rmenu/public/config.yaml index d856d11..5a74bef 100644 --- a/rmenu/public/config.yaml +++ b/rmenu/public/config.yaml @@ -33,6 +33,10 @@ plugins: exec: ["~/.config/rmenu/rmenu-network"] cache: false placeholder: "Connect to the Specified Wi-Fi" + window: + exec: ["~/.config/rmenu/rmenu-window"] + cache: false + placeholder: "Jump to the Specified Window" # custom keybindings keybinds: