Compare commits

...

90 commits

Author SHA1 Message Date
Skia
31ed37a70c Release 2.3.2
Bugfix release for big endian systems
2025-09-19 23:23:39 +02:00
Skia
59333d6804 fix: use NativeEndian to work also on big endian systems like s390x 2025-09-19 23:21:16 +02:00
Skia
9dd0fd6831 Make sure master's version is different from anything released or packaged 2025-09-09 13:37:04 +02:00
Skia
343998a09d Release 2.3.1
* No changes in swaysome itself, only bugfixes in the tests to make them
  compatible with Debian's autopkgtest.
2025-09-09 13:33:37 +02:00
Skia
317e463da1 tests: workaround rust-lang/rust#46379 2025-09-09 13:18:46 +02:00
Skia
2410c1164d tests: add test_binary_version as a poor safeguard 2025-09-09 13:18:20 +02:00
Skia
3514b9cd5b Cargo.toml: Make sure master's version is different from anything released or packaged 2025-09-09 13:17:57 +02:00
Skia
a83c9d45aa tests: fix error message 2025-09-09 13:05:40 +02:00
Skia
11396b677e tests: make use of 'CARGO_BIN_EXE_swaysome'
Instead of hardcoding a relative and making assumptions, this makes sure
the tests are calling the right binary.
2025-09-09 13:02:10 +02:00
Skia
75806d081f tests: make sure sway's subprocesses are killed before killing sway
This fixes the tests when run within Debian's autopkgtest. Since there
were still some living child processes, autopkgtest was stuck waiting
for them forever.
2025-09-09 12:57:47 +02:00
Skia
351cb5dd66 Release version 2.3.0
* Added some tests, and discovered some bugs along the way
* Fixed those afformentioned bugs 🎉
* Some codebase rework, needed to add the afformentioned tests
2025-09-01 23:44:41 +02:00
Skia
3146c2f186 tests: make the race easier to win 2025-09-01 23:40:51 +02:00
Skia
4b205bf2e9 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.
2025-09-01 23:34:02 +02:00
Skia
eb2dc1de1d tests: add testing of the binary's integration with sway 2025-09-01 23:29:59 +02:00
Skia
f9a0c22e9b fix: Path::is_file can return 'false' sometimes for reasons™ 2025-09-01 23:20:03 +02:00
Skia
d6eb1b62a6 rename focus_to_{next,prev}_group functions 2025-04-13 14:58:42 +02:00
Skia
a8a80eb514 Simplifications and bugfixes discovered when writing tests 2025-04-13 14:58:42 +02:00
Skia
3f61dbda90 Add some integration tests 2025-04-13 14:58:42 +02:00
Skia
f7bcdf1296 lib: refactor SwaySome::new to also have SwaySome::new_from_socket
`SwaySome::new_from_socket` takes a socket path in argument.
This allows the tests to be able to build a SwaySome instance with a
custom socket path.
2024-11-04 18:03:17 +01:00
Skia
ffc73fd832 lib: add get_tree, useful for testing 2024-11-04 17:58:24 +01:00
Skia
ab32878de5 lib: remove useless headers 2024-11-04 17:57:59 +01:00
Skia
e5c61a8694 Split the whole thing into a lib and tiny main
This is how binaries are supposed to be done in Rust, to help with
integration testing. Guess what's coming?
2024-11-02 19:46:34 +01:00
Skia
191a5ab001 man: small phrasing improvement 2024-11-02 19:46:34 +01:00
Skia
bc80f6a650 Release 2.1.2 - one bugfix and some doc 2024-10-15 17:25:13 +02:00
Skia
c5930274e5 Correctly wrap around when changing workspace group
Fix #19
2024-10-12 15:35:11 +01:00
Skia
df9b26335f Merge branch 'void' into 'master'
README: add instructions to install on Void Linux

Added in https://github.com/void-linux/void-packages/pull/49049 

See merge request hyask/swaysome!13
2024-07-29 07:59:52 +00:00
Luca Matei Pintilie
41227bac8b
README: add instructions to install on Void Linux 2024-07-26 19:11:19 +02:00
Skia
1f8ccb0207 Release 2.1.1 - documentation update only 2024-02-23 11:21:32 +01:00
Skia
a399d827de Greatly improve the man page 2024-02-16 17:07:12 +01:00
Skia
83c454cd6a Add manpage 2024-01-29 16:54:10 +01:00
Skia
f336a437c2 Improve README and put default config in a separate file 2024-01-29 16:54:10 +01:00
Skia
b36ba07d06 Release 2.1.0 2023-11-30 16:18:38 +01:00
Skia
5f2cd2b300 README: add a link to the Matrix room 2023-11-27 11:27:35 +01:00
Skia
aa3b47fbad get_current_*: don't use outdated cache is these functions
Fixes https://gitlab.com/hyask/swaysome/-/issues/18
Thanks a lot @mahieujeremy for reporting that and providing a fix so quickly!
2023-11-27 11:12:33 +01:00
Skia
9da99893c4 Improve documentation and default config 2023-11-17 17:56:17 +01:00
Skia
36c1ef68da Cargo: bump all dependencies 2023-11-17 17:55:48 +01:00
Skia
3ccf1d6eda Add WorkspaceGroup{Next,Prev}Output command 2023-11-16 15:20:54 +01:00
Skia
90ec4a2566 Rework everything to have a global SwaySome object with methods in it
This allows easily keeping state across the various function calls.
2023-11-14 09:44:36 +01:00
Skia
99a99648b1 simplify clap structures 2023-11-13 15:52:39 +01:00
Skia
6d8c19e8a7 cargo fmt 2023-11-13 15:42:25 +01:00
Skia
6d26286583 Improve socket discovery, handling, and logging
Implement the following logic:
* first try SWAYSOCK
* if failed, then try I3SOCK, the legacy one coming from i3
* if failed, then we abort with an error
2023-11-07 08:36:10 +02:00
Skia
cdd5a24dc6 Merge branch 'master' into 'master'
Added constant for maximum workspaces per group instead of hardcoded 10

Hello creators of swaysome.

I love this program, its quite nice, thank you for sharing it ^.^

However, I can't live with just 10 workspaces per group, I need more.

So, instead of having the base 10 hard-coded all around, I created a constant, which  I named `MAX_GROUP_WS` due to my super creative naming skills. This way, anyone could change the groups naming and the limit of workspaces with just modifying a simple constant.

Examples:
```
 MAX     Naming
  10 →   17,   27
 100 →  107,  207
1000 → 1007, 2007
  15 →   22,   37
```

On my local setup I use 100, so its easy to know which group I'm in, and I recommend others to either use 10 or 100. That said, nothing prevents you from using other bases, such as hexadecimal (16), or 69...

To keep backwards compatibility, and not break any setup, I kept the default as `10`.

If you like my change, feel free to merge it! 

See merge request hyask/swaysome!12
2023-06-06 08:41:39 +00:00
LluisE
91f697cb42 Feature MAX_GROUP_WS constant instead of hardcoded 10 2023-06-05 20:37:19 +02:00
Skia
c24d02e284 Merge branch 'master' into 'master'
Note about (un)bindcode or (un)bindsym and order numbering

Added a note about using either (un)bindcode or (un)bindsym and re-ordered numbering to use single digits instead of double digits in the README. 

See merge request hyask/swaysome!11
2023-05-02 08:37:40 +00:00
Andrei S
05ca3b8f4a Note about (un)bindcode or (un)bindsym and order numbering 2023-04-27 12:02:41 +00:00
Skia
9772931af9 get_output: always filter out inactive outputs 2023-03-09 23:50:02 +01:00
Skia
7b3de58cd9 README: improved description to include workspace groups 2023-03-03 17:50:21 +01:00
Skia
7daf8ffd77 Release 2.0.0 2023-03-03 17:13:46 +01:00
Nabos
bc8ac73ddb Adding Nabos to authors 2023-03-03 16:12:49 +01:00
Nabos
4e7eed7b99 Merge branch 'move_to_group' into 'master'
Added move-to-group

 

