swaysome-2.3.2/.cargo_vcs_info.json0000644000000001360000000000100127070ustar { "git": { "sha1": "31ed37a70cf5843510c02f96be3f5486a4e38e61" }, "path_in_vcs": "" }swaysome-2.3.2/.gitignore000064400000000000000000000000101046102023000134560ustar 00000000000000/target swaysome-2.3.2/.gitlab-ci.yml000064400000000000000000000012221046102023000141300ustar 00000000000000image: "rust:latest" lint: stage: build script: - rustc --version && cargo --version - rustup component add rustfmt - cargo fmt build: stage: build script: - rustc --version && cargo --version - cargo build --release artifacts: paths: - 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 <", "Nabos ", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "swaysome provides an awesome way to manage your multiple outputs with the sway windows manager" homepage = "https://gitlab.com/hyask/swaysome" readme = "README.md" license = "MIT" repository = "https://gitlab.com/hyask/swaysome" [lib] name = "swaysome" path = "src/lib.rs" [[bin]] name = "swaysome" path = "src/main.rs" [[test]] name = "integration" path = "tests/integration.rs" [[test]] name = "integration_bin" path = "tests/integration_bin.rs" [dependencies.byteorder] version = "1.5.0" [dependencies.clap] version = "4.4.8" features = ["derive"] [dependencies.serde] version = "1.0.192" features = ["derive"] [dependencies.serde_json] version = "1.0.108" [dev-dependencies.assert-json-diff] version = "2.0.2" swaysome-2.3.2/Cargo.toml.orig000064400000000000000000000012211046102023000143620ustar 00000000000000[package] name = "swaysome" version = "2.3.2" authors = ["Skia ", "Nabos "] edition = "2021" description = "swaysome provides an awesome way to manage your multiple outputs with the sway windows manager" license = "MIT" repository = "https://gitlab.com/hyask/swaysome" homepage = "https://gitlab.com/hyask/swaysome" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] byteorder = "1.5.0" clap = { version = "4.4.8", features = ["derive"] } serde = { version = "1.0.192", features = ["derive"] } serde_json = "1.0.108" [dev-dependencies] assert-json-diff = "2.0.2" swaysome-2.3.2/HACKING.md000064400000000000000000000007511046102023000130700ustar 00000000000000# Hacking on swaysome ## Get the coverage of the test to improve them You'll need more toolchain components: ``` rustup component add llvm-tools-preview ``` Then run the tests with coverage profiling, and generate the HTML report: ``` CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage ``` swaysome-2.3.2/LICENSE000064400000000000000000000020551046102023000125060ustar 00000000000000Copyright © 2021 Skia Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. swaysome-2.3.2/README.md000064400000000000000000000077751046102023000127760ustar 00000000000000# Swaysome An awesome way to manage your workspaces on [sway](https://swaywm.org/). Join us on [#swaysome:matrix.hya.sk](https://matrix.to/#/%23swaysome:matrix.hya.sk)! ## Description This binary helps you configure sway to work a bit more like [AwesomeWM](https://awesomewm.org/). This means that **workspaces** are namespaced in what are called **workspace groups**, and **workspace groups** can be moved around the differents outputs easily. For example, with workspace `11` on the first output, and workspace `21` on the second output, triggering the `swaysome focus 1` shortcut to get workspace `1` would lead you to workspace `11` if your focus is on the first output, and workspace `21` is the focus is on the second one. By default, `swaysome init` will create a **workspace group** per active output, but you may create other groups while working, by either triggering `swaysome focus-group ` and opening a new window, or sending an existing window to it first with `swaysome move-to-group `. Here is a common use-case for this: * `output-1`: * **workspace group** 1: * workspace `11`: chats * workspace `12`: emails * `output-2`: * **workspace group** 2: * workspace `21`: IDE for first project * workspace `22`: browser for first project * workspace `23`: terminals for first project * **workspace group** 3: * workspace `31`: IDE for second project * workspace `32`: browser for second project * workspace `33`: terminals for second project That way, when `output-2` is focused on **workspace group** 2, be it workspace `21` or `22`, the quick `$mod+` (bound to `swaysome focus `) shortcut won't leave **workspace group** 2, allowing you to open multiple projects in parallel without the hassle of manually remembering how to namespace them. In that situation, suppose you plug in a new output, `output-3`, you may then want to focus **workspace group 3** to send it to `output-3`: this is simply done by typing the shortcuts `$mod+Alt+3` (`swaysome focus-group 3`) then `$mod+Alt+o` (`swaysome workspace-group-next-output`). `swaysome` may also work with i3, but this is untested. `swaysome` should be compatible with [sworkstyle](https://lib.rs/crates/sworkstyle). If this is broken, please report a bug. ## Installation Arch Linux: Found on the AUR as [swaysome-git](https://aur.archlinux.org/packages/swaysome-git). Void Linux: `xbps-install swaysome` If you have Rust installed, then you can just `cargo install swaysome` and you're good to go. Otherwise, you may grab a [pre-built binary](https://gitlab.com/hyask/swaysome/-/jobs/artifacts/master/raw/target/release/swaysome?job=build:cargo) from the CI and put it in your `$PATH`. **WARNING**: please double-check that your `swaysome` binary is in `sway`'s `$PATH`. Depending on your setup, the `$PATH` you have in your shell may not be the same as `sway`'s, and if `swaysome` can't be called by `sway`, the symptoms will only look like non-functional shortcuts. If you're in this situation, a quick workaround is to call `swaysome` with its full absolute path from `sway`'s config to check that everything works before fixing your `$PATH` issue. ## Usage Copy the `swaysome.conf` file in `~/.config/sway/config.d/swaysome.conf`. Then append your `sway` configuration with this: ``` include ~/.config/sway/config.d/*.conf ``` On next startup of `sway`, you should end-up with workspaces from `1` to `0`, prefixed with a screen index, giving you workspace `11` on the first screen, and workspace `21` on the second one, both accessible with shortcut `$mod+1` when focused on the right output. The `init` command simply walks through every screen to initialize a prefixed workspace. It does it backwards so that you end-up focused on the first screen, as usual. ## Exhaustive swaysome commands list Just run `swaysome --help` for the most up to date list of command and documentation. ## Breaking changes * Starting with 2.0, `next_output` and `prev_output` have been changed to `next-output` and `prev-output`. swaysome-2.3.2/pkgbuild/.SRCINFO000064400000000000000000000005241046102023000144050ustar 00000000000000pkgbase = swaysome-git pkgdesc = Awesome WM like workspaces pkgver = 0.0.0 pkgrel = 1 url = https://gitlab.com/hyask/swaysome arch = x86_64 license = MIT makedepends = git makedepends = rust provides = swaysome conflicts = swaysome source = swaysome::git+https://gitlab.com/hyask/swaysome md5sums = SKIP pkgname = swaysome-git swaysome-2.3.2/pkgbuild/PKGBUILD000064400000000000000000000011561046102023000144270ustar 00000000000000# Maintainer: XXX pkgname=swaysome-git _pkgname=swaysome pkgver=0.0.0 pkgrel=1 pkgdesc='Awesome WM like workspaces' arch=('x86_64') url='https://gitlab.com/hyask/swaysome' license=('MIT') makedepends=('git' 'rust') provides=("$_pkgname") conflicts=("$_pkgname") source=("$_pkgname::git+$url") md5sums=('SKIP') pkgver() { cd "$_pkgname" echo $(grep '^version =' Cargo.toml|head -n1|cut -d\" -f2).r$(git rev-list --count HEAD).g$(git rev-parse --short HEAD) } build() { cd "$_pkgname" cargo build --release } package() { cd "$_pkgname" install -Dm755 "target/release/$_pkgname" "$pkgdir/usr/bin/$_pkgname" } swaysome-2.3.2/src/lib.rs000064400000000000000000000562111046102023000134070ustar 00000000000000use serde::{Deserialize, Serialize}; 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::{NativeEndian, 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; const GET_TREE: u32 = 4; pub 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 { 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); // } } } } swaysome-2.3.2/src/main.rs000064400000000000000000000063231046102023000135640ustar 00000000000000extern crate clap; use clap::{Args, Parser, Subcommand}; use swaysome::SwaySome; #[derive(Parser, Debug)] #[clap(author, version, about = "Better multimonitor handling for sway", long_about = None)] #[clap(propagate_version = true)] struct Cli { #[clap(subcommand)] command: Command, } #[derive(Subcommand, Debug)] enum Command { #[clap(about = "Initialize the workspace groups for all the outputs")] Init(Index), #[clap(about = "Move the focused container to another workspace on the same workspace group")] Move(Index), #[clap( about = "Move the focused container to the same workspace index on another workspace group" )] MoveToGroup(Index), #[clap(about = "Focus to another workspace on the same workspace group")] Focus(Index), #[clap(about = "Focus to workspace group")] FocusGroup(Index), #[clap(about = "Focus to another workspace on all the outputs")] FocusAllOutputs(Index), #[clap(about = "Move the focused container to the next output")] NextOutput, #[clap(about = "Move the focused container to the previous output")] PrevOutput, #[clap(about = "Move the focused workspace group to the next output")] WorkspaceGroupNextOutput, #[clap(about = "Move the focused workspace group to the previous output")] WorkspaceGroupPrevOutput, #[clap(about = "Move the focused container to the next group")] NextGroup, #[clap(about = "Move the focused container to the previous group")] PrevGroup, #[clap( about = "Rearrange already opened workspaces to the correct outputs, useful when plugging new monitors" )] RearrangeWorkspaces, } #[derive(Args, Debug)] struct Index { #[clap(value_name = "index", help = "The workspace index to work with")] index: usize, } fn main() { let cli = Cli::parse(); let swaysome = SwaySome::new(); match &cli.command { Command::Init(action) => { swaysome.init_workspaces(action.index); } Command::Move(action) => { swaysome.move_container_to_workspace(action.index); } Command::MoveToGroup(action) => { swaysome.move_container_to_workspace_group(action.index); } Command::Focus(action) => { swaysome.focus_to_workspace(action.index); } Command::FocusGroup(action) => { swaysome.focus_to_group(action.index); } Command::FocusAllOutputs(action) => { swaysome.focus_all_outputs_to_workspace(action.index); } Command::NextOutput => { swaysome.move_container_to_next_output(); } Command::PrevOutput => { swaysome.move_container_to_prev_output(); } Command::WorkspaceGroupNextOutput => { swaysome.move_workspace_group_to_next_output(); } Command::WorkspaceGroupPrevOutput => { swaysome.move_workspace_group_to_prev_output(); } Command::NextGroup => { swaysome.focus_to_next_group(); } Command::PrevGroup => { swaysome.focus_to_prev_group(); } Command::RearrangeWorkspaces => { swaysome.rearrange_workspaces(); } } } swaysome-2.3.2/swaysome.1000064400000000000000000000117421046102023000134350ustar 00000000000000.TH SWAYSOME "1" "Sep 2025" "2.3.2" "User Commands" . . .SH NAME swaysome - improve multi-output handling on sway . . .SH SYNOPSIS .B swaysome \fI\,[OPTIONS]\/\fR \fI\,\/\fR . . .SH DESCRIPTION \fBswaysome\fR helps you configure \fBsway\fR to work a bit more like \fBAwesomeWM\fR (\fIhttps://awesomewm\.org/\fR). This means that "\fBworkspaces\fR" are namespaced in what are called "\fBworkspace groups\fR", and "\fBworkspace groups\fR" can be moved around the differents outputs easily. .P For example, with workspace 11 on the first output, and workspace 21 on the second output, triggering the `swaysome focus 1` shortcut to get workspace 1 would lead you to workspace 11 if your focus is on the first output, and workspace 21 is the focus is on the second one. .P By default, `swaysome init` will create a \fBworkspace group\fR per active output, but you may create other groups while working, by either triggering `swaysome focus-group ` and opening a new window, or sending an existing window to it first with `swaysome move-to-group `. .P Here is a common use-case for this: .EX \fBoutput-1\fR: \fBworkspace group 1\fR: workspace 11: chats workspace 12: emails \fBoutput-2\fR: \fBworkspace group 2\fR: workspace 21: IDE for first project workspace 22: browser for first project workspace 23: terminals for first project \fBworkspace group 3\fR: workspace 31: IDE for second project workspace 32: browser for second project workspace 33: terminals for second project .EE .P That way, when \fBoutput-2\fR is focused on \fBworkspace group 2\fR, be it workspace 21 or 22, the quick `$mod+` (bound to `swaysome focus `) shortcut won't leave \fBworkspace group 2\fR, allowing you to open multiple projects in parallel without the hassle of manually remembering how to namespace them. .P In that situation, suppose you plug in a new output, `output-3`, you may then want to focus \fBworkspace group 3\fR to send it to `output-3`: this is simply done by typing the shortcuts `$mod+Alt+3` (`swaysome focus-group 3`) then `$mod+Alt+o` (`swaysome workspace-group-next-output`)\. .P \fBswaysome\fR may also work with \fBi3\fR, but this is untested. Patches welcome if needed. .P \fBswaysome\fR should be compatible with \fBsworkstyle\fR (\fIhttps://lib\.rs/crates/sworkstyle\fR). If this is broken, please report a bug. . . .SH USAGE .P If you installed \fBswaysome\fR from your distribution's package manager, it should have provided your system with the default configuration file. In that case, you can just include from your \fBsway\fR configuration by appending the following to it: include /etc/sway/config.d/swaysome.conf Otherwise, if installing by any other way, copy the \fBswaysome.conf\fR file in \fB~/.config/sway/config.d/swaysome.conf\fR, then append your \fBsway\fR configuration with this: include ~/.config/sway/config.d/sway.conf On next startup of `sway`, you should end-up with workspaces from `1` to `0`, prefixed with a screen index, giving you workspace `11` on the first screen, and workspace `21` on the second one, both accessible with shortcut `$mod+1` when focused on the right output. . . .SH COMMANDS .TP \fBinit\fR \fIINDEX\fR Initialize the workspace groups for all the outputs on workspace \fIINDEX\fR. This command simply walks through every screen to initialize a prefixed workspace. It does it backwards so that you end-up focused on the first screen, as usual. .TP \fBmove\fR \fIINDEX\fR Move the focused container to workspace \fIINDEX\fR (stay in the same workspace group) .TP \fBmove-to-group\fR \fIINDEX\fR Move the focused container to workspace group \fIINDEX\fR (keep the same workspace index) .TP \fBfocus\fR \fIINDEX\fR Focus to workspace \fIINDEX\fR (stay in the same workspace group) .TP \fBfocus-group\fR \fIINDEX\fR 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 (no keyboard shortcut bound by default for this function) .TP \fBnext-output\fR Move the focused container to the next output .TP \fBprev-output\fR Move the focused container to the previous output .TP \fBworkspace-group-next-output\fR Move the focused workspace group to the next output .TP \fBworkspace-group-prev-output\fR Move the focused workspace group to the previous output .TP \fBnext-group\fR Move the focused container to the next group .TP \fBprev-group\fR Move the focused container to the previous group .TP \fBrearrange-workspaces\fR Rearrange already opened workspaces to the correct outputs, useful when plugging new monitors .TP \fBhelp\fR Print this message or the help of the given subcommand(s) . . .SH OPTIONS .TP \fB-h\fR, \fB--help\fR Print help .TP \fB-V\fR, \fB--version\fR Print version . . .SH "VERSION" 2.3.2 . . .SH "HOMEPAGE" \fIhttps://gitlab.com/hyask/swaysome\fP .sp Please report any bug or feature requests on the bug tracker. . . .SH AUTHORS Florent 'Skia' Jacquet <\fIskia@hya.sk\fP> swaysome-2.3.2/swaysome.conf000064400000000000000000000056261046102023000142260ustar 00000000000000# Use (un)bindcode or (un)bindsym, depending on what you used in your main sway config file. # The `--no-warn` setting is only added to shortcuts that exist in the default config. You may want to add or remove # that flag on some bindings depending on your config. # Change focus between workspaces bindsym --no-warn $mod+1 exec "swaysome focus 1" bindsym --no-warn $mod+2 exec "swaysome focus 2" bindsym --no-warn $mod+3 exec "swaysome focus 3" bindsym --no-warn $mod+4 exec "swaysome focus 4" bindsym --no-warn $mod+5 exec "swaysome focus 5" bindsym --no-warn $mod+6 exec "swaysome focus 6" bindsym --no-warn $mod+7 exec "swaysome focus 7" bindsym --no-warn $mod+8 exec "swaysome focus 8" bindsym --no-warn $mod+9 exec "swaysome focus 9" bindsym --no-warn $mod+0 exec "swaysome focus 0" # Move containers between workspaces bindsym --no-warn $mod+Shift+1 exec "swaysome move 1" bindsym --no-warn $mod+Shift+2 exec "swaysome move 2" bindsym --no-warn $mod+Shift+3 exec "swaysome move 3" bindsym --no-warn $mod+Shift+4 exec "swaysome move 4" bindsym --no-warn $mod+Shift+5 exec "swaysome move 5" bindsym --no-warn $mod+Shift+6 exec "swaysome move 6" bindsym --no-warn $mod+Shift+7 exec "swaysome move 7" bindsym --no-warn $mod+Shift+8 exec "swaysome move 8" bindsym --no-warn $mod+Shift+9 exec "swaysome move 9" bindsym --no-warn $mod+Shift+0 exec "swaysome move 0" # Focus workspace groups bindsym $mod+Alt+1 exec "swaysome focus-group 1" bindsym $mod+Alt+2 exec "swaysome focus-group 2" bindsym $mod+Alt+3 exec "swaysome focus-group 3" bindsym $mod+Alt+4 exec "swaysome focus-group 4" bindsym $mod+Alt+5 exec "swaysome focus-group 5" bindsym $mod+Alt+6 exec "swaysome focus-group 6" bindsym $mod+Alt+7 exec "swaysome focus-group 7" bindsym $mod+Alt+8 exec "swaysome focus-group 8" bindsym $mod+Alt+9 exec "swaysome focus-group 9" bindsym $mod+Alt+0 exec "swaysome focus-group 0" # Move containers to other workspace groups bindsym $mod+Alt+Shift+1 exec "swaysome move-to-group 1" bindsym $mod+Alt+Shift+2 exec "swaysome move-to-group 2" bindsym $mod+Alt+Shift+3 exec "swaysome move-to-group 3" bindsym $mod+Alt+Shift+4 exec "swaysome move-to-group 4" bindsym $mod+Alt+Shift+5 exec "swaysome move-to-group 5" bindsym $mod+Alt+Shift+6 exec "swaysome move-to-group 6" bindsym $mod+Alt+Shift+7 exec "swaysome move-to-group 7" bindsym $mod+Alt+Shift+8 exec "swaysome move-to-group 8" bindsym $mod+Alt+Shift+9 exec "swaysome move-to-group 9" bindsym $mod+Alt+Shift+0 exec "swaysome move-to-group 0" # Move focused container to next output bindsym $mod+o exec "swaysome next-output" # Move focused container to previous output bindsym $mod+Shift+o exec "swaysome prev-output" # Move focused workspace group to next output bindsym $mod+Alt+o exec "swaysome workspace-group-next-output" # Move focused workspace group to previous output bindsym $mod+Alt+Shift+o exec "swaysome workspace-group-prev-output" # Init workspaces for every screen exec "swaysome init 1" swaysome-2.3.2/tests/integration.rs000064400000000000000000001404411046102023000155360ustar 00000000000000// 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}, ]}, ]}, ]})); } swaysome-2.3.2/tests/integration_bin.rs000064400000000000000000000066321046102023000163710ustar 00000000000000use std::process::Command; mod utils; use utils::Sway; static SWAYSOME_BIN: &str = env!("CARGO_BIN_EXE_swaysome"); // Poor safeguard that the tests are actually testing the right version, in // case there is some confusion with a swaysome installed from the system. That // obviously does not always worked, but has saved me a couple of times already 🙃 #[test] fn test_binary_version() { let output = Command::new(SWAYSOME_BIN) .args(["--version"]) .env_clear() .env("SWAYSOCK", "/dev/null") .output() .expect("Couldn't run swaysome"); assert_eq!( String::from_utf8(output.stdout).unwrap(), format!("swaysome {}\n", env!("CARGO_PKG_VERSION")) ); } // This is useful when working on swapping argument parsing libraries. #[test] fn test_binary_help() { let output = Command::new(SWAYSOME_BIN) .args(["-h"]) .env_clear() .env("SWAYSOCK", "/dev/null") .output() .expect("Couldn't run swaysome"); 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(SWAYSOME_BIN) .args(["init", "1"]) .env_clear() .env("SWAYSOCK", sway.sock.clone()) .output() .expect("Couldn't run swaysome"); 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"); } swaysome-2.3.2/tests/sway.conf000064400000000000000000000002151046102023000144710ustar 00000000000000output HEADLESS-1 { resolution 480x270 } output HEADLESS-2 { resolution 1920x1080 } output HEADLESS-3 { resolution 2560x1440 } swaysome-2.3.2/tests/utils/mod.rs000064400000000000000000000112401046102023000151240ustar 00000000000000use 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") } // work around https://github.com/rust-lang/rust/issues/46379 // TODO: maybe implement that: https://momori.dev/posts/organize-rust-integration-tests-without-dead-code-warning/ #[allow(dead_code)] 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. // 200ms 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(500)); self.send_command(["exec", "foot -T TERM2"].as_slice()); std::thread::sleep(std::time::Duration::from_millis(500)); self.send_command(["exec", "foot -T TERM3"].as_slice()); std::thread::sleep(std::time::Duration::from_millis(500)); } 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) { // in case some apps were spawned, kill them, and give them some time to be killed self.send_command(["[all] kill"].as_slice()); std::thread::sleep(std::time::Duration::from_millis(500)); // now terminate sway 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(); } }