mirror of
https://gitlab.com/hyask/swaysome.git
synced 2025-12-10 07:44:43 +01:00
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:
commit
4b205bf2e9
11 changed files with 1830 additions and 649 deletions
|
|
@ -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
11
Cargo.lock
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
14
HACKING.md
Normal 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
629
src/lib.rs
Normal 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(¤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);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
648
src/main.rs
648
src/main.rs
|
|
@ -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(¤t_output_name);
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
|
|
||||||
let mut focus_cmd: String = "move container to workspace ".to_string();
|
|
||||||
focus_cmd.push_str(&full_ws_name);
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
|
|
||||||
let mut focus_cmd: String = "focus output ".to_string();
|
|
||||||
focus_cmd.push_str(&target_output.name);
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
|
|
||||||
let mut focus_cmd: String = "workspace ".to_string();
|
|
||||||
focus_cmd.push_str(&focused_workspace_index.to_string());
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
|
|
||||||
let mut focus_cmd: String = "focus output ".to_string();
|
|
||||||
focus_cmd.push_str(¤t_output_name);
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// Else, we send the container on the current output
|
|
||||||
let mut focus_cmd: String = "move container to workspace ".to_string();
|
|
||||||
focus_cmd.push_str(&full_ws_name);
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_container_to_workspace_relative(&self, workspace_index: usize) {
|
|
||||||
let current_workspace_index: usize = self.get_current_workspace_index();
|
|
||||||
let focused_output_index = current_workspace_index / MAX_GROUP_WS;
|
|
||||||
|
|
||||||
let mut cmd: String = "move container to workspace number ".to_string();
|
|
||||||
let full_ws_name = format!("{}", focused_output_index * MAX_GROUP_WS + workspace_index);
|
|
||||||
cmd.push_str(&full_ws_name);
|
|
||||||
self.send_command(&cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_to_workspace(&self, workspace_index: usize) {
|
|
||||||
if workspace_index < MAX_GROUP_WS {
|
|
||||||
self.focus_to_workspace_relative(workspace_index);
|
|
||||||
} else {
|
|
||||||
self.focus_to_workspace_absolute(workspace_index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_to_workspace_absolute(&self, workspace_index: usize) {
|
|
||||||
let output_index = (workspace_index / MAX_GROUP_WS) as usize;
|
|
||||||
|
|
||||||
// If the workspace already exists
|
|
||||||
match self.workspaces.iter().find(|w| w.num == workspace_index) {
|
|
||||||
Some(_) => {
|
|
||||||
let mut focus_cmd: String = "workspace number ".to_string();
|
|
||||||
focus_cmd.push_str(&workspace_index.to_string());
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let target_group = workspace_index / MAX_GROUP_WS;
|
|
||||||
let target_screen_index = match self
|
|
||||||
.workspaces
|
|
||||||
.iter()
|
|
||||||
.find(|w| w.num / MAX_GROUP_WS == target_group)
|
|
||||||
{
|
|
||||||
// If other workspaces on the same group exists
|
|
||||||
Some(other_workspace) => Some(
|
|
||||||
self.outputs
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|i| i.1.name == other_workspace.output)
|
|
||||||
.unwrap()
|
|
||||||
.0,
|
|
||||||
),
|
|
||||||
None => {
|
|
||||||
// Or if the targeted output is currently connected
|
|
||||||
if output_index < self.outputs.len() {
|
|
||||||
Some(output_index)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match target_screen_index {
|
|
||||||
// If we have to send it to another screen
|
|
||||||
Some(target_screen_index) => {
|
|
||||||
let target_output = &self.outputs[target_screen_index - 1];
|
|
||||||
|
|
||||||
let mut focus_cmd: String = "focus output ".to_string();
|
|
||||||
focus_cmd.push_str(&target_output.name);
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
};
|
|
||||||
// Then we focus the workspace
|
|
||||||
let mut focus_cmd: String = "workspace number ".to_string();
|
|
||||||
focus_cmd.push_str(&workspace_index.to_string());
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_to_workspace_relative(&self, workspace_index: usize) {
|
|
||||||
let current_workspace_index: usize = self.get_current_workspace_index();
|
|
||||||
let focused_output_index = current_workspace_index / MAX_GROUP_WS;
|
|
||||||
|
|
||||||
let mut cmd: String = "workspace number ".to_string();
|
|
||||||
let full_ws_name = format!("{}", focused_output_index * MAX_GROUP_WS + workspace_index);
|
|
||||||
cmd.push_str(&full_ws_name);
|
|
||||||
self.send_command(&cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_to_group(&self, group_index: usize) {
|
|
||||||
let current_workspace_index: usize = self.get_current_workspace_index();
|
|
||||||
let target_workspace_relative_index = current_workspace_index % MAX_GROUP_WS;
|
|
||||||
|
|
||||||
let target_workspace_index = group_index * MAX_GROUP_WS + target_workspace_relative_index;
|
|
||||||
let full_ws_name = format!(
|
|
||||||
"{}",
|
|
||||||
group_index * MAX_GROUP_WS + target_workspace_relative_index
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the workspace already exists
|
|
||||||
match self
|
|
||||||
.workspaces
|
|
||||||
.iter()
|
|
||||||
.find(|w| w.num == target_workspace_index)
|
|
||||||
{
|
|
||||||
Some(_) => {
|
|
||||||
let mut focus_cmd: String = "workspace number ".to_string();
|
|
||||||
focus_cmd.push_str(&full_ws_name);
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let target_screen_index = match self
|
|
||||||
.workspaces
|
|
||||||
.iter()
|
|
||||||
.find(|w| w.num / MAX_GROUP_WS == group_index)
|
|
||||||
{
|
|
||||||
// If other workspaces on the same group exists
|
|
||||||
Some(other_workspace) => Some(
|
|
||||||
self.outputs
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|i| i.1.name == other_workspace.output)
|
|
||||||
.unwrap()
|
|
||||||
.0
|
|
||||||
+ 1,
|
|
||||||
),
|
|
||||||
None => {
|
|
||||||
// Or if the targeted output is currently connected
|
|
||||||
if group_index > 0 && group_index <= self.outputs.len() {
|
|
||||||
Some(group_index)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match target_screen_index {
|
|
||||||
// If we have to send it to another screen
|
|
||||||
Some(target_screen_index) => {
|
|
||||||
let target_output = &self.outputs[target_screen_index - 1];
|
|
||||||
|
|
||||||
let mut focus_cmd: String = "focus output ".to_string();
|
|
||||||
focus_cmd.push_str(&target_output.name);
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
};
|
|
||||||
// Then we focus the workspace
|
|
||||||
let mut focus_cmd: String = "workspace number ".to_string();
|
|
||||||
focus_cmd.push_str(&target_workspace_index.to_string());
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_all_outputs_to_workspace(&self, workspace_index: usize) {
|
|
||||||
let current_output = self.get_current_output_name();
|
|
||||||
|
|
||||||
// Iterate on all outputs to focus on the given workspace
|
|
||||||
for output in self.outputs.iter() {
|
|
||||||
let mut cmd: String = "focus output ".to_string();
|
|
||||||
cmd.push_str(output.name.as_str());
|
|
||||||
self.send_command(&cmd);
|
|
||||||
|
|
||||||
self.focus_to_workspace(workspace_index);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get back to currently focused output
|
|
||||||
let mut cmd: String = "focus output ".to_string();
|
|
||||||
cmd.push_str(¤t_output);
|
|
||||||
self.send_command(&cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_container_to_next_output(&self) {
|
|
||||||
self.move_container_to_next_or_prev_output(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_container_to_prev_output(&self) {
|
|
||||||
self.move_container_to_next_or_prev_output(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_container_to_next_or_prev_output(&self, go_to_prev: bool) {
|
|
||||||
let focused_output_index = self.get_current_output_index();
|
|
||||||
|
|
||||||
let target_output = if go_to_prev {
|
|
||||||
&self.outputs[(focused_output_index + self.outputs.len() - 1) % self.outputs.len()]
|
|
||||||
} else {
|
|
||||||
&self.outputs[(focused_output_index + 1) % self.outputs.len()]
|
|
||||||
};
|
|
||||||
|
|
||||||
let workspaces = self.get_workspaces();
|
|
||||||
let target_workspace = workspaces
|
|
||||||
.iter()
|
|
||||||
.find(|x| x.output == target_output.name && x.visible)
|
|
||||||
.unwrap();
|
|
||||||
let group_index = (target_workspace.num / MAX_GROUP_WS) as usize;
|
|
||||||
let full_ws_name = format!(
|
|
||||||
"{}",
|
|
||||||
group_index * MAX_GROUP_WS + target_workspace.num % MAX_GROUP_WS
|
|
||||||
);
|
|
||||||
|
|
||||||
// Move container to target workspace
|
|
||||||
let mut cmd: String = "move container to workspace number ".to_string();
|
|
||||||
cmd.push_str(&full_ws_name);
|
|
||||||
self.send_command(&cmd);
|
|
||||||
|
|
||||||
// Focus that workspace to follow the container
|
|
||||||
let mut cmd: String = "workspace number ".to_string();
|
|
||||||
cmd.push_str(&full_ws_name);
|
|
||||||
self.send_command(&cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_workspace_group_to_next_output(&self) {
|
|
||||||
self.move_workspace_group_to_next_or_prev_output(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_workspace_group_to_prev_output(&self) {
|
|
||||||
self.move_workspace_group_to_next_or_prev_output(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_workspace_group_to_next_or_prev_output(&self, go_to_prev: bool) {
|
|
||||||
let focused_output_index = self.get_current_output_index();
|
|
||||||
|
|
||||||
let target_output = if go_to_prev {
|
|
||||||
&self.outputs[(focused_output_index + self.outputs.len() - 1) % self.outputs.len()]
|
|
||||||
} else {
|
|
||||||
&self.outputs[(focused_output_index + 1) % self.outputs.len()]
|
|
||||||
};
|
|
||||||
let current_workspace = self.get_current_workspace_index();
|
|
||||||
let current_group_index = (current_workspace / MAX_GROUP_WS) as usize;
|
|
||||||
for workspace in self.get_workspaces() {
|
|
||||||
let ws_index = workspace.num / MAX_GROUP_WS;
|
|
||||||
if ws_index == current_group_index {
|
|
||||||
let cmd: String = format!("workspace number {}", workspace.num);
|
|
||||||
self.send_command(&cmd);
|
|
||||||
let cmd: String = format!("move workspace to {}", target_output.name);
|
|
||||||
self.send_command(&cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let cmd: String = format!("workspace number {}", current_workspace);
|
|
||||||
self.send_command(&cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_container_to_next_group(&self) {
|
|
||||||
self.focus_container_to_next_or_prev_group(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_container_to_prev_group(&self) {
|
|
||||||
self.focus_container_to_next_or_prev_group(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_container_to_next_or_prev_group(&self, go_to_prev: bool) {
|
|
||||||
let current_workspace_index: usize = self.get_current_workspace_index();
|
|
||||||
let focused_group_index = current_workspace_index / MAX_GROUP_WS;
|
|
||||||
|
|
||||||
let highest_group = self.workspaces.last().unwrap().num / MAX_GROUP_WS;
|
|
||||||
let target_group;
|
|
||||||
if go_to_prev {
|
|
||||||
if focused_group_index == 0 {
|
|
||||||
target_group = highest_group;
|
|
||||||
} else {
|
|
||||||
target_group = focused_group_index - 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if focused_group_index >= highest_group {
|
|
||||||
target_group = 0;
|
|
||||||
} else {
|
|
||||||
target_group = focused_group_index + 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.focus_to_group(target_group);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_workspaces(&self, workspace_index: usize) {
|
|
||||||
let cmd_prefix: String = "focus output ".to_string();
|
|
||||||
for output in self.outputs.iter().rev() {
|
|
||||||
let mut cmd = cmd_prefix.clone();
|
|
||||||
cmd.push_str(output.name.as_str());
|
|
||||||
self.send_command(&cmd);
|
|
||||||
|
|
||||||
let mut cmd: String = "workspace number ".to_string();
|
|
||||||
let full_ws_name = format!(
|
|
||||||
"{}",
|
|
||||||
(self.get_current_output_index() + 1) * MAX_GROUP_WS + workspace_index
|
|
||||||
);
|
|
||||||
cmd.push_str(&full_ws_name);
|
|
||||||
self.send_command(&cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rearrange_workspaces(&self) {
|
|
||||||
let focus_cmd_prefix: String = "workspace number ".to_string();
|
|
||||||
let move_cmd_prefix: String = "move workspace to ".to_string();
|
|
||||||
for workspace in self.workspaces.iter() {
|
|
||||||
let mut focus_cmd = focus_cmd_prefix.clone();
|
|
||||||
focus_cmd.push_str(&workspace.num.to_string());
|
|
||||||
self.send_command(&focus_cmd);
|
|
||||||
|
|
||||||
let group_index = workspace.num / MAX_GROUP_WS;
|
|
||||||
if group_index <= self.outputs.len() - 1 {
|
|
||||||
let mut move_cmd = move_cmd_prefix.clone();
|
|
||||||
move_cmd.push_str(&self.outputs[group_index.max(1) - 1].name);
|
|
||||||
self.send_command(&move_cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
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();
|
||||||
|
|
|
||||||
|
|
@ -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
988
tests/integration.rs
Normal 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
30
tests/integration_bin.rs
Normal 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
11
tests/sway.conf
Normal 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
125
tests/utils/mod.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue