diff --git a/Cargo.lock b/Cargo.lock index 95b7a15..66fed0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,15 @@ dependencies = [ "core_extensions", ] +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -118,6 +127,17 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.13", +] + [[package]] name = "atk" version = "0.16.0" @@ -286,6 +306,12 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58baae561b85ca19b3122a9ddd35c8ec40c3bcd14fe89921824eae73f7baffbf" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -381,6 +407,25 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.0", + "syn 1.0.107", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "downcast-rs" version = "1.2.0" @@ -816,6 +861,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "0.2.8" @@ -887,6 +938,39 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "hyprland" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2166a21b9f0018d522bfd25debccdb16af8d2100aee63f842f9da37256129571" +dependencies = [ + "async-trait", + "derive_more", + "doc-comment", + "futures", + "hex", + "hyprland-macros", + "lazy_static", + "num-traits", + "paste", + "regex", + "serde", + "serde_json", + "serde_repr", + "strum", + "tokio", +] + +[[package]] +name = "hyprland-macros" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de65550b4ec230167654f367b6ec02795acf2cfd9692f05bedb10ff09e46a6e" +dependencies = [ + "quote", + "syn 2.0.13", +] + [[package]] name = "iana-time-zone" version = "0.1.53" @@ -1386,6 +1470,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "randr" +version = "0.1.0" +dependencies = [ + "abi_stable", + "anyrun-plugin", + "fuzzy-matcher", + "hyprland", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1401,6 +1495,8 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] @@ -1557,6 +1653,12 @@ dependencies = [ "base64 0.21.0", ] +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + [[package]] name = "ryu" version = "1.0.12" @@ -1640,6 +1742,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.13", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1662,6 +1775,15 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.7" @@ -1705,6 +1827,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.107", +] + [[package]] name = "sublime_fuzzy" version = "0.7.0" @@ -1847,11 +1991,25 @@ dependencies = [ "memchr", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys", ] +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.107", +] + [[package]] name = "tokio-rustls" version = "0.23.4" diff --git a/Cargo.toml b/Cargo.toml index 8b2edad..c87583f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,5 @@ members = [ "plugins/shell", "plugins/kidex", "plugins/translate", + "plugins/randr", ] \ No newline at end of file diff --git a/README.md b/README.md index 95a98f4..1fb8fc8 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,9 @@ Anyrun requires plugins to function, as they provide the results for input. The - Run shell commands - [Kidex](plugins/kidex) - File search provided by [Kidex](https://github.com/Kirottu/kidex) +- [Randr](plugins/randr) + - Rotate and resize; quickly change monitor configurations on the fly. + - TODO: Only supports Hyprland, needs support for other compositors. ## Configuration diff --git a/plugins/randr/Cargo.toml b/plugins/randr/Cargo.toml new file mode 100644 index 0000000..cfeb6cf --- /dev/null +++ b/plugins/randr/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "randr" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +fuzzy-matcher = "0.3.7" +anyrun-plugin = { path = "../../anyrun-plugin" } +abi_stable = "0.11.1" +hyprland = "0.3" diff --git a/plugins/randr/src/lib.rs b/plugins/randr/src/lib.rs new file mode 100644 index 0000000..f524238 --- /dev/null +++ b/plugins/randr/src/lib.rs @@ -0,0 +1,163 @@ +use std::env; + +use abi_stable::std_types::{ROption, RString, RVec}; +use anyrun_plugin::{anyrun_interface::HandleResult, plugin, Match, PluginInfo}; +use fuzzy_matcher::FuzzyMatcher; +use randr::{dummy::Dummy, hyprland::Hyprland, Configure, Monitor, Randr}; + +mod randr; + +enum InnerState { + None, + Position(Monitor), +} + +pub struct State { + randr: Box, + inner: InnerState, +} + +pub fn init(_config_dir: RString) -> State { + // Determine which Randr implementation should be used + let randr: Box = if env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() { + Box::new(Hyprland::new()) + } else { + Box::new(Dummy) + }; + + State { + randr, + inner: InnerState::None, + } +} + +pub fn info() -> PluginInfo { + PluginInfo { + name: "Randr".into(), + icon: "video-display".into(), + } +} + +pub fn handler(_match: Match, state: &mut State) -> HandleResult { + match &state.inner { + InnerState::None => { + state.inner = InnerState::Position( + state + .randr + .get_monitors() + .into_iter() + .find(|mon| mon.id == _match.id.unwrap()) + .unwrap(), + ); + HandleResult::Refresh(true) + } + InnerState::Position(mon) => { + if _match.id.unwrap() == u64::MAX { + state.inner = InnerState::None; + return HandleResult::Refresh(false); + } + + let rel_id = (_match.id.unwrap() >> 32) as u32; + let action = _match.id.unwrap() as u32; + + let rel_mon = state + .randr + .get_monitors() + .into_iter() + .find(|mon| mon.id == rel_id as u64) + .unwrap(); + + state + .randr + .configure(mon, Configure::from_id(action, &rel_mon)); + + HandleResult::Close + } + } +} + +pub fn get_matches(input: RString, state: &mut State) -> RVec { + let matcher = fuzzy_matcher::skim::SkimMatcherV2::default().smart_case(); + let mut vec = match &state.inner { + InnerState::None => state + .randr + .get_monitors() + .into_iter() + .map(|mon| Match { + title: format!("Change position of {}", mon.name).into(), + description: ROption::RSome( + format!("{}x{} at {}x{}", mon.width, mon.height, mon.x, mon.y).into(), + ), + use_pango: false, + icon: ROption::RSome("object-flip-horizontal".into()), + id: ROption::RSome(mon.id), + }) + .collect::>(), + InnerState::Position(mon) => { + let mut vec = state + .randr + .get_monitors() + .into_iter() + .filter_map(|_mon| { + if _mon == *mon { + None + } else { + Some( + [ + Configure::Mirror(&_mon), + Configure::LeftOf(&_mon), + Configure::RightOf(&_mon), + Configure::Below(&_mon), + Configure::Above(&_mon), + ] + .iter() + .map(|configure| Match { + title: format!("{} {}", configure.to_string(), _mon.name).into(), + description: ROption::RNone, + use_pango: false, + icon: ROption::RSome(configure.icon().into()), + // Store 2 32 bit IDs in the single 64 bit integer, a bit of a hack + id: ROption::RSome(_mon.id << 32 | Into::::into(configure)), + }) + .collect::>(), + ) + } + }) + .flatten() + .collect::>(); + + vec.push(Match { + title: "Reset position".into(), + description: ROption::RNone, + use_pango: false, + icon: ROption::RSome(Configure::Zero.icon().into()), + id: ROption::RSome((&Configure::Zero).into()), + }); + + vec.push(Match { + title: "Back".into(), + description: ROption::RSome("Return to the previous menu".into()), + use_pango: false, + icon: ROption::RSome("edit-undo".into()), + id: ROption::RSome(u64::MAX), + }); + + vec + } + } + .into_iter() + .filter_map(|_match| { + matcher + .fuzzy_match(&_match.title, &input) + .map(|score| (_match, score)) + }) + .collect::>(); + + vec.sort_by(|a, b| b.1.cmp(&a.1)); + + vec.truncate(5); + + vec.into_iter().map(|(_match, _)| _match).collect() +} + +plugin!(init, info, get_matches, handler, State); diff --git a/plugins/randr/src/randr/dummy.rs b/plugins/randr/src/randr/dummy.rs new file mode 100644 index 0000000..bfea235 --- /dev/null +++ b/plugins/randr/src/randr/dummy.rs @@ -0,0 +1,11 @@ +use super::Randr; + +pub struct Dummy; + +impl Randr for Dummy { + fn get_monitors(&self) -> Vec { + Vec::new() + } + + fn configure(&self, _mon: &super::Monitor, _config: super::Configure) {} +} diff --git a/plugins/randr/src/randr/hyprland.rs b/plugins/randr/src/randr/hyprland.rs new file mode 100644 index 0000000..f864f10 --- /dev/null +++ b/plugins/randr/src/randr/hyprland.rs @@ -0,0 +1,141 @@ +use hyprland::{ + data, + keyword::Keyword, + shared::{HyprData, HyprDataVec}, +}; + +use super::{Configure, Monitor, Randr}; + +pub struct Hyprland { + monitors: Vec, +} + +impl Hyprland { + pub fn new() -> Self { + Self { + monitors: data::Monitors::get().unwrap().to_vec(), + } + } +} + +impl Randr for Hyprland { + fn get_monitors(&self) -> Vec { + self.monitors + .iter() + .cloned() + .map(|mon| Monitor { + x: mon.x, + y: mon.y, + width: mon.width as u32, + height: mon.height as u32, + refresh_rate: mon.refresh_rate, + scale: mon.scale, + name: mon.name, + id: mon.id as u64, + }) + .collect() + } + + fn configure(&self, mon: &Monitor, config: Configure) { + match config { + Configure::Mirror(rel) => Keyword::set( + "monitor", + format!("{},preferred,auto,1,mirror,{}", mon.name, rel.name), + ) + .expect("Failed to configure monitor"), + Configure::LeftOf(rel) => { + let mut x = rel.x - mon.width as i32; + if x < 0 { + Keyword::set( + "monitor", + format!( + "{},{}x{}@{},{}x{},{}", + rel.name, + rel.width, + rel.height, + rel.refresh_rate, + rel.x - x, + rel.y, + rel.scale + ), + ) + .expect("Failed to configure monitor"); + x = 0; + } + + Keyword::set( + "monitor", + format!( + "{},{}x{}@{},{}x{},{}", + mon.name, mon.width, mon.height, mon.refresh_rate, x, rel.y, mon.scale + ), + ) + .expect("Failed to configure monitor"); + } + Configure::RightOf(rel) => Keyword::set( + "monitor", + format!( + "{},{}x{}@{},{}x{},1", + mon.name, + mon.width, + mon.height, + mon.refresh_rate, + rel.x + rel.width as i32, + rel.y + ), + ) + .expect("Failed to configure monitor"), + Configure::Below(rel) => Keyword::set( + "monitor", + format!( + "{},{}x{}@{},{}x{},{}", + mon.name, + mon.width, + mon.height, + mon.refresh_rate, + rel.x, + rel.y + rel.height as i32, + mon.scale + ), + ) + .expect("Failed to configure monitor"), + Configure::Above(rel) => { + let mut y = rel.y - mon.height as i32; + if y < 0 { + Keyword::set( + "monitor", + format!( + "{},{}x{}@{},{}x{},{}", + rel.name, + rel.width, + rel.height, + rel.refresh_rate, + rel.x, + rel.y - y, + rel.scale + ), + ) + .expect("Failed to configure monitor"); + y = 0; + } + + Keyword::set( + "monitor", + format!( + "{},{}x{}@{},{}x{},{}", + mon.name, mon.width, mon.height, mon.refresh_rate, rel.x, y, mon.scale + ), + ) + .expect("Failed to configure monitor"); + } + Configure::Zero => Keyword::set( + "monitor", + format!( + "{},{}x{}@{},0x0,{}", + mon.name, mon.width, mon.height, mon.refresh_rate, mon.scale + ), + ) + .expect("Failed to configure monitor"), + } + } +} diff --git a/plugins/randr/src/randr/mod.rs b/plugins/randr/src/randr/mod.rs new file mode 100644 index 0000000..182205a --- /dev/null +++ b/plugins/randr/src/randr/mod.rs @@ -0,0 +1,79 @@ +pub mod dummy; +pub mod hyprland; + +#[derive(PartialEq)] +pub struct Monitor { + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, + pub scale: f32, + pub refresh_rate: f32, + pub name: String, + pub id: u64, +} + +pub enum Configure<'a> { + Mirror(&'a Monitor), + LeftOf(&'a Monitor), + RightOf(&'a Monitor), + Below(&'a Monitor), + Above(&'a Monitor), + Zero, +} + +impl<'a> Configure<'a> { + pub fn from_id(id: u32, mon: &'a Monitor) -> Self { + match id { + 0 => Configure::Mirror(mon), + 1 => Configure::LeftOf(mon), + 2 => Configure::RightOf(mon), + 3 => Configure::Below(mon), + 4 => Configure::Above(mon), + 5 => Configure::Zero, + _ => unreachable!(), + } + } + + pub fn icon(&self) -> &'static str { + match self { + Configure::Mirror(_) => "edit-copy", + Configure::LeftOf(_) => "go-previous", + Configure::RightOf(_) => "go-next", + Configure::Below(_) => "go-down", + Configure::Above(_) => "go-up", + Configure::Zero => "go-home", + } + } +} + +impl<'a> ToString for Configure<'a> { + fn to_string(&self) -> String { + match self { + Configure::Mirror(_) => "Mirror".to_string(), + Configure::LeftOf(_) => "Left of".to_string(), + Configure::RightOf(_) => "Right of".to_string(), + Configure::Below(_) => "Below".to_string(), + Configure::Above(_) => "Above".to_string(), + Configure::Zero => "Zero".to_string(), + } + } +} + +impl<'a> From<&Configure<'a>> for u64 { + fn from(configure: &Configure) -> u64 { + match configure { + Configure::Mirror(_) => 0, + Configure::LeftOf(_) => 1, + Configure::RightOf(_) => 2, + Configure::Below(_) => 3, + Configure::Above(_) => 4, + Configure::Zero => 5, + } + } +} + +pub trait Randr { + fn get_monitors(&self) -> Vec; + fn configure(&self, mon: &Monitor, config: Configure); +}