├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src ├── config.rs ├── main.rs ├── scratchpad.rs ├── socket.rs └── yabai_schema.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.58" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "1.3.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "dirs" 25 | version = "4.0.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 28 | dependencies = [ 29 | "dirs-sys", 30 | ] 31 | 32 | [[package]] 33 | name = "dirs-sys" 34 | version = "0.3.7" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 37 | dependencies = [ 38 | "libc", 39 | "redox_users", 40 | "winapi", 41 | ] 42 | 43 | [[package]] 44 | name = "getrandom" 45 | version = "0.2.7" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 48 | dependencies = [ 49 | "cfg-if", 50 | "libc", 51 | "wasi", 52 | ] 53 | 54 | [[package]] 55 | name = "itoa" 56 | version = "1.0.2" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" 59 | 60 | [[package]] 61 | name = "libc" 62 | version = "0.2.126" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 65 | 66 | [[package]] 67 | name = "proc-macro2" 68 | version = "1.0.40" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" 71 | dependencies = [ 72 | "unicode-ident", 73 | ] 74 | 75 | [[package]] 76 | name = "quote" 77 | version = "1.0.20" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" 80 | dependencies = [ 81 | "proc-macro2", 82 | ] 83 | 84 | [[package]] 85 | name = "redox_syscall" 86 | version = "0.2.13" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 89 | dependencies = [ 90 | "bitflags", 91 | ] 92 | 93 | [[package]] 94 | name = "redox_users" 95 | version = "0.4.3" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 98 | dependencies = [ 99 | "getrandom", 100 | "redox_syscall", 101 | "thiserror", 102 | ] 103 | 104 | [[package]] 105 | name = "ryu" 106 | version = "1.0.10" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" 109 | 110 | [[package]] 111 | name = "scratchpad" 112 | version = "0.1.0" 113 | dependencies = [ 114 | "anyhow", 115 | "serde", 116 | "serde_json", 117 | "shlex", 118 | "toml", 119 | "xdg", 120 | ] 121 | 122 | [[package]] 123 | name = "serde" 124 | version = "1.0.137" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" 127 | dependencies = [ 128 | "serde_derive", 129 | ] 130 | 131 | [[package]] 132 | name = "serde_derive" 133 | version = "1.0.137" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" 136 | dependencies = [ 137 | "proc-macro2", 138 | "quote", 139 | "syn", 140 | ] 141 | 142 | [[package]] 143 | name = "serde_json" 144 | version = "1.0.81" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" 147 | dependencies = [ 148 | "itoa", 149 | "ryu", 150 | "serde", 151 | ] 152 | 153 | [[package]] 154 | name = "shlex" 155 | version = "1.1.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" 158 | 159 | [[package]] 160 | name = "syn" 161 | version = "1.0.98" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" 164 | dependencies = [ 165 | "proc-macro2", 166 | "quote", 167 | "unicode-ident", 168 | ] 169 | 170 | [[package]] 171 | name = "thiserror" 172 | version = "1.0.31" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" 175 | dependencies = [ 176 | "thiserror-impl", 177 | ] 178 | 179 | [[package]] 180 | name = "thiserror-impl" 181 | version = "1.0.31" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" 184 | dependencies = [ 185 | "proc-macro2", 186 | "quote", 187 | "syn", 188 | ] 189 | 190 | [[package]] 191 | name = "toml" 192 | version = "0.5.9" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" 195 | dependencies = [ 196 | "serde", 197 | ] 198 | 199 | [[package]] 200 | name = "unicode-ident" 201 | version = "1.0.1" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" 204 | 205 | [[package]] 206 | name = "wasi" 207 | version = "0.11.0+wasi-snapshot-preview1" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 210 | 211 | [[package]] 212 | name = "winapi" 213 | version = "0.3.9" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 216 | dependencies = [ 217 | "winapi-i686-pc-windows-gnu", 218 | "winapi-x86_64-pc-windows-gnu", 219 | ] 220 | 221 | [[package]] 222 | name = "winapi-i686-pc-windows-gnu" 223 | version = "0.4.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 226 | 227 | [[package]] 228 | name = "winapi-x86_64-pc-windows-gnu" 229 | version = "0.4.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 232 | 233 | [[package]] 234 | name = "xdg" 235 | version = "2.4.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" 238 | dependencies = [ 239 | "dirs", 240 | ] 241 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scratchpad" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.58" 10 | shlex = "1.1.0" 11 | serde = { version = "1.0.137", features = ["derive"] } 12 | serde_json = "1.0.81" 13 | toml = "0.5.9" 14 | xdg = "2.4.1" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yabai Scratchpad 2 | A simple CLI tool that brings scratchpad functionality to the Yabai window manager on macOS. Inspired by the LeftWM scratchpad feature, this app allows you to quickly access and dismiss any application. 3 | 4 | ## Demo 5 | https://user-images.githubusercontent.com/46302068/177003301-54cf74ad-af72-4297-842e-68ba203c894b.mp4 6 | 7 | ## Installation Instruction 8 | * **Step 1:** Clone the repository 9 | * **Step 2:** `cargo build --release` 10 | * **Step 3:** `cp target/release/scratchpad /opt/homebrew/bin/` to copy `scratchpad` to path. 11 | * **Step 4:** Define configuration by creating `$HOME/.config/scratchpad/config.toml` 12 | * **Step 5:** Create SKHD config. eg. `alt - t : scratchpad --toggle telegram` 13 | 14 | *Note: Copying binary to `/usr/local/bin` won't work with SKHD* 15 | 16 | ## Usage 17 | ``` 18 | scratchpad --toggle {name} 19 | ``` 20 | 21 | ## Config Options 22 | 23 | | Field | Description | Example |Default | 24 | |------------------|------------------------------------|--------------------------------------|----------| 25 | |`name` | Name of scratchpad | `calculator` | N/A | 26 | |`target_type` | Type of target | {`app`,`title`} | `app` | 27 | |`target` | Target app name or title | `Discord` | N/A | 28 | |`position` | Position where scratchpad shows up | `[290, 175]` | N/A | 29 | |`size` | Size of scratchpad | `[1100, 700]` | N/A | 30 | |`rows` (Optional) | Number of grid rows | `6` | N/A | 31 | |`cols` (Optional) | Number of grid columns | `4` | N/A | 32 | |`launch_type` | Type of launch method | {`app`,`app_with_arg`, `command`} | `command`| 33 | |`launch_command` | Command or name of application | `open -n /Applications/Alacritty.app`| N/A | 34 | 35 | ## Configuration Example 36 | ``` 37 | scratchpad_space = 8 38 | launch_timeout = 5 39 | 40 | [[scratchpad]] 41 | name = "telegram" 42 | target_type = "app" 43 | target = "Telegram" 44 | position = [1, 1] 45 | size = [4, 4] 46 | rows = 6 47 | cols = 6 48 | launch_type = "app" 49 | launch_command = "Telegram.app" 50 | 51 | [[scratchpad]] 52 | name = "alacritty" 53 | target_type = "title" 54 | target = "AlacrittyScratchpad" 55 | position = [290, 175] 56 | size = [1100, 700] 57 | launch_type = "app_with_arg" 58 | launch_command = ["Alacritty.app", "--title", "AlacrittyScratchpad"] 59 | 60 | [[scratchpad]] 61 | name = "discord" 62 | target_type = "app" 63 | target = "Discord" 64 | position = [290, 175] 65 | size = [1100, 700] 66 | launch_type = "app" 67 | launch_command = "Discord.app" 68 | ``` 69 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::scratchpad::{LaunchOption, Scratchpad, Target}; 2 | use std::fs::File; 3 | use std::io::Read; 4 | use std::process; 5 | use toml::Value; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct Config { 9 | pub launch_timeout: u8, 10 | pub scratchpad_space: u8, 11 | pub scratchpads: Vec, 12 | } 13 | 14 | impl Config { 15 | pub fn get_config() -> Config { 16 | let xdg_dirs = xdg::BaseDirectories::with_prefix("scratchpad").unwrap(); 17 | let config_path = xdg_dirs 18 | .place_config_file("config.toml") 19 | .expect("cannot create configuration directory"); 20 | 21 | if !config_path.exists() { 22 | eprintln!("Couldn't find scratchpad config file!"); 23 | process::exit(0x1); 24 | } 25 | 26 | let mut config_file = File::open(config_path).expect("Coulnd't open the config file!"); 27 | let mut config_string = String::new(); 28 | 29 | config_file 30 | .read_to_string(&mut config_string) 31 | .expect("Failed to read config file!"); 32 | 33 | return Config::from(config_string.as_str()); 34 | } 35 | } 36 | 37 | impl From<&str> for Config { 38 | fn from(config_string: &str) -> Self { 39 | let config: Value = toml::from_str(config_string).expect("Invalid config!"); 40 | 41 | let mut launch_timeout: u8 = 10; 42 | let mut scratchpad_space: u8 = 8; 43 | 44 | match config.get("launch_timeout") { 45 | Some(value) => launch_timeout = value.as_integer().unwrap() as u8, 46 | _ => (), 47 | } 48 | 49 | match config.get("scratchpad_space") { 50 | Some(value) => scratchpad_space = value.as_integer().unwrap() as u8, 51 | _ => (), 52 | } 53 | 54 | let scratchpad_values = config 55 | .get("scratchpad") 56 | .expect("No scratchpad defined in config!"); 57 | 58 | let scratchpad = scratchpad_values 59 | .as_array() 60 | .unwrap() 61 | .iter() 62 | .map(|item| { 63 | let name = item 64 | .get("name") 65 | .expect("Name for scratchpad must be given!") 66 | .as_str() 67 | .unwrap() 68 | .to_string(); 69 | 70 | let target_str = item 71 | .get("target") 72 | .expect("Target for scratchpad must be given!") 73 | .as_str() 74 | .unwrap() 75 | .to_string(); 76 | 77 | let target = match item.get("target_type") { 78 | Some(Value::String(value)) => match value.as_str() { 79 | "app" => Target::App(target_str), 80 | "title" => Target::Title(target_str), 81 | _ => Target::App(target_str), 82 | }, 83 | _ => Target::App(target_str), 84 | }; 85 | 86 | let position_value = item 87 | .get("position") 88 | .expect("Position of scratchpad is required!") 89 | .as_array() 90 | .unwrap(); 91 | 92 | let position: [i16; 2] = position_value 93 | .iter() 94 | .map(|item| item.as_integer().unwrap() as i16) 95 | .collect::>() 96 | .try_into() 97 | .expect("Invalid position configuration!"); 98 | 99 | let rows: Option = match item.get("rows") { 100 | Some(value) => Some(value.as_integer().unwrap() as i16), 101 | _ => None, 102 | }; 103 | 104 | let cols: Option = match item.get("cols") { 105 | Some(value) => Some(value.as_integer().unwrap() as i16), 106 | _ => None, 107 | }; 108 | 109 | let size_value = item 110 | .get("size") 111 | .expect("Size of scratchpad is required!") 112 | .as_array() 113 | .unwrap(); 114 | 115 | let size: [i16; 2] = size_value 116 | .iter() 117 | .map(|item| item.as_integer().unwrap() as i16) 118 | .collect::>() 119 | .try_into() 120 | .expect("Invalid size configuration!"); 121 | 122 | let launch_command_value = item 123 | .get("launch_command") 124 | .expect("Launch command is required!"); 125 | 126 | let launch_type_str = match item.get("launch_type") { 127 | Some(Value::String(value)) => value.as_str(), 128 | _ => "command", 129 | }; 130 | 131 | let launch_command = if launch_type_str == "app_with_arg" { 132 | let tokens = launch_command_value 133 | .as_array() 134 | .unwrap() 135 | .iter() 136 | .map(|item| item.as_str().unwrap().to_string()) 137 | .collect::>(); 138 | 139 | LaunchOption::ApplicationWithArgs(tokens[0].clone(), tokens[1..].to_vec()) 140 | } else { 141 | let command_string = launch_command_value 142 | .as_str() 143 | .expect("Invalid launch command!") 144 | .to_string(); 145 | 146 | match launch_type_str { 147 | "app" => LaunchOption::Application(command_string), 148 | "command" => LaunchOption::Command(command_string), 149 | _ => LaunchOption::Application(command_string), 150 | } 151 | }; 152 | 153 | Scratchpad { 154 | name, 155 | target, 156 | position: position.into(), 157 | size: size.into(), 158 | rows, 159 | cols, 160 | launch_command, 161 | launch_timeout, 162 | scratchpad_space, 163 | } 164 | }) 165 | .collect::>(); 166 | 167 | Config { 168 | launch_timeout, 169 | scratchpad_space, 170 | scratchpads: scratchpad, 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use config::Config; 3 | use std::env; 4 | use std::process; 5 | 6 | mod config; 7 | mod scratchpad; 8 | mod socket; 9 | mod yabai_schema; 10 | 11 | fn main() -> Result<()> { 12 | match (env::args().nth(1), env::args().nth(2)) { 13 | (Some(command), Some(name)) => { 14 | if command == "--toggle" { 15 | let config = Config::get_config(); 16 | 17 | let scratchpad = config 18 | .scratchpads 19 | .into_iter() 20 | .find(|item| item.name == name); 21 | 22 | if scratchpad.is_none() { 23 | eprintln!("Didn't find scratchpad named `{}`", name); 24 | process::exit(0x1); 25 | } 26 | 27 | scratchpad.unwrap().toggle()?; 28 | } 29 | } 30 | _ => { 31 | eprintln!("Invalid arguments! Try `scratchpad --toggle example` to toggle scratchpad named `example`!"); 32 | process::exit(0x1); 33 | } 34 | }; 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /src/scratchpad.rs: -------------------------------------------------------------------------------- 1 | use crate::socket::query_socket; 2 | use crate::yabai_schema::{Space, Window}; 3 | use anyhow::Result; 4 | use serde::{Deserialize, Serialize}; 5 | use shlex::split; 6 | use std::process::Command as SysCommand; 7 | use std::thread; 8 | use std::time::{Duration, Instant}; 9 | 10 | #[derive(Debug, Clone)] 11 | pub enum Target { 12 | Title(String), 13 | App(String), 14 | } 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct Coordinate { 18 | pub x: i16, 19 | pub y: i16, 20 | } 21 | 22 | impl From<[i16; 2]> for Coordinate { 23 | fn from(nums: [i16; 2]) -> Self { 24 | return Self { 25 | x: nums[0], 26 | y: nums[1], 27 | }; 28 | } 29 | } 30 | 31 | impl ToString for Coordinate { 32 | fn to_string(&self) -> String { 33 | return format!("{}:{}:{}", "abs", self.x, self.y); 34 | } 35 | } 36 | 37 | #[derive(Debug, Clone)] 38 | pub struct Scratchpad { 39 | pub name: String, 40 | pub target: Target, 41 | pub position: Coordinate, 42 | pub size: Coordinate, 43 | pub rows: Option, 44 | pub cols: Option, 45 | pub launch_command: LaunchOption, 46 | pub launch_timeout: u8, 47 | pub scratchpad_space: u8, 48 | } 49 | 50 | #[derive(Serialize, Deserialize, Debug, Clone)] 51 | pub enum LaunchOption { 52 | Application(String), 53 | ApplicationWithArgs(String, Vec), 54 | Command(String), 55 | } 56 | 57 | impl Scratchpad { 58 | pub fn toggle(&self) -> Result<()> { 59 | let mut target_window_opt = self.get_target_window()?; 60 | 61 | if target_window_opt.is_none() { 62 | let timer = Instant::now(); 63 | self.launch()?; 64 | 65 | while target_window_opt.is_none() { 66 | target_window_opt = self.get_target_window()?; 67 | 68 | if timer.elapsed() > Duration::from_secs(self.launch_timeout as u64) { 69 | panic!("Application didn't launch within timeout period!"); 70 | } 71 | 72 | thread::sleep(Duration::from_millis(100)); 73 | } 74 | } 75 | 76 | let target_window = target_window_opt.unwrap(); 77 | let window_id = target_window.id; 78 | 79 | // If window already has focus, send it to scratchpad workspace 80 | if target_window.has_focus { 81 | query_socket(&[ 82 | "window", 83 | &window_id.to_string(), 84 | "--space", 85 | &self.scratchpad_space.to_string(), 86 | ])?; 87 | return Ok(()); 88 | } 89 | 90 | let focused_space_id = self.get_focused_space()?.unwrap().index; 91 | 92 | // Set window to floating if it isn't 93 | if !target_window.is_floating { 94 | query_socket(&["window", &window_id.to_string(), "--toggle", "float"])?; 95 | } 96 | 97 | // Move target window to focused space 98 | query_socket(&[ 99 | "window", 100 | &window_id.to_string(), 101 | "--space", 102 | &focused_space_id.to_string(), 103 | ])?; 104 | 105 | let set_with_grid = match (self.rows, self.cols) { 106 | (None, Some(_)) => panic!("Columns can't be set without rows!"), 107 | (Some(_), None) => panic!("Rows can't be set without columns!"), 108 | (None, None) => false, 109 | (Some(_), Some(_)) => true, 110 | }; 111 | 112 | if set_with_grid { 113 | query_socket(&[ 114 | "window", 115 | &window_id.to_string(), 116 | "--grid", 117 | &format!( 118 | "{}:{}:{}:{}:{}:{}", 119 | self.rows.unwrap(), 120 | self.cols.unwrap(), 121 | self.position.x, 122 | self.position.y, 123 | self.size.x, 124 | self.size.y, 125 | ), 126 | ])?; 127 | } else { 128 | // Move target window to target position 129 | query_socket(&[ 130 | "window", 131 | &window_id.to_string(), 132 | "--move", 133 | &self.position.to_string(), 134 | ])?; 135 | 136 | // Resize target window to target size 137 | query_socket(&[ 138 | "window", 139 | &window_id.to_string(), 140 | "--resize", 141 | &self.size.to_string(), 142 | ])?; 143 | } 144 | 145 | // Focus the target window 146 | query_socket(&["window", "--focus", &window_id.to_string()])?; 147 | 148 | return Ok(()); 149 | } 150 | 151 | pub fn launch(&self) -> Result<()> { 152 | match &self.launch_command { 153 | LaunchOption::Application(application) => SysCommand::new("open") 154 | .arg("-n") 155 | .arg(format!("/Applications/{}", application)) 156 | .spawn()?, 157 | 158 | LaunchOption::ApplicationWithArgs(application, args) => SysCommand::new("open") 159 | .arg("-n") 160 | .arg(format!("/Applications/{}", application)) 161 | .arg("--args") 162 | .args(args) 163 | .spawn()?, 164 | 165 | LaunchOption::Command(command) => { 166 | let splitted_cmd = split(command).expect("Invalid command!"); 167 | 168 | SysCommand::new(&splitted_cmd[0]) 169 | .args(&splitted_cmd[1..]) 170 | .spawn()? 171 | } 172 | }; 173 | 174 | Ok(()) 175 | } 176 | 177 | pub fn get_target_window(&self) -> Result> { 178 | let windows = Self::get_windows()?; 179 | 180 | let window = match &self.target { 181 | Target::Title(title) => windows.iter().find(|item| item.title == title.as_str()), 182 | Target::App(app) => windows.iter().find(|item| item.app == app.as_str()), 183 | }; 184 | 185 | match window { 186 | Some(target_window) => { 187 | return Ok(Some(target_window.clone())); 188 | } 189 | None => return Ok(None), 190 | } 191 | } 192 | 193 | pub fn get_focused_space(&self) -> Result> { 194 | let spaces = Self::get_spaces()?; 195 | 196 | let space = spaces.iter().find(|item| item.has_focus); 197 | 198 | match space { 199 | Some(focused_space) => { 200 | return Ok(Some(focused_space.clone())); 201 | } 202 | None => return Ok(None), 203 | } 204 | } 205 | 206 | pub fn get_windows() -> Result> { 207 | let string_data = query_socket(&["query", "--windows"])?; 208 | 209 | let deser_data = serde_json::from_str::>(&string_data)?; 210 | return Ok(deser_data); 211 | } 212 | 213 | pub fn get_spaces() -> Result> { 214 | let string_data = query_socket(&["query", "--spaces"])?; 215 | 216 | let deser_data = serde_json::from_str::>(&string_data)?; 217 | return Ok(deser_data); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/socket.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::env; 3 | use std::io::prelude::*; 4 | use std::os::unix::net::UnixStream; 5 | use std::path::Path; 6 | use std::str; 7 | use std::time::Duration; 8 | 9 | fn format_message(message: &[&str]) -> Vec { 10 | let mut command = Vec::from([0x0, 0x0, 0x0, 0x0]); 11 | 12 | for token in message { 13 | command.append(&mut token.as_bytes().to_vec()); 14 | command.push(0x0); 15 | } 16 | command.push(0x0); 17 | 18 | // First byte must denote number of bytes in message part minus the padding 19 | // fixme: First bytes denotes size but only first byte is being used here 20 | // Reference: https://github.com/koekeishiya/yabai/issues/1372#issuecomment-1226701120 21 | command[0] = (command.len() - 4) as u8; 22 | 23 | return command; 24 | } 25 | 26 | pub fn get_socket_stream() -> Result { 27 | let socket_path = format!("/tmp/yabai_{}.socket", env::var("USER")?); 28 | 29 | // Check if Yabai socket exists 30 | if !Path::new(&socket_path).exists() { 31 | panic!("Yabai socket doesn't exists! Is Yabai installed and running?"); 32 | } 33 | 34 | // Connect to the Yabai socket 35 | let stream = UnixStream::connect(socket_path)?; 36 | 37 | // Set read write timeout for socket 38 | stream.set_read_timeout(Some(Duration::new(2, 0)))?; 39 | stream.set_write_timeout(Some(Duration::new(2, 0)))?; 40 | 41 | return Ok(stream); 42 | } 43 | 44 | pub fn query_socket(message: &[&str]) -> Result { 45 | let mut socket_stream = get_socket_stream()?; 46 | 47 | let formatted_msg = format_message(message); 48 | 49 | socket_stream.write_all(&formatted_msg)?; 50 | 51 | let mut response = Vec::new(); 52 | socket_stream.read_to_end(&mut response)?; 53 | 54 | let string_data = String::from_utf8(response)?; 55 | 56 | return Ok(string_data); 57 | } 58 | -------------------------------------------------------------------------------- /src/yabai_schema.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct Frame { 5 | pub x: f32, 6 | pub y: f32, 7 | pub w: f32, 8 | pub h: f32, 9 | } 10 | 11 | #[derive(Serialize, Deserialize, Debug, Clone)] 12 | #[serde(rename_all = "kebab-case")] 13 | pub struct Window { 14 | pub id: u32, 15 | pub pid: u32, 16 | pub app: String, 17 | pub title: String, 18 | pub frame: Frame, 19 | pub role: String, 20 | pub subrole: String, 21 | pub display: u8, 22 | pub space: u8, 23 | pub level: i16, 24 | pub opacity: f32, 25 | pub split_type: String, 26 | pub stack_index: u8, 27 | pub can_move: bool, 28 | pub can_resize: bool, 29 | pub has_focus: bool, 30 | pub has_shadow: bool, 31 | pub has_parent_zoom: bool, 32 | pub has_fullscreen_zoom: bool, 33 | pub is_native_fullscreen: bool, 34 | pub is_visible: bool, 35 | pub is_minimized: bool, 36 | pub is_hidden: bool, 37 | pub is_floating: bool, 38 | pub is_sticky: bool, 39 | pub is_grabbed: bool, 40 | } 41 | 42 | #[derive(Serialize, Deserialize, Debug, Clone)] 43 | #[serde(rename_all = "kebab-case")] 44 | pub struct Space { 45 | pub id: u32, 46 | pub uuid: String, 47 | pub index: u32, 48 | pub label: String, 49 | // Can't use `type` as ident 50 | #[serde(rename = "type")] 51 | pub space_type: String, 52 | pub display: u8, 53 | pub windows: Vec, 54 | pub first_window: u32, 55 | pub last_window: u32, 56 | pub has_focus: bool, 57 | pub is_visible: bool, 58 | pub is_native_fullscreen: bool, 59 | } 60 | --------------------------------------------------------------------------------