See merge request hyask/swaysome!10
2023-03-03 14:41:51 +00:00
Nabos
cf86c9bd22 Added move-to-group 2023-03-03 14:41:51 +00:00
Skia
2541b0aa2c README: add a mention about '$PATH' issues (#11) 2023-03-02 12:49:10 +01:00
Skia
faf309ce81 Output: set a default for 'active' instead of failing 2023-02-07 11:32:38 +01:00
Skia
13b9687c0a README: small form improvements 2023-02-07 11:16:37 +01:00
Nabos
dd22264536 Merge branch 'fix_group_index' into 'master'
Group index fix

Check group index lower limit 

See merge request hyask/swaysome!9
2023-01-17 14:50:02 +00:00
Nabos
0db1faa1c5 Check group index lower limit 2023-01-17 15:47:27 +01:00
Skia
1234264d75 Merge branch 'workspaces_groups' 2023-01-16 11:27:38 +01:00
Skia
e8fc73c5da Merge branch 'workspaces_groups' into 'master'
Workspaces groups

This feature brings a lot of new possibilities.

A workspace group is group of ten workspaces inside a decimal range (ex: from 10 to 19).
It introduces `relative` and `absolute` scopes.

For a move or a focus, if the target index is only one digit it will focus the wanted workspace relative to the current group focused.

But if the target index is above 9, it will directly focus the workspace inside the group if it is already opened, if not, it will open the group on the current screen.

Example 1: I'm focused on 21. If I do `swaysome focus 5` it's going to focus the workspace 25 on the same output than the other 20s.

Example 2: I'm focused on 21. If I do `swaysome focus 35` it's going to focus the workspace 35. If there is no 30s opened yet, it's going to open it on my current screen, but I already have 32 opened on another screen, so it's going to open the workspace 35 on it.


It also brings the `next-group` and `prev-group` features to switch focus from 21 to 31 or 21 to 11 for example.

There is the `focus-group` action that allows you to directly focus a specified group and so creating multiple workspace groups on one output (For example, 20s for work and 30s for private stuff).

There is one breaking behavior: I had to edit the `init` function to begin from 10 and not from 1.

So with `swaysome init 1`, a triple screen is now initialized like this: 11, 21, 31 instead of 1, 11, 21. 

See merge request hyask/swaysome!6
2023-01-16 10:26:23 +00:00
Skia
0566cf106e README: fix '{next,prev}-output' and add 'Breaking changes' section 2023-01-16 11:15:01 +01:00
Skia
5380131ee9 Fix 'move_container_to_next_or_prev_output' and 'current_output_index' 2023-01-16 11:11:03 +01:00
Skia
29a0afe619 Merge branch 'cli_parser_argument_types' into 'master'
Cli parser argument types

Just a little stronger typing on the CLI arguments 

See merge request hyask/swaysome!5
2023-01-13 16:09:43 +00:00
Skia
dfb6e79a3a Merge branch 'rearrange_workspaces' into 'master'
Rearrange workspaces

Based of the args parser rework, I added a feature to rearrange the missplaced workspaces.
It is useful when you unplug a screen, create a workspace then plug the screen back.
In this scenario, the screen won't move back to the correct screen when the screen is plugged back.
I don't know if there could be a way to fix this behavior instead of creating a feature to repair it each time.
Let me know what you think 

See merge request hyask/swaysome!4
2023-01-13 16:08:40 +00:00
Nabos
1c8ab4246a Fix minimum output index 2022-12-23 09:59:06 +01:00
Nabos
1334bde4bd Updated Readme 2022-12-23 09:25:13 +01:00
Nabos
a0dc360e37 Removed debug print 2022-12-23 09:10:58 +01:00
Skia
997f50c759 Merge branch 'Renner0E-master-patch-46524' into 'master'
readme: install option for arch

 

See merge request hyask/swaysome!8
2022-11-30 08:55:13 +00:00
Renner0E
b57fb3d7b2 readme: install option for arch 2022-11-29 17:00:32 +00:00
Skia
d700e562ce pkgbuild: set an obviously dummy version number
This is to show that it isn't used and need no bump upon new release.
Was discussed on https://gitlab.com/hyask/swaysome/-/merge_requests/7
2022-11-29 10:16:51 +01:00
Skia
25b92a7d26 Merge branch 'AUR' into 'master'
added PKGBUILD for AUR

This is my first merge request and PKGBUILD for the AUR. 

See merge request hyask/swaysome!7
2022-11-29 09:12:26 +00:00
renner0e
685c75d889 added PKGBUILD for AUR 2022-11-14 16:42:03 +01:00
Nabos
4620e8ab2c Fix focus to group when already existing group 2022-08-25 15:25:32 +02:00
Nabos
4ec989ed72 Fix focus to group when no existing workspace 2022-08-23 15:14:19 +02:00
Nabos
72c34cb5e5 Fix focus next/prev group 2022-08-17 14:14:48 +02:00
Nabos
b6f81aa7bb Finished workspace groups 2022-08-17 11:17:47 +02:00
Nabos
286760ade6 Added Groups 2022-08-11 17:36:34 +02:00
Nabos
dd848dfe04 Strongly typed workspace indexes and output indexes 2022-08-05 12:58:39 +02:00
Nabos
6fe7ca7bc3 Check for screen number 2022-08-05 12:56:00 +02:00
Skia
f6278fcef2 Merge branch 'cli_parser_rework' into 'master'
Reworked argument parsing

Using Clap struct parser for cleaner and safer argument parsing. 

See merge request hyask/swaysome!3
2022-08-03 22:18:28 +00:00
Nabos
b39b51ad58 Added a rearrange workspaces feature 2022-08-03 16:05:38 +02:00
Nabos
90e1e46fb2 Reworked argument parsing 2022-08-03 16:04:56 +02:00
Skia
d57d98697b Release 1.1.5 2022-05-23 01:22:42 +02:00
Skia
0d19f3f651 Merge branch '4-workaround-missing-focused-field' into 'master'
Fix #4: Add workaround for missing `focused` property in outputs

If there is an inactive input, then the `focused` property may be missing in sway's response to a `get_output` message.
This fix uses a feature of serde where default values for missing fields can be defined.
Here, the `Default::default` for booleans is used, which is `false`.

Closes #4

See merge request hyask/swaysome!1
2022-05-22 23:20:56 +00:00
Daniel Albert
2497f0519f Fix #4: Add workaround for missing focused property in outputs
If there is an inactive input, then the `focused` property may be missing in sway's response to a `get_output` message.
This fix uses a feature of serde where default values for missing fields can be defined.
Here, the `Default::default` for booleans is used, which is `false`.
2022-05-22 21:15:00 +02:00
Skia
06eb9d29c2 Release 1.1.4 2022-04-12 23:54:44 +02:00
Skia
1e6986558d Fix underflow in 'prev_output' calculation 2022-04-12 23:53:58 +02:00
Skia
465a4aff84 Fix all cargo clippy reports 2022-04-12 23:53:27 +02:00
Skia
9af265efbe Use clap's 'crate_version' macro to avoid further mess 2022-04-12 23:33:17 +02:00
Skia
80b791fa2d Bump dependencies and release 1.1.3 2022-04-11 10:25:33 +02:00
Skia
57272671ed Always sort lists of objects returned by sway
Fixes #2
2021-11-07 00:54:32 +01:00
Skia
aa2a232320 Use more strongly typed JSON for Output and Workspace 2021-11-07 00:53:16 +01:00
15 changed files with 2476 additions and 440 deletions

View file

@ -1,17 +1,29 @@
image: "rust:latest"
lint:cargo:
lint:
stage: build
script:
- rustc --version && cargo --version
- rustup component add rustfmt
- cargo fmt
build:cargo:
build:
stage: build
script:
- rustc --version && cargo --version
- cargo build --release
artifacts:
paths:
- target/release/swaysome
- 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

262
Cargo.lock generated
View file

@ -1,92 +1,178 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "ansi_term"
version = "0.11.0"
name = "anstream"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
dependencies = [
"winapi",
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "atty"
version = "0.2.14"
name = "anstyle"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
[[package]]
name = "anstyle-parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140"
dependencies = [
"hermit-abi",
"libc",
"winapi",
"utf8parse",
]
[[package]]
name = "bitflags"
version = "1.3.2"
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
dependencies = [
"anstyle",
"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]]
name = "byteorder"
version = "1.4.2"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "clap"
version = "2.33.3"
version = "4.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim",
"textwrap",
"unicode-width",
"vec_map",
"clap_builder",
"clap_derive",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
name = "clap_builder"
version = "4.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc"
dependencies = [
"libc",
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "itoa"
version = "0.4.7"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "libc"
version = "0.2.101"
name = "proc-macro2"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.5"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "serde"
version = "1.0.123"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae"
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.64"
version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
dependencies = [
"itoa",
"ryu",
@ -95,58 +181,106 @@ dependencies = [
[[package]]
name = "strsim"
version = "0.8.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "swaysome"
version = "1.1.2"
version = "2.3.2"
dependencies = [
"assert-json-diff",
"byteorder",
"clap",
"serde",
"serde_json",
]
[[package]]
name = "textwrap"
version = "0.11.0"
name = "syn"
version = "2.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
dependencies = [
"unicode-width",
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-width"
version = "0.1.8"
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "vec_map"
version = "0.8.2"
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "winapi"
version = "0.3.9"
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
"windows-targets",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"

View file

@ -1,9 +1,9 @@
[package]
name = "swaysome"
version = "1.1.2"
authors = ["Skia <skia@hya.sk>"]
edition = "2018"
description = "swaysome provides a different way to manage your multiple outputs with the sway windows manager"
version = "2.3.2"
authors = ["Skia <skia@hya.sk>", "Nabos <nabos@glargh.fr>"]
edition = "2021"
description = "swaysome provides an awesome way to manage your multiple outputs with the sway windows manager"
license = "MIT"
repository = "https://gitlab.com/hyask/swaysome"
homepage = "https://gitlab.com/hyask/swaysome"
@ -11,6 +11,10 @@ homepage = "https://gitlab.com/hyask/swaysome"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
byteorder = "1"
serde_json = "1"
clap = "2"
byteorder = "1.5.0"
clap = { version = "4.4.8", features = ["derive"] }
serde = { version = "1.0.192", features = ["derive"] }
serde_json = "1.0.108"
[dev-dependencies]
assert-json-diff = "2.0.2"

14
HACKING.md Normal file
View file

@ -0,0 +1,14 @@
# Hacking on swaysome
## Get the coverage of the test to improve them
You'll need more toolchain components:
```
rustup component add llvm-tools-preview
```
Then run the tests with coverage profiling, and generate the HTML report:
```
CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test
grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage
```

138
README.md
View file

@ -1,14 +1,60 @@
# Swaysome
This binary helps you configure sway to work a bit more like Awesome. This
currently means workspaces that are name-spaced on a per-screen basis.
An awesome way to manage your workspaces on [sway](https://swaywm.org/).
It may also work with i3, but this is untested.
*swaysome* is compatible with [sworkstyle](https://lib.rs/crates/sworkstyle).
Join us on [#swaysome:matrix.hya.sk](https://matrix.to/#/%23swaysome:matrix.hya.sk)!
## Usage
## Description
This binary helps you configure sway to work a bit more like [AwesomeWM](https://awesomewm.org/). This means that
**workspaces** are namespaced in what are called **workspace groups**, and **workspace groups** can be moved around the
differents outputs easily.
For example, with workspace `11` on the first output, and workspace `21` on the second output, triggering the `swaysome focus 1`
shortcut to get workspace `1` would lead you to workspace `11` if your focus is on the first output, and workspace `21`
is the focus is on the second one.
By default, `swaysome init` will create a **workspace group** per active output, but you may create other groups while
working, by either triggering `swaysome focus-group <new-number>` and opening a new window, or sending an existing
window to it first with `swaysome move-to-group <new-number>`.
Here is a common use-case for this:
* `output-1`:
* **workspace group** 1:
* workspace `11`: chats
* workspace `12`: emails
* `output-2`:
* **workspace group** 2:
* workspace `21`: IDE for first project
* workspace `22`: browser for first project
* workspace `23`: terminals for first project
* **workspace group** 3:
* workspace `31`: IDE for second project
* workspace `32`: browser for second project
* workspace `33`: terminals for second project
That way, when `output-2` is focused on **workspace group** 2, be it workspace `21` or `22`, the quick `$mod+<number>`
(bound to `swaysome focus <number>`) shortcut won't leave **workspace group** 2, allowing you to open multiple projects
in parallel without the hassle of manually remembering how to namespace them.
In that situation, suppose you plug in a new output, `output-3`, you may then want to focus **workspace group 3**
to send it to `output-3`: this is simply done by typing the shortcuts `$mod+Alt+3` (`swaysome focus-group 3`) then
`$mod+Alt+o` (`swaysome workspace-group-next-output`).
`swaysome` may also work with i3, but this is untested.
`swaysome` should be compatible with [sworkstyle](https://lib.rs/crates/sworkstyle). If this is broken, please report a bug.
## Installation
Arch Linux: Found on the AUR as [swaysome-git](https://aur.archlinux.org/packages/swaysome-git).
Void Linux: `xbps-install swaysome`
If you have Rust installed, then you can just `cargo install swaysome` and you're good to go.
@ -16,71 +62,26 @@ Otherwise, you may grab a [pre-built
binary](https://gitlab.com/hyask/swaysome/-/jobs/artifacts/master/raw/target/release/swaysome?job=build:cargo) from the
CI and put it in your `$PATH`.
**WARNING**: please double-check that your `swaysome` binary is in `sway`'s `$PATH`. Depending on your setup, the
`$PATH` you have in your shell may not be the same as `sway`'s, and if `swaysome` can't be called by `sway`, the
symptoms will only look like non-functional shortcuts.
If you're in this situation, a quick workaround is to call `swaysome` with its full absolute path from `sway`'s config
to check that everything works before fixing your `$PATH` issue.
Then create the file (and the directory if needed) "~/.config/sway/config.d/swaysome.conf" and paste this inside:
```
# Change focus between workspaces
unbindsym $mod+1
unbindsym $mod+2
unbindsym $mod+3
unbindsym $mod+4
unbindsym $mod+5
unbindsym $mod+6
unbindsym $mod+7
unbindsym $mod+8
unbindsym $mod+9
unbindsym $mod+0
bindsym $mod+1 exec "swaysome focus 1"
bindsym $mod+2 exec "swaysome focus 2"
bindsym $mod+3 exec "swaysome focus 3"
bindsym $mod+4 exec "swaysome focus 4"
bindsym $mod+5 exec "swaysome focus 5"
bindsym $mod+6 exec "swaysome focus 6"
bindsym $mod+7 exec "swaysome focus 7"
bindsym $mod+8 exec "swaysome focus 8"
bindsym $mod+9 exec "swaysome focus 9"
bindsym $mod+0 exec "swaysome focus 0"
# Move containers between workspaces
unbindsym $mod+Shift+1
unbindsym $mod+Shift+2
unbindsym $mod+Shift+3
unbindsym $mod+Shift+4
unbindsym $mod+Shift+5
unbindsym $mod+Shift+6
unbindsym $mod+Shift+7
unbindsym $mod+Shift+8
unbindsym $mod+Shift+9
unbindsym $mod+Shift+0
bindsym $mod+Shift+1 exec "swaysome move 1"
bindsym $mod+Shift+2 exec "swaysome move 2"
bindsym $mod+Shift+3 exec "swaysome move 3"
bindsym $mod+Shift+4 exec "swaysome move 4"
bindsym $mod+Shift+5 exec "swaysome move 5"
bindsym $mod+Shift+6 exec "swaysome move 6"
bindsym $mod+Shift+7 exec "swaysome move 7"
bindsym $mod+Shift+8 exec "swaysome move 8"
bindsym $mod+Shift+9 exec "swaysome move 9"
bindsym $mod+Shift+0 exec "swaysome move 0"
## Usage
# Move focused container to next output
bindsym $mod+o exec "swaysome next_output"
Copy the `swaysome.conf` file in `~/.config/sway/config.d/swaysome.conf`.
# Move focused container to previous output
bindsym $mod+Shift+o exec "swaysome prev_output"
# Init workspaces for every screen
exec "swaysome init 1"
```
Finally append your `sway` configuration with this:
Then append your `sway` configuration with this:
```
include ~/.config/sway/config.d/*.conf
```
You should end-up with workspaces from `1` to `0`, prefixed with a screen index,
giving you workspace `01` on the first screen, and workspace `11` on the second
one, both accessible with shortcut `$mod+1`.
On next startup of `sway`, you should end-up with workspaces from `1` to `0`,
prefixed with a screen index, giving you workspace `11` on the first screen, and
workspace `21` on the second one, both accessible with shortcut `$mod+1` when
focused on the right output.
The `init` command simply walks through every screen to initialize a prefixed
workspace. It does it backwards so that you end-up focused on the first screen,
@ -89,10 +90,9 @@ as usual.
## Exhaustive swaysome commands list
* `move [name]`: move the focused container to `[name]`
* `next_output`: move the focused container to the next output
* `prev_output`: move the focused container to the previous output
* `focus [name]`: change focus to `[name]`
* `focus_all_outputs [name]`: change all outputs focus to `[name]`
* `init [name]`: cycle all outputs to create a default workspace with name `[name]`
Just run `swaysome --help` for the most up to date list of command and documentation.
## Breaking changes
* Starting with 2.0, `next_output` and `prev_output` have been changed to `next-output` and `prev-output`.

15
pkgbuild/.SRCINFO Normal file
View file

@ -0,0 +1,15 @@
pkgbase = swaysome-git
pkgdesc = Awesome WM like workspaces
pkgver = 0.0.0
pkgrel = 1
url = https://gitlab.com/hyask/swaysome
arch = x86_64
license = MIT
makedepends = git
makedepends = rust
provides = swaysome
conflicts = swaysome
source = swaysome::git+https://gitlab.com/hyask/swaysome
md5sums = SKIP
pkgname = swaysome-git

30
pkgbuild/PKGBUILD Normal file
View file

@ -0,0 +1,30 @@
# Maintainer: XXX
pkgname=swaysome-git
_pkgname=swaysome
pkgver=0.0.0
pkgrel=1
pkgdesc='Awesome WM like workspaces'
arch=('x86_64')
url='https://gitlab.com/hyask/swaysome'
license=('MIT')
makedepends=('git' 'rust')
provides=("$_pkgname")
conflicts=("$_pkgname")
source=("$_pkgname::git+$url")
md5sums=('SKIP')
pkgver() {
cd "$_pkgname"
echo $(grep '^version =' Cargo.toml|head -n1|cut -d\" -f2).r$(git rev-list --count HEAD).g$(git rev-parse --short HEAD)
}
build() {
cd "$_pkgname"
cargo build --release
}
package() {
cd "$_pkgname"
install -Dm755 "target/release/$_pkgname" "$pkgdir/usr/bin/$_pkgname"
}

629
src/lib.rs Normal file
View file

@ -0,0 +1,629 @@
use serde::{Deserialize, Serialize};
use std::cell::Cell;
use std::env;
use std::io::Cursor;
use std::io::{Read, Write};
use std::mem;
use std::os::unix::net::UnixStream;
use std::path::Path;
use byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
// Maximum workspaces per group. This will determine the naming.
// Examples:
// 10 → 17, 27
// 100 → 107, 207
// 15 → 22, 37
const MAX_GROUP_WS: usize = 10;
const RUN_COMMAND: u32 = 0;
const GET_WORKSPACES: u32 = 1;
// const SUBSCRIBE: u32 = 2;
const GET_OUTPUTS: u32 = 3;
const GET_TREE: u32 = 4;
pub struct SwaySome {
socket: Cell<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::<NativeEndian>(payload_length)
.expect("Unable to write");
msg_prefix[10..]
.as_mut()
.write_u32::<NativeEndian>(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::<NativeEndian>().unwrap();
let mut payload = vec![0; payload_length as usize];
socket.read_exact(&mut payload[..]).unwrap();
let payload_str = String::from_utf8(payload).unwrap();
self.socket.set(Some(socket));
Ok(payload_str)
} else {
eprint!("Not an i3-icp packet, emptying the buffer: ");
let mut v = vec![];
socket.read_to_end(&mut v).unwrap();
eprintln!("{:?}", v);
self.socket.set(Some(socket));
Err("Unable to read i3-ipc packet")
}
}
fn check_success(&self) {
match self.read_msg() {
Ok(msg) => {
let r: Vec<serde_json::Value> = serde_json::from_str(&msg).unwrap();
match r[0]["success"] {
serde_json::Value::Bool(true) => eprintln!("Command successful"),
_ => panic!("Command failed: {:#?}", r),
}
}
Err(_) => panic!("Unable to read response"),
};
}
/*
* Only used in tests
*/
pub fn get_tree(&self) -> serde_json::Value {
self.send_msg(GET_TREE, "");
match self.read_msg() {
Ok(msg) => serde_json::from_str(&msg).expect("Failed to parse JSON for get_tree"),
Err(_) => panic!("Unable to get current workspace"),
}
}
fn get_outputs(&self) -> Vec<Output> {
self.send_msg(GET_OUTPUTS, "");
let o = match self.read_msg() {
Ok(msg) => msg,
Err(_) => panic!("Unable to get outputs"),
};
let mut outputs: Vec<Output> = serde_json::from_str::<Vec<Output>>(&o)
.unwrap()
.into_iter()
.filter(|x| x.active)
.collect();
outputs.sort_by(|x, y| x.name.cmp(&y.name)); // sort_by_key doesn't work here (https://stackoverflow.com/a/47126516)
outputs
}
fn get_workspaces(&self) -> Vec<Workspace> {
self.send_msg(GET_WORKSPACES, "");
let ws = match self.read_msg() {
Ok(msg) => msg,
Err(_) => panic!("Unable to get current workspace"),
};
let mut workspaces: Vec<Workspace> = serde_json::from_str(&ws).unwrap();
workspaces.sort_by_key(|x| x.num);
workspaces
}
fn get_current_output_index(&self) -> usize {
// Do not use `self.outputs`, as the information here could be outdated, especially the `focused` attribute
let outputs = self.get_outputs();
match outputs.iter().position(|x| x.focused) {
Some(i) => i,
None => panic!("WTF! No focused output???"),
}
}
fn get_current_output_name(&self) -> String {
// Do not use `self.outputs`, as the information here could be outdated, especially the `focused` attribute
let outputs = self.get_outputs();
let focused_output_index = match outputs.iter().find(|x| x.focused) {
Some(i) => i.name.as_str(),
None => panic!("WTF! No focused output???"),
};
focused_output_index.to_string()
}
fn get_current_workspace_index(&self) -> usize {
// Do not use `self.outputs`, as the information here could be outdated, especially the `focused` attribute
let outputs = self.get_outputs();
// Do not use `self.workspaces`, as the information here could be outdated, especially the `visible` attribute
self.get_workspaces()
.iter()
.find(|w| w.visible && outputs.iter().find(|o| o.name == w.output).unwrap().focused)
.unwrap()
.num
}
pub fn move_container_to_workspace(&self, workspace_index: usize) {
if workspace_index < MAX_GROUP_WS {
self.move_container_to_workspace_relative(workspace_index);
} else {
self.move_container_to_workspace_absolute(workspace_index);
}
}
pub fn move_container_to_workspace_group(&self, target_group: usize) {
let current_workspace_index = self.get_current_workspace_index();
let current_workspace_index_relative = (current_workspace_index % MAX_GROUP_WS) as usize;
self.move_container_to_workspace_absolute(
current_workspace_index_relative + target_group * MAX_GROUP_WS,
);
}
fn move_container_to_workspace_absolute(&self, workspace_index: usize) {
let group_index = (workspace_index / MAX_GROUP_WS) as usize;
let full_ws_name = format!(
"{}",
group_index * MAX_GROUP_WS + workspace_index % MAX_GROUP_WS
);
// If the workspace already exists
match self.workspaces.iter().find(|w| w.num == workspace_index) {
Some(_) => {
let mut focus_cmd: String = "move container to workspace number ".to_string();
focus_cmd.push_str(&full_ws_name);
self.send_command(&focus_cmd);
}
None => {
let target_group = workspace_index / MAX_GROUP_WS;
let target_screen_index = match self
.workspaces
.iter()
.find(|w| w.num / MAX_GROUP_WS == target_group)
{
// If other workspaces on the same group exists
Some(other_workspace) => Some(
self.outputs
.iter()
.enumerate()
.find(|i| i.1.name == other_workspace.output)
.unwrap()
.0,
),
None => {
// Or if the targeted output is currently connected
if group_index < self.outputs.len() {
Some(group_index)
} else {
None
}
}
};
match target_screen_index {
Some(target_screen_index) => {
let target_output = &self.outputs[target_screen_index];
let current_output_name = self.get_current_output_name();
if target_output.name == current_output_name {
let mut focus_cmd: String = "move container to workspace ".to_string();
focus_cmd.push_str(&full_ws_name);
self.send_command(&focus_cmd);
} else {
// If we have to send it to another screen
let mut focus_cmd: String = "focus output ".to_string();
focus_cmd.push_str(&target_output.name);
self.send_command(&focus_cmd);
let focused_workspace_index = self.get_current_workspace_index();
let mut focus_cmd: String = "workspace ".to_string();
focus_cmd.push_str(&full_ws_name);
self.send_command(&focus_cmd);
let mut focus_cmd: String = "focus output ".to_string();
focus_cmd.push_str(&current_output_name);
self.send_command(&focus_cmd);
let mut focus_cmd: String = "move container to workspace ".to_string();
focus_cmd.push_str(&full_ws_name);
self.send_command(&focus_cmd);
let mut focus_cmd: String = "focus output ".to_string();
focus_cmd.push_str(&target_output.name);
self.send_command(&focus_cmd);
let mut focus_cmd: String = "workspace ".to_string();
focus_cmd.push_str(&focused_workspace_index.to_string());
self.send_command(&focus_cmd);
let mut focus_cmd: String = "focus output ".to_string();
focus_cmd.push_str(&current_output_name);
self.send_command(&focus_cmd);
}
}
None => {
// Else, we send the container on the current output
let mut focus_cmd: String = "move container to workspace ".to_string();
focus_cmd.push_str(&full_ws_name);
self.send_command(&focus_cmd);
}
};
}
}
}
fn move_container_to_workspace_relative(&self, workspace_index: usize) {
let current_workspace_index: usize = self.get_current_workspace_index();
let focused_output_index = current_workspace_index / MAX_GROUP_WS;
let mut cmd: String = "move container to workspace number ".to_string();
let full_ws_name = format!("{}", focused_output_index * MAX_GROUP_WS + workspace_index);
cmd.push_str(&full_ws_name);
self.send_command(&cmd);
}
pub fn focus_to_workspace(&self, workspace_index: usize) {
if workspace_index < MAX_GROUP_WS {
self.focus_to_workspace_relative(workspace_index);
} else {
self.focus_to_workspace_absolute(workspace_index);
}
}
fn focus_to_workspace_absolute(&self, workspace_index: usize) {
let target_group = workspace_index / MAX_GROUP_WS;
match self
.workspaces
.iter()
.find(|w| w.num / MAX_GROUP_WS == target_group)
{
// If other workspaces on the same group exists
Some(other_workspace) => {
// find the corresponding output and focus it
let target_output = self
.outputs
.iter()
.enumerate()
.find(|i| i.1.name == other_workspace.output)
.unwrap()
.1;
let mut focus_cmd: String = "focus output ".to_string();
focus_cmd.push_str(&target_output.name);
self.send_command(&focus_cmd);
}
None => {}
};
// Then we focus the workspace
let mut focus_cmd: String = "workspace number ".to_string();
focus_cmd.push_str(&workspace_index.to_string());
self.send_command(&focus_cmd);
}
fn focus_to_workspace_relative(&self, workspace_index: usize) {
let current_workspace_index: usize = self.get_current_workspace_index();
let focused_output_index = current_workspace_index / MAX_GROUP_WS;
let mut cmd: String = "workspace number ".to_string();
let full_ws_name = format!("{}", focused_output_index * MAX_GROUP_WS + workspace_index);
cmd.push_str(&full_ws_name);
self.send_command(&cmd);
}
pub fn focus_to_group(&self, group_index: usize) {
let current_workspace_index: usize = self.get_current_workspace_index();
let target_workspace_relative_index = current_workspace_index % MAX_GROUP_WS;
let target_workspace_index = group_index * MAX_GROUP_WS + target_workspace_relative_index;
let full_ws_name = format!(
"{}",
group_index * MAX_GROUP_WS + target_workspace_relative_index
);
// If the workspace already exists
match self
.workspaces
.iter()
.find(|w| w.num == target_workspace_index)
{
Some(_) => {
let mut focus_cmd: String = "workspace number ".to_string();
focus_cmd.push_str(&full_ws_name);
self.send_command(&focus_cmd);
}
None => {
let target_screen_index = match self
.workspaces
.iter()
.find(|w| w.num / MAX_GROUP_WS == group_index)
{
// If other workspaces on the same group exists
Some(other_workspace) => Some(
self.outputs
.iter()
.enumerate()
.find(|i| i.1.name == other_workspace.output)
.unwrap()
.0
+ 1,
),
None => {
// Or if the targeted output is currently connected
if group_index > 0 && group_index <= self.outputs.len() {
Some(group_index)
} else {
None
}
}
};
match target_screen_index {
// If we have to send it to another screen
Some(target_screen_index) => {
let target_output = &self.outputs[target_screen_index - 1];
let mut focus_cmd: String = "focus output ".to_string();
focus_cmd.push_str(&target_output.name);
self.send_command(&focus_cmd);
}
None => {}
};
// Then we focus the workspace
let mut focus_cmd: String = "workspace number ".to_string();
focus_cmd.push_str(&target_workspace_index.to_string());
self.send_command(&focus_cmd);
}
}
}
pub fn focus_all_outputs_to_workspace(&self, workspace_index: usize) {
let current_output = self.get_current_output_name();
// Iterate on all outputs to focus on the given workspace
for output in self.outputs.iter() {
let mut cmd: String = "focus output ".to_string();
cmd.push_str(output.name.as_str());
self.send_command(&cmd);
self.focus_to_workspace(workspace_index);
}
// Get back to currently focused output
let mut cmd: String = "focus output ".to_string();
cmd.push_str(&current_output);
self.send_command(&cmd);
}
pub fn move_container_to_next_output(&self) {
self.move_container_to_next_or_prev_output(false);
}
pub fn move_container_to_prev_output(&self) {
self.move_container_to_next_or_prev_output(true);
}
fn move_container_to_next_or_prev_output(&self, go_to_prev: bool) {
let focused_output_index = self.get_current_output_index();
let target_output = if go_to_prev {
&self.outputs[(focused_output_index + self.outputs.len() - 1) % self.outputs.len()]
} else {
&self.outputs[(focused_output_index + 1) % self.outputs.len()]
};
let workspaces = self.get_workspaces();
let target_workspace = workspaces
.iter()
.find(|x| x.output == target_output.name && x.visible)
.unwrap();
let group_index = (target_workspace.num / MAX_GROUP_WS) as usize;
let full_ws_name = format!(
"{}",
group_index * MAX_GROUP_WS + target_workspace.num % MAX_GROUP_WS
);
// Move container to target workspace
let mut cmd: String = "move container to workspace number ".to_string();
cmd.push_str(&full_ws_name);
self.send_command(&cmd);
// Focus that workspace to follow the container
let mut cmd: String = "workspace number ".to_string();
cmd.push_str(&full_ws_name);
self.send_command(&cmd);
}
pub fn move_workspace_group_to_next_output(&self) {
self.move_workspace_group_to_next_or_prev_output(false);
}
pub fn move_workspace_group_to_prev_output(&self) {
self.move_workspace_group_to_next_or_prev_output(true);
}
fn move_workspace_group_to_next_or_prev_output(&self, go_to_prev: bool) {
let focused_output_index = self.get_current_output_index();
let target_output = if go_to_prev {
&self.outputs[(focused_output_index + self.outputs.len() - 1) % self.outputs.len()]
} else {
&self.outputs[(focused_output_index + 1) % self.outputs.len()]
};
let current_workspace = self.get_current_workspace_index();
let current_group_index = (current_workspace / MAX_GROUP_WS) as usize;
for workspace in self.get_workspaces() {
let ws_index = workspace.num / MAX_GROUP_WS;
if ws_index == current_group_index {
let cmd: String = format!("workspace number {}", workspace.num);
self.send_command(&cmd);
let cmd: String = format!("move workspace to {}", target_output.name);
self.send_command(&cmd);
}
}
let cmd: String = format!("workspace number {}", current_workspace);
self.send_command(&cmd);
}
pub fn focus_to_next_group(&self) {
self.focus_to_next_or_prev_group(false);
}
pub fn focus_to_prev_group(&self) {
self.focus_to_next_or_prev_group(true);
}
fn focus_to_next_or_prev_group(&self, go_to_prev: bool) {
let current_workspace_index: usize = self.get_current_workspace_index();
let focused_group_index = current_workspace_index / MAX_GROUP_WS;
let highest_group = self.workspaces.last().unwrap().num / MAX_GROUP_WS;
let target_group;
if go_to_prev {
if focused_group_index == 0 {
target_group = highest_group;
} else {
target_group = focused_group_index - 1;
}
} else {
if focused_group_index >= highest_group {
target_group = 0;
} else {
target_group = focused_group_index + 1;
}
};
self.focus_to_group(target_group);
}
pub fn init_workspaces(&self, workspace_index: usize) {
let cmd_prefix: String = "focus output ".to_string();
for output in self.outputs.iter().rev() {
let mut cmd = cmd_prefix.clone();
cmd.push_str(output.name.as_str());
self.send_command(&cmd);
let mut cmd: String = "workspace number ".to_string();
let full_ws_name = format!(
"{}",
(self.get_current_output_index() + 1) * MAX_GROUP_WS + workspace_index
);
cmd.push_str(&full_ws_name);
self.send_command(&cmd);
}
}
pub fn rearrange_workspaces(&self) {
let focus_cmd_prefix: String = "workspace number ".to_string();
let move_cmd_prefix: String = "move workspace to ".to_string();
for workspace in self.workspaces.iter() {
let mut focus_cmd = focus_cmd_prefix.clone();
focus_cmd.push_str(&workspace.num.to_string());
self.send_command(&focus_cmd);
let group_index =
(workspace.num / MAX_GROUP_WS + self.outputs.len() - 1) % self.outputs.len();
// if group_index <= self.outputs.len() - 1 {
let mut move_cmd = move_cmd_prefix.clone();
move_cmd.push_str(&self.outputs[group_index].name);
self.send_command(&move_cmd);
// }
}
}
}

View file

@ -1,316 +1,112 @@
extern crate byteorder;
extern crate clap;
extern crate serde_json;
use clap::{App, Arg, SubCommand};
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 clap::{Args, Parser, Subcommand};
use swaysome::SwaySome;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
const RUN_COMMAND: u32 = 0;
const GET_WORKSPACES: u32 = 1;
// const SUBSCRIBE: u32 = 2;
const GET_OUTPUTS: u32 = 3;
fn get_stream() -> UnixStream {
let socket_path = match env::var("I3SOCK") {
Ok(val) => val,
Err(_e) => {
panic!("couldn't find i3/sway socket");
}
};
let socket = Path::new(&socket_path);
match UnixStream::connect(&socket) {
Err(_) => panic!("couldn't connect to i3/sway socket"),
Ok(stream) => stream,
}
#[derive(Parser, Debug)]
#[clap(author, version, about = "Better multimonitor handling for sway", long_about = None)]
#[clap(propagate_version = true)]
struct Cli {
#[clap(subcommand)]
command: Command,
}
fn send_msg(mut stream: &UnixStream, msg_type: u32, payload: &str) {
let payload_length = payload.len() as u32;
#[derive(Subcommand, Debug)]
enum Command {
#[clap(about = "Initialize the workspace groups for all the outputs")]
Init(Index),
let mut msg_prefix: [u8; 6 * mem::size_of::<u8>() + 2 * mem::size_of::<u32>()] =
*b"i3-ipc00000000";
#[clap(about = "Move the focused container to another workspace on the same workspace group")]
Move(Index),
msg_prefix[6..]
.as_mut()
.write_u32::<LittleEndian>(payload_length)
.expect("Unable to write");
#[clap(
about = "Move the focused container to the same workspace index on another workspace group"
)]
MoveToGroup(Index),
msg_prefix[10..]
.as_mut()
.write_u32::<LittleEndian>(msg_type)
.expect("Unable to write");
#[clap(about = "Focus to another workspace on the same workspace group")]
Focus(Index),
let mut msg: Vec<u8> = msg_prefix[..].to_vec();
msg.extend(payload.as_bytes());
#[clap(about = "Focus to workspace group")]
FocusGroup(Index),
match stream.write_all(&msg[..]) {
Err(_) => panic!("couldn't send message"),
Ok(_) => {}
}
#[clap(about = "Focus to another workspace on all the outputs")]
FocusAllOutputs(Index),
#[clap(about = "Move the focused container to the next output")]
NextOutput,
#[clap(about = "Move the focused container to the previous output")]
PrevOutput,
#[clap(about = "Move the focused workspace group to the next output")]
WorkspaceGroupNextOutput,
#[clap(about = "Move the focused workspace group to the previous output")]
WorkspaceGroupPrevOutput,
#[clap(about = "Move the focused container to the next group")]
NextGroup,
#[clap(about = "Move the focused container to the previous group")]
PrevGroup,
#[clap(
about = "Rearrange already opened workspaces to the correct outputs, useful when plugging new monitors"
)]
RearrangeWorkspaces,
}
fn send_command(stream: &UnixStream, command: &str) {
eprint!("Sending command: '{}' - ", &command);
send_msg(&stream, RUN_COMMAND, &command);
check_success(&stream);
}
fn read_msg(mut stream: &UnixStream) -> Result<String, &str> {
let mut response_header: [u8; 14] = *b"uninitialized.";
stream.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];
stream.read_exact(&mut payload[..]).unwrap();
let payload_str = String::from_utf8(payload).unwrap();
Ok(payload_str)
} else {
eprint!("Not an i3-icp packet, emptying the buffer: ");
let mut v = vec![];
stream.read_to_end(&mut v).unwrap();
eprintln!("{:?}", v);
Err("Unable to read i3-ipc packet")
}
}
fn check_success(stream: &UnixStream) {
match read_msg(&stream) {
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(stream: &UnixStream) -> Vec<serde_json::Value> {
send_msg(&stream, GET_OUTPUTS, "");
let o = match read_msg(&stream) {
Ok(msg) => msg,
Err(_) => panic!("Unable to get outputs"),
};
serde_json::from_str(&o).unwrap()
}
fn get_workspaces(stream: &UnixStream) -> Vec<serde_json::Value> {
send_msg(&stream, GET_WORKSPACES, "");
let ws = match read_msg(&stream) {
Ok(msg) => msg,
Err(_) => panic!("Unable to get current workspace"),
};
serde_json::from_str(&ws).unwrap()
}
fn get_current_output_index(stream: &UnixStream) -> String {
let outputs = get_outputs(&stream);
let focused_output_index = match outputs
.iter()
.position(|x| x["focused"] == serde_json::Value::Bool(true))
{
Some(i) => i,
None => panic!("WTF! No focused output???"),
};
format!("{}", focused_output_index)
}
fn get_current_output_name(stream: &UnixStream) -> String {
let outputs = get_outputs(&stream);
let focused_output_index = match outputs
.iter()
.find(|x| x["focused"] == serde_json::Value::Bool(true))
{
Some(i) => i["name"].as_str().unwrap(),
None => panic!("WTF! No focused output???"),
};
format!("{}", focused_output_index)
}
fn move_container_to_workspace(stream: &UnixStream, workspace_name: &String) {
let mut cmd: String = "move container to workspace number ".to_string();
let full_ws_name = format!("{}{}", get_current_output_index(stream), &workspace_name)
.parse::<i32>()
.unwrap();
cmd.push_str(&full_ws_name.to_string());
send_command(&stream, &cmd);
}
fn focus_to_workspace(stream: &UnixStream, workspace_name: &String) {
let mut cmd: String = "workspace number ".to_string();
let full_ws_name = format!("{}{}", get_current_output_index(stream), &workspace_name)
.parse::<i32>()
.unwrap();
cmd.push_str(&full_ws_name.to_string());
send_command(&stream, &cmd);
}
fn focus_all_outputs_to_workspace(stream: &UnixStream, workspace_name: &String) {
let current_output = get_current_output_name(stream);
// Iterate on all outputs to focus on the given workspace
let outputs = get_outputs(&stream);
for output in outputs.iter() {
let mut cmd: String = "focus output ".to_string();
cmd.push_str(&output["name"].as_str().unwrap());
send_command(&stream, &cmd);
focus_to_workspace(&stream, &workspace_name);
}
// Get back to currently focused output
let mut cmd: String = "focus output ".to_string();
cmd.push_str(&current_output);
send_command(&stream, &cmd);
}
fn move_container_to_next_output(stream: &UnixStream) {
move_container_to_next_or_prev_output(&stream, false);
}
fn move_container_to_prev_output(stream: &UnixStream) {
move_container_to_next_or_prev_output(&stream, true);
}
fn move_container_to_next_or_prev_output(stream: &UnixStream, go_to_prev: bool) {
let outputs = get_outputs(&stream);
let focused_output_index = match outputs
.iter()
.position(|x| x["focused"] == serde_json::Value::Bool(true))
{
Some(i) => i,
None => panic!("WTF! No focused output???"),
};
let target_output;
if go_to_prev {
target_output = &outputs[(focused_output_index - 1 + &outputs.len()) % &outputs.len()];
} else {
target_output = &outputs[(focused_output_index + 1) % &outputs.len()];
}
let workspaces = get_workspaces(&stream);
let target_workspace = workspaces
.iter()
.filter(|x| {
x["output"] == target_output["name"] && x["visible"] == serde_json::Value::Bool(true)
})
.next()
.unwrap();
// Move container to target workspace
let mut cmd: String = "move container to workspace number ".to_string();
cmd.push_str(&target_workspace["num"].to_string());
send_command(&stream, &cmd);
// Focus that workspace to follow the container
let mut cmd: String = "workspace number ".to_string();
cmd.push_str(&target_workspace["num"].to_string());
send_command(&stream, &cmd);
}
fn init_workspaces(stream: &UnixStream, workspace_name: &String) {
let outputs = get_outputs(&stream);
let cmd_prefix: String = "focus output ".to_string();
for output in outputs.iter().filter(|x| x["active"] == true).rev() {
let mut cmd = cmd_prefix.clone();
cmd.push_str(&output["name"].as_str().unwrap());
send_command(&stream, &cmd);
focus_to_workspace(&stream, &workspace_name);
}
#[derive(Args, Debug)]
struct Index {
#[clap(value_name = "index", help = "The workspace index to work with")]
index: usize,
}
fn main() {
let matches = App::new("swaysome")
.version("1.1.2")
.author("Skia <skia@hya.sk>")
.about("Better multimonitor handling for sway")
.subcommand(
SubCommand::with_name("init")
.about("Initialize the workspaces for all the outputs")
.arg(
Arg::with_name("index")
.help("The index to initialize with")
.required(true)
.takes_value(true),
),
)
.subcommand(
SubCommand::with_name("focus")
.about("Focus to another workspace on the same output")
.arg(
Arg::with_name("index")
.help("The index to focus on")
.required(true)
.takes_value(true),
),
)
.subcommand(
SubCommand::with_name("focus_all_outputs")
.about("Focus to another workspace on all the outputs")
.arg(
Arg::with_name("index")
.help("The index to focus on")
.required(true)
.takes_value(true),
),
)
.subcommand(
SubCommand::with_name("move")
.about("Move the focused container to another workspace on the same output")
.arg(
Arg::with_name("index")
.help("The index to move the container to")
.required(true)
.takes_value(true),
),
)
.subcommand(
SubCommand::with_name("next_output")
.about("Move the focused container to the next output"),
)
.subcommand(
SubCommand::with_name("prev_output")
.about("Move the focused container to the previous output"),
)
.get_matches();
let cli = Cli::parse();
let stream = get_stream();
let swaysome = SwaySome::new();
if let Some(matches) = matches.subcommand_matches("init") {
init_workspaces(&stream, &matches.value_of("index").unwrap().to_string());
} else if let Some(matches) = matches.subcommand_matches("move") {
move_container_to_workspace(&stream, &matches.value_of("index").unwrap().to_string());
} else if let Some(matches) = matches.subcommand_matches("focus") {
focus_to_workspace(&stream, &matches.value_of("index").unwrap().to_string());
} else if let Some(matches) = matches.subcommand_matches("focus_all_outputs") {
focus_all_outputs_to_workspace(&stream, &matches.value_of("index").unwrap().to_string());
} else if let Some(_) = matches.subcommand_matches("next_output") {
move_container_to_next_output(&stream);
} else if let Some(_) = matches.subcommand_matches("prev_output") {
move_container_to_prev_output(&stream);
match &cli.command {
Command::Init(action) => {
swaysome.init_workspaces(action.index);
}
Command::Move(action) => {
swaysome.move_container_to_workspace(action.index);
}
Command::MoveToGroup(action) => {
swaysome.move_container_to_workspace_group(action.index);
}
Command::Focus(action) => {
swaysome.focus_to_workspace(action.index);
}
Command::FocusGroup(action) => {
swaysome.focus_to_group(action.index);
}
Command::FocusAllOutputs(action) => {
swaysome.focus_all_outputs_to_workspace(action.index);
}
Command::NextOutput => {
swaysome.move_container_to_next_output();
}
Command::PrevOutput => {
swaysome.move_container_to_prev_output();
}
Command::WorkspaceGroupNextOutput => {
swaysome.move_workspace_group_to_next_output();
}
Command::WorkspaceGroupPrevOutput => {
swaysome.move_workspace_group_to_prev_output();
}
Command::NextGroup => {
swaysome.focus_to_next_group();
}
Command::PrevGroup => {
swaysome.focus_to_prev_group();
}
Command::RearrangeWorkspaces => {
swaysome.rearrange_workspaces();
}
}
}

155
swaysome.1 Normal file
View file

@ -0,0 +1,155 @@
.TH SWAYSOME "1" "Sep 2025" "2.3.2" "User Commands"
.
.
.SH NAME
swaysome - improve multi-output handling on sway
.
.
.SH SYNOPSIS
.B swaysome
\fI\,[OPTIONS]\/\fR
\fI\,<COMMAND>\/\fR
.
.
.SH DESCRIPTION
\fBswaysome\fR helps you configure \fBsway\fR to work a bit more
like \fBAwesomeWM\fR (\fIhttps://awesomewm\.org/\fR). This means that
"\fBworkspaces\fR" are namespaced in what are called "\fBworkspace groups\fR",
and "\fBworkspace groups\fR" can be moved around the differents outputs easily.
.P
For example, with workspace 11 on the first output, and workspace 21 on the
second output, triggering the `swaysome focus 1` shortcut to get workspace
1 would lead you to workspace 11 if your focus is on the first output, and
workspace 21 is the focus is on the second one.
.P
By default, `swaysome init` will create a \fBworkspace group\fR per active
output, but you may create other groups while working, by either triggering
`swaysome focus-group <new-number>` and opening a new window, or sending an
existing window to it first with `swaysome move-to-group <new-number>`.
.P
Here is a common use-case for this:
.EX
\fBoutput-1\fR:
\fBworkspace group 1\fR:
workspace 11: chats
workspace 12: emails
\fBoutput-2\fR:
\fBworkspace group 2\fR:
workspace 21: IDE for first project
workspace 22: browser for first project
workspace 23: terminals for first project
\fBworkspace group 3\fR:
workspace 31: IDE for second project
workspace 32: browser for second project
workspace 33: terminals for second project
.EE
.P
That way, when \fBoutput-2\fR is focused on \fBworkspace group 2\fR, be
it workspace 21 or 22, the quick `$mod+<number>` (bound to `swaysome focus
<number>`) shortcut won't leave \fBworkspace group 2\fR, allowing you to open
multiple projects in parallel without the hassle of manually remembering how to
namespace them.
.P
In that situation, suppose you plug in a new output, `output-3`, you may then
want to focus \fBworkspace group 3\fR to send it to `output-3`: this is simply
done by typing the shortcuts `$mod+Alt+3` (`swaysome focus-group 3`) then
`$mod+Alt+o` (`swaysome workspace-group-next-output`)\.
.P
\fBswaysome\fR may also work with \fBi3\fR, but this is untested. Patches welcome if needed.
.P
\fBswaysome\fR should be compatible with \fBsworkstyle\fR (\fIhttps://lib\.rs/crates/sworkstyle\fR).
If this is broken, please report a bug.
.
.
.SH USAGE
.P
If you installed \fBswaysome\fR from your distribution's package manager, it
should have provided your system with the default configuration file. In that
case, you can just include from your \fBsway\fR configuration by appending the
following to it:
include /etc/sway/config.d/swaysome.conf
Otherwise, if installing by any other way, copy the \fBswaysome.conf\fR file
in \fB~/.config/sway/config.d/swaysome.conf\fR, then append your \fBsway\fR
configuration with this:
include ~/.config/sway/config.d/sway.conf
On next startup of `sway`, you should end-up with workspaces from `1` to `0`,
prefixed with a screen index, giving you workspace `11` on the first screen, and
workspace `21` on the second one, both accessible with shortcut `$mod+1` when
focused on the right output.
.
.
.SH COMMANDS
.TP
\fBinit\fR \fIINDEX\fR
Initialize the workspace groups for all the outputs on workspace \fIINDEX\fR.
This command simply walks through every screen to initialize a prefixed
workspace. It does it backwards so that you end-up focused on the first screen,
as usual.
.TP
\fBmove\fR \fIINDEX\fR
Move the focused container to workspace \fIINDEX\fR (stay in the same workspace group)
.TP
\fBmove-to-group\fR \fIINDEX\fR
Move the focused container to workspace group \fIINDEX\fR (keep the same workspace index)
.TP
\fBfocus\fR \fIINDEX\fR
Focus to workspace \fIINDEX\fR (stay in the same workspace group)
.TP
\fBfocus-group\fR \fIINDEX\fR
Focus to workspace group \fIINDEX\fR (keep the same workspace index)
.TP
\fBfocus-all-outputs\fR \fIINDEX\fR
Focus to workspace \fIINDEX\fR on all the outputs at once (no keyboard shortcut bound by default for this function)
.TP
\fBnext-output\fR
Move the focused container to the next output
.TP
\fBprev-output\fR
Move the focused container to the previous output
.TP
\fBworkspace-group-next-output\fR
Move the focused workspace group to the next output
.TP
\fBworkspace-group-prev-output\fR
Move the focused workspace group to the previous output
.TP
\fBnext-group\fR
Move the focused container to the next group
.TP
\fBprev-group\fR
Move the focused container to the previous group
.TP
\fBrearrange-workspaces\fR
Rearrange already opened workspaces to the correct outputs, useful when plugging new monitors
.TP
\fBhelp\fR
Print this message or the help of the given subcommand(s)
.
.
.SH OPTIONS
.TP
\fB-h\fR, \fB--help\fR
Print help
.TP
\fB-V\fR, \fB--version\fR
Print version
.
.
.SH "VERSION"
2.3.2
.
.
.SH "HOMEPAGE"
\fIhttps://gitlab.com/hyask/swaysome\fP
.sp
Please report any bug or feature requests on the bug tracker.
.
.
.SH AUTHORS
Florent 'Skia' Jacquet <\fIskia@hya.sk\fP>

66
swaysome.conf Normal file
View file

@ -0,0 +1,66 @@
# Use (un)bindcode or (un)bindsym, depending on what you used in your main sway config file.
# The `--no-warn` setting is only added to shortcuts that exist in the default config. You may want to add or remove
# that flag on some bindings depending on your config.
# Change focus between workspaces
bindsym --no-warn $mod+1 exec "swaysome focus 1"
bindsym --no-warn $mod+2 exec "swaysome focus 2"
bindsym --no-warn $mod+3 exec "swaysome focus 3"
bindsym --no-warn $mod+4 exec "swaysome focus 4"
bindsym --no-warn $mod+5 exec "swaysome focus 5"
bindsym --no-warn $mod+6 exec "swaysome focus 6"
bindsym --no-warn $mod+7 exec "swaysome focus 7"
bindsym --no-warn $mod+8 exec "swaysome focus 8"
bindsym --no-warn $mod+9 exec "swaysome focus 9"
bindsym --no-warn $mod+0 exec "swaysome focus 0"
# Move containers between workspaces
bindsym --no-warn $mod+Shift+1 exec "swaysome move 1"
bindsym --no-warn $mod+Shift+2 exec "swaysome move 2"
bindsym --no-warn $mod+Shift+3 exec "swaysome move 3"
bindsym --no-warn $mod+Shift+4 exec "swaysome move 4"
bindsym --no-warn $mod+Shift+5 exec "swaysome move 5"
bindsym --no-warn $mod+Shift+6 exec "swaysome move 6"
bindsym --no-warn $mod+Shift+7 exec "swaysome move 7"
bindsym --no-warn $mod+Shift+8 exec "swaysome move 8"
bindsym --no-warn $mod+Shift+9 exec "swaysome move 9"
bindsym --no-warn $mod+Shift+0 exec "swaysome move 0"
# Focus workspace groups
bindsym $mod+Alt+1 exec "swaysome focus-group 1"
bindsym $mod+Alt+2 exec "swaysome focus-group 2"
bindsym $mod+Alt+3 exec "swaysome focus-group 3"
bindsym $mod+Alt+4 exec "swaysome focus-group 4"
bindsym $mod+Alt+5 exec "swaysome focus-group 5"
bindsym $mod+Alt+6 exec "swaysome focus-group 6"
bindsym $mod+Alt+7 exec "swaysome focus-group 7"
bindsym $mod+Alt+8 exec "swaysome focus-group 8"
bindsym $mod+Alt+9 exec "swaysome focus-group 9"
bindsym $mod+Alt+0 exec "swaysome focus-group 0"
# Move containers to other workspace groups
bindsym $mod+Alt+Shift+1 exec "swaysome move-to-group 1"
bindsym $mod+Alt+Shift+2 exec "swaysome move-to-group 2"
bindsym $mod+Alt+Shift+3 exec "swaysome move-to-group 3"
bindsym $mod+Alt+Shift+4 exec "swaysome move-to-group 4"
bindsym $mod+Alt+Shift+5 exec "swaysome move-to-group 5"
bindsym $mod+Alt+Shift+6 exec "swaysome move-to-group 6"
bindsym $mod+Alt+Shift+7 exec "swaysome move-to-group 7"
bindsym $mod+Alt+Shift+8 exec "swaysome move-to-group 8"
bindsym $mod+Alt+Shift+9 exec "swaysome move-to-group 9"
bindsym $mod+Alt+Shift+0 exec "swaysome move-to-group 0"
# Move focused container to next output
bindsym $mod+o exec "swaysome next-output"
# Move focused container to previous output
bindsym $mod+Shift+o exec "swaysome prev-output"
# Move focused workspace group to next output
bindsym $mod+Alt+o exec "swaysome workspace-group-next-output"
# Move focused workspace group to previous output
bindsym $mod+Alt+Shift+o exec "swaysome workspace-group-prev-output"
# Init workspaces for every screen
exec "swaysome init 1"

988
tests/integration.rs Normal file
View file

@ -0,0 +1,988 @@
// extern crate assert_json_diff;
use assert_json_diff::{assert_json_eq, assert_json_include};
use serde_json::json;
use swaysome::SwaySome;
mod utils;
use utils::*;
#[test]
fn test_init_three_outputs() {
let sway = Sway::start();
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "1", "num": 1, "focused": true, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "2", "num": 2, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "3", "num": 3, "focused": false, "nodes": []},
]},
]}));
eprintln!("Init 1");
swaysome.init_workspaces(1);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": true, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
}
#[test]
fn test_three_outputs_moving_around_same_workspace_group() {
let sway = Sway::start();
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
swaysome.init_workspaces(1);
sway.spawn_some_apps();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": false},
{"name": "TERM3", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
eprintln!("Move TERM3 to 2");
swaysome.move_container_to_workspace(2);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
{"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
eprintln!("Focus to 2");
swaysome.focus_to_workspace(2);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": false},
]},
{"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [
{"name": "TERM3", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
}
#[test]
fn test_three_outputs_moving_around_across_workspace_groups() {
let sway = Sway::start();
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
swaysome.init_workspaces(1);
sway.spawn_some_apps();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": false},
{"name": "TERM3", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
eprintln!("Move TERM3 to group 2");
swaysome.move_container_to_workspace_group(2);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
eprintln!("Focus to group 2");
swaysome.focus_to_group(2);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM3", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
// HEADLESS-3 is still empty
assert_json_eq!(
swaysome.get_tree()["nodes"][3]["nodes"][0]["nodes"],
json!([])
);
eprintln!("Focus to group 1");
swaysome.focus_to_group(1);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
// HEADLESS-3 is still empty
assert_json_eq!(
swaysome.get_tree()["nodes"][3]["nodes"][0]["nodes"],
json!([])
);
eprintln!("Move TERM2 to group 2");
swaysome.move_container_to_workspace_group(3);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
]},
]},
]}));
eprintln!("Focus all to 4");
swaysome.focus_all_outputs_to_workspace(4);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
]},
{"type": "workspace", "name": "14", "num": 14, "focused": true, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
{"type": "workspace", "name": "24", "num": 24, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
]},
{"type": "workspace", "name": "34", "num": 34, "focused": false, "nodes": []},
]},
]}));
eprintln!("Focus all back to 1");
swaysome.focus_all_outputs_to_workspace(1);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
]},
]},
]}));
// shadow and re-init swaysome to get up-to-date internals (outputs, workspaces)
// XXX this is more of a hack than anything else. Ideally, swaysome would never have out-of-date internals
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
eprintln!("Focus to prev group");
swaysome.focus_to_prev_group();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "1", "num": 1, "focused": true, "nodes": []},
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
]},
]},
]}));
eprintln!("Focus to prev group again");
swaysome.focus_to_prev_group();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "1", "num": 1, "focused": false, "nodes": []},
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM2", "focused": true},
]},
]},
]}));
eprintln!("Focus to next group");
swaysome.focus_to_next_group();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "1", "num": 1, "focused": true, "nodes": []},
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
]},
]},
]}));
eprintln!("Focus to next group again");
swaysome.focus_to_next_group();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
]},
]},
]}));
}
#[test]
fn test_three_outputs_moving_around_across_outputs() {
let sway = Sway::start();
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
swaysome.init_workspaces(1);
sway.spawn_some_apps();
eprintln!("Move TERM3 to group 3");
swaysome.move_container_to_workspace_group(3);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
]}));
eprintln!("Move TERM2 to next output");
swaysome.move_container_to_next_output();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM2", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
]}));
eprintln!("Move TERM2 to prev output");
swaysome.move_container_to_prev_output();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
]}));
eprintln!("Move TERM2 to prev output again");
swaysome.move_container_to_prev_output();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
{"name": "TERM2", "focused": true},
]},
]},
]}));
}
#[test]
fn test_three_outputs_moving_around_across_outputs_without_init() {
let sway = Sway::start();
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
sway.spawn_some_apps();
eprintln!("Move TERM3 to group 3");
swaysome.move_container_to_workspace_group(3);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "1", "num": 1, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "2", "num": 2, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "3", "num": 3, "focused": false, "nodes": []},
]},
]}));
}
#[test]
fn test_three_outputs_moving_around_absolute() {
let sway = Sway::start();
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
swaysome.init_workspaces(1);
sway.spawn_some_apps();
// shadow and re-init swaysome to get up-to-date internals (outputs, workspaces)
// XXX this is more of a hack than anything else. Ideally, swaysome would never have out-of-date internals
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
eprintln!("Moving TERM3 to 12");
swaysome.move_container_to_workspace(12);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
{"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
eprintln!("Moving TERM2 to 42");
swaysome.move_container_to_workspace(42);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": true},
]},
{"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
{"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
// shadow and re-init swaysome to get up-to-date internals (outputs, workspaces)
// XXX this is more of a hack than anything else. Ideally, swaysome would never have out-of-date internals
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
eprintln!("Moving TERM1 to 42");
swaysome.move_container_to_workspace(42);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": true, "nodes": []},
{"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
{"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
{"name": "TERM1", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
eprintln!("Spawn TERM4");
sway.send_command(["exec", "foot -T TERM4"].as_slice());
std::thread::sleep(std::time::Duration::from_millis(200));
eprintln!("Moving TERM4 to 22");
swaysome.move_container_to_workspace(22);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": true, "nodes": []},
{"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
{"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
{"name": "TERM1", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
{"type": "workspace", "name": "22", "num": 22, "focused": false, "nodes": [
{"name": "TERM4", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
eprintln!("Focus on 42");
swaysome.focus_to_workspace(42);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
{"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
{"name": "TERM1", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
{"type": "workspace", "name": "22", "num": 22, "focused": false, "nodes": [
{"name": "TERM4", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
eprintln!("Focus on 41");
swaysome.focus_to_workspace(41);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
{"type": "workspace", "name": "41", "num": 41, "focused": true, "nodes": []},
{"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
{"name": "TERM1", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
{"type": "workspace", "name": "22", "num": 22, "focused": false, "nodes": [
{"name": "TERM4", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
eprintln!("Focus on 51");
swaysome.focus_to_workspace(51);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
{"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
{"name": "TERM1", "focused": false},
]},
{"type": "workspace", "name": "51", "num": 51, "focused": true, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
{"type": "workspace", "name": "22", "num": 22, "focused": false, "nodes": [
{"name": "TERM4", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": []},
]},
]}));
eprintln!("Focus on 32");
swaysome.focus_to_workspace(32);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "12", "num": 12, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
{"type": "workspace", "name": "42", "num": 42, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
{"name": "TERM1", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
{"type": "workspace", "name": "22", "num": 22, "focused": false, "nodes": [
{"name": "TERM4", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "32", "num": 32, "focused": true, "nodes": []},
]},
]}));
}
#[test]
fn test_three_outputs_moving_groups_across_outputs() {
let sway = Sway::start();
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
swaysome.init_workspaces(1);
sway.spawn_some_apps();
eprintln!("Move TERM3 to group 3");
swaysome.move_container_to_workspace_group(3);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
]}));
eprintln!("Move group 1 to prev output");
swaysome.move_workspace_group_to_prev_output();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "1", "num": 1, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
]}));
eprintln!("Move group 1 to next output");
swaysome.move_workspace_group_to_next_output();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
]}));
eprintln!("Move group 1 to next output again");
swaysome.move_workspace_group_to_next_output();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "1", "num": 1, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
]}));
}
#[test]
fn test_three_outputs_plugging_unplugging_outputs() {
let sway = Sway::start();
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
swaysome.init_workspaces(1);
sway.spawn_some_apps();
eprintln!("Move TERM3 to group 3");
swaysome.move_container_to_workspace_group(3);
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
]}));
eprintln!("Disabling output 3");
sway.send_command(["output HEADLESS-3 disable"].as_slice());
std::thread::sleep(std::time::Duration::from_millis(200));
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
]}));
assert_eq!(swaysome.get_tree()["nodes"].as_array().unwrap().len(), 3);
eprintln!("Enabling output 3");
sway.send_command(["output HEADLESS-3 enable"].as_slice());
std::thread::sleep(std::time::Duration::from_millis(200));
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": true},
]},
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
]}));
assert_eq!(swaysome.get_tree()["nodes"].as_array().unwrap().len(), 4);
// shadow and re-init swaysome to get up-to-date internals (outputs, workspaces)
// XXX this is more of a hack than anything else. Ideally, swaysome would never have out-of-date internals
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
swaysome.rearrange_workspaces();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
{"name": "TERM2", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": []},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": true},
]},
]},
]}));
swaysome.focus_to_workspace(11);
swaysome.move_container_to_workspace(21);
eprintln!("Disabling output 2");
sway.send_command(["output HEADLESS-2 disable"].as_slice());
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": true},
]},
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
]}));
eprintln!("Enabling output 2");
sway.send_command(["output HEADLESS-2 enable"].as_slice());
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": true},
]},
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": false},
]},
]},
]}));
// shadow and re-init swaysome to get up-to-date internals (outputs, workspaces)
// XXX this is more of a hack than anything else. Ideally, swaysome would never have out-of-date internals
let swaysome = SwaySome::new_from_socket(&sway.sock).expect("SwaySome couldn't initialize");
swaysome.rearrange_workspaces();
assert_json_include!(actual: swaysome.get_tree(), expected: json!({
"nodes": [
{},
{"type": "output", "name": "HEADLESS-1", "current_mode": {"height": 270, "width": 480}, "nodes": [
{"type": "workspace", "name": "11", "num": 11, "focused": false, "nodes": [
{"name": "TERM1", "focused": false},
]},
]},
{"type": "output", "name": "HEADLESS-3", "current_mode": {"height": 1440, "width": 2560}, "nodes": [
{"type": "workspace", "name": "31", "num": 31, "focused": false, "nodes": [
{"name": "TERM3", "focused": true},
]},
]},
{"type": "output", "name": "HEADLESS-2", "current_mode": {"height": 1080, "width": 1920}, "nodes": [
{"type": "workspace", "name": "21", "num": 21, "focused": false, "nodes": [
{"name": "TERM2", "focused": false},
]},
]},
]}));
}

50
tests/integration_bin.rs Normal file
View file

@ -0,0 +1,50 @@
use std::process::Command;
mod utils;
use utils::Sway;
static SWAYSOME_BIN: &str = env!("CARGO_BIN_EXE_swaysome");
// Poor safeguard that the tests are actually testing the right version, in
// case there is some confusion with a swaysome installed from the system. That
// obviously does not always worked, but has saved me a couple of times already 🙃
#[test]
fn test_binary_version() {
let output = Command::new(SWAYSOME_BIN)
.args(["--version"])
.env_clear()
.env("SWAYSOCK", "/dev/null")
.output()
.expect("Couldn't run swaysome");
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
format!("swaysome {}\n", env!("CARGO_PKG_VERSION"))
);
}
// This is useful when working on swapping argument parsing libraries.
#[test]
fn test_binary_help() {
let output = Command::new(SWAYSOME_BIN)
.args(["-h"])
.env_clear()
.env("SWAYSOCK", "/dev/null")
.output()
.expect("Couldn't run swaysome");
assert_eq!(String::from_utf8(output.stdout).unwrap(), "Better multimonitor handling for sway\n\nUsage: swaysome <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(SWAYSOME_BIN)
.args(["init", "1"])
.env_clear()
.env("SWAYSOCK", sway.sock.clone())
.output()
.expect("Couldn't run swaysome");
assert_eq!(String::from_utf8(output.stderr).unwrap(), "successful connection to socket '/tmp/swaysome_tests/test_binary_interaction_with_sway/swaysock'\nSending command: 'focus output HEADLESS-3' - Command successful\nSending command: 'workspace number 31' - Command successful\nSending command: 'focus output HEADLESS-2' - Command successful\nSending command: 'workspace number 21' - Command successful\nSending command: 'focus output HEADLESS-1' - Command successful\nSending command: 'workspace number 11' - Command successful\n");
}

11
tests/sway.conf Normal file
View file

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

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

@ -0,0 +1,132 @@
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")
}
// work around https://github.com/rust-lang/rust/issues/46379
// TODO: maybe implement that: https://momori.dev/posts/organize-rust-integration-tests-without-dead-code-warning/
#[allow(dead_code)]
pub fn spawn_some_apps(&self) {
self.send_command(["exec", "foot -T TERM1"].as_slice());
// Make sure the app are created in the right order.
// 200ms would still sometimes be racy on my Ryzen 5 PRO 4650U, so let's
// take a safe bet and give plenty of time for shared CI runners.
std::thread::sleep(std::time::Duration::from_millis(500));
self.send_command(["exec", "foot -T TERM2"].as_slice());
std::thread::sleep(std::time::Duration::from_millis(500));
self.send_command(["exec", "foot -T TERM3"].as_slice());
std::thread::sleep(std::time::Duration::from_millis(500));
}
fn check_connection(&self, flag: &str) -> Result<(), ()> {
let mut retries = 100; // wait for max 10s
while retries > 0 {
let version = self.send_command(["-t", "get_version"].as_slice());
if String::from_utf8(version.stdout).unwrap().contains(flag)
|| String::from_utf8(version.stderr).unwrap().contains(flag)
{
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(100));
retries -= 1;
}
return Err(());
}
fn stop(&mut self) {
// in case some apps were spawned, kill them, and give them some time to be killed
self.send_command(["[all] kill"].as_slice());
std::thread::sleep(std::time::Duration::from_millis(500));
// now terminate sway
signal(self.process.id(), "TERM");
match self.check_connection("Unable to connect to") {
Ok(()) => eprintln!("Sway terminated correctly on its own"),
Err(_) => {
self.process.kill().expect("Failed to kill sway");
eprintln!("Sway had to be killed");
}
}
}
}
impl Drop for Sway {
fn drop(&mut self) {
self.stop();
}
}