Merge branch 'skia/add_tests'

This branch adds a whole bunch of rework and now has integration tests.
The coverage might not be perfect, but it's a pretty good start that
should give some confidence in case of any sort of rework.
This commit is contained in:
Skia 2025-09-01 23:34:02 +02:00
commit 4b205bf2e9
11 changed files with 1830 additions and 649 deletions

View file

@ -1,17 +1,29 @@
image: "rust:latest" image: "rust:latest"
lint:cargo: lint:
stage: build stage: build
script: script:
- rustc --version && cargo --version - rustc --version && cargo --version
- rustup component add rustfmt - rustup component add rustfmt
- cargo fmt - cargo fmt
build:cargo: build:
stage: build stage: build
script: script:
- rustc --version && cargo --version - rustc --version && cargo --version
- cargo build --release - cargo build --release
artifacts: artifacts:
paths: paths:
- target/release/swaysome - 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 <<EOSU # sway won't run as root
- set -e
# This is weird syntax to only run integration tests (the only ones swaysome has)
- cargo test --verbose --test '*'
- EOSU

11
Cargo.lock generated
View file

@ -50,6 +50,16 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "assert-json-diff"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@ -179,6 +189,7 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
name = "swaysome" name = "swaysome"
version = "2.1.2" version = "2.1.2"
dependencies = [ dependencies = [
"assert-json-diff",
"byteorder", "byteorder",
"clap", "clap",
"serde", "serde",

View file

@ -15,3 +15,6 @@ byteorder = "1.5.0"
clap = { version = "4.4.8", features = ["derive"] } clap = { version = "4.4.8", features = ["derive"] }
serde = { version = "1.0.192", features = ["derive"] } serde = { version = "1.0.192", features = ["derive"] }
serde_json = "1.0.108" serde_json = "1.0.108"
[dev-dependencies]
assert-json-diff = "2.0.2"

14
HACKING.md Normal file
View file

@ -0,0 +1,14 @@
# 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
```

629
src/lib.rs Normal file
View file

@ -0,0 +1,629 @@
use 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::{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;
const GET_TREE: u32 = 4;
pub struct SwaySome {
socket: Cell<Option<UnixStream>>,
outputs: Vec<Output>,
// current_output: Output,
workspaces: Vec<Workspace>,
// 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<SwaySome, String> {
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::<u8>() + 2 * mem::size_of::<u32>()] =
*b"i3-ipc00000000";
msg_prefix[6..]
.as_mut()
.write_u32::<LittleEndian>(payload_length)
.expect("Unable to write");
msg_prefix[10..]
.as_mut()
.write_u32::<LittleEndian>(msg_type)
.expect("Unable to write");
let mut msg: Vec<u8> = 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<String, &str> {
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::<LittleEndian>().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::Value> = 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<Output> {
self.send_msg(GET_OUTPUTS, "");
let o = match self.read_msg() {
Ok(msg) => msg,
Err(_) => panic!("Unable to get outputs"),
};
let mut outputs: Vec<Output> = serde_json::from_str::<Vec<Output>>(&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<Workspace> {
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<Workspace> = 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(&current_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(&current_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(&current_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);
// }
}
}
}

View file

@ -1,31 +1,7 @@
extern crate byteorder;
extern crate clap; extern crate clap;
extern crate serde_json;
use serde::{Deserialize, Serialize};
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use std::cell::Cell; use swaysome::SwaySome;
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;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap(author, version, about = "Better multimonitor handling for sway", long_about = None)] #[clap(author, version, about = "Better multimonitor handling for sway", long_about = None)]
@ -87,624 +63,6 @@ struct Index {
index: usize, index: usize,
} }
struct SwaySome {
socket: Cell<Option<UnixStream>>,
outputs: Vec<Output>,
// current_output: Output,
workspaces: Vec<Workspace>,
// 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::<u8>() + 2 * mem::size_of::<u32>()] =
*b"i3-ipc00000000";
msg_prefix[6..]
.as_mut()
.write_u32::<LittleEndian>(payload_length)
.expect("Unable to write");
msg_prefix[10..]
.as_mut()
.write_u32::<LittleEndian>(msg_type)
.expect("Unable to write");
let mut msg: Vec<u8> = 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<String, &str> {
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::<LittleEndian>().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::Value> = 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<Output> {
self.send_msg(GET_OUTPUTS, "");
let o = match self.read_msg() {
Ok(msg) => msg,
Err(_) => panic!("Unable to get outputs"),
};
let mut outputs: Vec<Output> = serde_json::from_str::<Vec<Output>>(&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<Workspace> {
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<Workspace> = 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(&current_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(&current_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(&current_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() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
@ -742,10 +100,10 @@ fn main() {
swaysome.move_workspace_group_to_prev_output(); swaysome.move_workspace_group_to_prev_output();
} }
Command::NextGroup => { Command::NextGroup => {
swaysome.focus_container_to_next_group(); swaysome.focus_to_next_group();
} }
Command::PrevGroup => { Command::PrevGroup => {
swaysome.focus_container_to_prev_group(); swaysome.focus_to_prev_group();
} }
Command::RearrangeWorkspaces => { Command::RearrangeWorkspaces => {
swaysome.rearrange_workspaces(); swaysome.rearrange_workspaces();

View file

@ -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) Focus to workspace group \fIINDEX\fR (keep the same workspace index)
.TP .TP
\fBfocus-all-outputs\fR \fIINDEX\fR \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 .TP
\fBnext-output\fR \fBnext-output\fR
Move the focused container to the next output Move the focused container to the next output

988
tests/integration.rs Normal file
View file

@ -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},
]},
]},
]}));
}

30
tests/integration_bin.rs Normal file
View file

@ -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 <COMMAND>\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");
}

11
tests/sway.conf Normal file
View file

@ -0,0 +1,11 @@
output HEADLESS-1 {
resolution 480x270
}
output HEADLESS-2 {
resolution 1920x1080
}
output HEADLESS-3 {
resolution 2560x1440
}

125
tests/utils/mod.rs Normal file
View file

@ -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();
}
}