diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a4071f3..1c2d76f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,17 +1,29 @@ image: "rust:latest" -lint:cargo: +lint: stage: build script: - rustc --version && cargo --version - rustup component add rustfmt - cargo fmt -build:cargo: +build: stage: build script: - rustc --version && cargo --version - cargo build --release artifacts: paths: - - target/release/swaysome \ No newline at end of file + - target/release/swaysome + +test:integration: + stage: test + script: + - apt update && apt install -y --no-install-recommends sway foot + - adduser test + - chown -R test:test . + - su test <>, + outputs: Vec, + // current_output: Output, + workspaces: Vec, + // current_workspace: Workspace, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Output { + name: String, + #[serde(default)] + focused: bool, + #[serde(default)] + active: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Workspace { + num: usize, + output: String, + visible: bool, +} + +impl SwaySome { + pub fn new() -> SwaySome { + for socket_var in ["SWAYSOCK", "I3SOCK"] { + let socket_path = match env::var(socket_var) { + Ok(val) => val, + Err(_e) => { + eprintln!("{} not found in environment", socket_var); + continue; + } + }; + + let socket = Path::new(&socket_path); + match SwaySome::new_from_socket(socket) { + Ok(swaysome) => return swaysome, + Err(e) => eprintln!("Error with value found in ${}: {}", socket_var, e), + } + } + panic!("couldn't find any i3/sway socket") + } + + pub fn new_from_socket(socket: &Path) -> Result { + let stream = match UnixStream::connect(&socket) { + Err(_) => { + return Err(format!( + "counldn't connect to socket '{}'", + socket.display() + )); + } + Ok(stream) => { + eprintln!("successful connection to socket '{}'", socket.display()); + stream + } + }; + + let mut swaysome = SwaySome { + socket: Cell::new(Some(stream)), + outputs: vec![], + workspaces: vec![], + }; + swaysome.outputs = swaysome.get_outputs(); + swaysome.workspaces = swaysome.get_workspaces(); + Ok(swaysome) + } + + fn send_msg(&self, msg_type: u32, payload: &str) { + let payload_length = payload.len() as u32; + + let mut msg_prefix: [u8; 6 * mem::size_of::() + 2 * mem::size_of::()] = + *b"i3-ipc00000000"; + + msg_prefix[6..] + .as_mut() + .write_u32::(payload_length) + .expect("Unable to write"); + + msg_prefix[10..] + .as_mut() + .write_u32::(msg_type) + .expect("Unable to write"); + + let mut msg: Vec = msg_prefix[..].to_vec(); + msg.extend(payload.as_bytes()); + + let mut socket = self + .socket + .take() + .expect("Unexisting socket, there probably is a logic error"); + + if socket.write_all(&msg[..]).is_err() { + panic!("couldn't send message"); + } + self.socket.set(Some(socket)); + } + + fn send_command(&self, command: &str) { + eprint!("Sending command: '{}' - ", &command); + self.send_msg(RUN_COMMAND, command); + self.check_success(); + } + + fn read_msg(&self) -> Result { + let mut response_header: [u8; 14] = *b"uninitialized."; + let mut socket = self + .socket + .take() + .expect("Unexisting socket, there probably is a logic error"); + socket.read_exact(&mut response_header).unwrap(); + + if &response_header[0..6] == b"i3-ipc" { + let mut v = Cursor::new(vec![ + response_header[6], + response_header[7], + response_header[8], + response_header[9], + ]); + let payload_length = v.read_u32::().unwrap(); + + let mut payload = vec![0; payload_length as usize]; + socket.read_exact(&mut payload[..]).unwrap(); + let payload_str = String::from_utf8(payload).unwrap(); + self.socket.set(Some(socket)); + Ok(payload_str) + } else { + eprint!("Not an i3-icp packet, emptying the buffer: "); + let mut v = vec![]; + socket.read_to_end(&mut v).unwrap(); + eprintln!("{:?}", v); + self.socket.set(Some(socket)); + Err("Unable to read i3-ipc packet") + } + } + + fn check_success(&self) { + match self.read_msg() { + Ok(msg) => { + let r: Vec = serde_json::from_str(&msg).unwrap(); + match r[0]["success"] { + serde_json::Value::Bool(true) => eprintln!("Command successful"), + _ => panic!("Command failed: {:#?}", r), + } + } + Err(_) => panic!("Unable to read response"), + }; + } + + /* + * Only used in tests + */ + pub fn get_tree(&self) -> serde_json::Value { + self.send_msg(GET_TREE, ""); + match self.read_msg() { + Ok(msg) => serde_json::from_str(&msg).expect("Failed to parse JSON for get_tree"), + Err(_) => panic!("Unable to get current workspace"), + } + } + + fn get_outputs(&self) -> Vec { + self.send_msg(GET_OUTPUTS, ""); + let o = match self.read_msg() { + Ok(msg) => msg, + Err(_) => panic!("Unable to get outputs"), + }; + let mut outputs: Vec = serde_json::from_str::>(&o) + .unwrap() + .into_iter() + .filter(|x| x.active) + .collect(); + outputs.sort_by(|x, y| x.name.cmp(&y.name)); // sort_by_key doesn't work here (https://stackoverflow.com/a/47126516) + outputs + } + + fn get_workspaces(&self) -> Vec { + self.send_msg(GET_WORKSPACES, ""); + let ws = match self.read_msg() { + Ok(msg) => msg, + Err(_) => panic!("Unable to get current workspace"), + }; + let mut workspaces: Vec = serde_json::from_str(&ws).unwrap(); + workspaces.sort_by_key(|x| x.num); + workspaces + } + + fn get_current_output_index(&self) -> usize { + // Do not use `self.outputs`, as the information here could be outdated, especially the `focused` attribute + let outputs = self.get_outputs(); + match outputs.iter().position(|x| x.focused) { + Some(i) => i, + None => panic!("WTF! No focused output???"), + } + } + + fn get_current_output_name(&self) -> String { + // Do not use `self.outputs`, as the information here could be outdated, especially the `focused` attribute + let outputs = self.get_outputs(); + let focused_output_index = match outputs.iter().find(|x| x.focused) { + Some(i) => i.name.as_str(), + None => panic!("WTF! No focused output???"), + }; + + focused_output_index.to_string() + } + + fn get_current_workspace_index(&self) -> usize { + // Do not use `self.outputs`, as the information here could be outdated, especially the `focused` attribute + let outputs = self.get_outputs(); + // Do not use `self.workspaces`, as the information here could be outdated, especially the `visible` attribute + self.get_workspaces() + .iter() + .find(|w| w.visible && outputs.iter().find(|o| o.name == w.output).unwrap().focused) + .unwrap() + .num + } + + pub fn move_container_to_workspace(&self, workspace_index: usize) { + if workspace_index < MAX_GROUP_WS { + self.move_container_to_workspace_relative(workspace_index); + } else { + self.move_container_to_workspace_absolute(workspace_index); + } + } + + pub fn move_container_to_workspace_group(&self, target_group: usize) { + let current_workspace_index = self.get_current_workspace_index(); + let current_workspace_index_relative = (current_workspace_index % MAX_GROUP_WS) as usize; + self.move_container_to_workspace_absolute( + current_workspace_index_relative + target_group * MAX_GROUP_WS, + ); + } + + fn move_container_to_workspace_absolute(&self, workspace_index: usize) { + let group_index = (workspace_index / MAX_GROUP_WS) as usize; + let full_ws_name = format!( + "{}", + group_index * MAX_GROUP_WS + workspace_index % MAX_GROUP_WS + ); + + // If the workspace already exists + match self.workspaces.iter().find(|w| w.num == workspace_index) { + Some(_) => { + let mut focus_cmd: String = "move container to workspace number ".to_string(); + focus_cmd.push_str(&full_ws_name); + self.send_command(&focus_cmd); + } + None => { + let target_group = workspace_index / MAX_GROUP_WS; + let target_screen_index = match self + .workspaces + .iter() + .find(|w| w.num / MAX_GROUP_WS == target_group) + { + // If other workspaces on the same group exists + Some(other_workspace) => Some( + self.outputs + .iter() + .enumerate() + .find(|i| i.1.name == other_workspace.output) + .unwrap() + .0, + ), + None => { + // Or if the targeted output is currently connected + if group_index < self.outputs.len() { + Some(group_index) + } else { + None + } + } + }; + + match target_screen_index { + Some(target_screen_index) => { + let target_output = &self.outputs[target_screen_index]; + + let current_output_name = self.get_current_output_name(); + + if target_output.name == current_output_name { + let mut focus_cmd: String = "move container to workspace ".to_string(); + focus_cmd.push_str(&full_ws_name); + self.send_command(&focus_cmd); + } else { + // If we have to send it to another screen + let mut focus_cmd: String = "focus output ".to_string(); + focus_cmd.push_str(&target_output.name); + self.send_command(&focus_cmd); + + let focused_workspace_index = self.get_current_workspace_index(); + + let mut focus_cmd: String = "workspace ".to_string(); + focus_cmd.push_str(&full_ws_name); + self.send_command(&focus_cmd); + + let mut focus_cmd: String = "focus output ".to_string(); + focus_cmd.push_str(¤t_output_name); + self.send_command(&focus_cmd); + + let mut focus_cmd: String = "move container to workspace ".to_string(); + focus_cmd.push_str(&full_ws_name); + self.send_command(&focus_cmd); + + let mut focus_cmd: String = "focus output ".to_string(); + focus_cmd.push_str(&target_output.name); + self.send_command(&focus_cmd); + + let mut focus_cmd: String = "workspace ".to_string(); + focus_cmd.push_str(&focused_workspace_index.to_string()); + self.send_command(&focus_cmd); + + let mut focus_cmd: String = "focus output ".to_string(); + focus_cmd.push_str(¤t_output_name); + self.send_command(&focus_cmd); + } + } + None => { + // Else, we send the container on the current output + let mut focus_cmd: String = "move container to workspace ".to_string(); + focus_cmd.push_str(&full_ws_name); + self.send_command(&focus_cmd); + } + }; + } + } + } + + fn move_container_to_workspace_relative(&self, workspace_index: usize) { + let current_workspace_index: usize = self.get_current_workspace_index(); + let focused_output_index = current_workspace_index / MAX_GROUP_WS; + + let mut cmd: String = "move container to workspace number ".to_string(); + let full_ws_name = format!("{}", focused_output_index * MAX_GROUP_WS + workspace_index); + cmd.push_str(&full_ws_name); + self.send_command(&cmd); + } + + pub fn focus_to_workspace(&self, workspace_index: usize) { + if workspace_index < MAX_GROUP_WS { + self.focus_to_workspace_relative(workspace_index); + } else { + self.focus_to_workspace_absolute(workspace_index); + } + } + + fn focus_to_workspace_absolute(&self, workspace_index: usize) { + let target_group = workspace_index / MAX_GROUP_WS; + match self + .workspaces + .iter() + .find(|w| w.num / MAX_GROUP_WS == target_group) + { + // If other workspaces on the same group exists + Some(other_workspace) => { + // find the corresponding output and focus it + let target_output = self + .outputs + .iter() + .enumerate() + .find(|i| i.1.name == other_workspace.output) + .unwrap() + .1; + let mut focus_cmd: String = "focus output ".to_string(); + focus_cmd.push_str(&target_output.name); + self.send_command(&focus_cmd); + } + None => {} + }; + + // Then we focus the workspace + let mut focus_cmd: String = "workspace number ".to_string(); + focus_cmd.push_str(&workspace_index.to_string()); + self.send_command(&focus_cmd); + } + + fn focus_to_workspace_relative(&self, workspace_index: usize) { + let current_workspace_index: usize = self.get_current_workspace_index(); + let focused_output_index = current_workspace_index / MAX_GROUP_WS; + + let mut cmd: String = "workspace number ".to_string(); + let full_ws_name = format!("{}", focused_output_index * MAX_GROUP_WS + workspace_index); + cmd.push_str(&full_ws_name); + self.send_command(&cmd); + } + + pub fn focus_to_group(&self, group_index: usize) { + let current_workspace_index: usize = self.get_current_workspace_index(); + let target_workspace_relative_index = current_workspace_index % MAX_GROUP_WS; + + let target_workspace_index = group_index * MAX_GROUP_WS + target_workspace_relative_index; + let full_ws_name = format!( + "{}", + group_index * MAX_GROUP_WS + target_workspace_relative_index + ); + + // If the workspace already exists + match self + .workspaces + .iter() + .find(|w| w.num == target_workspace_index) + { + Some(_) => { + let mut focus_cmd: String = "workspace number ".to_string(); + focus_cmd.push_str(&full_ws_name); + self.send_command(&focus_cmd); + } + None => { + let target_screen_index = match self + .workspaces + .iter() + .find(|w| w.num / MAX_GROUP_WS == group_index) + { + // If other workspaces on the same group exists + Some(other_workspace) => Some( + self.outputs + .iter() + .enumerate() + .find(|i| i.1.name == other_workspace.output) + .unwrap() + .0 + + 1, + ), + None => { + // Or if the targeted output is currently connected + if group_index > 0 && group_index <= self.outputs.len() { + Some(group_index) + } else { + None + } + } + }; + + match target_screen_index { + // If we have to send it to another screen + Some(target_screen_index) => { + let target_output = &self.outputs[target_screen_index - 1]; + + let mut focus_cmd: String = "focus output ".to_string(); + focus_cmd.push_str(&target_output.name); + self.send_command(&focus_cmd); + } + None => {} + }; + // Then we focus the workspace + let mut focus_cmd: String = "workspace number ".to_string(); + focus_cmd.push_str(&target_workspace_index.to_string()); + self.send_command(&focus_cmd); + } + } + } + + pub fn focus_all_outputs_to_workspace(&self, workspace_index: usize) { + let current_output = self.get_current_output_name(); + + // Iterate on all outputs to focus on the given workspace + for output in self.outputs.iter() { + let mut cmd: String = "focus output ".to_string(); + cmd.push_str(output.name.as_str()); + self.send_command(&cmd); + + self.focus_to_workspace(workspace_index); + } + + // Get back to currently focused output + let mut cmd: String = "focus output ".to_string(); + cmd.push_str(¤t_output); + self.send_command(&cmd); + } + + pub fn move_container_to_next_output(&self) { + self.move_container_to_next_or_prev_output(false); + } + + pub fn move_container_to_prev_output(&self) { + self.move_container_to_next_or_prev_output(true); + } + + fn move_container_to_next_or_prev_output(&self, go_to_prev: bool) { + let focused_output_index = self.get_current_output_index(); + + let target_output = if go_to_prev { + &self.outputs[(focused_output_index + self.outputs.len() - 1) % self.outputs.len()] + } else { + &self.outputs[(focused_output_index + 1) % self.outputs.len()] + }; + + let workspaces = self.get_workspaces(); + let target_workspace = workspaces + .iter() + .find(|x| x.output == target_output.name && x.visible) + .unwrap(); + let group_index = (target_workspace.num / MAX_GROUP_WS) as usize; + let full_ws_name = format!( + "{}", + group_index * MAX_GROUP_WS + target_workspace.num % MAX_GROUP_WS + ); + + // Move container to target workspace + let mut cmd: String = "move container to workspace number ".to_string(); + cmd.push_str(&full_ws_name); + self.send_command(&cmd); + + // Focus that workspace to follow the container + let mut cmd: String = "workspace number ".to_string(); + cmd.push_str(&full_ws_name); + self.send_command(&cmd); + } + + pub fn move_workspace_group_to_next_output(&self) { + self.move_workspace_group_to_next_or_prev_output(false); + } + + pub fn move_workspace_group_to_prev_output(&self) { + self.move_workspace_group_to_next_or_prev_output(true); + } + + fn move_workspace_group_to_next_or_prev_output(&self, go_to_prev: bool) { + let focused_output_index = self.get_current_output_index(); + + let target_output = if go_to_prev { + &self.outputs[(focused_output_index + self.outputs.len() - 1) % self.outputs.len()] + } else { + &self.outputs[(focused_output_index + 1) % self.outputs.len()] + }; + let current_workspace = self.get_current_workspace_index(); + let current_group_index = (current_workspace / MAX_GROUP_WS) as usize; + for workspace in self.get_workspaces() { + let ws_index = workspace.num / MAX_GROUP_WS; + if ws_index == current_group_index { + let cmd: String = format!("workspace number {}", workspace.num); + self.send_command(&cmd); + let cmd: String = format!("move workspace to {}", target_output.name); + self.send_command(&cmd); + } + } + let cmd: String = format!("workspace number {}", current_workspace); + self.send_command(&cmd); + } + + pub fn focus_to_next_group(&self) { + self.focus_to_next_or_prev_group(false); + } + + pub fn focus_to_prev_group(&self) { + self.focus_to_next_or_prev_group(true); + } + + fn focus_to_next_or_prev_group(&self, go_to_prev: bool) { + let current_workspace_index: usize = self.get_current_workspace_index(); + let focused_group_index = current_workspace_index / MAX_GROUP_WS; + + let highest_group = self.workspaces.last().unwrap().num / MAX_GROUP_WS; + let target_group; + if go_to_prev { + if focused_group_index == 0 { + target_group = highest_group; + } else { + target_group = focused_group_index - 1; + } + } else { + if focused_group_index >= highest_group { + target_group = 0; + } else { + target_group = focused_group_index + 1; + } + }; + self.focus_to_group(target_group); + } + + pub fn init_workspaces(&self, workspace_index: usize) { + let cmd_prefix: String = "focus output ".to_string(); + for output in self.outputs.iter().rev() { + let mut cmd = cmd_prefix.clone(); + cmd.push_str(output.name.as_str()); + self.send_command(&cmd); + + let mut cmd: String = "workspace number ".to_string(); + let full_ws_name = format!( + "{}", + (self.get_current_output_index() + 1) * MAX_GROUP_WS + workspace_index + ); + cmd.push_str(&full_ws_name); + self.send_command(&cmd); + } + } + + pub fn rearrange_workspaces(&self) { + let focus_cmd_prefix: String = "workspace number ".to_string(); + let move_cmd_prefix: String = "move workspace to ".to_string(); + for workspace in self.workspaces.iter() { + let mut focus_cmd = focus_cmd_prefix.clone(); + focus_cmd.push_str(&workspace.num.to_string()); + self.send_command(&focus_cmd); + + let group_index = + (workspace.num / MAX_GROUP_WS + self.outputs.len() - 1) % self.outputs.len(); + // if group_index <= self.outputs.len() - 1 { + let mut move_cmd = move_cmd_prefix.clone(); + move_cmd.push_str(&self.outputs[group_index].name); + self.send_command(&move_cmd); + // } + } + } +} diff --git a/src/main.rs b/src/main.rs index 9810a57..06f7ffa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,31 +1,7 @@ -extern crate byteorder; extern crate clap; -extern crate serde_json; - -use serde::{Deserialize, Serialize}; use clap::{Args, Parser, Subcommand}; -use std::cell::Cell; -use std::env; -use std::io::Cursor; -use std::io::{Read, Write}; -use std::mem; -use std::os::unix::net::UnixStream; -use std::path::Path; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -// Maximum workspaces per group. This will determine the naming. -// Examples: -// 10 → 17, 27 -// 100 → 107, 207 -// 15 → 22, 37 -const MAX_GROUP_WS: usize = 10; - -const RUN_COMMAND: u32 = 0; -const GET_WORKSPACES: u32 = 1; -// const SUBSCRIBE: u32 = 2; -const GET_OUTPUTS: u32 = 3; +use swaysome::SwaySome; #[derive(Parser, Debug)] #[clap(author, version, about = "Better multimonitor handling for sway", long_about = None)] @@ -87,624 +63,6 @@ struct Index { index: usize, } -struct SwaySome { - socket: Cell>, - outputs: Vec, - // current_output: Output, - workspaces: Vec, - // current_workspace: Workspace, -} - -#[derive(Serialize, Deserialize, Debug)] -struct Output { - name: String, - #[serde(default)] - focused: bool, - #[serde(default)] - active: bool, -} - -#[derive(Serialize, Deserialize, Debug)] -struct Workspace { - num: usize, - output: String, - visible: bool, -} - -impl SwaySome { - fn new() -> SwaySome { - let mut swaysome = SwaySome { - socket: Cell::new(Some(SwaySome::get_stream())), - outputs: vec![], - workspaces: vec![], - }; - swaysome.outputs = swaysome.get_outputs(); - swaysome.workspaces = swaysome.get_workspaces(); - swaysome - } - - fn get_stream() -> UnixStream { - for socket_var in ["SWAYSOCK", "I3SOCK"] { - let socket_path = match env::var(socket_var) { - Ok(val) => val, - Err(_e) => { - eprintln!("{} not found in environment", socket_var); - continue; - } - }; - - let socket = Path::new(&socket_path); - - match UnixStream::connect(&socket) { - Err(_) => { - eprintln!( - "counldn't connect to socket '{}' found in ${}", - socket_path, socket_var - ); - continue; - } - Ok(stream) => { - eprintln!( - "successful connection to socket '{}' found in ${}", - socket_path, socket_var - ); - return stream; - } - } - } - panic!("couldn't find any i3/sway socket") - } - - fn send_msg(&self, msg_type: u32, payload: &str) { - let payload_length = payload.len() as u32; - - let mut msg_prefix: [u8; 6 * mem::size_of::() + 2 * mem::size_of::()] = - *b"i3-ipc00000000"; - - msg_prefix[6..] - .as_mut() - .write_u32::(payload_length) - .expect("Unable to write"); - - msg_prefix[10..] - .as_mut() - .write_u32::(msg_type) - .expect("Unable to write"); - - let mut msg: Vec = msg_prefix[..].to_vec(); - msg.extend(payload.as_bytes()); - - let mut socket = self - .socket - .take() - .expect("Unexisting socket, there probably is a logic error"); - - if socket.write_all(&msg[..]).is_err() { - panic!("couldn't send message"); - } - self.socket.set(Some(socket)); - } - - fn send_command(&self, command: &str) { - eprint!("Sending command: '{}' - ", &command); - self.send_msg(RUN_COMMAND, command); - self.check_success(); - } - - fn read_msg(&self) -> Result { - let mut response_header: [u8; 14] = *b"uninitialized."; - let mut socket = self - .socket - .take() - .expect("Unexisting socket, there probably is a logic error"); - socket.read_exact(&mut response_header).unwrap(); - - if &response_header[0..6] == b"i3-ipc" { - let mut v = Cursor::new(vec![ - response_header[6], - response_header[7], - response_header[8], - response_header[9], - ]); - let payload_length = v.read_u32::().unwrap(); - - let mut payload = vec![0; payload_length as usize]; - socket.read_exact(&mut payload[..]).unwrap(); - let payload_str = String::from_utf8(payload).unwrap(); - self.socket.set(Some(socket)); - Ok(payload_str) - } else { - eprint!("Not an i3-icp packet, emptying the buffer: "); - let mut v = vec![]; - socket.read_to_end(&mut v).unwrap(); - eprintln!("{:?}", v); - self.socket.set(Some(socket)); - Err("Unable to read i3-ipc packet") - } - } - - fn check_success(&self) { - match self.read_msg() { - Ok(msg) => { - let r: Vec = serde_json::from_str(&msg).unwrap(); - match r[0]["success"] { - serde_json::Value::Bool(true) => eprintln!("Command successful"), - _ => panic!("Command failed: {:#?}", r), - } - } - Err(_) => panic!("Unable to read response"), - }; - } - - fn get_outputs(&self) -> Vec { - self.send_msg(GET_OUTPUTS, ""); - let o = match self.read_msg() { - Ok(msg) => msg, - Err(_) => panic!("Unable to get outputs"), - }; - let mut outputs: Vec = serde_json::from_str::>(&o) - .unwrap() - .into_iter() - .filter(|x| x.active) - .collect(); - outputs.sort_by(|x, y| x.name.cmp(&y.name)); // sort_by_key doesn't work here (https://stackoverflow.com/a/47126516) - outputs - } - - fn get_workspaces(&self) -> Vec { - self.send_msg(GET_WORKSPACES, ""); - let ws = match self.read_msg() { - Ok(msg) => msg, - Err(_) => panic!("Unable to get current workspace"), - }; - let mut workspaces: Vec = serde_json::from_str(&ws).unwrap(); - workspaces.sort_by_key(|x| x.num); - workspaces - } - - fn get_current_output_index(&self) -> usize { - // Do not use `self.outputs`, as the information here could be outdated, especially the `focused` attribute - let outputs = self.get_outputs(); - match outputs.iter().position(|x| x.focused) { - Some(i) => i, - None => panic!("WTF! No focused output???"), - } - } - - fn get_current_output_name(&self) -> String { - // Do not use `self.outputs`, as the information here could be outdated, especially the `focused` attribute - let outputs = self.get_outputs(); - let focused_output_index = match outputs.iter().find(|x| x.focused) { - Some(i) => i.name.as_str(), - None => panic!("WTF! No focused output???"), - }; - - focused_output_index.to_string() - } - - fn get_current_workspace_index(&self) -> usize { - // Do not use `self.outputs`, as the information here could be outdated, especially the `focused` attribute - let outputs = self.get_outputs(); - // Do not use `self.workspaces`, as the information here could be outdated, especially the `visible` attribute - self.get_workspaces() - .iter() - .find(|w| w.visible && outputs.iter().find(|o| o.name == w.output).unwrap().focused) - .unwrap() - .num - } - - fn move_container_to_workspace(&self, workspace_index: usize) { - if workspace_index < MAX_GROUP_WS { - self.move_container_to_workspace_relative(workspace_index); - } else { - self.move_container_to_workspace_absolute(workspace_index); - } - } - - fn move_container_to_workspace_group(&self, target_group: usize) { - let current_workspace_index = self.get_current_workspace_index(); - let current_workspace_index_relative = (current_workspace_index % MAX_GROUP_WS) as usize; - self.move_container_to_workspace_absolute( - current_workspace_index_relative + target_group * MAX_GROUP_WS, - ); - } - - fn move_container_to_workspace_absolute(&self, workspace_index: usize) { - let group_index = (workspace_index / MAX_GROUP_WS) as usize; - let full_ws_name = format!( - "{}", - group_index * MAX_GROUP_WS + workspace_index % MAX_GROUP_WS - ); - - // If the workspace already exists - match self.workspaces.iter().find(|w| w.num == workspace_index) { - Some(_) => { - let mut focus_cmd: String = "move container to workspace number ".to_string(); - focus_cmd.push_str(&full_ws_name); - self.send_command(&focus_cmd); - } - None => { - let target_group = workspace_index / MAX_GROUP_WS; - let target_screen_index = match self - .workspaces - .iter() - .find(|w| w.num / MAX_GROUP_WS == target_group) - { - // If other workspaces on the same group exists - Some(other_workspace) => Some( - self.outputs - .iter() - .enumerate() - .find(|i| i.1.name == other_workspace.output) - .unwrap() - .0, - ), - None => { - // Or if the targeted output is currently connected - if group_index < self.outputs.len() { - Some(group_index) - } else { - None - } - } - }; - - match target_screen_index { - Some(target_screen_index) => { - let target_output = &self.outputs[target_screen_index]; - - let current_output_name = self.get_current_output_name(); - - if target_output.name == current_output_name { - let mut focus_cmd: String = "move container to workspace ".to_string(); - focus_cmd.push_str(&full_ws_name); - self.send_command(&focus_cmd); - } else { - // If we have to send it to another screen - let mut focus_cmd: String = "focus output ".to_string(); - focus_cmd.push_str(&target_output.name); - self.send_command(&focus_cmd); - - let focused_workspace_index = self.get_current_workspace_index(); - - let mut focus_cmd: String = "workspace ".to_string(); - focus_cmd.push_str(&full_ws_name); - self.send_command(&focus_cmd); - - let mut focus_cmd: String = "focus output ".to_string(); - focus_cmd.push_str(¤t_output_name); - self.send_command(&focus_cmd); - - let mut focus_cmd: String = "move container to workspace ".to_string(); - focus_cmd.push_str(&full_ws_name); - self.send_command(&focus_cmd); - - let mut focus_cmd: String = "focus output ".to_string(); - focus_cmd.push_str(&target_output.name); - self.send_command(&focus_cmd); - - let mut focus_cmd: String = "workspace ".to_string(); - focus_cmd.push_str(&focused_workspace_index.to_string()); - self.send_command(&focus_cmd); - - let mut focus_cmd: String = "focus output ".to_string(); - focus_cmd.push_str(¤t_output_name); - self.send_command(&focus_cmd); - } - } - None => { - // Else, we send the container on the current output - let mut focus_cmd: String = "move container to workspace ".to_string(); - focus_cmd.push_str(&full_ws_name); - self.send_command(&focus_cmd); - } - }; - } - } - } - - fn move_container_to_workspace_relative(&self, workspace_index: usize) { - let current_workspace_index: usize = self.get_current_workspace_index(); - let focused_output_index = current_workspace_index / MAX_GROUP_WS; - - let mut cmd: String = "move container to workspace number ".to_string(); - let full_ws_name = format!("{}", focused_output_index * MAX_GROUP_WS + workspace_index); - cmd.push_str(&full_ws_name); - self.send_command(&cmd); - } - - fn focus_to_workspace(&self, workspace_index: usize) { - if workspace_index < MAX_GROUP_WS { - self.focus_to_workspace_relative(workspace_index); - } else { - self.focus_to_workspace_absolute(workspace_index); - } - } - - fn focus_to_workspace_absolute(&self, workspace_index: usize) { - let output_index = (workspace_index / MAX_GROUP_WS) as usize; - - // If the workspace already exists - match self.workspaces.iter().find(|w| w.num == workspace_index) { - Some(_) => { - let mut focus_cmd: String = "workspace number ".to_string(); - focus_cmd.push_str(&workspace_index.to_string()); - self.send_command(&focus_cmd); - } - None => { - let target_group = workspace_index / MAX_GROUP_WS; - let target_screen_index = match self - .workspaces - .iter() - .find(|w| w.num / MAX_GROUP_WS == target_group) - { - // If other workspaces on the same group exists - Some(other_workspace) => Some( - self.outputs - .iter() - .enumerate() - .find(|i| i.1.name == other_workspace.output) - .unwrap() - .0, - ), - None => { - // Or if the targeted output is currently connected - if output_index < self.outputs.len() { - Some(output_index) - } else { - None - } - } - }; - - match target_screen_index { - // If we have to send it to another screen - Some(target_screen_index) => { - let target_output = &self.outputs[target_screen_index - 1]; - - let mut focus_cmd: String = "focus output ".to_string(); - focus_cmd.push_str(&target_output.name); - self.send_command(&focus_cmd); - } - None => {} - }; - // Then we focus the workspace - let mut focus_cmd: String = "workspace number ".to_string(); - focus_cmd.push_str(&workspace_index.to_string()); - self.send_command(&focus_cmd); - } - } - } - - fn focus_to_workspace_relative(&self, workspace_index: usize) { - let current_workspace_index: usize = self.get_current_workspace_index(); - let focused_output_index = current_workspace_index / MAX_GROUP_WS; - - let mut cmd: String = "workspace number ".to_string(); - let full_ws_name = format!("{}", focused_output_index * MAX_GROUP_WS + workspace_index); - cmd.push_str(&full_ws_name); - self.send_command(&cmd); - } - - fn focus_to_group(&self, group_index: usize) { - let current_workspace_index: usize = self.get_current_workspace_index(); - let target_workspace_relative_index = current_workspace_index % MAX_GROUP_WS; - - let target_workspace_index = group_index * MAX_GROUP_WS + target_workspace_relative_index; - let full_ws_name = format!( - "{}", - group_index * MAX_GROUP_WS + target_workspace_relative_index - ); - - // If the workspace already exists - match self - .workspaces - .iter() - .find(|w| w.num == target_workspace_index) - { - Some(_) => { - let mut focus_cmd: String = "workspace number ".to_string(); - focus_cmd.push_str(&full_ws_name); - self.send_command(&focus_cmd); - } - None => { - let target_screen_index = match self - .workspaces - .iter() - .find(|w| w.num / MAX_GROUP_WS == group_index) - { - // If other workspaces on the same group exists - Some(other_workspace) => Some( - self.outputs - .iter() - .enumerate() - .find(|i| i.1.name == other_workspace.output) - .unwrap() - .0 - + 1, - ), - None => { - // Or if the targeted output is currently connected - if group_index > 0 && group_index <= self.outputs.len() { - Some(group_index) - } else { - None - } - } - }; - - match target_screen_index { - // If we have to send it to another screen - Some(target_screen_index) => { - let target_output = &self.outputs[target_screen_index - 1]; - - let mut focus_cmd: String = "focus output ".to_string(); - focus_cmd.push_str(&target_output.name); - self.send_command(&focus_cmd); - } - None => {} - }; - // Then we focus the workspace - let mut focus_cmd: String = "workspace number ".to_string(); - focus_cmd.push_str(&target_workspace_index.to_string()); - self.send_command(&focus_cmd); - } - } - } - - fn focus_all_outputs_to_workspace(&self, workspace_index: usize) { - let current_output = self.get_current_output_name(); - - // Iterate on all outputs to focus on the given workspace - for output in self.outputs.iter() { - let mut cmd: String = "focus output ".to_string(); - cmd.push_str(output.name.as_str()); - self.send_command(&cmd); - - self.focus_to_workspace(workspace_index); - } - - // Get back to currently focused output - let mut cmd: String = "focus output ".to_string(); - cmd.push_str(¤t_output); - self.send_command(&cmd); - } - - fn move_container_to_next_output(&self) { - self.move_container_to_next_or_prev_output(false); - } - - fn move_container_to_prev_output(&self) { - self.move_container_to_next_or_prev_output(true); - } - - fn move_container_to_next_or_prev_output(&self, go_to_prev: bool) { - let focused_output_index = self.get_current_output_index(); - - let target_output = if go_to_prev { - &self.outputs[(focused_output_index + self.outputs.len() - 1) % self.outputs.len()] - } else { - &self.outputs[(focused_output_index + 1) % self.outputs.len()] - }; - - let workspaces = self.get_workspaces(); - let target_workspace = workspaces - .iter() - .find(|x| x.output == target_output.name && x.visible) - .unwrap(); - let group_index = (target_workspace.num / MAX_GROUP_WS) as usize; - let full_ws_name = format!( - "{}", - group_index * MAX_GROUP_WS + target_workspace.num % MAX_GROUP_WS - ); - - // Move container to target workspace - let mut cmd: String = "move container to workspace number ".to_string(); - cmd.push_str(&full_ws_name); - self.send_command(&cmd); - - // Focus that workspace to follow the container - let mut cmd: String = "workspace number ".to_string(); - cmd.push_str(&full_ws_name); - self.send_command(&cmd); - } - - fn move_workspace_group_to_next_output(&self) { - self.move_workspace_group_to_next_or_prev_output(false); - } - - fn move_workspace_group_to_prev_output(&self) { - self.move_workspace_group_to_next_or_prev_output(true); - } - - fn move_workspace_group_to_next_or_prev_output(&self, go_to_prev: bool) { - let focused_output_index = self.get_current_output_index(); - - let target_output = if go_to_prev { - &self.outputs[(focused_output_index + self.outputs.len() - 1) % self.outputs.len()] - } else { - &self.outputs[(focused_output_index + 1) % self.outputs.len()] - }; - let current_workspace = self.get_current_workspace_index(); - let current_group_index = (current_workspace / MAX_GROUP_WS) as usize; - for workspace in self.get_workspaces() { - let ws_index = workspace.num / MAX_GROUP_WS; - if ws_index == current_group_index { - let cmd: String = format!("workspace number {}", workspace.num); - self.send_command(&cmd); - let cmd: String = format!("move workspace to {}", target_output.name); - self.send_command(&cmd); - } - } - let cmd: String = format!("workspace number {}", current_workspace); - self.send_command(&cmd); - } - - fn focus_container_to_next_group(&self) { - self.focus_container_to_next_or_prev_group(false); - } - - fn focus_container_to_prev_group(&self) { - self.focus_container_to_next_or_prev_group(true); - } - - fn focus_container_to_next_or_prev_group(&self, go_to_prev: bool) { - let current_workspace_index: usize = self.get_current_workspace_index(); - let focused_group_index = current_workspace_index / MAX_GROUP_WS; - - let highest_group = self.workspaces.last().unwrap().num / MAX_GROUP_WS; - let target_group; - if go_to_prev { - if focused_group_index == 0 { - target_group = highest_group; - } else { - target_group = focused_group_index - 1; - } - } else { - if focused_group_index >= highest_group { - target_group = 0; - } else { - target_group = focused_group_index + 1; - } - }; - self.focus_to_group(target_group); - } - - fn init_workspaces(&self, workspace_index: usize) { - let cmd_prefix: String = "focus output ".to_string(); - for output in self.outputs.iter().rev() { - let mut cmd = cmd_prefix.clone(); - cmd.push_str(output.name.as_str()); - self.send_command(&cmd); - - let mut cmd: String = "workspace number ".to_string(); - let full_ws_name = format!( - "{}", - (self.get_current_output_index() + 1) * MAX_GROUP_WS + workspace_index - ); - cmd.push_str(&full_ws_name); - self.send_command(&cmd); - } - } - - fn rearrange_workspaces(&self) { - let focus_cmd_prefix: String = "workspace number ".to_string(); - let move_cmd_prefix: String = "move workspace to ".to_string(); - for workspace in self.workspaces.iter() { - let mut focus_cmd = focus_cmd_prefix.clone(); - focus_cmd.push_str(&workspace.num.to_string()); - self.send_command(&focus_cmd); - - let group_index = workspace.num / MAX_GROUP_WS; - if group_index <= self.outputs.len() - 1 { - let mut move_cmd = move_cmd_prefix.clone(); - move_cmd.push_str(&self.outputs[group_index.max(1) - 1].name); - self.send_command(&move_cmd); - } - } - } -} - fn main() { let cli = Cli::parse(); @@ -742,10 +100,10 @@ fn main() { swaysome.move_workspace_group_to_prev_output(); } Command::NextGroup => { - swaysome.focus_container_to_next_group(); + swaysome.focus_to_next_group(); } Command::PrevGroup => { - swaysome.focus_container_to_prev_group(); + swaysome.focus_to_prev_group(); } Command::RearrangeWorkspaces => { swaysome.rearrange_workspaces(); diff --git a/swaysome.1 b/swaysome.1 index 9332dd4..432bd46 100644 --- a/swaysome.1 +++ b/swaysome.1 @@ -105,7 +105,7 @@ Focus to workspace \fIINDEX\fR (stay in the same workspace group) Focus to workspace group \fIINDEX\fR (keep the same workspace index) .TP \fBfocus-all-outputs\fR \fIINDEX\fR -Focus to workspace \fIINDEX\fR on all the outputs at once (not bound by default) +Focus to workspace \fIINDEX\fR on all the outputs at once (no keyboard shortcut bound by default for this function) .TP \fBnext-output\fR Move the focused container to the next output diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..e8f6869 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,988 @@ +// extern crate assert_json_diff; + +use assert_json_diff::{assert_json_eq, assert_json_include}; +use serde_json::json; +use swaysome::SwaySome; + +mod utils; +use utils::*; + +#[test] +fn test_init_three_outputs() { + let sway = Sway::start(); + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "1", "num": 1, "focused": true, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "2", "num": 2, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "3", "num": 3, "focused": false, "nodes": []}, + ]}, + ]})); + + eprintln!("Init 1"); + swaysome.init_workspaces(1); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": true, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); +} + +#[test] +fn test_three_outputs_moving_around_same_workspace_group() { + let sway = Sway::start(); + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + + swaysome.init_workspaces(1); + sway.spawn_some_apps(); + + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": false}, + {"name": "TERM3", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + + eprintln!("Move TERM3 to 2"); + swaysome.move_container_to_workspace(2); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + {"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + + eprintln!("Focus to 2"); + swaysome.focus_to_workspace(2); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": false}, + ]}, + {"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [ + {"name": "TERM3", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); +} + +#[test] +fn test_three_outputs_moving_around_across_workspace_groups() { + let sway = Sway::start(); + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + + swaysome.init_workspaces(1); + sway.spawn_some_apps(); + + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": false}, + {"name": "TERM3", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + + eprintln!("Move TERM3 to group 2"); + swaysome.move_container_to_workspace_group(2); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + + eprintln!("Focus to group 2"); + swaysome.focus_to_group(2); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM3", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + // HEADLESS-3 is still empty + assert_json_eq!( + swaysome.get_tree()["nodes"][3]["nodes"][0]["nodes"], + json!([]) + ); + + eprintln!("Focus to group 1"); + swaysome.focus_to_group(1); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + // HEADLESS-3 is still empty + assert_json_eq!( + swaysome.get_tree()["nodes"][3]["nodes"][0]["nodes"], + json!([]) + ); + + eprintln!("Move TERM2 to group 2"); + swaysome.move_container_to_workspace_group(3); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + ]}, + ]}, + ]})); + + eprintln!("Focus all to 4"); + swaysome.focus_all_outputs_to_workspace(4); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + ]}, + {"type": "workspace", "name": "14", "num": 14, "focused": true, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + {"type": "workspace", "name": "24", "num": 24, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + ]}, + {"type": "workspace", "name": "34", "num": 34, "focused": false, "nodes": []}, + ]}, + ]})); + + eprintln!("Focus all back to 1"); + swaysome.focus_all_outputs_to_workspace(1); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + ]}, + ]}, + ]})); + + // shadow and re-init swaysome to get up-to-date internals (outputs, workspaces) + // XXX this is more of a hack than anything else. Ideally, swaysome would never have out-of-date internals + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + + eprintln!("Focus to prev group"); + swaysome.focus_to_prev_group(); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "1", "num": 1, "focused": true, "nodes": []}, + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + ]}, + ]}, + ]})); + + eprintln!("Focus to prev group again"); + swaysome.focus_to_prev_group(); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "1", "num": 1, "focused": false, "nodes": []}, + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM2", "focused": true}, + ]}, + ]}, + ]})); + + eprintln!("Focus to next group"); + swaysome.focus_to_next_group(); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "1", "num": 1, "focused": true, "nodes": []}, + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + ]}, + ]}, + ]})); + + eprintln!("Focus to next group again"); + swaysome.focus_to_next_group(); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + ]}, + ]}, + ]})); +} + +#[test] +fn test_three_outputs_moving_around_across_outputs() { + let sway = Sway::start(); + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + + swaysome.init_workspaces(1); + sway.spawn_some_apps(); + + eprintln!("Move TERM3 to group 3"); + swaysome.move_container_to_workspace_group(3); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + ]})); + + eprintln!("Move TERM2 to next output"); + swaysome.move_container_to_next_output(); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM2", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + ]})); + + eprintln!("Move TERM2 to prev output"); + swaysome.move_container_to_prev_output(); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + ]})); + + eprintln!("Move TERM2 to prev output again"); + swaysome.move_container_to_prev_output(); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + ]}, + ]})); +} + +#[test] +fn test_three_outputs_moving_around_across_outputs_without_init() { + let sway = Sway::start(); + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + + sway.spawn_some_apps(); + + eprintln!("Move TERM3 to group 3"); + swaysome.move_container_to_workspace_group(3); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "1", "num": 1, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "2", "num": 2, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "3", "num": 3, "focused": false, "nodes": []}, + ]}, + ]})); +} + +#[test] +fn test_three_outputs_moving_around_absolute() { + let sway = Sway::start(); + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + + swaysome.init_workspaces(1); + sway.spawn_some_apps(); + + // shadow and re-init swaysome to get up-to-date internals (outputs, workspaces) + // XXX this is more of a hack than anything else. Ideally, swaysome would never have out-of-date internals + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + + eprintln!("Moving TERM3 to 12"); + swaysome.move_container_to_workspace(12); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + {"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + + eprintln!("Moving TERM2 to 42"); + swaysome.move_container_to_workspace(42); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": true}, + ]}, + {"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + + ]}, + {"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + + // shadow and re-init swaysome to get up-to-date internals (outputs, workspaces) + // XXX this is more of a hack than anything else. Ideally, swaysome would never have out-of-date internals + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + + eprintln!("Moving TERM1 to 42"); + swaysome.move_container_to_workspace(42); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": true, "nodes": []}, + {"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + + ]}, + {"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + {"name": "TERM1", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + + eprintln!("Spawn TERM4"); + sway.send_command(["exec", "foot -T TERM4"].as_slice()); + std::thread::sleep(std::time::Duration::from_millis(200)); + + eprintln!("Moving TERM4 to 22"); + swaysome.move_container_to_workspace(22); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": true, "nodes": []}, + {"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + + ]}, + {"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + {"name": "TERM1", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + {"type": "workspace", "name": "22", "num": 22, "focused": false, "nodes": [ + {"name": "TERM4", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + + eprintln!("Focus on 42"); + swaysome.focus_to_workspace(42); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + + ]}, + {"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + {"name": "TERM1", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + {"type": "workspace", "name": "22", "num": 22, "focused": false, "nodes": [ + {"name": "TERM4", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + + eprintln!("Focus on 41"); + swaysome.focus_to_workspace(41); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + + ]}, + {"type": "workspace", "name": "41", "num": 41, "focused": true, "nodes": []}, + {"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + {"name": "TERM1", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + {"type": "workspace", "name": "22", "num": 22, "focused": false, "nodes": [ + {"name": "TERM4", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + + eprintln!("Focus on 51"); + swaysome.focus_to_workspace(51); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + + ]}, + {"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + {"name": "TERM1", "focused": false}, + ]}, + {"type": "workspace", "name": "51", "num": 51, "focused": true, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + {"type": "workspace", "name": "22", "num": 22, "focused": false, "nodes": [ + {"name": "TERM4", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []}, + ]}, + ]})); + + eprintln!("Focus on 32"); + swaysome.focus_to_workspace(32); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + + ]}, + {"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + {"name": "TERM1", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + {"type": "workspace", "name": "22", "num": 22, "focused": false, "nodes": [ + {"name": "TERM4", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "32", "num": 32, "focused": true, "nodes": []}, + ]}, + ]})); +} + +#[test] +fn test_three_outputs_moving_groups_across_outputs() { + let sway = Sway::start(); + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + + swaysome.init_workspaces(1); + sway.spawn_some_apps(); + + eprintln!("Move TERM3 to group 3"); + swaysome.move_container_to_workspace_group(3); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + ]})); + + eprintln!("Move group 1 to prev output"); + swaysome.move_workspace_group_to_prev_output(); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "1", "num": 1, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + ]})); + + eprintln!("Move group 1 to next output"); + swaysome.move_workspace_group_to_next_output(); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + ]})); + + eprintln!("Move group 1 to next output again"); + swaysome.move_workspace_group_to_next_output(); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "1", "num": 1, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + ]})); +} + +#[test] +fn test_three_outputs_plugging_unplugging_outputs() { + let sway = Sway::start(); + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + + swaysome.init_workspaces(1); + sway.spawn_some_apps(); + + eprintln!("Move TERM3 to group 3"); + swaysome.move_container_to_workspace_group(3); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + ]})); + + eprintln!("Disabling output 3"); + sway.send_command(["output HEADLESS-3 disable"].as_slice()); + std::thread::sleep(std::time::Duration::from_millis(200)); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + ]})); + assert_eq!(swaysome.get_tree()["nodes"].as_array().unwrap().len(), 3); + + eprintln!("Enabling output 3"); + sway.send_command(["output HEADLESS-3 enable"].as_slice()); + std::thread::sleep(std::time::Duration::from_millis(200)); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": true}, + ]}, + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + ]})); + assert_eq!(swaysome.get_tree()["nodes"].as_array().unwrap().len(), 4); + + // shadow and re-init swaysome to get up-to-date internals (outputs, workspaces) + // XXX this is more of a hack than anything else. Ideally, swaysome would never have out-of-date internals + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + swaysome.rearrange_workspaces(); + + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + {"name": "TERM2", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": true}, + ]}, + ]}, + ]})); + + swaysome.focus_to_workspace(11); + swaysome.move_container_to_workspace(21); + + eprintln!("Disabling output 2"); + sway.send_command(["output HEADLESS-2 disable"].as_slice()); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": true}, + ]}, + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + ]})); + + eprintln!("Enabling output 2"); + sway.send_command(["output HEADLESS-2 enable"].as_slice()); + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": true}, + ]}, + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": false}, + ]}, + ]}, + ]})); + + // shadow and re-init swaysome to get up-to-date internals (outputs, workspaces) + // XXX this is more of a hack than anything else. Ideally, swaysome would never have out-of-date internals + let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize"); + swaysome.rearrange_workspaces(); + + assert_json_include!(actual: swaysome.get_tree(), expected: json!({ + "nodes": [ + {}, + {"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [ + {"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [ + {"name": "TERM1", "focused": false}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [ + {"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [ + {"name": "TERM3", "focused": true}, + ]}, + ]}, + {"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [ + {"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [ + {"name": "TERM2", "focused": false}, + ]}, + ]}, + ]})); +} diff --git a/tests/integration_bin.rs b/tests/integration_bin.rs new file mode 100644 index 0000000..868dfd4 --- /dev/null +++ b/tests/integration_bin.rs @@ -0,0 +1,30 @@ +use std::process::Command; + +mod utils; +use utils::Sway; + +#[test] +fn test_binary_help() { + let output = Command::new("./target/debug/swaysome") + .args(["-h"]) + .env_clear() + .env("SWAYSOCK", "/dev/null") + .output() + .expect("Couldn't run swaymsg"); + assert_eq!(String::from_utf8(output.stdout).unwrap(), "Better multimonitor handling for sway\n\nUsage: swaysome \n\nCommands:\n init Initialize the workspace groups for all the outputs\n move Move the focused container to another workspace on the same workspace group\n move-to-group Move the focused container to the same workspace index on another workspace group\n focus Focus to another workspace on the same workspace group\n focus-group Focus to workspace group\n focus-all-outputs Focus to another workspace on all the outputs\n next-output Move the focused container to the next output\n prev-output Move the focused container to the previous output\n workspace-group-next-output Move the focused workspace group to the next output\n workspace-group-prev-output Move the focused workspace group to the previous output\n next-group Move the focused container to the next group\n prev-group Move the focused container to the previous group\n rearrange-workspaces Rearrange already opened workspaces to the correct outputs, useful when plugging new monitors\n help Print this message or the help of the given subcommand(s)\n\nOptions:\n -h, --help Print help\n -V, --version Print version\n"); +} + +/// We only test the 'init' command, given that the exhaustive command testing +/// is done in the library integration tests. Here, we only verify that the +/// interaction with `sway` works seamslessly. +#[test] +fn test_binary_interaction_with_sway() { + let sway = Sway::start(); + let output = Command::new("./target/debug/swaysome") + .args(["init", "1"]) + .env_clear() + .env("SWAYSOCK", sway.sock.clone()) + .output() + .expect("Couldn't run swaymsg"); + assert_eq!(String::from_utf8(output.stderr).unwrap(), "successful connection to socket '/tmp/swaysome_tests/test_binary_interaction_with_sway/swaysock'\nSending command: 'focus output HEADLESS-3' - Command successful\nSending command: 'workspace number 31' - Command successful\nSending command: 'focus output HEADLESS-2' - Command successful\nSending command: 'workspace number 21' - Command successful\nSending command: 'focus output HEADLESS-1' - Command successful\nSending command: 'workspace number 11' - Command successful\n"); +} diff --git a/tests/sway.conf b/tests/sway.conf new file mode 100644 index 0000000..2ac7c45 --- /dev/null +++ b/tests/sway.conf @@ -0,0 +1,11 @@ +output HEADLESS-1 { + resolution 480x270 +} + +output HEADLESS-2 { + resolution 1920x1080 +} + +output HEADLESS-3 { + resolution 2560x1440 +} diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs new file mode 100644 index 0000000..598723c --- /dev/null +++ b/tests/utils/mod.rs @@ -0,0 +1,125 @@ +use std::{ + fs::File, + io::Read, + path::PathBuf, + process::{Child, Command, Output}, +}; + +fn signal(pid: u32, sig: &str) { + Command::new("kill") + .arg("-s") + .arg(sig) + .arg(format!("{}", pid)) + .status() + .expect("failed to execute 'kill'"); +} + +pub struct Sway { + pub sock: PathBuf, + process: Child, +} + +impl Sway { + pub fn start() -> Sway { + let tmp = std::env::temp_dir() + .join("swaysome_tests") + .join(std::thread::current().name().unwrap()); + std::fs::create_dir_all(&tmp).expect("Unable to create temporary working directory"); + + let pwd = std::env::current_dir().expect("Unable to get current dir"); + let conf_path = pwd.join("tests/sway.conf"); + let swaysock_path = tmp.join("swaysock"); + let sway_log = + File::create(tmp.join("sway.log")).expect("Unable to create sway's log file"); + + let sway = Command::new("sway") + .arg("-c") + .arg(conf_path.clone()) + .env_clear() + .env("WLR_BACKENDS", "headless") + .env("WLR_LIBINPUT_NO_DEVICES", "1") + .env("XDG_RUNTIME_DIR", &tmp) + .env("SWAYSOCK", &swaysock_path) + .stderr(sway_log) + .spawn() + .expect("failed to execute sway"); + + // check that sway works correctly without using swaysome + let sway = Sway { + sock: swaysock_path, + process: sway, + }; + match sway.check_connection("loaded_config_file_name") { + Ok(()) => { + // Let's do some common initialization of the desktop + sway.send_command(["create_output"].as_slice()); + sway.send_command(["create_output"].as_slice()); + return sway; + } + Err(()) => { + eprintln!("Failed to start 'sway', aborting the tests"); + eprintln!("---- sway stderr ----"); + let mut buffer = String::new(); + let mut sway_stderr = + File::open(tmp.join("sway.log")).expect("Unable to open sway's log file"); + sway_stderr.read_to_string(&mut buffer).unwrap(); + eprintln!("{}", buffer); + eprintln!("---------------------"); + panic!(); + } + } + } + + pub fn send_command(&self, commands: &[&str]) -> Output { + Command::new("swaymsg") + .args(commands) + .env_clear() + .env("SWAYSOCK", self.sock.clone()) + .output() + .expect("Couldn't run swaymsg") + } + + pub fn spawn_some_apps(&self) { + self.send_command(["exec", "foot -T TERM1"].as_slice()); + // Make sure the app are created in the right order. + // 50ms would still sometimes be racy on my Ryzen 5 PRO 4650U, so let's + // take a safe bet and give plenty of time for shared CI runners. + std::thread::sleep(std::time::Duration::from_millis(200)); + self.send_command(["exec", "foot -T TERM2"].as_slice()); + std::thread::sleep(std::time::Duration::from_millis(200)); + self.send_command(["exec", "foot -T TERM3"].as_slice()); + std::thread::sleep(std::time::Duration::from_millis(200)); + } + + fn check_connection(&self, flag: &str) -> Result<(), ()> { + let mut retries = 100; // wait for max 10s + while retries > 0 { + let version = self.send_command(["-t", "get_version"].as_slice()); + if String::from_utf8(version.stdout).unwrap().contains(flag) + || String::from_utf8(version.stderr).unwrap().contains(flag) + { + return Ok(()); + } + std::thread::sleep(std::time::Duration::from_millis(100)); + retries -= 1; + } + return Err(()); + } + + fn stop(&mut self) { + signal(self.process.id(), "TERM"); + match self.check_connection("Unable to connect to") { + Ok(()) => eprintln!("Sway terminated correctly on its own"), + Err(_) => { + self.process.kill().expect("Failed to kill sway"); + eprintln!("Sway had to be killed"); + } + } + } +} + +impl Drop for Sway { + fn drop(&mut self) { + self.stop(); + } +}