mirror of
https://gitlab.com/gabmus/envision.git
synced 2025-08-03 14:49:04 +00:00
Compare commits
108 commits
Author | SHA1 | Date | |
---|---|---|---|
|
0a46a7d332 | ||
|
ec4d3d2f57 | ||
|
1ad8a29df1 | ||
|
8311adc3dd | ||
|
bdb19b5738 | ||
|
b0f0f4647c | ||
|
830344d665 | ||
|
1a1d1682fe | ||
|
8f3f9b8759 | ||
|
bd0cc9e2b1 | ||
|
1cad5c4d1b | ||
|
754395586e | ||
|
5139ed7ba3 | ||
|
eed85abb2a |
||
|
d42de840a2 |
||
|
b174fab6bf | ||
|
93ea2501b4 |
||
|
fc4a2d3993 | ||
|
27d37198c7 | ||
|
4709a50483 | ||
|
d0df943e48 | ||
|
99af59056d | ||
|
e0eae7c13a | ||
|
743dbfa3a1 | ||
|
8ffac63e7e | ||
|
c794037377 | ||
|
9d85f1c24f | ||
|
3e23073f4c | ||
|
71a8223ce8 | ||
|
e4d3980b14 | ||
|
9ea754bb2e | ||
|
139f72e294 |
||
|
7a02fcc5d1 |
||
|
2f5ec57a0a |
||
|
8742a27b7c |
||
|
25c90d175f |
||
|
2fc33b10b0 |
||
|
f38199601e |
||
|
db45103d1b | ||
|
e117986715 | ||
|
1ac253ecbf | ||
|
92d17512a4 | ||
|
40503d0895 | ||
|
96717d193f |
||
|
9c6bfe110a |
||
|
39ace1d8db | ||
|
33db18bd62 |
||
|
1ed031a2bf |
||
|
338e711455 |
||
|
3680e305a9 |
||
|
9bdda7d63d |
||
|
67e2ade501 |
||
|
35d268e01b |
||
|
2bec37ee24 | ||
|
160d733054 |
||
|
eda2105566 |
||
|
18e5670d90 |
||
|
879637115c | ||
|
b24c8e4c0b | ||
|
5187a00971 | ||
|
1a71c82d1a |
||
|
96e1a20eda | ||
|
869927bb5c | ||
|
e62d0ced36 | ||
|
1c3b4decb5 | ||
|
8ffb44aa11 | ||
|
a651b87cc3 | ||
|
cfb874fa35 | ||
|
69eba0153b | ||
|
aa9bd09372 | ||
|
6fa7d1e2a3 | ||
|
e5a59ebf62 | ||
|
eef963793d | ||
|
4767a4eb13 | ||
|
0adf894b45 | ||
|
31b22b59f3 | ||
|
db5c295435 | ||
|
d38acf0a7e | ||
|
e5435d0aa3 | ||
|
696c541598 | ||
|
e69a7a9bd6 | ||
|
ca813d6168 | ||
|
0020dcf3d4 | ||
|
36322b3b2c | ||
|
bc5c4a4a40 | ||
|
e781736ffa | ||
|
f04723c1c4 | ||
|
67172df567 | ||
|
b61f2d963f | ||
|
9711c257a6 | ||
|
380f800fa8 | ||
|
46df6d36e5 | ||
|
68d7757aa4 | ||
|
ce5f486596 | ||
|
4f80aed3c2 | ||
|
7f05d696c4 | ||
|
9a4ef01ed9 | ||
|
92cd8f6a94 | ||
|
4905c8fed1 | ||
|
e685cf757d | ||
|
4ea0ce53b0 | ||
|
a9fa4f8cf4 | ||
|
61f13dbd8f | ||
|
c78b844b60 | ||
|
592709ab56 | ||
|
448b97469e | ||
|
3f846b26e0 |
||
|
2217f84ff4 |
87 changed files with 3982 additions and 847 deletions
|
@ -1,4 +1,4 @@
|
||||||
image: "debian:unstable"
|
image: "ubuntu:24.04"
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- check
|
- check
|
||||||
|
@ -13,52 +13,6 @@ commitcheck:
|
||||||
# only run for merge requests
|
# only run for merge requests
|
||||||
- if [ -z "$CI_MERGE_REQUEST_TITLTE" ]; then true; else python ./dist/tagging/check_conventional_commit.py "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"; fi
|
- if [ -z "$CI_MERGE_REQUEST_TITLTE" ]; then true; else python ./dist/tagging/check_conventional_commit.py "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"; fi
|
||||||
|
|
||||||
cargo:fmtcheck:
|
|
||||||
image: "rust:slim"
|
|
||||||
stage: check
|
|
||||||
script:
|
|
||||||
- rustup component add rustfmt
|
|
||||||
# Create blank versions of our configured files
|
|
||||||
# so rustfmt does not yell about non-existent files or completely empty files
|
|
||||||
- echo -e "" >> src/constants.rs
|
|
||||||
- rustc -Vv && cargo -Vv
|
|
||||||
- cargo fmt --version
|
|
||||||
- cargo fmt --all -- --check
|
|
||||||
|
|
||||||
cargo:clippy:
|
|
||||||
stage: check
|
|
||||||
variables:
|
|
||||||
RUSTFLAGS: "-Dwarnings"
|
|
||||||
script:
|
|
||||||
- apt-get update
|
|
||||||
- apt-get install libgtk-4-dev libadwaita-1-dev libssl-dev libjxl-dev libvte-2.91-gtk4-dev meson ninja-build git desktop-file-utils gettext file libusb-dev libusb-1.0-0-dev libopenxr-dev curl -y
|
|
||||||
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup.sh
|
|
||||||
- chmod +x /tmp/rustup.sh
|
|
||||||
- /tmp/rustup.sh -y
|
|
||||||
- source "$HOME/.cargo/env"
|
|
||||||
- rustup component add clippy
|
|
||||||
- rustc -Vv && cargo -Vv
|
|
||||||
- cp src/constants.rs.in src/constants.rs
|
|
||||||
- cargo clippy --version
|
|
||||||
- cargo clippy --all-targets --all-features
|
|
||||||
|
|
||||||
cargo:test:
|
|
||||||
stage: check
|
|
||||||
script:
|
|
||||||
- apt-get update
|
|
||||||
- apt-get install libgtk-4-dev libadwaita-1-dev libssl-dev libjxl-dev libvte-2.91-gtk4-dev meson ninja-build git desktop-file-utils gettext file libusb-dev libusb-1.0-0-dev libopenxr-dev curl -y
|
|
||||||
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup.sh
|
|
||||||
- chmod +x /tmp/rustup.sh
|
|
||||||
- /tmp/rustup.sh -y
|
|
||||||
- source "$HOME/.cargo/env"
|
|
||||||
- rustc --version && cargo --version # Print version info for debugging
|
|
||||||
- meson setup build -Dprefix="$PWD/build/localprefix" -Dprofile=development
|
|
||||||
- ninja -C build
|
|
||||||
- cargo test --workspace --verbose
|
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- /var/cache/apt
|
|
||||||
|
|
||||||
appimage:
|
appimage:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
script:
|
script:
|
||||||
|
@ -68,6 +22,7 @@ appimage:
|
||||||
- chmod +x /tmp/rustup.sh
|
- chmod +x /tmp/rustup.sh
|
||||||
- /tmp/rustup.sh -y
|
- /tmp/rustup.sh -y
|
||||||
- source "$HOME/.cargo/env"
|
- source "$HOME/.cargo/env"
|
||||||
|
- rustup component add clippy
|
||||||
- bash ./dist/appimage/build_appimage.sh
|
- bash ./dist/appimage/build_appimage.sh
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
|
|
197
Cargo.lock
generated
197
Cargo.lock
generated
|
@ -1,6 +1,6 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
|
@ -420,6 +420,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.20"
|
version = "0.8.20"
|
||||||
|
@ -436,6 +445,16 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "delicious-adwaita"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e53548c789a95211e0ce6d26c213067002b9b4360f8de69046d84de78ad9da3f"
|
||||||
|
dependencies = [
|
||||||
|
"gtk4",
|
||||||
|
"libadwaita",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
|
@ -554,10 +573,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "envision"
|
name = "envision"
|
||||||
version = "1.1.1"
|
version = "3.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ash",
|
"ash",
|
||||||
|
"delicious-adwaita",
|
||||||
"gettext-rs",
|
"gettext-rs",
|
||||||
"git2",
|
"git2",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
|
@ -574,8 +594,12 @@ dependencies = [
|
||||||
"rusb",
|
"rusb",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
"sha2",
|
"sha2",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-appender",
|
||||||
|
"tracing-subscriber",
|
||||||
"tracker",
|
"tracker",
|
||||||
"uuid",
|
"uuid",
|
||||||
"vte4",
|
"vte4",
|
||||||
|
@ -1060,9 +1084,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gtk4"
|
name = "gtk4"
|
||||||
version = "0.9.4"
|
version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9376d14d7e33486c54823a42bef296e882b9f25cb4c52b52f4d1d57bbadb5b6d"
|
checksum = "af1c491051f030994fd0cde6f3c44f3f5640210308cff1298c7673c47408091d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cairo-rs",
|
"cairo-rs",
|
||||||
"field-offset",
|
"field-offset",
|
||||||
|
@ -1093,9 +1117,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gtk4-sys"
|
name = "gtk4-sys"
|
||||||
version = "0.9.4"
|
version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e653b0a9001ba9be1ffddb9373bfe9a111f688222f5aeee2841481300d91b55a"
|
checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cairo-sys-rs",
|
"cairo-sys-rs",
|
||||||
"gdk-pixbuf-sys",
|
"gdk-pixbuf-sys",
|
||||||
|
@ -1510,9 +1534,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libadwaita"
|
name = "libadwaita"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8611ee9fb85e7606c362b513afcaf5b59853f79e4d98caaaf581d99465014247"
|
checksum = "500135d29c16aabf67baafd3e7741d48e8b8978ca98bac39e589165c8dc78191"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gdk4",
|
"gdk4",
|
||||||
"gio",
|
"gio",
|
||||||
|
@ -1698,6 +1722,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata 0.1.10",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.4"
|
version = "2.7.4"
|
||||||
|
@ -1820,6 +1853,16 @@ dependencies = [
|
||||||
"zbus 4.4.0",
|
"zbus 4.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||||
|
dependencies = [
|
||||||
|
"overload",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1945,6 +1988,12 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "overload"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.20.6"
|
version = "0.20.6"
|
||||||
|
@ -2180,8 +2229,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata",
|
"regex-automata 0.4.9",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
dependencies = [
|
||||||
|
"regex-syntax 0.6.29",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2192,9 +2250,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
|
@ -2503,6 +2567,19 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.34+deprecated"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
|
@ -2525,6 +2602,15 @@ dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
@ -2713,6 +2799,16 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.36"
|
version = "0.3.36"
|
||||||
|
@ -2720,10 +2816,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
|
"itoa",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
"serde",
|
||||||
"time-core",
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2732,6 +2830,16 @@ version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
|
@ -2844,6 +2952,18 @@ dependencies = [
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-appender"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"thiserror",
|
||||||
|
"time",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.28"
|
version = "0.1.28"
|
||||||
|
@ -2862,6 +2982,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
|
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-serde"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||||
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
|
"tracing-serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2925,6 +3088,12 @@ version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -2964,6 +3133,12 @@ dependencies = [
|
||||||
"rand",
|
"rand",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
|
16
Cargo.toml
16
Cargo.toml
|
@ -1,7 +1,16 @@
|
||||||
[package]
|
[package]
|
||||||
name = "envision"
|
name = "envision"
|
||||||
version = "1.1.1"
|
version = "3.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
authors = [
|
||||||
|
"Gabriele Musco <gabmus@disroot.org>",
|
||||||
|
]
|
||||||
|
description = "Orchestrator for the free XR stack"
|
||||||
|
repository = "https://gitlab.com/gabmus/envision"
|
||||||
|
documentation = "https://gitlab.com/gabmus/envision"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
keywords = ["desktop", "linux", "vr", "xr", "gtk"]
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
@ -31,3 +40,8 @@ sha2 = "0.10.8"
|
||||||
tokio = { version = "1.39.3", features = ["process"] }
|
tokio = { version = "1.39.3", features = ["process"] }
|
||||||
notify-rust = "4.11.3"
|
notify-rust = "4.11.3"
|
||||||
zbus = { version = "5.1.1", features = ["tokio"] }
|
zbus = { version = "5.1.1", features = ["tokio"] }
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-appender = "0.2.3"
|
||||||
|
serde_yaml = "0.9.34"
|
||||||
|
delicious-adwaita = { version = "0.3.0", features = ["all_themes"] }
|
||||||
|
|
|
@ -60,6 +60,10 @@ cd envision
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
# Debugging
|
||||||
|
|
||||||
|
To view all the logs you need to run envision with the env var `RUST_LOG=trace`.
|
||||||
|
|
||||||
# Common issues
|
# Common issues
|
||||||
|
|
||||||
## NOSUID with systemd-homed
|
## NOSUID with systemd-homed
|
||||||
|
|
|
@ -2,22 +2,174 @@
|
||||||
<component type="desktop-application">
|
<component type="desktop-application">
|
||||||
<id>@APP_ID@</id>
|
<id>@APP_ID@</id>
|
||||||
<metadata_license>CC0</metadata_license>
|
<metadata_license>CC0</metadata_license>
|
||||||
<project_license>AGPL-3.0</project_license>
|
<project_license>AGPL-3.0-or-later</project_license>
|
||||||
<name translatable="no">@PRETTY_NAME@</name>
|
<name translatable="no">@PRETTY_NAME@</name>
|
||||||
<summary>GUI for Monado</summary>
|
<summary>Orchestrator for the free XR stack</summary>
|
||||||
<description>
|
<description>
|
||||||
<p>GUI for Monado</p> <!-- temporary -->
|
<p>Orchestrator for the free XR stack</p>
|
||||||
</description>
|
</description>
|
||||||
<!--screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<image>https://gitlab.com/gabmus/envision/raw/main/misc/screenshots/screenshot1.png</image>
|
<image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/01.png</image>
|
||||||
<caption>Main window</caption>
|
<caption>Main window</caption>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots-->
|
<screenshot type="default">
|
||||||
|
<image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/02.png</image>
|
||||||
|
<caption>Profile editor</caption>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot type="default">
|
||||||
|
<image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/03.png</image>
|
||||||
|
<caption>Profile running</caption>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot type="default">
|
||||||
|
<image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/04.png</image>
|
||||||
|
<caption>Profile running with debug view open</caption>
|
||||||
|
</screenshot>
|
||||||
|
</screenshots>
|
||||||
<url type="homepage">@REPO_URL@</url>
|
<url type="homepage">@REPO_URL@</url>
|
||||||
<url type="bugtracker">@REPO_URL@/issues</url>
|
<url type="bugtracker">@REPO_URL@/issues</url>
|
||||||
<content_rating type="oars-1.0" />
|
<content_rating type="oars-1.0" />
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="3.1.1" date="2025-04-22">
|
||||||
|
<description>
|
||||||
|
<p>Fixes</p>
|
||||||
|
<ul>
|
||||||
|
<li>add libusb and libusb-dev deps</li>
|
||||||
|
<li>Revert "disable and blacklist wayvr dashboard plugin"</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.1.0" date="2025-04-08">
|
||||||
|
<description>
|
||||||
|
<p>What's new</p>
|
||||||
|
<ul>
|
||||||
|
<li>don't set openvrpaths as read only during profile startup</li>
|
||||||
|
<li>small design changes to build window ui</li>
|
||||||
|
<li>add support for vapor openvr compatibility module</li>
|
||||||
|
<li>remove monado vulkan layers check for nvidia</li>
|
||||||
|
</ul>
|
||||||
|
<p>Fixes</p>
|
||||||
|
<ul>
|
||||||
|
<li>disable and blacklist wayvr dashboard plugin</li>
|
||||||
|
<li>monado dependencies: use wayland-protocols-devel on Fedora</li>
|
||||||
|
</ul>
|
||||||
|
<p>Other changes</p>
|
||||||
|
<ul>
|
||||||
|
<li>clippy</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.0.1" date="2025-03-02">
|
||||||
|
<description>
|
||||||
|
<p>Fixes</p>
|
||||||
|
<ul>
|
||||||
|
<li>libnotify headers path in wivrn depcheck</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.0.0" date="2025-03-01">
|
||||||
|
<description>
|
||||||
|
<p>Breaking changes</p>
|
||||||
|
<ul>
|
||||||
|
<li>plugin store</li>
|
||||||
|
</ul>
|
||||||
|
<p>What's new</p>
|
||||||
|
<ul>
|
||||||
|
<li>Add WayVR Dashboard to the plugin list</li>
|
||||||
|
<li>wivrn: replace pulse dependency with pipewire</li>
|
||||||
|
<li>set wivrn launch options in the default profile</li>
|
||||||
|
<li>support for plugin dependencies and wayvr dashboards (using unreleased api)</li>
|
||||||
|
<li>launch options for plugins</li>
|
||||||
|
<li>homepage and author in plugin details</li>
|
||||||
|
<li>write rolling logs to file</li>
|
||||||
|
<li>add xrizer as an option for openvr compatibility module</li>
|
||||||
|
<li>switch wlx manifest</li>
|
||||||
|
<li>fetch plugins manifests online</li>
|
||||||
|
<li>add telescope to plugin store</li>
|
||||||
|
<li>ask to build profile after editing it</li>
|
||||||
|
<li>add cpu to debug info</li>
|
||||||
|
<li>make env var description selectable</li>
|
||||||
|
<li>press enter on env var entry to add</li>
|
||||||
|
<li>clearer messaging around setcap failures; getcap after setcap</li>
|
||||||
|
<li>version command line option</li>
|
||||||
|
<li>single stage ci with tests, clippy and fmt check all in one</li>
|
||||||
|
<li>use ubuntu for the ci</li>
|
||||||
|
</ul>
|
||||||
|
<p>Fixes</p>
|
||||||
|
<ul>
|
||||||
|
<li>onnxruntime build error when latest release has no artifacts</li>
|
||||||
|
<li>typo in install wivrn box</li>
|
||||||
|
<li>typo in XRT_COMPOSITOR_SCALE_PERCENTAGE</li>
|
||||||
|
<li>add plugin to config via function instead of signal</li>
|
||||||
|
<li>refresh all rows on plugin install (fixes dependencies showing up as not installed)</li>
|
||||||
|
<li>always mark plugin executable as executable</li>
|
||||||
|
<li>wrap single plugin cmd parts in single quotes</li>
|
||||||
|
<li>use correct wayland-protocols package name for open suse</li>
|
||||||
|
<li>typo in plugin schema</li>
|
||||||
|
<li>remove canonicalize from get steamvr bin dir path function</li>
|
||||||
|
<li>actually return steamvr dir in get_steamvr_base_dir</li>
|
||||||
|
<li>canonicalize some steamvr related paths to hopefully resolve symlinks</li>
|
||||||
|
<li>get ovr compatibility module runtime dir from profile ovr compatibility module struct</li>
|
||||||
|
<li>use exists() to verify existance of socket file</li>
|
||||||
|
<li>correct wording of lighthouse calibration</li>
|
||||||
|
<li>get steamvr bin dir by parsing libraryfolders.vdf</li>
|
||||||
|
<li>switch to searching for the xml for deb based distros</li>
|
||||||
|
<li>Include not shared object wayland-protocols</li>
|
||||||
|
<li>add libbsd deps for monado</li>
|
||||||
|
<li>add wayland drm-lease protocols dep for monado</li>
|
||||||
|
<li>use boost dev packages</li>
|
||||||
|
<li>debian package name for gstreamer plugins base</li>
|
||||||
|
<li>print active runtime related informative logs as debug</li>
|
||||||
|
</ul>
|
||||||
|
<p>Other changes</p>
|
||||||
|
<ul>
|
||||||
|
<li>plugins: point to stardust hosted manifest</li>
|
||||||
|
<li>cargo: revert back to serde_yaml over unsound advisory</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="2.0.1" date="2024-12-11">
|
||||||
|
<description>
|
||||||
|
<p>Fixes</p>
|
||||||
|
<ul>
|
||||||
|
<li>add screenshots to appdata</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="2.0.0" date="2024-12-09">
|
||||||
|
<description>
|
||||||
|
<p>Breaking changes</p>
|
||||||
|
<ul>
|
||||||
|
<li>enable support for different openvr compatibility modules other than opencomposite</li>
|
||||||
|
</ul>
|
||||||
|
<p>What's new</p>
|
||||||
|
<ul>
|
||||||
|
<li>add metadata to Cargo.toml; get developers from Cargo.toml authors; rectify SPDX id for license as AGPL-3.0-or-later</li>
|
||||||
|
<li>refactor builders cmake vars and env to use inner blocks</li>
|
||||||
|
<li>disable wivrnctl; refactor cmake vars in wivrn builder</li>
|
||||||
|
<li>make left and right qwerty controllers appear as no controller detected</li>
|
||||||
|
<li>try to find libmonado and openxr shared objects by reading openxr config</li>
|
||||||
|
<li>prefer symlinks over generating files for openxr active runtime json file</li>
|
||||||
|
<li>move steam library folders parser to own module; function to find steam openxr json; format</li>
|
||||||
|
<li>proper logging framework</li>
|
||||||
|
</ul>
|
||||||
|
<p>Fixes</p>
|
||||||
|
<ul>
|
||||||
|
<li>build profile can be specified manually</li>
|
||||||
|
<li>update wivrn libmonado path to wirvn/libmonado_wivrn.so</li>
|
||||||
|
<li>create openxr config dir when starting profile</li>
|
||||||
|
<li>add libnotify-dev dependency for wivrn</li>
|
||||||
|
<li>openssl dep is an include</li>
|
||||||
|
<li>add openssl-devel dep for wivrn</li>
|
||||||
|
<li>negative logic and early return in start xrservice func</li>
|
||||||
|
<li>use let err instead of match in restore xr files func</li>
|
||||||
|
</ul>
|
||||||
|
<p>Other changes</p>
|
||||||
|
<ul>
|
||||||
|
<li>format</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="1.1.1" date="2024-11-29">
|
<release version="1.1.1" date="2024-11-29">
|
||||||
<description>
|
<description>
|
||||||
<p>Fixes</p>
|
<p>Fixes</p>
|
||||||
|
|
BIN
data/screenshots/01.png
Normal file
BIN
data/screenshots/01.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
BIN
data/screenshots/02.png
Normal file
BIN
data/screenshots/02.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
BIN
data/screenshots/03.png
Normal file
BIN
data/screenshots/03.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
BIN
data/screenshots/04.png
Normal file
BIN
data/screenshots/04.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 95 KiB |
1
dist/appimage/build_appimage.sh
vendored
1
dist/appimage/build_appimage.sh
vendored
|
@ -8,6 +8,7 @@ if [[ ! -f Cargo.toml ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
meson setup appimage_build -Dprefix=/usr -Dprofile=default
|
meson setup appimage_build -Dprefix=/usr -Dprofile=default
|
||||||
|
meson test -C appimage_build --print-errorlogs
|
||||||
DESTDIR="$PWD/AppDir" ninja -C appimage_build install
|
DESTDIR="$PWD/AppDir" ninja -C appimage_build install
|
||||||
curl -SsLO https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
curl -SsLO https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||||
chmod +x linuxdeploy-x86_64.AppImage
|
chmod +x linuxdeploy-x86_64.AppImage
|
||||||
|
|
1
dist/arch/PKGBUILD
vendored
1
dist/arch/PKGBUILD
vendored
|
@ -33,7 +33,6 @@ makedepends=(
|
||||||
)
|
)
|
||||||
optdepends=(
|
optdepends=(
|
||||||
'libudev0-shim: steamvr_lh lighthouse driver support'
|
'libudev0-shim: steamvr_lh lighthouse driver support'
|
||||||
'monado-vulkan-layers-git: Vulkan layers for NVIDIA users'
|
|
||||||
)
|
)
|
||||||
provides=(envision)
|
provides=(envision)
|
||||||
conflicts=(envision)
|
conflicts=(envision)
|
||||||
|
|
17
meson.build
17
meson.build
|
@ -1,9 +1,9 @@
|
||||||
project(
|
project(
|
||||||
'envision',
|
'envision',
|
||||||
'rust',
|
'rust',
|
||||||
version: '1.1.1', # version number row
|
version: '3.1.1', # version number row
|
||||||
meson_version: '>= 0.59',
|
meson_version: '>= 0.59',
|
||||||
license: 'AGPL-3.0',
|
license: 'AGPL-3.0-or-later',
|
||||||
)
|
)
|
||||||
|
|
||||||
i18n = import('i18n')
|
i18n = import('i18n')
|
||||||
|
@ -38,6 +38,10 @@ iconsdir = datadir / 'icons'
|
||||||
podir = meson.project_source_root() / 'po'
|
podir = meson.project_source_root() / 'po'
|
||||||
gettext_package = meson.project_name()
|
gettext_package = meson.project_name()
|
||||||
|
|
||||||
|
opt_profile = get_option('profile')
|
||||||
|
|
||||||
|
# if a profile isn't specified infer from git
|
||||||
|
if opt_profile == 'default'
|
||||||
# are we building a tagged version?
|
# are we building a tagged version?
|
||||||
if run_command('git', 'describe', '--tags', '--exact-match').returncode() != 0
|
if run_command('git', 'describe', '--tags', '--exact-match').returncode() != 0
|
||||||
profile = 'Devel'
|
profile = 'Devel'
|
||||||
|
@ -53,6 +57,15 @@ else
|
||||||
version_suffix = ''
|
version_suffix = ''
|
||||||
application_id = base_id
|
application_id = base_id
|
||||||
endif
|
endif
|
||||||
|
elif opt_profile == 'development'
|
||||||
|
profile = 'Devel'
|
||||||
|
version_suffix = '-devel'
|
||||||
|
application_id = '@0@.@1@'.format(base_id, profile)
|
||||||
|
elif opt_profile == 'release'
|
||||||
|
profile = ''
|
||||||
|
version_suffix = ''
|
||||||
|
application_id = base_id
|
||||||
|
endif
|
||||||
|
|
||||||
meson.add_dist_script(
|
meson.add_dist_script(
|
||||||
'build-aux/dist-vendor.sh',
|
'build-aux/dist-vendor.sh',
|
||||||
|
|
|
@ -3,6 +3,7 @@ option(
|
||||||
type: 'combo',
|
type: 'combo',
|
||||||
choices: [
|
choices: [
|
||||||
'default',
|
'default',
|
||||||
|
'release',
|
||||||
'development'
|
'development'
|
||||||
],
|
],
|
||||||
value: 'default',
|
value: 'default',
|
||||||
|
|
|
@ -2,42 +2,6 @@
|
||||||
|
|
||||||
set -ev
|
set -ev
|
||||||
|
|
||||||
PREFIX=$1
|
|
||||||
|
|
||||||
CACHE_DIR=$2
|
|
||||||
|
|
||||||
if [[ -z $PREFIX ]] || [[ -z $CACHE_DIR ]]; then
|
|
||||||
echo "Usage: $0 PREFIX CACHE_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ONNX_VER=$(curl -sSL "https://api.github.com/repos/microsoft/onnxruntime/releases/latest" | jq -r .tag_name | tr -d v)
|
|
||||||
SYS_ARCH=$(uname -m)
|
|
||||||
|
|
||||||
if [[ $SYS_ARCH == x*64 ]]; then
|
|
||||||
ARCH="x64"
|
|
||||||
elif [[ $SYS_ARCH == arm64 ]] || [[ $ARCH == aarch64 ]]; then
|
|
||||||
ARCH="aarch64"
|
|
||||||
else
|
|
||||||
echo "CPU architecture '$SYS_ARCH' is not supported"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ONNX="onnxruntime-linux-${ARCH}-${ONNX_VER}"
|
|
||||||
ONNX_URL="https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VER}/${ONNX}.tgz"
|
|
||||||
|
|
||||||
mkdir -p "$CACHE_DIR"
|
|
||||||
|
|
||||||
curl -sSL "$ONNX_URL" -o "${CACHE_DIR}/onnxruntime.tgz"
|
|
||||||
|
|
||||||
tar xf "${CACHE_DIR}/onnxruntime.tgz" --directory="${CACHE_DIR}"
|
|
||||||
|
|
||||||
mkdir -p "${PREFIX}/lib"
|
|
||||||
mkdir -p "${PREFIX}/include"
|
|
||||||
|
|
||||||
cp -r "${CACHE_DIR}/${ONNX}/include/"* "${PREFIX}/include/"
|
|
||||||
cp -r "${CACHE_DIR}/${ONNX}/lib/"* "${PREFIX}/lib/"
|
|
||||||
|
|
||||||
if [[ -z $XDG_DATA_HOME ]]; then
|
if [[ -z $XDG_DATA_HOME ]]; then
|
||||||
DATA_HOME=$HOME/.local/share
|
DATA_HOME=$HOME/.local/share
|
||||||
else
|
else
|
||||||
|
|
|
@ -22,7 +22,7 @@ impl Cmake {
|
||||||
if k.contains(' ') {
|
if k.contains(' ') {
|
||||||
panic!("Cmake vars cannot contain spaces!");
|
panic!("Cmake vars cannot contain spaces!");
|
||||||
}
|
}
|
||||||
args.push(format!("-D{k}={v}", k = k, v = v));
|
args.push(format!("-D{k}={v}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
args.push(self.source_dir.to_string_lossy().to_string());
|
args.push(self.source_dir.to_string_lossy().to_string());
|
||||||
|
|
|
@ -5,7 +5,10 @@ use crate::{
|
||||||
ui::job_worker::job::WorkerJob,
|
ui::job_worker::job::WorkerJob,
|
||||||
util::file_utils::rm_rf,
|
util::file_utils::rm_rf,
|
||||||
};
|
};
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::{
|
||||||
|
collections::{HashMap, VecDeque},
|
||||||
|
num::NonZero,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> {
|
pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> {
|
||||||
let mut jobs = VecDeque::<WorkerJob>::new();
|
let mut jobs = VecDeque::<WorkerJob>::new();
|
||||||
|
@ -35,28 +38,52 @@ pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<W
|
||||||
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
||||||
|
|
||||||
let build_dir = profile.features.basalt.path.as_ref().unwrap().join("build");
|
let build_dir = profile.features.basalt.path.as_ref().unwrap().join("build");
|
||||||
|
|
||||||
|
let cmake = Cmake {
|
||||||
|
env: Some({
|
||||||
|
let mut cmake_env: HashMap<String, String> = HashMap::new();
|
||||||
|
for (k, v) in [
|
||||||
|
// The basalt build uses a lot of RAM, so we have to limit the number of
|
||||||
|
// build processes to not starve the system of memory
|
||||||
|
// Limit to 6 build processes at most
|
||||||
|
(
|
||||||
|
"CMAKE_BUILD_PARALLEL_LEVEL",
|
||||||
|
std::cmp::min(
|
||||||
|
6,
|
||||||
|
std::thread::available_parallelism()
|
||||||
|
.map(NonZero::get)
|
||||||
|
.unwrap_or(2),
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
("CMAKE_BUILD_TYPE", "RelWithDebInfo".into()),
|
||||||
|
("CMAKE_POLICY_VERSION_MINIMUM", "3.5".into()),
|
||||||
|
("BUILD_TESTS", "off".into()),
|
||||||
|
] {
|
||||||
|
cmake_env.insert(k.to_string(), v);
|
||||||
|
}
|
||||||
|
cmake_env
|
||||||
|
}),
|
||||||
|
vars: Some({
|
||||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||||
cmake_vars.insert("CMAKE_EXPORT_COMPILE_COMMANDS".into(), "ON".into());
|
for (k, v) in [
|
||||||
cmake_vars.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
|
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||||
|
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||||
|
("BUILD_TESTS", "OFF"),
|
||||||
|
("BASALT_INSTANTIATIONS_DOUBLE", "OFF"),
|
||||||
|
] {
|
||||||
|
cmake_vars.insert(k.to_string(), v.to_string());
|
||||||
|
}
|
||||||
cmake_vars.insert(
|
cmake_vars.insert(
|
||||||
"CMAKE_INSTALL_PREFIX".into(),
|
"CMAKE_INSTALL_PREFIX".into(),
|
||||||
profile.prefix.to_string_lossy().to_string(),
|
profile.prefix.to_string_lossy().to_string(),
|
||||||
);
|
);
|
||||||
cmake_vars.insert("BUILD_TESTS".into(), "OFF".into());
|
|
||||||
cmake_vars.insert("BASALT_INSTANTIATIONS_DOUBLE".into(), "OFF".into());
|
|
||||||
cmake_vars.insert(
|
cmake_vars.insert(
|
||||||
"CMAKE_INSTALL_LIBDIR".into(),
|
"CMAKE_INSTALL_LIBDIR".into(),
|
||||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||||
);
|
);
|
||||||
|
cmake_vars
|
||||||
let mut cmake_env: HashMap<String, String> = HashMap::new();
|
}),
|
||||||
cmake_env.insert("CMAKE_BUILD_PARALLEL_LEVEL".into(), "2".into());
|
|
||||||
cmake_env.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
|
|
||||||
cmake_env.insert("BUILD_TESTS".into(), "off".into());
|
|
||||||
|
|
||||||
let cmake = Cmake {
|
|
||||||
env: Some(cmake_env),
|
|
||||||
vars: Some(cmake_vars),
|
|
||||||
source_dir: profile.features.basalt.path.as_ref().unwrap().clone(),
|
source_dir: profile.features.basalt.path.as_ref().unwrap().clone(),
|
||||||
build_dir: build_dir.clone(),
|
build_dir: build_dir.clone(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -44,12 +44,20 @@ pub fn get_build_libsurvive_jobs(profile: &Profile, clean_build: bool) -> VecDeq
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.join("build");
|
.join("build");
|
||||||
|
|
||||||
|
let cmake = Cmake {
|
||||||
|
env: None,
|
||||||
|
vars: Some({
|
||||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||||
cmake_vars.insert("CMAKE_EXPORT_COMPILE_COMMANDS".into(), "ON".into());
|
for (k, v) in [
|
||||||
cmake_vars.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
|
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||||
cmake_vars.insert("ENABLE_api_example".into(), "OFF".into());
|
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||||
cmake_vars.insert("USE_HIDAPI".into(), "ON".into());
|
("ENABLE_api_example", "OFF"),
|
||||||
cmake_vars.insert("CMAKE_SKIP_INSTALL_RPATH".into(), "YES".into());
|
("USE_HIDAPI", "ON"),
|
||||||
|
("CMAKE_SKIP_INSTALL_RPATH", "YES"),
|
||||||
|
] {
|
||||||
|
cmake_vars.insert(k.to_string(), v.to_string());
|
||||||
|
}
|
||||||
cmake_vars.insert(
|
cmake_vars.insert(
|
||||||
"CMAKE_INSTALL_PREFIX".into(),
|
"CMAKE_INSTALL_PREFIX".into(),
|
||||||
profile.prefix.to_string_lossy().to_string(),
|
profile.prefix.to_string_lossy().to_string(),
|
||||||
|
@ -58,10 +66,8 @@ pub fn get_build_libsurvive_jobs(profile: &Profile, clean_build: bool) -> VecDeq
|
||||||
"CMAKE_INSTALL_LIBDIR".into(),
|
"CMAKE_INSTALL_LIBDIR".into(),
|
||||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||||
);
|
);
|
||||||
|
cmake_vars
|
||||||
let cmake = Cmake {
|
}),
|
||||||
env: None,
|
|
||||||
vars: Some(cmake_vars),
|
|
||||||
source_dir: profile.features.libsurvive.path.as_ref().unwrap().clone(),
|
source_dir: profile.features.libsurvive.path.as_ref().unwrap().clone(),
|
||||||
build_dir: build_dir.clone(),
|
build_dir: build_dir.clone(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use crate::{
|
use crate::{constants::pkg_data_dir, termcolor::TermColor, ui::job_worker::job::WorkerJob};
|
||||||
constants::pkg_data_dir, paths::get_cache_dir, profile::Profile, termcolor::TermColor,
|
|
||||||
ui::job_worker::job::WorkerJob,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn get_build_mercury_jobs(profile: &Profile) -> VecDeque<WorkerJob> {
|
pub fn get_build_mercury_jobs() -> VecDeque<WorkerJob> {
|
||||||
let mut jobs = VecDeque::new();
|
let mut jobs = VecDeque::new();
|
||||||
jobs.push_back(WorkerJob::new_printer(
|
jobs.push_back(WorkerJob::new_printer(
|
||||||
"Building Mercury...",
|
"Building Mercury...",
|
||||||
|
@ -17,10 +14,7 @@ pub fn get_build_mercury_jobs(profile: &Profile) -> VecDeque<WorkerJob> {
|
||||||
.join("scripts/build_mercury.sh")
|
.join("scripts/build_mercury.sh")
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
Some(vec![
|
None,
|
||||||
profile.prefix.to_string_lossy().to_string(),
|
|
||||||
get_cache_dir().to_string_lossy().to_string(),
|
|
||||||
]),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
jobs
|
jobs
|
||||||
|
|
|
@ -43,10 +43,18 @@ pub fn get_build_monado_jobs(profile: &Profile, clean_build: bool) -> VecDeque<W
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let cmake = Cmake {
|
||||||
|
env: Some(env),
|
||||||
|
vars: Some({
|
||||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||||
cmake_vars.insert("CMAKE_EXPORT_COMPILE_COMMANDS".into(), "ON".into());
|
for (k, v) in [
|
||||||
cmake_vars.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
|
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||||
cmake_vars.insert("XRT_HAVE_SYSTEM_CJSON".into(), "NO".into());
|
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||||
|
("XRT_HAVE_SYSTEM_CJSON", "NO"),
|
||||||
|
] {
|
||||||
|
cmake_vars.insert(k.to_string(), v.to_string());
|
||||||
|
}
|
||||||
cmake_vars.insert(
|
cmake_vars.insert(
|
||||||
"CMAKE_LIBDIR".into(),
|
"CMAKE_LIBDIR".into(),
|
||||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||||
|
@ -70,10 +78,8 @@ pub fn get_build_monado_jobs(profile: &Profile, clean_build: bool) -> VecDeque<W
|
||||||
cmake_vars.insert(k.clone(), v.clone());
|
cmake_vars.insert(k.clone(), v.clone());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
cmake_vars
|
||||||
let cmake = Cmake {
|
}),
|
||||||
env: Some(env),
|
|
||||||
vars: Some(cmake_vars),
|
|
||||||
source_dir: profile.xrservice_path.clone(),
|
source_dir: profile.xrservice_path.clone(),
|
||||||
build_dir: build_dir.clone(),
|
build_dir: build_dir.clone(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,13 +19,15 @@ pub fn get_build_opencomposite_jobs(profile: &Profile, clean_build: bool) -> Vec
|
||||||
|
|
||||||
let git = Git {
|
let git = Git {
|
||||||
repo: profile
|
repo: profile
|
||||||
.opencomposite_repo
|
.ovr_comp
|
||||||
|
.repo
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap_or(&"https://gitlab.com/znixian/OpenOVR.git".into())
|
.unwrap_or(&"https://gitlab.com/znixian/OpenOVR.git".into())
|
||||||
.clone(),
|
.clone(),
|
||||||
dir: profile.opencomposite_path.clone(),
|
dir: profile.ovr_comp.path.clone(),
|
||||||
branch: profile
|
branch: profile
|
||||||
.opencomposite_branch
|
.ovr_comp
|
||||||
|
.branch
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap_or(&"openxr".into())
|
.unwrap_or(&"openxr".into())
|
||||||
.clone(),
|
.clone(),
|
||||||
|
@ -33,14 +35,20 @@ pub fn get_build_opencomposite_jobs(profile: &Profile, clean_build: bool) -> Vec
|
||||||
|
|
||||||
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
||||||
|
|
||||||
let build_dir = profile.opencomposite_path.join("build");
|
let build_dir = profile.ovr_comp.path.join("build");
|
||||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
|
||||||
cmake_vars.insert("CMAKE_EXPORT_COMPILE_COMMANDS".into(), "ON".into());
|
|
||||||
cmake_vars.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
|
|
||||||
let cmake = Cmake {
|
let cmake = Cmake {
|
||||||
env: None,
|
env: None,
|
||||||
vars: Some(cmake_vars),
|
vars: Some({
|
||||||
source_dir: profile.opencomposite_path.clone(),
|
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||||
|
for (k, v) in [
|
||||||
|
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||||
|
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||||
|
] {
|
||||||
|
cmake_vars.insert(k.to_string(), v.to_string());
|
||||||
|
}
|
||||||
|
cmake_vars
|
||||||
|
}),
|
||||||
|
source_dir: profile.ovr_comp.path.clone(),
|
||||||
build_dir: build_dir.clone(),
|
build_dir: build_dir.clone(),
|
||||||
};
|
};
|
||||||
if !Path::new(&build_dir).is_dir() || clean_build {
|
if !Path::new(&build_dir).is_dir() || clean_build {
|
||||||
|
|
68
src/builders/build_vapor.rs
Normal file
68
src/builders/build_vapor.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use crate::{
|
||||||
|
build_tools::{cmake::Cmake, git::Git},
|
||||||
|
profile::Profile,
|
||||||
|
termcolor::TermColor,
|
||||||
|
ui::job_worker::job::WorkerJob,
|
||||||
|
util::file_utils::rm_rf,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, VecDeque},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn get_build_vapor_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> {
|
||||||
|
let mut jobs = VecDeque::<WorkerJob>::new();
|
||||||
|
jobs.push_back(WorkerJob::new_printer(
|
||||||
|
"Building VapoR...",
|
||||||
|
Some(TermColor::Blue),
|
||||||
|
));
|
||||||
|
|
||||||
|
let git = Git {
|
||||||
|
repo: profile
|
||||||
|
.ovr_comp
|
||||||
|
.repo
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&"https://github.com/micheal65536/VapoR.git".into())
|
||||||
|
.clone(),
|
||||||
|
dir: profile.ovr_comp.path.clone(),
|
||||||
|
branch: profile
|
||||||
|
.ovr_comp
|
||||||
|
.branch
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&"master".into())
|
||||||
|
.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
||||||
|
|
||||||
|
let build_dir = profile.ovr_comp.path.join("build");
|
||||||
|
let install_dir = build_dir.join("install_pfx");
|
||||||
|
let cmake = Cmake {
|
||||||
|
env: None,
|
||||||
|
vars: Some({
|
||||||
|
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||||
|
for (k, v) in [
|
||||||
|
("VAPOR_LOG_SILENT", "ON"),
|
||||||
|
("USE_SYSTEM_OPENXR", "OFF"),
|
||||||
|
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||||
|
] {
|
||||||
|
cmake_vars.insert(k.to_string(), v.to_string());
|
||||||
|
}
|
||||||
|
cmake_vars.insert(
|
||||||
|
"CMAKE_INSTALL_PREFIX".into(),
|
||||||
|
install_dir.to_string_lossy().to_string(),
|
||||||
|
);
|
||||||
|
cmake_vars
|
||||||
|
}),
|
||||||
|
source_dir: profile.ovr_comp.path.clone(),
|
||||||
|
build_dir: build_dir.clone(),
|
||||||
|
};
|
||||||
|
if !Path::new(&build_dir).is_dir() || clean_build {
|
||||||
|
rm_rf(&build_dir);
|
||||||
|
jobs.push_back(cmake.get_prepare_job());
|
||||||
|
}
|
||||||
|
jobs.push_back(cmake.get_build_job());
|
||||||
|
jobs.push_back(cmake.get_install_job());
|
||||||
|
|
||||||
|
jobs
|
||||||
|
}
|
|
@ -34,23 +34,29 @@ pub fn get_build_wivrn_jobs(profile: &Profile, clean_build: bool) -> VecDeque<Wo
|
||||||
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
||||||
|
|
||||||
let build_dir = profile.xrservice_path.join("build");
|
let build_dir = profile.xrservice_path.join("build");
|
||||||
|
|
||||||
|
let cmake = Cmake {
|
||||||
|
env: None,
|
||||||
|
vars: Some({
|
||||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||||
cmake_vars.insert("CMAKE_EXPORT_COMPILE_COMMANDS".into(), "ON".into());
|
for (k, v) in [
|
||||||
cmake_vars.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
|
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||||
cmake_vars.insert("XRT_HAVE_SYSTEM_CJSON".into(), "NO".into());
|
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||||
cmake_vars.insert("WIVRN_BUILD_CLIENT".into(), "OFF".into());
|
("XRT_HAVE_SYSTEM_CJSON", "NO"),
|
||||||
|
("WIVRN_BUILD_CLIENT", "OFF"),
|
||||||
|
("WIVRN_BUILD_WIVRNCTL", "OFF"),
|
||||||
|
] {
|
||||||
|
cmake_vars.insert(k.to_string(), v.to_string());
|
||||||
|
}
|
||||||
cmake_vars.insert(
|
cmake_vars.insert(
|
||||||
"CMAKE_INSTALL_PREFIX".into(),
|
"CMAKE_INSTALL_PREFIX".into(),
|
||||||
profile.prefix.to_string_lossy().to_string(),
|
profile.prefix.to_string_lossy().to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
profile.xrservice_cmake_flags.iter().for_each(|(k, v)| {
|
profile.xrservice_cmake_flags.iter().for_each(|(k, v)| {
|
||||||
cmake_vars.insert(k.clone(), v.clone());
|
cmake_vars.insert(k.clone(), v.clone());
|
||||||
});
|
});
|
||||||
|
cmake_vars
|
||||||
let cmake = Cmake {
|
}),
|
||||||
env: None,
|
|
||||||
vars: Some(cmake_vars),
|
|
||||||
source_dir: profile.xrservice_path.clone(),
|
source_dir: profile.xrservice_path.clone(),
|
||||||
build_dir: build_dir.clone(),
|
build_dir: build_dir.clone(),
|
||||||
};
|
};
|
||||||
|
|
50
src/builders/build_xrizer.rs
Normal file
50
src/builders/build_xrizer.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
use crate::{
|
||||||
|
build_tools::git::Git, profile::Profile, termcolor::TermColor, ui::job_worker::job::WorkerJob,
|
||||||
|
util::file_utils::rm_rf,
|
||||||
|
};
|
||||||
|
use std::{collections::VecDeque, path::Path};
|
||||||
|
|
||||||
|
pub fn get_build_xrizer_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> {
|
||||||
|
let mut jobs = VecDeque::<WorkerJob>::new();
|
||||||
|
jobs.push_back(WorkerJob::new_printer(
|
||||||
|
"Building xrizer...",
|
||||||
|
Some(TermColor::Blue),
|
||||||
|
));
|
||||||
|
|
||||||
|
let git = Git {
|
||||||
|
repo: profile
|
||||||
|
.ovr_comp
|
||||||
|
.repo
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&"https://github.com/Supreeeme/xrizer".into())
|
||||||
|
.clone(),
|
||||||
|
dir: profile.ovr_comp.path.clone(),
|
||||||
|
branch: profile
|
||||||
|
.ovr_comp
|
||||||
|
.branch
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&"main".into())
|
||||||
|
.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
||||||
|
|
||||||
|
let build_dir = profile.ovr_comp.path.join("target");
|
||||||
|
if !Path::new(&build_dir).is_dir() || clean_build {
|
||||||
|
rm_rf(&build_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs.push_back(WorkerJob::new_cmd(
|
||||||
|
None,
|
||||||
|
"sh".into(),
|
||||||
|
Some(vec![
|
||||||
|
"-c".into(),
|
||||||
|
format!(
|
||||||
|
"cd '{}' && cargo xbuild --release",
|
||||||
|
profile.ovr_comp.path.to_string_lossy()
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
));
|
||||||
|
|
||||||
|
jobs
|
||||||
|
}
|
|
@ -4,4 +4,6 @@ pub mod build_mercury;
|
||||||
pub mod build_monado;
|
pub mod build_monado;
|
||||||
pub mod build_opencomposite;
|
pub mod build_opencomposite;
|
||||||
pub mod build_openhmd;
|
pub mod build_openhmd;
|
||||||
|
pub mod build_vapor;
|
||||||
pub mod build_wivrn;
|
pub mod build_wivrn;
|
||||||
|
pub mod build_xrizer;
|
||||||
|
|
|
@ -7,21 +7,48 @@ use crate::{
|
||||||
lighthouse::lighthouse_profile, openhmd::openhmd_profile, simulated::simulated_profile,
|
lighthouse::lighthouse_profile, openhmd::openhmd_profile, simulated::simulated_profile,
|
||||||
survive::survive_profile, wivrn::wivrn_profile, wmr::wmr_profile,
|
survive::survive_profile, wivrn::wivrn_profile, wmr::wmr_profile,
|
||||||
},
|
},
|
||||||
|
ui::plugins::Plugin,
|
||||||
util::file_utils::get_writer,
|
util::file_utils::get_writer,
|
||||||
};
|
};
|
||||||
use serde::{de::Error, Deserialize, Serialize};
|
use serde::{de::Error, Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::BufReader,
|
io::BufReader,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct PluginConfig {
|
||||||
|
pub plugin: Plugin,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Plugin> for PluginConfig {
|
||||||
|
fn from(p: &Plugin) -> Self {
|
||||||
|
Self {
|
||||||
|
plugin: p.clone(),
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&PluginConfig> for Plugin {
|
||||||
|
fn from(cp: &PluginConfig) -> Self {
|
||||||
|
cp.plugin.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_WIN_SIZE: [i32; 2] = [360, 400];
|
const DEFAULT_WIN_SIZE: [i32; 2] = [360, 400];
|
||||||
|
|
||||||
const fn default_win_size() -> [i32; 2] {
|
const fn default_win_size() -> [i32; 2] {
|
||||||
DEFAULT_WIN_SIZE
|
DEFAULT_WIN_SIZE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_theme_name() -> String {
|
||||||
|
"Follow system".into()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub selected_profile_uuid: String,
|
pub selected_profile_uuid: String,
|
||||||
|
@ -29,6 +56,10 @@ pub struct Config {
|
||||||
pub user_profiles: Vec<Profile>,
|
pub user_profiles: Vec<Profile>,
|
||||||
#[serde(default = "default_win_size")]
|
#[serde(default = "default_win_size")]
|
||||||
pub win_size: [i32; 2],
|
pub win_size: [i32; 2],
|
||||||
|
#[serde(default)]
|
||||||
|
pub plugins: HashMap<String, PluginConfig>,
|
||||||
|
#[serde(default = "default_theme_name")]
|
||||||
|
pub theme_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
@ -37,8 +68,10 @@ impl Default for Config {
|
||||||
// TODO: using an empty string here is ugly
|
// TODO: using an empty string here is ugly
|
||||||
selected_profile_uuid: "".to_string(),
|
selected_profile_uuid: "".to_string(),
|
||||||
debug_view_enabled: false,
|
debug_view_enabled: false,
|
||||||
user_profiles: vec![],
|
user_profiles: Vec::default(),
|
||||||
win_size: DEFAULT_WIN_SIZE,
|
win_size: DEFAULT_WIN_SIZE,
|
||||||
|
plugins: HashMap::default(),
|
||||||
|
theme_name: default_theme_name(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,10 +98,42 @@ impl Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_path(path: &Path) -> Self {
|
fn from_path(path: &Path) -> Self {
|
||||||
File::open(path)
|
let mut this: Self = File::open(path)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|file| serde_json::from_reader(BufReader::new(file)).ok())
|
.and_then(|file| serde_json::from_reader(BufReader::new(file)).ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut needs_save = false;
|
||||||
|
|
||||||
|
// remap legacy opencomposite data to new ovr_comp
|
||||||
|
#[allow(deprecated)]
|
||||||
|
for prof in this.user_profiles.iter_mut() {
|
||||||
|
if prof
|
||||||
|
.ovr_comp
|
||||||
|
.path
|
||||||
|
.file_name()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
== "__envision__fallbackovrcomp"
|
||||||
|
{
|
||||||
|
prof.ovr_comp.path = prof.opencomposite_path.clone();
|
||||||
|
needs_save = true;
|
||||||
|
}
|
||||||
|
if prof.opencomposite_repo.is_some() && prof.ovr_comp.repo.is_none() {
|
||||||
|
prof.ovr_comp.repo = prof.opencomposite_repo.take();
|
||||||
|
needs_save = true;
|
||||||
|
}
|
||||||
|
if prof.opencomposite_branch.is_some() && prof.ovr_comp.branch.is_none() {
|
||||||
|
prof.ovr_comp.branch = prof.opencomposite_branch.take();
|
||||||
|
needs_save = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if needs_save {
|
||||||
|
this.save_to_path(path).expect("Failed to save config");
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_to_path(&self, path: &Path) -> Result<(), serde_json::Error> {
|
fn save_to_path(&self, path: &Path) -> Result<(), serde_json::Error> {
|
||||||
|
|
|
@ -16,10 +16,6 @@ pub const LOCALE_DIR: &str = "@LOCALEDIR@";
|
||||||
pub const BUILD_PROFILE: &str = "@PROFILE@";
|
pub const BUILD_PROFILE: &str = "@PROFILE@";
|
||||||
pub const BUILD_DATETIME: &str = "@BUILD_DATETIME@";
|
pub const BUILD_DATETIME: &str = "@BUILD_DATETIME@";
|
||||||
|
|
||||||
pub fn get_developers() -> Vec<String> {
|
|
||||||
vec!["Gabriele Musco <gabmus@disroot.org>".into()]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_artists() -> Vec<String> {
|
pub fn get_artists() -> Vec<String> {
|
||||||
vec!["App Icon: Yannick (@Yandr)".into()]
|
vec!["App Icon: Yannick (@Yandr)".into()]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use super::{
|
use super::{
|
||||||
boost_deps::boost_deps,
|
boost_deps::boost_deps,
|
||||||
common::{dep_cmake, dep_eigen, dep_gpp, dep_libgl, dep_ninja, dep_opencv},
|
common::{dep_cmake, dep_eigen, dep_gpp, dep_libgl, dep_ninja, dep_opencv},
|
||||||
DepType, Dependency, DependencyCheckResult,
|
DepType, DepcheckResultGetMissing, Dependency, DependencyCheckResult,
|
||||||
};
|
};
|
||||||
use crate::linux_distro::LinuxDistro;
|
use crate::linux_distro::LinuxDistro;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -181,9 +181,5 @@ pub fn check_basalt_deps() -> Vec<DependencyCheckResult> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_missing_basalt_deps() -> Vec<Dependency> {
|
pub fn get_missing_basalt_deps() -> Vec<Dependency> {
|
||||||
check_basalt_deps()
|
check_basalt_deps().filter_missing_deps()
|
||||||
.iter()
|
|
||||||
.filter(|res| !res.found)
|
|
||||||
.map(|res| res.dependency.clone())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,8 +52,8 @@ pub fn boost_deps() -> Vec<Dependency> {
|
||||||
packages: HashMap::from([
|
packages: HashMap::from([
|
||||||
(LinuxDistro::Arch, "boost".into()),
|
(LinuxDistro::Arch, "boost".into()),
|
||||||
(LinuxDistro::Debian, "libboost-all-dev".into()),
|
(LinuxDistro::Debian, "libboost-all-dev".into()),
|
||||||
(LinuxDistro::Fedora, "boost".into()),
|
(LinuxDistro::Fedora, "boost-devel".into()),
|
||||||
(LinuxDistro::Alpine, "boost".into()),
|
(LinuxDistro::Alpine, "boost-dev".into()),
|
||||||
(LinuxDistro::Gentoo, "dev-libs/boost".into()),
|
(LinuxDistro::Gentoo, "dev-libs/boost".into()),
|
||||||
(LinuxDistro::Suse, package.into()),
|
(LinuxDistro::Suse, package.into()),
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -303,3 +303,35 @@ pub fn dep_adb() -> Dependency {
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dep_getcap_setcap() -> Dependency {
|
||||||
|
Dependency {
|
||||||
|
name: "libcap".into(),
|
||||||
|
dep_type: DepType::Executable,
|
||||||
|
filename: "setcap".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "libcap".into()),
|
||||||
|
(LinuxDistro::Debian, "libcap2-bin".into()),
|
||||||
|
(LinuxDistro::Fedora, "libcap".into()),
|
||||||
|
(LinuxDistro::Alpine, "libcap".into()),
|
||||||
|
(LinuxDistro::Gentoo, "sys-libs/libcap".into()),
|
||||||
|
(LinuxDistro::Suse, "libcap-progs".into()),
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dep_glslc() -> Dependency {
|
||||||
|
Dependency {
|
||||||
|
name: "glslc".into(),
|
||||||
|
dep_type: DepType::Executable,
|
||||||
|
filename: "glslc".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "shaderc".into()),
|
||||||
|
(LinuxDistro::Debian, "glslc".into()),
|
||||||
|
(LinuxDistro::Fedora, "glslc".into()),
|
||||||
|
(LinuxDistro::Alpine, "shaderc".into()),
|
||||||
|
(LinuxDistro::Gentoo, "media-libs/shaderc".into()),
|
||||||
|
(LinuxDistro::Suse, "shaderc".into()),
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::{
|
use super::{
|
||||||
common::{dep_cmake, dep_eigen, dep_gcc, dep_git, dep_gpp, dep_ninja},
|
common::{dep_cmake, dep_eigen, dep_gcc, dep_git, dep_gpp, dep_ninja},
|
||||||
Dependency, DependencyCheckResult,
|
DepcheckResultGetMissing, Dependency, DependencyCheckResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn libsurvive_deps() -> Vec<Dependency> {
|
fn libsurvive_deps() -> Vec<Dependency> {
|
||||||
|
@ -19,9 +19,5 @@ pub fn check_libsurvive_deps() -> Vec<DependencyCheckResult> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_missing_libsurvive_deps() -> Vec<Dependency> {
|
pub fn get_missing_libsurvive_deps() -> Vec<Dependency> {
|
||||||
check_libsurvive_deps()
|
check_libsurvive_deps().filter_missing_deps()
|
||||||
.iter()
|
|
||||||
.filter(|res| !res.found)
|
|
||||||
.map(|res| res.dependency.clone())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,27 @@
|
||||||
use super::{common::dep_opencv, DepType, Dependency, DependencyCheckResult};
|
use super::{
|
||||||
|
common::dep_opencv, DepType, DepcheckResultGetMissing, Dependency, DependencyCheckResult,
|
||||||
|
};
|
||||||
use crate::linux_distro::LinuxDistro;
|
use crate::linux_distro::LinuxDistro;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
fn mercury_deps() -> Vec<Dependency> {
|
fn mercury_deps() -> Vec<Dependency> {
|
||||||
vec![
|
vec![
|
||||||
dep_opencv(),
|
dep_opencv(),
|
||||||
|
Dependency {
|
||||||
|
name: "onnxruntime-dev".into(),
|
||||||
|
dep_type: DepType::Include,
|
||||||
|
filename: "onnxruntime/onnxruntime_c_api.h".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "onnxruntime".into()),
|
||||||
|
(LinuxDistro::Debian, "libonnxruntime-dev".into()),
|
||||||
|
(LinuxDistro::Fedora, "onnxruntime-devel".into()),
|
||||||
|
// alpine doesn't seem to have the package
|
||||||
|
// (LinuxDistro::Alpine, "".into()),
|
||||||
|
(LinuxDistro::Gentoo, "sci-ml/onnx".into()),
|
||||||
|
// opensuse doesn't seem to have the package
|
||||||
|
// (LinuxDistro::Suse, "".into()),
|
||||||
|
]),
|
||||||
|
},
|
||||||
Dependency {
|
Dependency {
|
||||||
name: "jq".into(),
|
name: "jq".into(),
|
||||||
dep_type: DepType::Executable,
|
dep_type: DepType::Executable,
|
||||||
|
@ -39,9 +56,5 @@ pub fn check_mercury_deps() -> Vec<DependencyCheckResult> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_missing_mercury_deps() -> Vec<Dependency> {
|
pub fn get_missing_mercury_deps() -> Vec<Dependency> {
|
||||||
check_mercury_deps()
|
check_mercury_deps().filter_missing_deps()
|
||||||
.iter()
|
|
||||||
.filter(|res| !res.found)
|
|
||||||
.map(|res| res.dependency.clone())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ pub mod mercury_deps;
|
||||||
pub mod monado_deps;
|
pub mod monado_deps;
|
||||||
pub mod openhmd_deps;
|
pub mod openhmd_deps;
|
||||||
pub mod wivrn_deps;
|
pub mod wivrn_deps;
|
||||||
|
pub mod xrizer_deps;
|
||||||
|
|
||||||
use crate::linux_distro::LinuxDistro;
|
use crate::linux_distro::LinuxDistro;
|
||||||
use std::{collections::HashMap, env, fmt::Display, path::Path};
|
use std::{collections::HashMap, env, fmt::Display, path::Path};
|
||||||
|
@ -16,6 +17,7 @@ pub enum DepType {
|
||||||
Executable,
|
Executable,
|
||||||
Include,
|
Include,
|
||||||
UdevRule,
|
UdevRule,
|
||||||
|
Share,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
@ -49,6 +51,7 @@ impl Dependency {
|
||||||
.collect(),
|
.collect(),
|
||||||
DepType::Include => include_paths(),
|
DepType::Include => include_paths(),
|
||||||
DepType::UdevRule => udev_rules_paths(),
|
DepType::UdevRule => udev_rules_paths(),
|
||||||
|
DepType::Share => share_paths(),
|
||||||
} {
|
} {
|
||||||
let path_s = &format!("{dir}/{fname}", dir = dir, fname = self.filename);
|
let path_s = &format!("{dir}/{fname}", dir = dir, fname = self.filename);
|
||||||
let path = Path::new(&path_s);
|
let path = Path::new(&path_s);
|
||||||
|
@ -106,6 +109,24 @@ impl Display for DependencyCheckResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait DepcheckResultGetMissing {
|
||||||
|
fn filter_missing_deps(self) -> Vec<Dependency>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DepcheckResultGetMissing for Vec<DependencyCheckResult> {
|
||||||
|
fn filter_missing_deps(self) -> Vec<Dependency> {
|
||||||
|
self.into_iter()
|
||||||
|
.filter_map(|res| {
|
||||||
|
if !res.found {
|
||||||
|
Some(res.dependency)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn shared_obj_paths() -> Vec<String> {
|
fn shared_obj_paths() -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
"/lib".into(),
|
"/lib".into(),
|
||||||
|
@ -115,6 +136,24 @@ fn shared_obj_paths() -> Vec<String> {
|
||||||
"/usr/local/lib64".into(),
|
"/usr/local/lib64".into(),
|
||||||
"/usr/lib/x86_64-linux-gnu".into(),
|
"/usr/lib/x86_64-linux-gnu".into(),
|
||||||
"/usr/lib/aarch64-linux-gnu".into(),
|
"/usr/lib/aarch64-linux-gnu".into(),
|
||||||
|
// Debian puts libclang in /usr/lib/llvm-[llvm major version]/lib.
|
||||||
|
"/usr/lib/llvm-15/lib".into(),
|
||||||
|
"/usr/lib/llvm-16/lib".into(),
|
||||||
|
"/usr/lib/llvm-19/lib".into(),
|
||||||
|
// Fedora puts libclang in /usr/lib64/llvm[llvm major version]/lib as well as /usr/lib64.
|
||||||
|
"/usr/lib64/llvm15/lib".into(),
|
||||||
|
"/usr/lib64/llvm16/lib".into(),
|
||||||
|
"/usr/lib64/llvm17/lib".into(),
|
||||||
|
"/usr/lib64/llvm18/lib".into(),
|
||||||
|
"/usr/lib64/llvm19/lib".into(),
|
||||||
|
"/usr/lib64/llvm20/lib".into(),
|
||||||
|
// Gentoo puts libclang in /usr/lib/llvm/[llvm major version]/lib64.
|
||||||
|
"/usr/lib/llvm/15/lib64".into(),
|
||||||
|
"/usr/lib/llvm/16/lib64".into(),
|
||||||
|
"/usr/lib/llvm/17/lib64".into(),
|
||||||
|
"/usr/lib/llvm/18/lib64".into(),
|
||||||
|
"/usr/lib/llvm/19/lib64".into(),
|
||||||
|
"/usr/lib/llvm/20/lib64".into(),
|
||||||
"/lib/x86_64-linux-gnu".into(),
|
"/lib/x86_64-linux-gnu".into(),
|
||||||
"/lib/aarch64-linux-gnu".into(),
|
"/lib/aarch64-linux-gnu".into(),
|
||||||
"/app/lib".into(),
|
"/app/lib".into(),
|
||||||
|
@ -138,6 +177,8 @@ fn include_paths() -> Vec<String> {
|
||||||
"/usr/include/ffmpeg/libpostproc".into(),
|
"/usr/include/ffmpeg/libpostproc".into(),
|
||||||
"/usr/include/ffmpeg/libswresample".into(),
|
"/usr/include/ffmpeg/libswresample".into(),
|
||||||
"/usr/include/ffmpeg/libswscale".into(),
|
"/usr/include/ffmpeg/libswscale".into(),
|
||||||
|
// opensuse puts wayland-client.h here
|
||||||
|
"/usr/include/wayland".into(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,6 +186,10 @@ fn udev_rules_paths() -> Vec<String> {
|
||||||
vec!["/usr/lib/udev/rules.d".into()]
|
vec!["/usr/lib/udev/rules.d".into()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn share_paths() -> Vec<String> {
|
||||||
|
vec!["/usr/share".into()]
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{DepType, Dependency};
|
use super::{DepType, Dependency};
|
||||||
|
|
|
@ -4,9 +4,12 @@ use super::{
|
||||||
dep_libgl, dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr, dep_vulkan_headers,
|
dep_libgl, dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr, dep_vulkan_headers,
|
||||||
dep_vulkan_icd_loader,
|
dep_vulkan_icd_loader,
|
||||||
},
|
},
|
||||||
DepType, Dependency, DependencyCheckResult,
|
DepType, DepcheckResultGetMissing, Dependency, DependencyCheckResult,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
depcheck::common::{dep_glslc, dep_libxrandr},
|
||||||
|
linux_distro::LinuxDistro,
|
||||||
};
|
};
|
||||||
use crate::{depcheck::common::dep_libxrandr, linux_distro::LinuxDistro};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
fn monado_deps() -> Vec<Dependency> {
|
fn monado_deps() -> Vec<Dependency> {
|
||||||
|
@ -30,25 +33,37 @@ fn monado_deps() -> Vec<Dependency> {
|
||||||
(LinuxDistro::Suse, "wayland-devel".into()),
|
(LinuxDistro::Suse, "wayland-devel".into()),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
Dependency {
|
||||||
|
name: "wayland-protocols".into(),
|
||||||
|
dep_type: DepType::Share,
|
||||||
|
filename: "wayland-protocols/staging/drm-lease/drm-lease-v1.xml".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "wayland-protocols".into()),
|
||||||
|
(LinuxDistro::Debian, "wayland-protocols".into()),
|
||||||
|
(LinuxDistro::Fedora, "wayland-protocols-devel".into()),
|
||||||
|
(LinuxDistro::Gentoo, "dev-libs/wayland-protocols".into()),
|
||||||
|
(LinuxDistro::Suse, "wayland-protocols-devel".into()),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
Dependency {
|
||||||
|
name: "libbsd".into(),
|
||||||
|
dep_type: DepType::SharedObject,
|
||||||
|
filename: "libbsd.so".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "libbsd".into()),
|
||||||
|
(LinuxDistro::Debian, "libbsd-dev".into()),
|
||||||
|
(LinuxDistro::Fedora, "libbsd-devel".into()),
|
||||||
|
(LinuxDistro::Gentoo, "dev-libs/libbsd".into()),
|
||||||
|
(LinuxDistro::Suse, "libbsd-devel".into()),
|
||||||
|
]),
|
||||||
|
},
|
||||||
dep_cmake(),
|
dep_cmake(),
|
||||||
dep_eigen(),
|
dep_eigen(),
|
||||||
dep_git(),
|
dep_git(),
|
||||||
dep_ninja(),
|
dep_ninja(),
|
||||||
dep_gcc(),
|
dep_gcc(),
|
||||||
dep_gpp(),
|
dep_gpp(),
|
||||||
Dependency {
|
dep_glslc(),
|
||||||
name: "glslc".into(),
|
|
||||||
dep_type: DepType::Executable,
|
|
||||||
filename: "glslc".into(),
|
|
||||||
packages: HashMap::from([
|
|
||||||
(LinuxDistro::Arch, "shaderc".into()),
|
|
||||||
(LinuxDistro::Debian, "glslc".into()),
|
|
||||||
(LinuxDistro::Fedora, "glslc".into()),
|
|
||||||
(LinuxDistro::Alpine, "shaderc".into()),
|
|
||||||
(LinuxDistro::Gentoo, "media-libs/shaderc".into()),
|
|
||||||
(LinuxDistro::Suse, "shaderc".into()),
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
dep_glslang_validator(),
|
dep_glslang_validator(),
|
||||||
Dependency {
|
Dependency {
|
||||||
name: "sdl2".into(),
|
name: "sdl2".into(),
|
||||||
|
@ -59,10 +74,34 @@ fn monado_deps() -> Vec<Dependency> {
|
||||||
(LinuxDistro::Debian, "libsdl2-dev".into()),
|
(LinuxDistro::Debian, "libsdl2-dev".into()),
|
||||||
(LinuxDistro::Fedora, "SDL2-devel".into()),
|
(LinuxDistro::Fedora, "SDL2-devel".into()),
|
||||||
(LinuxDistro::Gentoo, "media-libs/libsdl2".into()),
|
(LinuxDistro::Gentoo, "media-libs/libsdl2".into()),
|
||||||
(LinuxDistro::Suse, "SDL2-devel".into()),
|
(LinuxDistro::Suse, "sdl2-compat-devel".into()),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
dep_libudev(),
|
dep_libudev(),
|
||||||
|
Dependency {
|
||||||
|
name: "libusb".into(),
|
||||||
|
dep_type: DepType::SharedObject,
|
||||||
|
filename: "libusb-1.0.so".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "libusb".into()),
|
||||||
|
(LinuxDistro::Debian, "libusb-1.0-0".into()),
|
||||||
|
(LinuxDistro::Fedora, "libusb1".into()),
|
||||||
|
(LinuxDistro::Gentoo, "dev-libs/libusb".into()),
|
||||||
|
(LinuxDistro::Suse, "libusb-1_0-0".into()),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
Dependency {
|
||||||
|
name: "libusb-dev".into(),
|
||||||
|
dep_type: DepType::Include,
|
||||||
|
filename: "libusb-1.0/libusb.h".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "libusb".into()),
|
||||||
|
(LinuxDistro::Debian, "libusb-1.0-0-dev".into()),
|
||||||
|
(LinuxDistro::Fedora, "libusb1-devel".into()),
|
||||||
|
(LinuxDistro::Gentoo, "dev-libs/libusb".into()),
|
||||||
|
(LinuxDistro::Suse, "libusb-1_0-devel".into()),
|
||||||
|
]),
|
||||||
|
},
|
||||||
Dependency {
|
Dependency {
|
||||||
name: "mesa-common-dev".into(),
|
name: "mesa-common-dev".into(),
|
||||||
dep_type: DepType::Include,
|
dep_type: DepType::Include,
|
||||||
|
@ -83,9 +122,5 @@ pub fn check_monado_deps() -> Vec<DependencyCheckResult> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_missing_monado_deps() -> Vec<Dependency> {
|
pub fn get_missing_monado_deps() -> Vec<Dependency> {
|
||||||
check_monado_deps()
|
check_monado_deps().filter_missing_deps()
|
||||||
.iter()
|
|
||||||
.filter(|res| !res.found)
|
|
||||||
.map(|res| res.dependency.clone())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::{
|
use super::{
|
||||||
common::{dep_gcc, dep_git, dep_gpp, dep_ninja},
|
common::{dep_gcc, dep_git, dep_gpp, dep_ninja},
|
||||||
Dependency, DependencyCheckResult,
|
DepcheckResultGetMissing, Dependency, DependencyCheckResult,
|
||||||
};
|
};
|
||||||
use crate::linux_distro::LinuxDistro;
|
use crate::linux_distro::LinuxDistro;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -31,9 +31,5 @@ pub fn check_openhmd_deps() -> Vec<DependencyCheckResult> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_missing_openhmd_deps() -> Vec<Dependency> {
|
pub fn get_missing_openhmd_deps() -> Vec<Dependency> {
|
||||||
check_openhmd_deps()
|
check_openhmd_deps().filter_missing_deps()
|
||||||
.iter()
|
|
||||||
.filter(|res| !res.found)
|
|
||||||
.map(|res| res.dependency.clone())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use super::{
|
||||||
dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr, dep_vulkan_headers,
|
dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr, dep_vulkan_headers,
|
||||||
dep_vulkan_icd_loader,
|
dep_vulkan_icd_loader,
|
||||||
},
|
},
|
||||||
DepType, Dependency, DependencyCheckResult,
|
DepType, DepcheckResultGetMissing, Dependency, DependencyCheckResult,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
depcheck::common::{dep_libgl, dep_libxrandr},
|
depcheck::common::{dep_libgl, dep_libxrandr},
|
||||||
|
@ -78,15 +78,15 @@ fn wivrn_deps() -> Vec<Dependency> {
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
Dependency {
|
Dependency {
|
||||||
name: "libpulse-dev".into(),
|
name: "libpipewire-dev".into(),
|
||||||
dep_type: DepType::Include,
|
dep_type: DepType::Include,
|
||||||
filename: "pulse/context.h".into(),
|
filename: "pipewire-0.3/pipewire/pipewire.h".into(),
|
||||||
packages: HashMap::from([
|
packages: HashMap::from([
|
||||||
(LinuxDistro::Arch, "libpulse".into()),
|
(LinuxDistro::Arch, "libpipewire".into()),
|
||||||
(LinuxDistro::Debian, "libpulse-dev".into()),
|
(LinuxDistro::Debian, "libpipewire-0.3-dev".into()),
|
||||||
(LinuxDistro::Fedora, "pulseaudio-libs-devel".into()),
|
(LinuxDistro::Fedora, "pipewire-devel".into()),
|
||||||
(LinuxDistro::Gentoo, "media-libs/libpulse".into()),
|
(LinuxDistro::Gentoo, "media-video/pipewire".into()),
|
||||||
(LinuxDistro::Suse, "libpulse-devel".into()),
|
(LinuxDistro::Suse, "pipewire-devel".into()),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
dep_eigen(),
|
dep_eigen(),
|
||||||
|
@ -169,7 +169,10 @@ fn wivrn_deps() -> Vec<Dependency> {
|
||||||
filename: "pkgconfig/gstreamer-app-1.0.pc".into(),
|
filename: "pkgconfig/gstreamer-app-1.0.pc".into(),
|
||||||
packages: HashMap::from([
|
packages: HashMap::from([
|
||||||
(LinuxDistro::Arch, "gst-plugins-base-libs".into()),
|
(LinuxDistro::Arch, "gst-plugins-base-libs".into()),
|
||||||
(LinuxDistro::Debian, "libgstreamer1.0-dev".into()),
|
(
|
||||||
|
LinuxDistro::Debian,
|
||||||
|
"libgstreamer-plugins-base1.0-dev".into(),
|
||||||
|
),
|
||||||
(LinuxDistro::Fedora, "gstreamer1-plugins-base-devel".into()),
|
(LinuxDistro::Fedora, "gstreamer1-plugins-base-devel".into()),
|
||||||
(LinuxDistro::Gentoo, "media-libs/gst-plugins-base".into()),
|
(LinuxDistro::Gentoo, "media-libs/gst-plugins-base".into()),
|
||||||
(LinuxDistro::Suse, "gstreamer-plugins-base-devel".into()),
|
(LinuxDistro::Suse, "gstreamer-plugins-base-devel".into()),
|
||||||
|
@ -235,6 +238,30 @@ fn wivrn_deps() -> Vec<Dependency> {
|
||||||
(LinuxDistro::Suse, "glib2-devel".into()),
|
(LinuxDistro::Suse, "glib2-devel".into()),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
Dependency {
|
||||||
|
name: "openssl-dev".into(),
|
||||||
|
dep_type: DepType::Include,
|
||||||
|
filename: "openssl/ssl3.h".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "openssl".into()),
|
||||||
|
(LinuxDistro::Alpine, "openssl-dev".into()),
|
||||||
|
(LinuxDistro::Debian, "libssl-dev".into()),
|
||||||
|
(LinuxDistro::Fedora, "openssl-devel".into()),
|
||||||
|
(LinuxDistro::Suse, "openssl-devel".into()),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
Dependency {
|
||||||
|
name: "libnotify-dev".into(),
|
||||||
|
dep_type: DepType::Include,
|
||||||
|
filename: "libnotify/notify.h".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "libnotify".into()),
|
||||||
|
(LinuxDistro::Alpine, "libnotify-dev".into()),
|
||||||
|
(LinuxDistro::Debian, "libnotify-dev".into()),
|
||||||
|
(LinuxDistro::Fedora, "libnotify-devel".into()),
|
||||||
|
(LinuxDistro::Suse, "libnotify-devel".into()),
|
||||||
|
]),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,9 +270,5 @@ pub fn check_wivrn_deps() -> Vec<DependencyCheckResult> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_missing_wivrn_deps() -> Vec<Dependency> {
|
pub fn get_missing_wivrn_deps() -> Vec<Dependency> {
|
||||||
check_wivrn_deps()
|
check_wivrn_deps().filter_missing_deps()
|
||||||
.iter()
|
|
||||||
.filter(|res| !res.found)
|
|
||||||
.map(|res| res.dependency.clone())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
65
src/depcheck/xrizer_deps.rs
Normal file
65
src/depcheck/xrizer_deps.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use super::{DepType, DepcheckResultGetMissing, Dependency, DependencyCheckResult};
|
||||||
|
use crate::{depcheck::common::dep_glslc, linux_distro::LinuxDistro};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn xrizer_deps() -> Vec<Dependency> {
|
||||||
|
vec![
|
||||||
|
dep_glslc(),
|
||||||
|
Dependency {
|
||||||
|
name: "cargo".into(),
|
||||||
|
dep_type: DepType::Executable,
|
||||||
|
filename: "cargo".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "rust".into()),
|
||||||
|
(LinuxDistro::Debian, "cargo".into()),
|
||||||
|
(LinuxDistro::Fedora, "cargo".into()),
|
||||||
|
(LinuxDistro::Alpine, "cargo".into()),
|
||||||
|
(LinuxDistro::Suse, "cargo".into()),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
Dependency {
|
||||||
|
name: "libxcb-glx".into(),
|
||||||
|
dep_type: DepType::Include,
|
||||||
|
filename: "xcb/glx.h".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "libxcb".into()),
|
||||||
|
(LinuxDistro::Debian, "libxcb-glx0-dev".into()),
|
||||||
|
(LinuxDistro::Fedora, "libxcb-devel".into()),
|
||||||
|
(LinuxDistro::Gentoo, "x11-libs/libxcb".into()),
|
||||||
|
(LinuxDistro::Suse, "libxcb-devel".into()),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
Dependency {
|
||||||
|
name: "libclang".into(),
|
||||||
|
dep_type: DepType::SharedObject,
|
||||||
|
filename: "libclang.so".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "clang".into()),
|
||||||
|
(LinuxDistro::Debian, "libclang-19-dev".into()),
|
||||||
|
(LinuxDistro::Fedora, "clang19-devel".into()),
|
||||||
|
(LinuxDistro::Gentoo, "llvm-core/clang-runtime".into()),
|
||||||
|
(LinuxDistro::Suse, "clang19-devel".into()),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
Dependency {
|
||||||
|
name: "wayland-dev".into(),
|
||||||
|
dep_type: DepType::Include,
|
||||||
|
filename: "wayland-client.h".into(),
|
||||||
|
packages: HashMap::from([
|
||||||
|
(LinuxDistro::Arch, "wayland".into()),
|
||||||
|
(LinuxDistro::Debian, "libwayland-dev".into()),
|
||||||
|
(LinuxDistro::Fedora, "wayland-devel".into()),
|
||||||
|
(LinuxDistro::Gentoo, "dev-libs/wayland".into()),
|
||||||
|
(LinuxDistro::Suse, "wayland-devel".into()),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_xrizer_deps() -> Vec<DependencyCheckResult> {
|
||||||
|
Dependency::check_many(xrizer_deps())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_missing_xrizer_deps() -> Vec<Dependency> {
|
||||||
|
check_xrizer_deps().filter_missing_deps()
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ const CHUNK_SIZE: usize = 1024;
|
||||||
|
|
||||||
fn headers() -> HeaderMap {
|
fn headers() -> HeaderMap {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(USER_AGENT, format!("{}/1.0", APP_ID).parse().unwrap());
|
headers.insert(USER_AGENT, format!("{APP_ID}/1.0").parse().unwrap());
|
||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use lazy_static::lazy_static;
|
||||||
fn env_var_descriptions() -> Vec<(&'static str, &'static str)> {
|
fn env_var_descriptions() -> Vec<(&'static str, &'static str)> {
|
||||||
vec![
|
vec![
|
||||||
(
|
(
|
||||||
"XRT_COMPOSITOR_SCALE_PECENTAGE",
|
"XRT_COMPOSITOR_SCALE_PERCENTAGE",
|
||||||
"Render resolution percentage. A percentage higher than the native resolution (>100) will help with antialiasing and image clarity."
|
"Render resolution percentage. A percentage higher than the native resolution (>100) will help with antialiasing and image clarity."
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@ -65,7 +65,7 @@ fn env_var_descriptions() -> Vec<(&'static str, &'static str)> {
|
||||||
fn env_var_descriptions_as_paragraph() -> String {
|
fn env_var_descriptions_as_paragraph() -> String {
|
||||||
ENV_VAR_DESCRIPTIONS
|
ENV_VAR_DESCRIPTIONS
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| format!("<span size=\"large\" weight=\"bold\">{}</span>\n{}", k, v))
|
.map(|(k, v)| format!("<span size=\"large\" weight=\"bold\">{k}</span>\n{v}"))
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("\n\n")
|
.join("\n\n")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
paths::{get_backup_dir, SYSTEM_PREFIX},
|
paths::SYSTEM_PREFIX,
|
||||||
profile::Profile,
|
profile::Profile,
|
||||||
util::file_utils::{copy_file, deserialize_file, get_writer, set_file_readonly},
|
util::file_utils::{deserialize_file, get_writer, set_file_readonly},
|
||||||
xdg::XDG,
|
xdg::XDG,
|
||||||
};
|
};
|
||||||
|
use anyhow::bail;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
fs::remove_file,
|
fs::{create_dir_all, remove_file, rename},
|
||||||
|
os::unix::fs::symlink,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct ActiveRuntimeInnerRuntime {
|
pub struct ActiveRuntimeInnerRuntime {
|
||||||
|
@ -34,29 +37,6 @@ fn get_active_runtime_json_path() -> PathBuf {
|
||||||
get_openxr_conf_dir().join("1/active_runtime.json")
|
get_openxr_conf_dir().join("1/active_runtime.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_steam(active_runtime: &ActiveRuntime) -> bool {
|
|
||||||
matches!(active_runtime.runtime.valve_runtime_is_steamvr, Some(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_backup_steam_active_runtime_path() -> PathBuf {
|
|
||||||
get_backup_dir().join("active_runtime.json.steam.bak")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_backed_up_steam_active_runtime() -> Option<ActiveRuntime> {
|
|
||||||
get_active_runtime_from_path(&get_backup_steam_active_runtime_path())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn backup_steam_active_runtime() {
|
|
||||||
if let Some(ar) = get_current_active_runtime() {
|
|
||||||
if is_steam(&ar) {
|
|
||||||
copy_file(
|
|
||||||
&get_active_runtime_json_path(),
|
|
||||||
&get_backup_steam_active_runtime_path(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_active_runtime_from_path(path: &Path) -> Option<ActiveRuntime> {
|
fn get_active_runtime_from_path(path: &Path) -> Option<ActiveRuntime> {
|
||||||
deserialize_file(path)
|
deserialize_file(path)
|
||||||
}
|
}
|
||||||
|
@ -78,29 +58,6 @@ pub fn dump_current_active_runtime(active_runtime: &ActiveRuntime) -> anyhow::Re
|
||||||
dump_active_runtime_to_path(active_runtime, &get_active_runtime_json_path())
|
dump_active_runtime_to_path(active_runtime, &get_active_runtime_json_path())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_steam_active_runtime() -> ActiveRuntime {
|
|
||||||
if let Some(backup) = get_backed_up_steam_active_runtime() {
|
|
||||||
return backup;
|
|
||||||
}
|
|
||||||
ActiveRuntime {
|
|
||||||
file_format_version: "1.0.0".into(),
|
|
||||||
runtime: ActiveRuntimeInnerRuntime {
|
|
||||||
valve_runtime_is_steamvr: Some(true),
|
|
||||||
libmonado_path: None,
|
|
||||||
library_path: XDG
|
|
||||||
.get_data_home()
|
|
||||||
.join("Steam/steamapps/common/SteamVR/bin/linux64/vrclient.so"),
|
|
||||||
name: Some("SteamVR".into()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_current_active_runtime_to_steam() -> anyhow::Result<()> {
|
|
||||||
set_file_readonly(&get_active_runtime_json_path(), false)?;
|
|
||||||
dump_current_active_runtime(&build_steam_active_runtime())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_profile_active_runtime(profile: &Profile) -> anyhow::Result<ActiveRuntime> {
|
pub fn build_profile_active_runtime(profile: &Profile) -> anyhow::Result<ActiveRuntime> {
|
||||||
let Some(libopenxr_path) = profile.libopenxr_so() else {
|
let Some(libopenxr_path) = profile.libopenxr_so() else {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
|
@ -137,10 +94,29 @@ fn relativize_active_runtime_lib_path(ar: &ActiveRuntime, path: &Path) -> Active
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ACTIVE_RUNTIME_BAK: &str = "active_runtime.json.envision.bak";
|
||||||
|
|
||||||
pub fn set_current_active_runtime_to_profile(profile: &Profile) -> anyhow::Result<()> {
|
pub fn set_current_active_runtime_to_profile(profile: &Profile) -> anyhow::Result<()> {
|
||||||
let dest = get_active_runtime_json_path();
|
let dest = get_active_runtime_json_path();
|
||||||
|
if dest.is_dir() {
|
||||||
|
bail!("{} is a directory", dest.to_string_lossy());
|
||||||
|
}
|
||||||
|
if !dest.is_symlink() {
|
||||||
set_file_readonly(&dest, false)?;
|
set_file_readonly(&dest, false)?;
|
||||||
backup_steam_active_runtime();
|
}
|
||||||
|
if dest.is_file() || dest.is_symlink() {
|
||||||
|
rename(&dest, dest.parent().unwrap().join(ACTIVE_RUNTIME_BAK))?;
|
||||||
|
} else {
|
||||||
|
debug!("no active_runtime.json file to backup")
|
||||||
|
}
|
||||||
|
|
||||||
|
let profile_openxr_json = profile.openxr_json_path();
|
||||||
|
if profile_openxr_json.is_file() {
|
||||||
|
create_dir_all(dest.parent().unwrap())?;
|
||||||
|
symlink(profile_openxr_json, &dest)?;
|
||||||
|
} else {
|
||||||
|
warn!("profile openxr json file doesn't exist");
|
||||||
|
// fallback: build the file from scratch
|
||||||
let pfx = profile.clone().prefix;
|
let pfx = profile.clone().prefix;
|
||||||
let mut ar = build_profile_active_runtime(profile)?;
|
let mut ar = build_profile_active_runtime(profile)?;
|
||||||
// hack: relativize libopenxr_monado.so path for system installs
|
// hack: relativize libopenxr_monado.so path for system installs
|
||||||
|
@ -149,6 +125,36 @@ pub fn set_current_active_runtime_to_profile(profile: &Profile) -> anyhow::Resul
|
||||||
}
|
}
|
||||||
dump_current_active_runtime(&ar)?;
|
dump_current_active_runtime(&ar)?;
|
||||||
set_file_readonly(&dest, true)?;
|
set_file_readonly(&dest, true)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_current_active_runtime() -> anyhow::Result<()> {
|
||||||
|
let dest = get_active_runtime_json_path();
|
||||||
|
if dest.is_dir() {
|
||||||
|
bail!("{} is a directory", dest.to_string_lossy());
|
||||||
|
}
|
||||||
|
if !dest.exists() {
|
||||||
|
debug!("no current active_runtime.json to remove")
|
||||||
|
}
|
||||||
|
Ok(remove_file(dest)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_active_runtime_backup() -> anyhow::Result<()> {
|
||||||
|
let dest = get_active_runtime_json_path();
|
||||||
|
let bak = dest.parent().unwrap().join(ACTIVE_RUNTIME_BAK);
|
||||||
|
if bak.is_file() || bak.is_symlink() {
|
||||||
|
if dest.is_dir() {
|
||||||
|
bail!("{} is a directory", dest.to_string_lossy());
|
||||||
|
}
|
||||||
|
if !dest.is_symlink() {
|
||||||
|
set_file_readonly(&dest, false)?;
|
||||||
|
}
|
||||||
|
rename(&bak, &dest)?;
|
||||||
|
} else {
|
||||||
|
debug!("{ACTIVE_RUNTIME_BAK} does not exist, nothing to restore");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod active_runtime_json;
|
pub mod active_runtime_json;
|
||||||
pub mod monado_autorun;
|
pub mod monado_autorun;
|
||||||
pub mod openvrpaths_vrpath;
|
pub mod openvrpaths_vrpath;
|
||||||
|
pub mod wayvr_dashboard_config;
|
||||||
pub mod wivrn_config;
|
pub mod wivrn_config;
|
||||||
pub mod wivrn_encoder_presets;
|
pub mod wivrn_encoder_presets;
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
paths::get_backup_dir,
|
paths::get_backup_dir,
|
||||||
profile::Profile,
|
profile::Profile,
|
||||||
|
@ -7,6 +5,7 @@ use crate::{
|
||||||
xdg::XDG,
|
xdg::XDG,
|
||||||
};
|
};
|
||||||
use serde::{ser::Error, Deserialize, Serialize};
|
use serde::{ser::Error, Deserialize, Serialize};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct OpenVrPaths {
|
pub struct OpenVrPaths {
|
||||||
|
@ -86,6 +85,7 @@ fn build_steam_openvrpaths() -> OpenVrPaths {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_current_openvrpaths_to_steam() -> anyhow::Result<()> {
|
pub fn set_current_openvrpaths_to_steam() -> anyhow::Result<()> {
|
||||||
|
// removing readonly flag just in case, remove this line in the future
|
||||||
set_file_readonly(&get_openvrpaths_vrpath_path(), false)?;
|
set_file_readonly(&get_openvrpaths_vrpath_path(), false)?;
|
||||||
dump_current_openvrpaths(&build_steam_openvrpaths())?;
|
dump_current_openvrpaths(&build_steam_openvrpaths())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -98,25 +98,24 @@ pub fn build_profile_openvrpaths(profile: &Profile) -> OpenVrPaths {
|
||||||
external_drivers: None,
|
external_drivers: None,
|
||||||
jsonid: "vrpathreg".into(),
|
jsonid: "vrpathreg".into(),
|
||||||
log: vec![datadir.join("Steam/logs")],
|
log: vec![datadir.join("Steam/logs")],
|
||||||
runtime: vec![profile.opencomposite_path.join("build")],
|
runtime: vec![profile.ovr_comp.runtime_dir()],
|
||||||
version: 1,
|
version: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_current_openvrpaths_to_profile(profile: &Profile) -> anyhow::Result<()> {
|
pub fn set_current_openvrpaths_to_profile(profile: &Profile) -> anyhow::Result<()> {
|
||||||
let dest = get_openvrpaths_vrpath_path();
|
let dest = get_openvrpaths_vrpath_path();
|
||||||
|
// removing readonly flag just in case, remove this line in the future
|
||||||
set_file_readonly(&dest, false)?;
|
set_file_readonly(&dest, false)?;
|
||||||
backup_steam_openvrpaths();
|
backup_steam_openvrpaths();
|
||||||
dump_current_openvrpaths(&build_profile_openvrpaths(profile))?;
|
dump_current_openvrpaths(&build_profile_openvrpaths(profile))?;
|
||||||
set_file_readonly(&dest, true)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use super::{dump_openvrpaths_to_path, get_openvrpaths_from_path, OpenVrPaths};
|
use super::{dump_openvrpaths_to_path, get_openvrpaths_from_path, OpenVrPaths};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_read_openvrpaths_vrpath_steamvr() {
|
fn can_read_openvrpaths_vrpath_steamvr() {
|
||||||
|
|
13
src/file_builders/wayvr_dashboard_config.rs
Normal file
13
src/file_builders/wayvr_dashboard_config.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct WayVrDashboardConfigFragmentInner {
|
||||||
|
pub exec: String,
|
||||||
|
pub args: Option<String>,
|
||||||
|
pub env: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct WayVrDashboardConfigFragment {
|
||||||
|
pub dashboard: WayVrDashboardConfigFragmentInner,
|
||||||
|
}
|
|
@ -143,7 +143,7 @@ pub struct WivrnConfig {
|
||||||
pub encoders: Vec<WivrnConfEncoder>,
|
pub encoders: Vec<WivrnConfEncoder>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub application: Option<WivrnConfigApplication>,
|
pub application: Option<WivrnConfigApplication>,
|
||||||
#[serde(default)]
|
#[serde(default, rename = "tcp-only")]
|
||||||
pub tcp_only: bool,
|
pub tcp_only: bool,
|
||||||
/// contains unknown fields
|
/// contains unknown fields
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
|
|
|
@ -116,7 +116,7 @@ fn list_gpus() -> Vec<GpuSysDrm> {
|
||||||
|
|
||||||
for i in 0..5 {
|
for i in 0..5 {
|
||||||
// arbitrary range, find a better way
|
// arbitrary range, find a better way
|
||||||
let card_dir = PathBuf::from(format!("/sys/class/drm/card{}", i));
|
let card_dir = PathBuf::from(format!("/sys/class/drm/card{i}"));
|
||||||
let vendor_file = card_dir.join("device/vendor");
|
let vendor_file = card_dir.join("device/vendor");
|
||||||
if let Some(mut reader) = get_reader(&vendor_file) {
|
if let Some(mut reader) = get_reader(&vendor_file) {
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
|
|
|
@ -49,13 +49,13 @@ impl LinuxDistro {
|
||||||
Ok(_) if buf.starts_with("PRETTY_NAME=\"") => {
|
Ok(_) if buf.starts_with("PRETTY_NAME=\"") => {
|
||||||
return buf
|
return buf
|
||||||
.split('=')
|
.split('=')
|
||||||
.last()
|
.next_back()
|
||||||
.map(|b| b.trim().trim_matches('"').trim().to_string());
|
.map(|b| b.trim().trim_matches('"').trim().to_string());
|
||||||
}
|
}
|
||||||
Ok(_) if buf.starts_with("NAME=\"") => {
|
Ok(_) if buf.starts_with("NAME=\"") => {
|
||||||
name = buf
|
name = buf
|
||||||
.split('=')
|
.split('=')
|
||||||
.last()
|
.next_back()
|
||||||
.map(|b| b.trim().trim_matches('"').trim().to_string());
|
.map(|b| b.trim().trim_matches('"').trim().to_string());
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -79,7 +79,7 @@ impl LinuxDistro {
|
||||||
{
|
{
|
||||||
let name = buf
|
let name = buf
|
||||||
.split('=')
|
.split('=')
|
||||||
.last()
|
.next_back()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.trim()
|
.trim()
|
||||||
.trim_matches('"')
|
.trim_matches('"')
|
||||||
|
@ -115,6 +115,7 @@ impl LinuxDistro {
|
||||||
|| s.contains("steamos")
|
|| s.contains("steamos")
|
||||||
|| s.contains("steam os")
|
|| s.contains("steam os")
|
||||||
|| s.contains("endeavour")
|
|| s.contains("endeavour")
|
||||||
|
|| s.contains("cachyos")
|
||||||
|| s.contains("garuda")
|
|| s.contains("garuda")
|
||||||
{
|
{
|
||||||
return Some(Self::Arch);
|
return Some(Self::Arch);
|
||||||
|
@ -150,7 +151,33 @@ impl LinuxDistro {
|
||||||
Self::Alpine => format!("sudo apk add {}", packages.join(" ")),
|
Self::Alpine => format!("sudo apk add {}", packages.join(" ")),
|
||||||
Self::Debian => format!("sudo apt install {}", packages.join(" ")),
|
Self::Debian => format!("sudo apt install {}", packages.join(" ")),
|
||||||
Self::Gentoo => format!("sudo emerge -av {}", packages.join(" ")),
|
Self::Gentoo => format!("sudo emerge -av {}", packages.join(" ")),
|
||||||
Self::Suse => format!("sudo zypper install {}", packages.join(" ")),
|
Self::Suse => {
|
||||||
|
let mut opi_pkgs = Vec::new();
|
||||||
|
let mut zypper_pkgs = Vec::new();
|
||||||
|
for pkg in packages {
|
||||||
|
if ["OpenXR-SDK-devel"].contains(&pkg.as_str()) {
|
||||||
|
opi_pkgs.push(pkg.clone());
|
||||||
|
} else {
|
||||||
|
zypper_pkgs.push(pkg.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[
|
||||||
|
if opi_pkgs.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(format!("opi {}", opi_pkgs.join(" ")))
|
||||||
|
},
|
||||||
|
if zypper_pkgs.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(format!("sudo zypper install {}", zypper_pkgs.join(" ")))
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.filter_map(|c| c.clone())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" && ")
|
||||||
|
}
|
||||||
Self::Fedora => {
|
Self::Fedora => {
|
||||||
let mut install_rpmfusion_cmd: Option<String> = None;
|
let mut install_rpmfusion_cmd: Option<String> = None;
|
||||||
let mut swap_ffmpeg_cmd: Option<String> = None;
|
let mut swap_ffmpeg_cmd: Option<String> = None;
|
||||||
|
@ -190,9 +217,9 @@ impl LinuxDistro {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use super::LinuxDistro;
|
use super::LinuxDistro;
|
||||||
|
use crate::depcheck::common::{dep_openxr, dep_pkexec, dep_vulkan_icd_loader};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_detect_arch_linux_from_etc_os_release() {
|
fn can_detect_arch_linux_from_etc_os_release() {
|
||||||
|
@ -203,4 +230,34 @@ mod tests {
|
||||||
Some(LinuxDistro::Arch)
|
Some(LinuxDistro::Arch)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_account_for_opensuse_opi_packages() {
|
||||||
|
assert_eq!(
|
||||||
|
LinuxDistro::Suse
|
||||||
|
.install_command(
|
||||||
|
&[dep_openxr(), dep_vulkan_icd_loader()]
|
||||||
|
.iter()
|
||||||
|
.map(|dep| dep.package_name_for_distro(Some(&LinuxDistro::Suse)))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
"opi OpenXR-SDK-devel && sudo zypper install vulkan-devel"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opensuse_opi_does_not_interfere_if_not_needed() {
|
||||||
|
assert_eq!(
|
||||||
|
LinuxDistro::Suse
|
||||||
|
.install_command(
|
||||||
|
&[dep_pkexec(), dep_vulkan_icd_loader()]
|
||||||
|
.iter()
|
||||||
|
.map(|dep| dep.package_name_for_distro(Some(&LinuxDistro::Suse)))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
"sudo zypper install polkit vulkan-devel"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
107
src/main.rs
107
src/main.rs
|
@ -1,17 +1,29 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use constants::{resources, APP_ID, APP_NAME, GETTEXT_PACKAGE, LOCALE_DIR, RESOURCES_BASE_PATH};
|
use constants::{resources, APP_ID, APP_NAME, GETTEXT_PACKAGE, LOCALE_DIR, RESOURCES_BASE_PATH};
|
||||||
use file_builders::{
|
use file_builders::{
|
||||||
active_runtime_json::{get_current_active_runtime, set_current_active_runtime_to_steam},
|
active_runtime_json::restore_active_runtime_backup,
|
||||||
openvrpaths_vrpath::{get_current_openvrpaths, set_current_openvrpaths_to_steam},
|
openvrpaths_vrpath::{get_current_openvrpaths, set_current_openvrpaths_to_steam},
|
||||||
};
|
};
|
||||||
use gettextrs::LocaleCategory;
|
use gettextrs::LocaleCategory;
|
||||||
|
use paths::get_logs_dir;
|
||||||
use relm4::{
|
use relm4::{
|
||||||
adw,
|
adw,
|
||||||
gtk::{self, gdk, gio, glib, prelude::*},
|
gtk::{self, gdk, gio, glib, prelude::*},
|
||||||
MessageBroker, RelmApp,
|
MessageBroker, RelmApp,
|
||||||
};
|
};
|
||||||
use std::env;
|
use std::{
|
||||||
use steam_linux_runtime_injector::restore_runtime_entrypoint;
|
env,
|
||||||
|
fs::{read_dir, remove_file},
|
||||||
|
os::unix::fs::MetadataExt,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
use steam_linux_runtime_injector::{
|
||||||
|
restore_sniper_runtime_entrypoint, restore_soldier_runtime_entrypoint,
|
||||||
|
};
|
||||||
|
use tracing::{error, warn};
|
||||||
|
use tracing_subscriber::{
|
||||||
|
filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
|
||||||
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
app::{App, AppInit, Msg},
|
app::{App, AppInit, Msg},
|
||||||
cmdline_opts::CmdLineOpts,
|
cmdline_opts::CmdLineOpts,
|
||||||
|
@ -22,6 +34,7 @@ pub mod build_tools;
|
||||||
pub mod builders;
|
pub mod builders;
|
||||||
pub mod cmd_runner;
|
pub mod cmd_runner;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
#[rustfmt::skip]
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod depcheck;
|
pub mod depcheck;
|
||||||
pub mod device_prober;
|
pub mod device_prober;
|
||||||
|
@ -46,25 +59,61 @@ pub mod xdg;
|
||||||
pub mod xr_devices;
|
pub mod xr_devices;
|
||||||
|
|
||||||
fn restore_steam_xr_files() {
|
fn restore_steam_xr_files() {
|
||||||
let active_runtime = get_current_active_runtime();
|
|
||||||
let openvrpaths = get_current_openvrpaths();
|
let openvrpaths = get_current_openvrpaths();
|
||||||
if let Some(ar) = active_runtime {
|
if let Err(e) = restore_active_runtime_backup() {
|
||||||
if !file_builders::active_runtime_json::is_steam(&ar) {
|
warn!("failed to restore active runtime to steam: {e}");
|
||||||
match set_current_active_runtime_to_steam() {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => eprintln!("Warning: failed to restore active runtime to steam: {e}"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let Some(ovrp) = openvrpaths {
|
if let Some(ovrp) = openvrpaths {
|
||||||
if !file_builders::openvrpaths_vrpath::is_steam(&ovrp) {
|
if !file_builders::openvrpaths_vrpath::is_steam(&ovrp) {
|
||||||
match set_current_openvrpaths_to_steam() {
|
if let Err(e) = set_current_openvrpaths_to_steam() {
|
||||||
Ok(_) => {}
|
warn!("failed to restore openvrpaths to steam: {e}");
|
||||||
Err(e) => eprintln!("Warning: failed to restore openvrpaths to steam: {e}"),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
restore_runtime_entrypoint();
|
}
|
||||||
|
restore_sniper_runtime_entrypoint();
|
||||||
|
restore_soldier_runtime_entrypoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOGS_MAX_SIZE_BYTES: u64 = 1000000000; // 1GB
|
||||||
|
|
||||||
|
fn remove_old_logs(dir: &Path, log_files: Option<Vec<PathBuf>>) -> anyhow::Result<()> {
|
||||||
|
let log_files: Vec<PathBuf> = log_files
|
||||||
|
.map::<anyhow::Result<Vec<PathBuf>>, _>(Ok)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let mut files: Vec<PathBuf> = read_dir(dir)?
|
||||||
|
.filter_map(|de| {
|
||||||
|
let p = de.ok()?.path();
|
||||||
|
if p.is_file() && !p.is_symlink() {
|
||||||
|
Some(p)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
files.sort_unstable();
|
||||||
|
Ok(files)
|
||||||
|
})?;
|
||||||
|
let total_size = log_files
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| Some(p.metadata().ok()?.size()))
|
||||||
|
.reduce(u64::saturating_add)
|
||||||
|
.unwrap_or(0);
|
||||||
|
// if size is under threshold, finish
|
||||||
|
if total_size < LOGS_MAX_SIZE_BYTES {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// keep a minimum of 3 logs
|
||||||
|
if log_files.len() <= 3 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_file(log_files.first().ok_or_else(||
|
||||||
|
anyhow::Error::msg(
|
||||||
|
"Could not get first item in log files list, but they should be more than 3! This is a bug!"
|
||||||
|
)
|
||||||
|
)?)?;
|
||||||
|
|
||||||
|
remove_old_logs(dir, Some(log_files))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
@ -72,6 +121,30 @@ fn main() -> Result<()> {
|
||||||
panic!("{APP_NAME} cannot run as root");
|
panic!("{APP_NAME} cannot run as root");
|
||||||
}
|
}
|
||||||
restore_steam_xr_files();
|
restore_steam_xr_files();
|
||||||
|
// deferring error logging for this since tracing isn't initialized yet
|
||||||
|
let old_logs_removal_res = remove_old_logs(&get_logs_dir(), None);
|
||||||
|
|
||||||
|
let rolling_log_writer = tracing_appender::rolling::daily(get_logs_dir(), "log");
|
||||||
|
let (non_blocking_appender, _appender_guard) =
|
||||||
|
tracing_appender::non_blocking(rolling_log_writer);
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::layer().pretty().with_filter(
|
||||||
|
EnvFilter::builder()
|
||||||
|
.with_default_directive(LevelFilter::INFO.into())
|
||||||
|
.from_env_lossy(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.json()
|
||||||
|
.with_writer(non_blocking_appender),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
if let Err(e) = old_logs_removal_res {
|
||||||
|
error!("Failed to remove old log files: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare i18n
|
// Prepare i18n
|
||||||
gettextrs::setlocale(LocaleCategory::LcAll, "");
|
gettextrs::setlocale(LocaleCategory::LcAll, "");
|
||||||
|
@ -87,7 +160,7 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let provider = gtk::CssProvider::new();
|
let provider = gtk::CssProvider::new();
|
||||||
provider.load_from_resource(&format!("{}/style.css", RESOURCES_BASE_PATH));
|
provider.load_from_resource(&format!("{RESOURCES_BASE_PATH}/style.css"));
|
||||||
if let Some(display) = gdk::Display::default() {
|
if let Some(display) = gdk::Display::default() {
|
||||||
gtk::style_context_add_provider_for_display(
|
gtk::style_context_add_provider_for_display(
|
||||||
&display,
|
&display,
|
||||||
|
|
|
@ -3,7 +3,7 @@ config = configure_file(
|
||||||
output: 'constants.rs',
|
output: 'constants.rs',
|
||||||
configuration: global_conf
|
configuration: global_conf
|
||||||
)
|
)
|
||||||
# Copy the config.rs output to the source directory.
|
# Copy the constants.rs output to the source directory.
|
||||||
run_command(
|
run_command(
|
||||||
'cp',
|
'cp',
|
||||||
meson.project_build_root() / 'src' / 'constants.rs',
|
meson.project_build_root() / 'src' / 'constants.rs',
|
||||||
|
@ -43,3 +43,24 @@ cargo_build = custom_target(
|
||||||
'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@',
|
'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'cargo-fmt-check',
|
||||||
|
cargo,
|
||||||
|
args: ['fmt', '--all', '--check']
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'cargo-clippy',
|
||||||
|
cargo,
|
||||||
|
env: ['RUSTFLAGS=-Dwarnings'],
|
||||||
|
args: ['clippy', '--all-targets', '--all-features'],
|
||||||
|
timeout: 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'cargo-test',
|
||||||
|
cargo,
|
||||||
|
args: ['test'],
|
||||||
|
timeout: 0,
|
||||||
|
)
|
||||||
|
|
|
@ -17,7 +17,7 @@ pub fn is_openxr_ready() -> bool {
|
||||||
|
|
||||||
let Ok(xr_instance) = entry.create_instance(
|
let Ok(xr_instance) = entry.create_instance(
|
||||||
&xr::ApplicationInfo {
|
&xr::ApplicationInfo {
|
||||||
application_name: &format!("{}-openxr-prober", CMD_NAME),
|
application_name: &format!("{CMD_NAME}-openxr-prober"),
|
||||||
application_version: 0,
|
application_version: 0,
|
||||||
engine_name: CMD_NAME,
|
engine_name: CMD_NAME,
|
||||||
engine_version: 0,
|
engine_version: 0,
|
||||||
|
|
33
src/paths.rs
33
src/paths.rs
|
@ -1,4 +1,6 @@
|
||||||
use crate::{constants::CMD_NAME, xdg::XDG};
|
use anyhow::bail;
|
||||||
|
|
||||||
|
use crate::{constants::CMD_NAME, util::steam_library_folder::SteamLibraryFolder, xdg::XDG};
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
fs::create_dir_all,
|
fs::create_dir_all,
|
||||||
|
@ -54,6 +56,10 @@ pub fn get_cache_dir() -> PathBuf {
|
||||||
XDG.get_cache_home().join(CMD_NAME)
|
XDG.get_cache_home().join(CMD_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_logs_dir() -> PathBuf {
|
||||||
|
get_cache_dir().join("logs")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_backup_dir() -> PathBuf {
|
pub fn get_backup_dir() -> PathBuf {
|
||||||
let p = get_data_dir().join("backups");
|
let p = get_data_dir().join("backups");
|
||||||
if !p.is_dir() {
|
if !p.is_dir() {
|
||||||
|
@ -83,7 +89,26 @@ pub fn get_exec_prefix() -> PathBuf {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_steamvr_bin_dir_path() -> PathBuf {
|
const STEAMVR_STEAM_APPID: u32 = 250820;
|
||||||
XDG.get_data_home()
|
|
||||||
.join("Steam/steamapps/common/SteamVR/bin/linux64")
|
fn get_steamvr_base_dir() -> anyhow::Result<PathBuf> {
|
||||||
|
SteamLibraryFolder::get_folders()?
|
||||||
|
.into_iter()
|
||||||
|
.find(|(_, lf)| lf.apps.contains_key(&STEAMVR_STEAM_APPID))
|
||||||
|
.map(|(_, lf)| PathBuf::from(lf.path).join("steamapps/common/SteamVR"))
|
||||||
|
.ok_or(anyhow::Error::msg(
|
||||||
|
"Could not find SteamVR in Steam libraryfolders.vdf",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_steamvr_bin_dir_path() -> anyhow::Result<PathBuf> {
|
||||||
|
let res = get_steamvr_base_dir()?.join("bin/linux64");
|
||||||
|
if !res.is_dir() {
|
||||||
|
bail!("SteamVR bin dir `{}` does not exist", res.to_string_lossy());
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_plugins_dir() -> PathBuf {
|
||||||
|
get_data_dir().join("plugins")
|
||||||
}
|
}
|
||||||
|
|
220
src/profile.rs
220
src/profile.rs
|
@ -2,10 +2,12 @@ use crate::{
|
||||||
depcheck::{
|
depcheck::{
|
||||||
basalt_deps::get_missing_basalt_deps, libsurvive_deps::get_missing_libsurvive_deps,
|
basalt_deps::get_missing_basalt_deps, libsurvive_deps::get_missing_libsurvive_deps,
|
||||||
mercury_deps::get_missing_mercury_deps, monado_deps::get_missing_monado_deps,
|
mercury_deps::get_missing_mercury_deps, monado_deps::get_missing_monado_deps,
|
||||||
openhmd_deps::get_missing_openhmd_deps, wivrn_deps::get_missing_wivrn_deps, Dependency,
|
openhmd_deps::get_missing_openhmd_deps, wivrn_deps::get_missing_wivrn_deps,
|
||||||
|
xrizer_deps::get_missing_xrizer_deps, Dependency,
|
||||||
},
|
},
|
||||||
|
file_builders::active_runtime_json::ActiveRuntime,
|
||||||
paths::{get_data_dir, BWRAP_SYSTEM_PREFIX, SYSTEM_PREFIX},
|
paths::{get_data_dir, BWRAP_SYSTEM_PREFIX, SYSTEM_PREFIX},
|
||||||
util::file_utils::get_writer,
|
util::file_utils::{deserialize_file, get_writer},
|
||||||
xdg::XDG,
|
xdg::XDG,
|
||||||
};
|
};
|
||||||
use nix::NixPath;
|
use nix::NixPath;
|
||||||
|
@ -13,10 +15,11 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
fs::File,
|
fs::{remove_dir_all, File},
|
||||||
io::BufReader,
|
io::BufReader,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
slice::Iter,
|
slice::Iter,
|
||||||
|
str::FromStr,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -43,7 +46,14 @@ impl XRServiceType {
|
||||||
pub fn libmonado_path(&self) -> &'static str {
|
pub fn libmonado_path(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Monado => "libmonado.so",
|
Self::Monado => "libmonado.so",
|
||||||
Self::Wivrn => "wivrn/libmonado.so",
|
Self::Wivrn => "wivrn/libmonado_wivrn.so",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn openxr_json_rel_path(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Monado => "share/openxr/1/openxr_monado.json",
|
||||||
|
Self::Wivrn => "share/openxr/1/openxr_wivrn.json",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,6 +261,105 @@ impl Display for LighthouseDriver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
pub enum OvrCompatibilityModuleType {
|
||||||
|
#[default]
|
||||||
|
Opencomposite,
|
||||||
|
Xrizer,
|
||||||
|
Vapor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for OvrCompatibilityModuleType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::Opencomposite => "OpenComposite",
|
||||||
|
Self::Xrizer => "xrizer",
|
||||||
|
Self::Vapor => "VapoR",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OvrCompatibilityModuleType {
|
||||||
|
pub fn iter() -> Iter<'static, Self> {
|
||||||
|
[Self::Opencomposite, Self::Xrizer, Self::Vapor].iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_missing_deps(&self) -> Vec<Dependency> {
|
||||||
|
match self {
|
||||||
|
OvrCompatibilityModuleType::Xrizer => get_missing_xrizer_deps(),
|
||||||
|
OvrCompatibilityModuleType::Opencomposite | OvrCompatibilityModuleType::Vapor => {
|
||||||
|
Vec::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for OvrCompatibilityModuleType {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().trim() {
|
||||||
|
"opencomposite" => Ok(Self::Opencomposite),
|
||||||
|
"xrizer" => Ok(Self::Xrizer),
|
||||||
|
"vapor" => Ok(Self::Vapor),
|
||||||
|
_ => Err(format!("no match for ovr compatibility module `{s}`")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for OvrCompatibilityModuleType {
|
||||||
|
fn from(value: u32) -> Self {
|
||||||
|
match value {
|
||||||
|
0 => Self::Opencomposite,
|
||||||
|
1 => Self::Xrizer,
|
||||||
|
2 => Self::Vapor,
|
||||||
|
_ => panic!("OvrCompatibilityModuleType index out of bounds"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ProfileOvrCompatibilityModule {
|
||||||
|
pub mod_type: OvrCompatibilityModuleType,
|
||||||
|
pub repo: Option<String>,
|
||||||
|
pub branch: Option<String>,
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileOvrCompatibilityModule {
|
||||||
|
pub fn default_for_uuid(uuid: &str) -> Self {
|
||||||
|
let mod_type = OvrCompatibilityModuleType::default();
|
||||||
|
Self {
|
||||||
|
mod_type,
|
||||||
|
repo: None,
|
||||||
|
branch: None,
|
||||||
|
path: get_data_dir().join(uuid).join(mod_type.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the directory corresponding to the openvr runtime.
|
||||||
|
/// this should correspond to the build output directory
|
||||||
|
pub fn runtime_dir(&self) -> PathBuf {
|
||||||
|
match self.mod_type {
|
||||||
|
OvrCompatibilityModuleType::Opencomposite => self.path.join("build"),
|
||||||
|
OvrCompatibilityModuleType::Vapor => self.path.join("build/install_pfx/lib/VapoR"),
|
||||||
|
OvrCompatibilityModuleType::Xrizer => self.path.join("target/release"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProfileOvrCompatibilityModule {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mod_type = OvrCompatibilityModuleType::default();
|
||||||
|
Self {
|
||||||
|
mod_type,
|
||||||
|
repo: None,
|
||||||
|
branch: None,
|
||||||
|
path: get_data_dir().join("__envision__fallbackovrcomp"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct Profile {
|
pub struct Profile {
|
||||||
pub uuid: String,
|
pub uuid: String,
|
||||||
|
@ -261,9 +370,15 @@ pub struct Profile {
|
||||||
pub xrservice_branch: Option<String>,
|
pub xrservice_branch: Option<String>,
|
||||||
#[serde(default = "HashMap::<String, String>::default")]
|
#[serde(default = "HashMap::<String, String>::default")]
|
||||||
pub xrservice_cmake_flags: HashMap<String, String>,
|
pub xrservice_cmake_flags: HashMap<String, String>,
|
||||||
|
#[deprecated]
|
||||||
|
#[serde(default)]
|
||||||
pub opencomposite_path: PathBuf,
|
pub opencomposite_path: PathBuf,
|
||||||
|
#[deprecated]
|
||||||
pub opencomposite_repo: Option<String>,
|
pub opencomposite_repo: Option<String>,
|
||||||
|
#[deprecated]
|
||||||
pub opencomposite_branch: Option<String>,
|
pub opencomposite_branch: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ovr_comp: ProfileOvrCompatibilityModule,
|
||||||
pub features: ProfileFeatures,
|
pub features: ProfileFeatures,
|
||||||
pub environment: HashMap<String, String>,
|
pub environment: HashMap<String, String>,
|
||||||
/// Install prefix
|
/// Install prefix
|
||||||
|
@ -276,7 +391,6 @@ pub struct Profile {
|
||||||
pub lighthouse_driver: LighthouseDriver,
|
pub lighthouse_driver: LighthouseDriver,
|
||||||
#[serde(default = "String::default")]
|
#[serde(default = "String::default")]
|
||||||
pub xrservice_launch_options: String,
|
pub xrservice_launch_options: String,
|
||||||
pub autostart_command: Option<String>,
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub skip_dependency_check: bool,
|
pub skip_dependency_check: bool,
|
||||||
}
|
}
|
||||||
|
@ -288,6 +402,7 @@ impl Display for Profile {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Profile {
|
impl Default for Profile {
|
||||||
|
#[allow(deprecated)]
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let uuid = Self::new_uuid();
|
let uuid = Self::new_uuid();
|
||||||
let profile_dir = get_data_dir().join(&uuid);
|
let profile_dir = get_data_dir().join(&uuid);
|
||||||
|
@ -323,23 +438,50 @@ impl Default for Profile {
|
||||||
mercury_enabled: false,
|
mercury_enabled: false,
|
||||||
},
|
},
|
||||||
environment: HashMap::new(),
|
environment: HashMap::new(),
|
||||||
prefix: get_data_dir().join("prefixes").join(&uuid),
|
prefix: Self::default_prefix_path(&uuid),
|
||||||
can_be_built: true,
|
can_be_built: true,
|
||||||
pull_on_build: true,
|
pull_on_build: true,
|
||||||
opencomposite_path: profile_dir.join("opencomposite"),
|
opencomposite_path: profile_dir.join("opencomposite"),
|
||||||
opencomposite_repo: None,
|
opencomposite_repo: None,
|
||||||
opencomposite_branch: None,
|
opencomposite_branch: None,
|
||||||
|
ovr_comp: ProfileOvrCompatibilityModule::default_for_uuid(&uuid),
|
||||||
editable: true,
|
editable: true,
|
||||||
lighthouse_driver: LighthouseDriver::default(),
|
lighthouse_driver: LighthouseDriver::default(),
|
||||||
xrservice_launch_options: String::default(),
|
xrservice_launch_options: String::default(),
|
||||||
uuid,
|
uuid,
|
||||||
autostart_command: None,
|
|
||||||
skip_dependency_check: false,
|
skip_dependency_check: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Profile {
|
impl Profile {
|
||||||
|
fn default_prefix_path(uuid: &str) -> PathBuf {
|
||||||
|
get_data_dir().join("prefixes").join(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// deletes files and folders associated to this profile (mostly repo clones)
|
||||||
|
pub fn delete_files(&self) -> Vec<std::io::Result<()>> {
|
||||||
|
[
|
||||||
|
Some(&self.xrservice_path),
|
||||||
|
Some(&self.ovr_comp.path),
|
||||||
|
self.features.libsurvive.path.as_ref(),
|
||||||
|
self.features.basalt.path.as_ref(),
|
||||||
|
self.features.openhmd.path.as_ref(),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|dir| match dir {
|
||||||
|
Some(dir) => {
|
||||||
|
if dir.try_exists().unwrap_or_default() {
|
||||||
|
remove_dir_all(dir)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Ok(()),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn xr_runtime_json_env_var(&self) -> String {
|
pub fn xr_runtime_json_env_var(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
"XR_RUNTIME_JSON=\"{prefix}/share/openxr/1/openxr_{runtime}.json\"",
|
"XR_RUNTIME_JSON=\"{prefix}/share/openxr/1/openxr_{runtime}.json\"",
|
||||||
|
@ -358,8 +500,8 @@ impl Profile {
|
||||||
pub fn env_vars_full(&self) -> Vec<String> {
|
pub fn env_vars_full(&self) -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
// format!(
|
// format!(
|
||||||
// "VR_OVERRIDE={opencomp}/build",
|
// "VR_OVERRIDE={}",
|
||||||
// opencomp = self.opencomposite_path,
|
// self.ovr_comp.runtime_dir(),
|
||||||
// ),
|
// ),
|
||||||
self.xr_runtime_json_env_var(),
|
self.xr_runtime_json_env_var(),
|
||||||
format!(
|
format!(
|
||||||
|
@ -417,8 +559,8 @@ impl Profile {
|
||||||
}
|
}
|
||||||
let uuid = Self::new_uuid();
|
let uuid = Self::new_uuid();
|
||||||
let profile_dir = get_data_dir().join(&uuid);
|
let profile_dir = get_data_dir().join(&uuid);
|
||||||
|
#[allow(deprecated)]
|
||||||
let mut dup = Self {
|
let mut dup = Self {
|
||||||
uuid,
|
|
||||||
name: format!("Duplicate of {}", self.name),
|
name: format!("Duplicate of {}", self.name),
|
||||||
xrservice_type: self.xrservice_type.clone(),
|
xrservice_type: self.xrservice_type.clone(),
|
||||||
xrservice_repo: self.xrservice_repo.clone(),
|
xrservice_repo: self.xrservice_repo.clone(),
|
||||||
|
@ -450,7 +592,6 @@ impl Profile {
|
||||||
mercury_enabled: self.features.mercury_enabled,
|
mercury_enabled: self.features.mercury_enabled,
|
||||||
},
|
},
|
||||||
environment: self.environment.clone(),
|
environment: self.environment.clone(),
|
||||||
autostart_command: self.autostart_command.clone(),
|
|
||||||
pull_on_build: self.pull_on_build,
|
pull_on_build: self.pull_on_build,
|
||||||
lighthouse_driver: self.lighthouse_driver,
|
lighthouse_driver: self.lighthouse_driver,
|
||||||
opencomposite_repo: self.opencomposite_repo.clone(),
|
opencomposite_repo: self.opencomposite_repo.clone(),
|
||||||
|
@ -458,7 +599,16 @@ impl Profile {
|
||||||
opencomposite_path: profile_dir.join("opencomposite"),
|
opencomposite_path: profile_dir.join("opencomposite"),
|
||||||
skip_dependency_check: self.skip_dependency_check,
|
skip_dependency_check: self.skip_dependency_check,
|
||||||
xrservice_launch_options: self.xrservice_launch_options.clone(),
|
xrservice_launch_options: self.xrservice_launch_options.clone(),
|
||||||
..Default::default()
|
prefix: Self::default_prefix_path(&uuid),
|
||||||
|
ovr_comp: ProfileOvrCompatibilityModule {
|
||||||
|
mod_type: self.ovr_comp.mod_type,
|
||||||
|
repo: self.ovr_comp.repo.clone(),
|
||||||
|
branch: self.ovr_comp.branch.clone(),
|
||||||
|
path: profile_dir.join(self.ovr_comp.mod_type.to_string()),
|
||||||
|
},
|
||||||
|
can_be_built: self.can_be_built,
|
||||||
|
editable: true,
|
||||||
|
uuid,
|
||||||
};
|
};
|
||||||
if dup.environment.contains_key("LD_LIBRARY_PATH") {
|
if dup.environment.contains_key("LD_LIBRARY_PATH") {
|
||||||
dup.environment.insert(
|
dup.environment.insert(
|
||||||
|
@ -544,21 +694,37 @@ impl Profile {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// absolute path to a given shared object in the profile prefix
|
/// absolute path to a given shared object in the profile prefix
|
||||||
pub fn find_so(&self, rel_path: &str) -> Option<PathBuf> {
|
pub fn find_so<P: AsRef<Path>>(&self, rel_path: P) -> Option<PathBuf> {
|
||||||
["lib", "lib64"]
|
["lib", "lib64"]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|lib| self.prefix.join(lib).join(rel_path))
|
.map(|lib| self.prefix.join(lib).join(rel_path.as_ref()))
|
||||||
.find(|path| path.is_file())
|
.find(|path| path.is_file())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// absolute path to the libmonado shared object
|
/// absolute path to the libmonado shared object
|
||||||
pub fn libmonado_so(&self) -> Option<PathBuf> {
|
pub fn libmonado_so(&self) -> Option<PathBuf> {
|
||||||
self.find_so(self.xrservice_type.libmonado_path())
|
// try by reading the openxr json file
|
||||||
|
self.openxr_config()
|
||||||
|
.and_then(|conf| conf.runtime.libmonado_path)
|
||||||
|
.and_then(|libmonado_path| self.find_so(&libmonado_path))
|
||||||
|
.or_else(||
|
||||||
|
// try with the hardcoded paths
|
||||||
|
self.find_so(self.xrservice_type.libmonado_path()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openxr_config(&self) -> Option<ActiveRuntime> {
|
||||||
|
deserialize_file(&self.openxr_json_path())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// absolute path to the libopenxr shared object
|
/// absolute path to the libopenxr shared object
|
||||||
pub fn libopenxr_so(&self) -> Option<PathBuf> {
|
pub fn libopenxr_so(&self) -> Option<PathBuf> {
|
||||||
self.find_so(self.xrservice_type.libopenxr_path())
|
// try by reading the openxr json file
|
||||||
|
self.openxr_config()
|
||||||
|
.map(|conf| conf.runtime.library_path)
|
||||||
|
.and_then(|libmonado_path| self.find_so(&libmonado_path))
|
||||||
|
.or_else(||
|
||||||
|
// try with the hardcoded paths
|
||||||
|
self.find_so(self.xrservice_type.libopenxr_path()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn missing_dependencies(&self) -> Vec<Dependency> {
|
pub fn missing_dependencies(&self) -> Vec<Dependency> {
|
||||||
|
@ -580,12 +746,18 @@ impl Profile {
|
||||||
if self.features.mercury_enabled {
|
if self.features.mercury_enabled {
|
||||||
missing_deps.extend(get_missing_mercury_deps());
|
missing_deps.extend(get_missing_mercury_deps());
|
||||||
}
|
}
|
||||||
// no listed deps for opencomp
|
missing_deps.extend(self.ovr_comp.mod_type.get_missing_deps());
|
||||||
}
|
}
|
||||||
missing_deps.sort_unstable();
|
missing_deps.sort_unstable();
|
||||||
missing_deps.dedup(); // dedup only works if sorted, hence the above
|
missing_deps.dedup(); // dedup only works if sorted, hence the above
|
||||||
missing_deps
|
missing_deps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// the file that will become active_runtime.json, as installed in the
|
||||||
|
/// prefix
|
||||||
|
pub fn openxr_json_path(&self) -> PathBuf {
|
||||||
|
self.prefix.join(self.xrservice_type.openxr_json_rel_path())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepare_ld_library_path(prefix: &Path) -> String {
|
pub fn prepare_ld_library_path(prefix: &Path) -> String {
|
||||||
|
@ -599,7 +771,10 @@ mod tests {
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::profile::{ProfileFeature, ProfileFeatureType, ProfileFeatures, XRServiceType};
|
use crate::profile::{
|
||||||
|
OvrCompatibilityModuleType, ProfileFeature, ProfileFeatureType, ProfileFeatures,
|
||||||
|
ProfileOvrCompatibilityModule, XRServiceType,
|
||||||
|
};
|
||||||
|
|
||||||
use super::Profile;
|
use super::Profile;
|
||||||
|
|
||||||
|
@ -609,7 +784,7 @@ mod tests {
|
||||||
assert_eq!(profile.name, "Demo profile");
|
assert_eq!(profile.name, "Demo profile");
|
||||||
assert_eq!(profile.xrservice_path, PathBuf::from("/home/user/monado"));
|
assert_eq!(profile.xrservice_path, PathBuf::from("/home/user/monado"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
profile.opencomposite_path,
|
profile.ovr_comp.path,
|
||||||
PathBuf::from("/home/user/opencomposite")
|
PathBuf::from("/home/user/opencomposite")
|
||||||
);
|
);
|
||||||
assert_eq!(profile.prefix, PathBuf::from("/home/user/envisionprefix"));
|
assert_eq!(profile.prefix, PathBuf::from("/home/user/envisionprefix"));
|
||||||
|
@ -640,7 +815,12 @@ mod tests {
|
||||||
name: "Demo profile".into(),
|
name: "Demo profile".into(),
|
||||||
xrservice_path: PathBuf::from("/home/user/monado"),
|
xrservice_path: PathBuf::from("/home/user/monado"),
|
||||||
xrservice_type: XRServiceType::Monado,
|
xrservice_type: XRServiceType::Monado,
|
||||||
opencomposite_path: PathBuf::from("/home/user/opencomposite"),
|
ovr_comp: ProfileOvrCompatibilityModule {
|
||||||
|
path: PathBuf::from("/home/user/opencomposite"),
|
||||||
|
repo: None,
|
||||||
|
branch: None,
|
||||||
|
mod_type: OvrCompatibilityModuleType::default(),
|
||||||
|
},
|
||||||
features: ProfileFeatures {
|
features: ProfileFeatures {
|
||||||
libsurvive: ProfileFeature {
|
libsurvive: ProfileFeature {
|
||||||
feature_type: ProfileFeatureType::Libsurvive,
|
feature_type: ProfileFeatureType::Libsurvive,
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::APP_NAME,
|
constants::APP_NAME,
|
||||||
paths::{data_monado_path, data_opencomposite_path, get_data_dir},
|
paths::{data_monado_path, data_opencomposite_path, get_data_dir},
|
||||||
profile::{prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeatures, XRServiceType},
|
profile::{
|
||||||
|
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeatures,
|
||||||
|
ProfileOvrCompatibilityModule, XRServiceType,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
@ -18,10 +21,13 @@ pub fn lighthouse_profile() -> Profile {
|
||||||
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
|
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
|
||||||
Profile {
|
Profile {
|
||||||
uuid: "lighthouse-default".into(),
|
uuid: "lighthouse-default".into(),
|
||||||
name: format!("Lighthouse Driver - {name} Default", name = APP_NAME),
|
name: format!("Lighthouse Driver - {APP_NAME} Default"),
|
||||||
xrservice_path: data_monado_path(),
|
xrservice_path: data_monado_path(),
|
||||||
xrservice_type: XRServiceType::Monado,
|
xrservice_type: XRServiceType::Monado,
|
||||||
opencomposite_path: data_opencomposite_path(),
|
ovr_comp: ProfileOvrCompatibilityModule {
|
||||||
|
path: data_opencomposite_path(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
features: ProfileFeatures::default(),
|
features: ProfileFeatures::default(),
|
||||||
environment,
|
environment,
|
||||||
prefix,
|
prefix,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
paths::{data_monado_path, data_opencomposite_path, data_openhmd_path, get_data_dir},
|
paths::{data_monado_path, data_opencomposite_path, data_openhmd_path, get_data_dir},
|
||||||
profile::{
|
profile::{
|
||||||
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
|
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
|
||||||
ProfileFeatures, XRServiceType,
|
ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -21,10 +21,13 @@ pub fn openhmd_profile() -> Profile {
|
||||||
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
|
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
|
||||||
Profile {
|
Profile {
|
||||||
uuid: "openhmd-default".into(),
|
uuid: "openhmd-default".into(),
|
||||||
name: format!("OpenHMD - {name} Default", name = APP_NAME),
|
name: format!("OpenHMD - {APP_NAME} Default"),
|
||||||
xrservice_path: data_monado_path(),
|
xrservice_path: data_monado_path(),
|
||||||
xrservice_type: XRServiceType::Monado,
|
xrservice_type: XRServiceType::Monado,
|
||||||
opencomposite_path: data_opencomposite_path(),
|
ovr_comp: ProfileOvrCompatibilityModule {
|
||||||
|
path: data_opencomposite_path(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
features: ProfileFeatures {
|
features: ProfileFeatures {
|
||||||
openhmd: ProfileFeature {
|
openhmd: ProfileFeature {
|
||||||
feature_type: ProfileFeatureType::OpenHmd,
|
feature_type: ProfileFeatureType::OpenHmd,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::APP_NAME,
|
constants::APP_NAME,
|
||||||
paths::{data_monado_path, data_opencomposite_path, get_data_dir},
|
paths::{data_monado_path, data_opencomposite_path, get_data_dir},
|
||||||
profile::{Profile, ProfileFeatures, XRServiceType},
|
profile::{Profile, ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType},
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
@ -22,10 +22,13 @@ pub fn simulated_profile() -> Profile {
|
||||||
);
|
);
|
||||||
Profile {
|
Profile {
|
||||||
uuid: "simulated-default".into(),
|
uuid: "simulated-default".into(),
|
||||||
name: format!("Simulated Driver - {name} Default", name = APP_NAME),
|
name: format!("Simulated Driver - {APP_NAME} Default"),
|
||||||
xrservice_path: data_monado_path(),
|
xrservice_path: data_monado_path(),
|
||||||
xrservice_type: XRServiceType::Monado,
|
xrservice_type: XRServiceType::Monado,
|
||||||
opencomposite_path: data_opencomposite_path(),
|
ovr_comp: ProfileOvrCompatibilityModule {
|
||||||
|
path: data_opencomposite_path(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
features: ProfileFeatures::default(),
|
features: ProfileFeatures::default(),
|
||||||
environment,
|
environment,
|
||||||
prefix,
|
prefix,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
paths::{data_libsurvive_path, data_monado_path, data_opencomposite_path, get_data_dir},
|
paths::{data_libsurvive_path, data_monado_path, data_opencomposite_path, get_data_dir},
|
||||||
profile::{
|
profile::{
|
||||||
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
|
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
|
||||||
ProfileFeatures, XRServiceType,
|
ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -23,10 +23,13 @@ pub fn survive_profile() -> Profile {
|
||||||
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
|
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
|
||||||
Profile {
|
Profile {
|
||||||
uuid: "survive-default".into(),
|
uuid: "survive-default".into(),
|
||||||
name: format!("Survive - {name} Default", name = APP_NAME),
|
name: format!("Survive - {APP_NAME} Default"),
|
||||||
xrservice_path: data_monado_path(),
|
xrservice_path: data_monado_path(),
|
||||||
xrservice_type: XRServiceType::Monado,
|
xrservice_type: XRServiceType::Monado,
|
||||||
opencomposite_path: data_opencomposite_path(),
|
ovr_comp: ProfileOvrCompatibilityModule {
|
||||||
|
path: data_opencomposite_path(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
features: ProfileFeatures {
|
features: ProfileFeatures {
|
||||||
libsurvive: ProfileFeature {
|
libsurvive: ProfileFeature {
|
||||||
feature_type: ProfileFeatureType::Libsurvive,
|
feature_type: ProfileFeatureType::Libsurvive,
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::APP_NAME,
|
constants::APP_NAME,
|
||||||
paths::{data_opencomposite_path, data_wivrn_path, get_data_dir},
|
paths::{data_opencomposite_path, data_wivrn_path, get_data_dir},
|
||||||
profile::{prepare_ld_library_path, Profile, ProfileFeatures, XRServiceType},
|
profile::{
|
||||||
|
prepare_ld_library_path, Profile, ProfileFeatures, ProfileOvrCompatibilityModule,
|
||||||
|
XRServiceType,
|
||||||
|
},
|
||||||
|
ui::job_worker::internal_worker::LAUNCH_OPTS_CMD_PLACEHOLDER,
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
@ -15,13 +19,19 @@ pub fn wivrn_profile() -> Profile {
|
||||||
environment.insert("U_PACING_APP_USE_MIN_FRAME_PERIOD".into(), "1".into());
|
environment.insert("U_PACING_APP_USE_MIN_FRAME_PERIOD".into(), "1".into());
|
||||||
Profile {
|
Profile {
|
||||||
uuid: "wivrn-default".into(),
|
uuid: "wivrn-default".into(),
|
||||||
name: format!("WiVRn - {name} Default", name = APP_NAME),
|
name: format!("WiVRn - {APP_NAME} Default"),
|
||||||
xrservice_path: data_wivrn_path(),
|
xrservice_path: data_wivrn_path(),
|
||||||
xrservice_type: XRServiceType::Wivrn,
|
xrservice_type: XRServiceType::Wivrn,
|
||||||
opencomposite_path: data_opencomposite_path(),
|
ovr_comp: ProfileOvrCompatibilityModule {
|
||||||
|
path: data_opencomposite_path(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
features: ProfileFeatures {
|
features: ProfileFeatures {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
xrservice_launch_options: format!(
|
||||||
|
"{LAUNCH_OPTS_CMD_PLACEHOLDER} --no-instructions --no-manage-active-runtime"
|
||||||
|
),
|
||||||
environment,
|
environment,
|
||||||
prefix,
|
prefix,
|
||||||
can_be_built: true,
|
can_be_built: true,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
paths::{data_basalt_path, data_monado_path, data_opencomposite_path, get_data_dir},
|
paths::{data_basalt_path, data_monado_path, data_opencomposite_path, get_data_dir},
|
||||||
profile::{
|
profile::{
|
||||||
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
|
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
|
||||||
ProfileFeatures, XRServiceType,
|
ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -21,10 +21,13 @@ pub fn wmr_profile() -> Profile {
|
||||||
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
|
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
|
||||||
Profile {
|
Profile {
|
||||||
uuid: "wmr-default".into(),
|
uuid: "wmr-default".into(),
|
||||||
name: format!("WMR - {name} Default", name = APP_NAME),
|
name: format!("WMR - {APP_NAME} Default"),
|
||||||
xrservice_path: data_monado_path(),
|
xrservice_path: data_monado_path(),
|
||||||
xrservice_type: XRServiceType::Monado,
|
xrservice_type: XRServiceType::Monado,
|
||||||
opencomposite_path: data_opencomposite_path(),
|
ovr_comp: ProfileOvrCompatibilityModule {
|
||||||
|
path: data_opencomposite_path(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
features: ProfileFeatures {
|
features: ProfileFeatures {
|
||||||
basalt: ProfileFeature {
|
basalt: ProfileFeature {
|
||||||
feature_type: ProfileFeatureType::Basalt,
|
feature_type: ProfileFeatureType::Basalt,
|
||||||
|
|
|
@ -1,101 +1,99 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
paths::{get_backup_dir, get_home_dir},
|
paths::get_backup_dir,
|
||||||
profile::Profile,
|
profile::Profile,
|
||||||
util::file_utils::{copy_file, get_writer},
|
util::{
|
||||||
|
file_utils::{copy_file, get_writer, mark_as_executable},
|
||||||
|
steam_library_folder::SteamLibraryFolder,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde::Deserialize;
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
|
||||||
fs::read_to_string,
|
fs::read_to_string,
|
||||||
io::Write,
|
io::Write,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
pub const SNIPER_RUNTIME_STEAM_APPID: u32 = 1628350;
|
||||||
struct LibraryFolder {
|
pub const SOLDIER_RUNTIME_STEAM_APPID: u32 = 1391110;
|
||||||
pub path: String,
|
|
||||||
pub apps: HashMap<u32, usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const PRESSURE_VESSEL_STEAM_APPID: u32 = 1628350;
|
fn get_sniper_runtime_entrypoint_path() -> Option<PathBuf> {
|
||||||
|
match SteamLibraryFolder::get_folders() {
|
||||||
fn get_steam_main_dir_path() -> anyhow::Result<PathBuf> {
|
Ok(libraryfolders) => libraryfolders
|
||||||
let steam_root: PathBuf = get_home_dir().join(".steam/root");
|
|
||||||
|
|
||||||
if steam_root.is_symlink() {
|
|
||||||
Ok(steam_root.read_link()?)
|
|
||||||
} else if steam_root.is_dir() {
|
|
||||||
Ok(steam_root)
|
|
||||||
} else {
|
|
||||||
bail!(
|
|
||||||
"Canonical steam root '{}' is not a dir nor a symlink!",
|
|
||||||
steam_root.to_string_lossy()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_steam_libraryfolders_vdf(path: &Path) -> anyhow::Result<HashMap<u32, LibraryFolder>> {
|
|
||||||
Ok(keyvalues_serde::from_str(read_to_string(path)?.as_str())?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_runtime_entrypoint_path() -> Option<PathBuf> {
|
|
||||||
match get_steam_main_dir_path() {
|
|
||||||
Ok(steam_root) => {
|
|
||||||
let steam_libraryfolders_path = steam_root.join("steamapps/libraryfolders.vdf");
|
|
||||||
|
|
||||||
if !steam_libraryfolders_path.is_file() {
|
|
||||||
eprintln!(
|
|
||||||
"Steam libraryfolders.vdf does not exist in its canonical location {}",
|
|
||||||
steam_libraryfolders_path.to_string_lossy()
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let libraryfolders: HashMap<u32, LibraryFolder> =
|
|
||||||
parse_steam_libraryfolders_vdf(&steam_libraryfolders_path).ok()?;
|
|
||||||
|
|
||||||
libraryfolders
|
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, libraryfolder)| {
|
.find(|(_, folder)| folder.apps.contains_key(&SNIPER_RUNTIME_STEAM_APPID))
|
||||||
libraryfolder
|
.map(|(_, folder)| {
|
||||||
.apps
|
PathBuf::from(&folder.path)
|
||||||
.contains_key(&PRESSURE_VESSEL_STEAM_APPID)
|
|
||||||
})
|
|
||||||
.map(|(_, libraryfolder)| {
|
|
||||||
PathBuf::from(&libraryfolder.path)
|
|
||||||
.join("steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point")
|
.join("steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point")
|
||||||
})
|
}),
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error getting steam root path: {e}");
|
error!("unable to get sniper runtime entrypoint path: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_soldier_runtime_entrypoint_path() -> Option<PathBuf> {
|
||||||
|
match SteamLibraryFolder::get_folders() {
|
||||||
|
Ok(libraryfolders) => libraryfolders
|
||||||
|
.iter()
|
||||||
|
.find(|(_, folder)| folder.apps.contains_key(&SOLDIER_RUNTIME_STEAM_APPID))
|
||||||
|
.map(|(_, folder)| {
|
||||||
|
PathBuf::from(&folder.path)
|
||||||
|
.join("steamapps/common/SteamLinuxRuntime_soldier/_v2-entry-point")
|
||||||
|
}),
|
||||||
|
Err(e) => {
|
||||||
|
error!("unable to get soldier runtime entrypoint path: {e}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref STEAM_RUNTIME_ENTRYPOINT_PATH: Option<PathBuf> = get_runtime_entrypoint_path();
|
static ref STEAM_SNIPER_RUNTIME_ENTRYPOINT_PATH: Option<PathBuf> =
|
||||||
|
get_sniper_runtime_entrypoint_path();
|
||||||
|
static ref STEAM_SOLDIER_RUNTIME_ENTRYPOINT_PATH: Option<PathBuf> =
|
||||||
|
get_soldier_runtime_entrypoint_path();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_backup_runtime_entrypoint_location() -> PathBuf {
|
fn get_backup_sniper_runtime_entrypoint_location() -> PathBuf {
|
||||||
get_backup_dir().join("_v2-entry-point.bak")
|
get_backup_dir().join("_v2-entry-point.bak")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn backup_runtime_entrypoint(path: &Path) {
|
fn get_backup_soldier_runtime_entrypoint_location() -> PathBuf {
|
||||||
copy_file(path, &get_backup_runtime_entrypoint_location());
|
get_backup_dir().join("_v2-entry-point.soldier.bak")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restore_runtime_entrypoint() {
|
fn backup_sniper_runtime_entrypoint(path: &Path) {
|
||||||
if let Some(path) = STEAM_RUNTIME_ENTRYPOINT_PATH.as_ref() {
|
copy_file(path, &get_backup_sniper_runtime_entrypoint_location());
|
||||||
let backup = get_backup_runtime_entrypoint_location();
|
}
|
||||||
|
|
||||||
|
fn backup_soldier_runtime_entrypoint(path: &Path) {
|
||||||
|
copy_file(path, &get_backup_soldier_runtime_entrypoint_location());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_sniper_runtime_entrypoint() {
|
||||||
|
if let Some(path) = STEAM_SNIPER_RUNTIME_ENTRYPOINT_PATH.as_ref() {
|
||||||
|
let backup = get_backup_sniper_runtime_entrypoint_location();
|
||||||
if Path::new(&backup).is_file() {
|
if Path::new(&backup).is_file() {
|
||||||
copy_file(&backup, path);
|
copy_file(&backup, path);
|
||||||
|
let _ = mark_as_executable(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn restore_soldier_runtime_entrypoint() {
|
||||||
|
if let Some(path) = STEAM_SOLDIER_RUNTIME_ENTRYPOINT_PATH.as_ref() {
|
||||||
|
let backup = get_backup_soldier_runtime_entrypoint_location();
|
||||||
|
if Path::new(&backup).is_file() {
|
||||||
|
copy_file(&backup, path);
|
||||||
|
let _ = mark_as_executable(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// this implementation is identical for both sniper and soldier runtimes
|
||||||
fn append_to_runtime_entrypoint(data: &str, path: &Path) -> anyhow::Result<()> {
|
fn append_to_runtime_entrypoint(data: &str, path: &Path) -> anyhow::Result<()> {
|
||||||
let existing = read_to_string(path)?;
|
let existing = read_to_string(path)?;
|
||||||
let new = existing.replace(
|
let new = existing.replace(
|
||||||
|
@ -107,10 +105,12 @@ fn append_to_runtime_entrypoint(data: &str, path: &Path) -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_runtime_entrypoint_launch_opts_from_profile(profile: &Profile) -> anyhow::Result<()> {
|
pub fn set_sniper_runtime_entrypoint_launch_opts_from_profile(
|
||||||
restore_runtime_entrypoint();
|
profile: &Profile,
|
||||||
if let Some(dest) = STEAM_RUNTIME_ENTRYPOINT_PATH.as_ref() {
|
) -> anyhow::Result<()> {
|
||||||
backup_runtime_entrypoint(dest);
|
restore_sniper_runtime_entrypoint();
|
||||||
|
if let Some(dest) = STEAM_SNIPER_RUNTIME_ENTRYPOINT_PATH.as_ref() {
|
||||||
|
backup_sniper_runtime_entrypoint(dest);
|
||||||
append_to_runtime_entrypoint(
|
append_to_runtime_entrypoint(
|
||||||
&profile
|
&profile
|
||||||
.get_env_vars()
|
.get_env_vars()
|
||||||
|
@ -120,27 +120,31 @@ pub fn set_runtime_entrypoint_launch_opts_from_profile(profile: &Profile) -> any
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
dest,
|
dest,
|
||||||
)?;
|
)?;
|
||||||
|
mark_as_executable(dest)?;
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
bail!("Could not find valid runtime entrypoint");
|
bail!("Could not find valid sniper runtime entrypoint");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
pub fn set_soldier_runtime_entrypoint_launch_opts_from_profile(
|
||||||
mod tests {
|
profile: &Profile,
|
||||||
use std::path::Path;
|
) -> anyhow::Result<()> {
|
||||||
|
restore_soldier_runtime_entrypoint();
|
||||||
|
if let Some(dest) = STEAM_SOLDIER_RUNTIME_ENTRYPOINT_PATH.as_ref() {
|
||||||
|
backup_soldier_runtime_entrypoint(dest);
|
||||||
|
append_to_runtime_entrypoint(
|
||||||
|
&profile
|
||||||
|
.get_env_vars()
|
||||||
|
.iter()
|
||||||
|
.map(|ev| "export ".to_string() + ev)
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n"),
|
||||||
|
dest,
|
||||||
|
)?;
|
||||||
|
mark_as_executable(dest)?;
|
||||||
|
|
||||||
use super::parse_steam_libraryfolders_vdf;
|
return Ok(());
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize_steam_libraryfolders_vdf() {
|
|
||||||
let lf = parse_steam_libraryfolders_vdf(Path::new("./test/files/steam_libraryfolders.vdf"))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(lf.len(), 1);
|
|
||||||
let first = lf.get(&0).unwrap();
|
|
||||||
assert_eq!(first.path, "/home/gabmus/.local/share/Steam");
|
|
||||||
assert_eq!(first.apps.len(), 10);
|
|
||||||
assert_eq!(first.apps.get(&228980).unwrap(), &29212173);
|
|
||||||
assert_eq!(first.apps.get(&632360).unwrap(), &0);
|
|
||||||
}
|
}
|
||||||
|
bail!("Could not find valid soldier runtime entrypoint");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::{
|
constants::{
|
||||||
get_artists, get_developers, APP_ID, APP_NAME, BUILD_DATETIME, ISSUES_URL, REPO_URL,
|
get_artists, APP_ID, APP_NAME, BUILD_DATETIME, ISSUES_URL, REPO_URL, SINGLE_DEVELOPER,
|
||||||
SINGLE_DEVELOPER, VERSION,
|
VERSION,
|
||||||
},
|
},
|
||||||
device_prober::PhysicalXRDevice,
|
device_prober::PhysicalXRDevice,
|
||||||
linux_distro::LinuxDistro,
|
linux_distro::LinuxDistro,
|
||||||
|
@ -20,13 +20,20 @@ pub fn create_about_dialog() -> adw::AboutDialog {
|
||||||
.website(REPO_URL)
|
.website(REPO_URL)
|
||||||
.issue_url(ISSUES_URL)
|
.issue_url(ISSUES_URL)
|
||||||
.developer_name(SINGLE_DEVELOPER)
|
.developer_name(SINGLE_DEVELOPER)
|
||||||
.developers(get_developers())
|
.developers(
|
||||||
|
env!("CARGO_PKG_AUTHORS")
|
||||||
|
.split(':')
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect::<Vec<String>>(),
|
||||||
|
)
|
||||||
.artists(get_artists())
|
.artists(get_artists())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UNKNOWN: &str = "UNKNOWN";
|
||||||
|
|
||||||
pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo>) {
|
pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo>) {
|
||||||
if dialog.debug_info().len() > 0 {
|
if !dialog.debug_info().is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let distro_family = LinuxDistro::get();
|
let distro_family = LinuxDistro::get();
|
||||||
|
@ -37,10 +44,10 @@ pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo
|
||||||
format!("Build time: {BUILD_DATETIME}"),
|
format!("Build time: {BUILD_DATETIME}"),
|
||||||
format!(
|
format!(
|
||||||
"Operating system: {d} ({f})",
|
"Operating system: {d} ({f})",
|
||||||
d = distro.unwrap_or("unknown".into()),
|
d = distro.unwrap_or(UNKNOWN.into()),
|
||||||
f = distro_family
|
f = distro_family
|
||||||
.map(|f| f.to_string())
|
.map(|f| f.to_string())
|
||||||
.unwrap_or("unknown".into())
|
.unwrap_or(UNKNOWN.into())
|
||||||
),
|
),
|
||||||
format!(
|
format!(
|
||||||
"Kernel: {}",
|
"Kernel: {}",
|
||||||
|
@ -50,23 +57,29 @@ pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo
|
||||||
),
|
),
|
||||||
format!(
|
format!(
|
||||||
"Session type: {}",
|
"Session type: {}",
|
||||||
env::var("XDG_SESSION_TYPE").unwrap_or("unknown".into())
|
env::var("XDG_SESSION_TYPE").unwrap_or(UNKNOWN.into())
|
||||||
),
|
),
|
||||||
format!(
|
format!(
|
||||||
"Desktop: {}",
|
"Desktop: {}",
|
||||||
env::var("XDG_CURRENT_DESKTOP").unwrap_or("unknown".into())
|
env::var("XDG_CURRENT_DESKTOP").unwrap_or(UNKNOWN.into())
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"CPU: {}",
|
||||||
|
read_to_string("/proc/cpuinfo")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| {
|
||||||
|
s.split("\n")
|
||||||
|
.find(|line| line.starts_with("model name"))
|
||||||
|
.map(|line| line.split(':').next_back().map(|s| s.trim().to_string()))
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or(UNKNOWN.into())
|
||||||
),
|
),
|
||||||
format!(
|
format!(
|
||||||
"GPUs: {}",
|
"GPUs: {}",
|
||||||
vkinfo
|
vkinfo
|
||||||
.map(|i| i.gpu_names.join(", "))
|
.map(|i| i.gpu_names.join(", "))
|
||||||
.unwrap_or("unknown".into())
|
.unwrap_or(UNKNOWN.into())
|
||||||
),
|
|
||||||
format!(
|
|
||||||
"Monado Vulkan Layers: {}",
|
|
||||||
vkinfo
|
|
||||||
.map(|i| i.has_monado_vulkan_layers.to_string())
|
|
||||||
.unwrap_or("unknown".into())
|
|
||||||
),
|
),
|
||||||
format!("Detected XR Devices: {}", {
|
format!("Detected XR Devices: {}", {
|
||||||
let devs = PhysicalXRDevice::from_usb();
|
let devs = PhysicalXRDevice::from_usb();
|
||||||
|
|
323
src/ui/app.rs
323
src/ui/app.rs
|
@ -11,6 +11,7 @@ use super::{
|
||||||
},
|
},
|
||||||
libsurvive_setup_window::{LibsurviveSetupMsg, LibsurviveSetupWindow},
|
libsurvive_setup_window::{LibsurviveSetupMsg, LibsurviveSetupWindow},
|
||||||
main_view::{MainView, MainViewInit, MainViewMsg, MainViewOutMsg},
|
main_view::{MainView, MainViewInit, MainViewMsg, MainViewOutMsg},
|
||||||
|
plugins::store::{PluginStore, PluginStoreInit, PluginStoreMsg, PluginStoreOutMsg},
|
||||||
util::{copiable_code_snippet, copy_text, open_with_default_handler},
|
util::{copiable_code_snippet, copy_text, open_with_default_handler},
|
||||||
wivrn_conf_editor::{WivrnConfEditor, WivrnConfEditorInit, WivrnConfEditorMsg},
|
wivrn_conf_editor::{WivrnConfEditor, WivrnConfEditorInit, WivrnConfEditorMsg},
|
||||||
};
|
};
|
||||||
|
@ -19,14 +20,16 @@ use crate::{
|
||||||
build_basalt::get_build_basalt_jobs, build_libsurvive::get_build_libsurvive_jobs,
|
build_basalt::get_build_basalt_jobs, build_libsurvive::get_build_libsurvive_jobs,
|
||||||
build_mercury::get_build_mercury_jobs, build_monado::get_build_monado_jobs,
|
build_mercury::get_build_mercury_jobs, build_monado::get_build_monado_jobs,
|
||||||
build_opencomposite::get_build_opencomposite_jobs, build_openhmd::get_build_openhmd_jobs,
|
build_opencomposite::get_build_opencomposite_jobs, build_openhmd::get_build_openhmd_jobs,
|
||||||
build_wivrn::get_build_wivrn_jobs,
|
build_vapor::get_build_vapor_jobs, build_wivrn::get_build_wivrn_jobs,
|
||||||
|
build_xrizer::get_build_xrizer_jobs,
|
||||||
},
|
},
|
||||||
config::Config,
|
config::{Config, PluginConfig},
|
||||||
constants::APP_NAME,
|
constants::APP_NAME,
|
||||||
depcheck::common::dep_pkexec,
|
depcheck::common::dep_pkexec,
|
||||||
file_builders::{
|
file_builders::{
|
||||||
active_runtime_json::{
|
active_runtime_json::{
|
||||||
set_current_active_runtime_to_profile, set_current_active_runtime_to_steam,
|
remove_current_active_runtime, restore_active_runtime_backup,
|
||||||
|
set_current_active_runtime_to_profile,
|
||||||
},
|
},
|
||||||
openvrpaths_vrpath::{
|
openvrpaths_vrpath::{
|
||||||
set_current_openvrpaths_to_profile, set_current_openvrpaths_to_steam,
|
set_current_openvrpaths_to_profile, set_current_openvrpaths_to_steam,
|
||||||
|
@ -35,17 +38,23 @@ use crate::{
|
||||||
linux_distro::LinuxDistro,
|
linux_distro::LinuxDistro,
|
||||||
openxr_prober::is_openxr_ready,
|
openxr_prober::is_openxr_ready,
|
||||||
paths::get_data_dir,
|
paths::get_data_dir,
|
||||||
profile::{Profile, XRServiceType},
|
profile::{OvrCompatibilityModuleType, Profile, XRServiceType},
|
||||||
stateless_action,
|
stateless_action,
|
||||||
steam_linux_runtime_injector::{
|
steam_linux_runtime_injector::{
|
||||||
restore_runtime_entrypoint, set_runtime_entrypoint_launch_opts_from_profile,
|
restore_sniper_runtime_entrypoint, restore_soldier_runtime_entrypoint,
|
||||||
|
set_sniper_runtime_entrypoint_launch_opts_from_profile,
|
||||||
|
set_soldier_runtime_entrypoint_launch_opts_from_profile,
|
||||||
|
},
|
||||||
|
termcolor::TermColor,
|
||||||
|
util::file_utils::{
|
||||||
|
setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd, verify_cap_sys_nice_eip,
|
||||||
},
|
},
|
||||||
util::file_utils::{setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd},
|
|
||||||
vulkaninfo::VulkanInfo,
|
vulkaninfo::VulkanInfo,
|
||||||
wivrn_dbus,
|
wivrn_dbus,
|
||||||
xr_devices::XRDevice,
|
xr_devices::XRDevice,
|
||||||
};
|
};
|
||||||
use adw::{prelude::*, ResponseAppearance};
|
use adw::{prelude::*, ResponseAppearance};
|
||||||
|
use delicious_adwaita::{theme::Theme, ThemeEngine};
|
||||||
use gtk::glib::{self, clone};
|
use gtk::glib::{self, clone};
|
||||||
use notify_rust::NotificationHandle;
|
use notify_rust::NotificationHandle;
|
||||||
use relm4::{
|
use relm4::{
|
||||||
|
@ -53,7 +62,12 @@ use relm4::{
|
||||||
new_action_group, new_stateful_action, new_stateless_action,
|
new_action_group, new_stateful_action, new_stateless_action,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use std::{collections::VecDeque, fs::remove_file, time::Duration};
|
use std::{
|
||||||
|
collections::{HashMap, VecDeque},
|
||||||
|
fs::remove_file,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
application: adw::Application,
|
application: adw::Application,
|
||||||
|
@ -70,7 +84,7 @@ pub struct App {
|
||||||
|
|
||||||
config: Config,
|
config: Config,
|
||||||
xrservice_worker: Option<JobWorker>,
|
xrservice_worker: Option<JobWorker>,
|
||||||
autostart_worker: Option<JobWorker>,
|
plugins_worker: Option<JobWorker>,
|
||||||
restart_xrservice: bool,
|
restart_xrservice: bool,
|
||||||
build_worker: Option<JobWorker>,
|
build_worker: Option<JobWorker>,
|
||||||
profiles: Vec<Profile>,
|
profiles: Vec<Profile>,
|
||||||
|
@ -85,13 +99,16 @@ pub struct App {
|
||||||
vkinfo: Option<VulkanInfo>,
|
vkinfo: Option<VulkanInfo>,
|
||||||
|
|
||||||
inhibit_fail_notif: Option<NotificationHandle>,
|
inhibit_fail_notif: Option<NotificationHandle>,
|
||||||
|
pluginstore: Option<AsyncController<PluginStore>>,
|
||||||
|
|
||||||
|
theme_engine: ThemeEngine,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
OnServiceLog(Vec<String>),
|
OnServiceLog(Vec<String>),
|
||||||
OnServiceExit(i32),
|
OnServiceExit(i32),
|
||||||
OnAutostartExit(i32),
|
OnPluginsExit(i32),
|
||||||
OnBuildLog(Vec<String>),
|
OnBuildLog(Vec<String>),
|
||||||
OnBuildExit(i32),
|
OnBuildExit(i32),
|
||||||
ClockTicking,
|
ClockTicking,
|
||||||
|
@ -102,7 +119,8 @@ pub enum Msg {
|
||||||
StartWithDebug,
|
StartWithDebug,
|
||||||
RestartXRService,
|
RestartXRService,
|
||||||
ProfileSelected(Profile),
|
ProfileSelected(Profile),
|
||||||
DeleteProfile,
|
/// bool param: delete files
|
||||||
|
DeleteProfile(bool),
|
||||||
SaveProfile(Profile),
|
SaveProfile(Profile),
|
||||||
RunSetCap,
|
RunSetCap,
|
||||||
OpenLibsurviveSetup,
|
OpenLibsurviveSetup,
|
||||||
|
@ -116,6 +134,10 @@ pub enum Msg {
|
||||||
StartProber,
|
StartProber,
|
||||||
OnProberExit(bool),
|
OnProberExit(bool),
|
||||||
WivrnCheckPairMode,
|
WivrnCheckPairMode,
|
||||||
|
OpenPluginStore,
|
||||||
|
UpdateConfigPlugins(HashMap<String, PluginConfig>),
|
||||||
|
ShowThemeManager,
|
||||||
|
SaveThemeConfig,
|
||||||
NoOp,
|
NoOp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +169,7 @@ impl App {
|
||||||
} {
|
} {
|
||||||
Ok(n) => Some(n),
|
Ok(n) => Some(n),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to send desktop notification: {e:?}");
|
error!("failed to send desktop notification: {e:?}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,7 +185,17 @@ impl App {
|
||||||
pub fn start_xrservice(&mut self, sender: AsyncComponentSender<Self>, debug: bool) {
|
pub fn start_xrservice(&mut self, sender: AsyncComponentSender<Self>, debug: bool) {
|
||||||
self.xrservice_ready = false;
|
self.xrservice_ready = false;
|
||||||
let prof = self.get_selected_profile();
|
let prof = self.get_selected_profile();
|
||||||
if prof.can_start() {
|
if !prof.can_start() {
|
||||||
|
alert(
|
||||||
|
"Failed to start profile",
|
||||||
|
Some(concat!(
|
||||||
|
"You need to build the current profile before starting it.",
|
||||||
|
"\n\nYou can do this from the menu."
|
||||||
|
)),
|
||||||
|
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Err(e) = set_current_active_runtime_to_profile(&prof) {
|
if let Err(e) = set_current_active_runtime_to_profile(&prof) {
|
||||||
alert(
|
alert(
|
||||||
"Failed to start XR Service",
|
"Failed to start XR Service",
|
||||||
|
@ -186,9 +218,13 @@ impl App {
|
||||||
};
|
};
|
||||||
self.debug_view.sender().emit(DebugViewMsg::ClearLog);
|
self.debug_view.sender().emit(DebugViewMsg::ClearLog);
|
||||||
self.xr_devices = vec![];
|
self.xr_devices = vec![];
|
||||||
remove_file(prof.xrservice_type.ipc_file_path())
|
{
|
||||||
.is_err()
|
let ipc_file = prof.xrservice_type.ipc_file_path();
|
||||||
.then(|| println!("Failed to remove xrservice IPC file"));
|
if ipc_file.exists() {
|
||||||
|
remove_file(ipc_file)
|
||||||
|
.unwrap_or_else(|e| error!("failed to remove xrservice IPC file: {e}"));
|
||||||
|
};
|
||||||
|
}
|
||||||
let worker = JobWorker::xrservice_worker_wrap_from_profile(
|
let worker = JobWorker::xrservice_worker_wrap_from_profile(
|
||||||
&prof,
|
&prof,
|
||||||
sender.input_sender(),
|
sender.input_sender(),
|
||||||
|
@ -200,54 +236,92 @@ impl App {
|
||||||
);
|
);
|
||||||
worker.start();
|
worker.start();
|
||||||
self.xrservice_worker = Some(worker);
|
self.xrservice_worker = Some(worker);
|
||||||
|
let set_sniper_launch_opts_res =
|
||||||
|
set_sniper_runtime_entrypoint_launch_opts_from_profile(&prof);
|
||||||
|
let set_soldier_launch_opts_res =
|
||||||
|
set_soldier_runtime_entrypoint_launch_opts_from_profile(&prof);
|
||||||
self.main_view
|
self.main_view
|
||||||
.sender()
|
.sender()
|
||||||
.emit(MainViewMsg::XRServiceActiveChanged(
|
.emit(MainViewMsg::XRServiceActiveChanged(
|
||||||
true,
|
true,
|
||||||
Some(self.get_selected_profile()),
|
Some(self.get_selected_profile()),
|
||||||
// show launch opts only if setting the runtime entrypoint fails
|
// show launch opts only if setting the runtime entrypoint fails
|
||||||
set_runtime_entrypoint_launch_opts_from_profile(&prof).is_err(),
|
set_sniper_launch_opts_res.is_err() || set_soldier_launch_opts_res.is_err(),
|
||||||
));
|
));
|
||||||
self.debug_view
|
self.debug_view
|
||||||
.sender()
|
.sender()
|
||||||
.emit(DebugViewMsg::XRServiceActiveChanged(true));
|
.emit(DebugViewMsg::XRServiceActiveChanged(true));
|
||||||
self.set_inhibit_session(true);
|
self.set_inhibit_session(true);
|
||||||
sender.input(Msg::StartProber);
|
sender.input(Msg::StartProber);
|
||||||
} else {
|
|
||||||
alert(
|
|
||||||
"Failed to start profile",
|
|
||||||
Some(concat!(
|
|
||||||
"You need to build the current profile before starting it.",
|
|
||||||
"\n\nYou can do this from the menu."
|
|
||||||
)),
|
|
||||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_autostart(&mut self, sender: AsyncComponentSender<Self>) {
|
pub fn run_autostart(&mut self, sender: AsyncComponentSender<Self>) {
|
||||||
let prof = self.get_selected_profile();
|
let prof = self.get_selected_profile();
|
||||||
if let Some(autostart_cmd) = &prof.autostart_command {
|
let plugins_cmd = self
|
||||||
|
.config
|
||||||
|
.plugins
|
||||||
|
.values()
|
||||||
|
.filter_map(|cp| {
|
||||||
|
if cp.enabled && cp.plugin.validate() {
|
||||||
|
if let Err(e) = cp.plugin.mark_as_executable() {
|
||||||
|
error!(
|
||||||
|
"failed to mark plugin {} as executable: {e}",
|
||||||
|
cp.plugin.appid
|
||||||
|
);
|
||||||
|
None
|
||||||
|
} else if !cp.plugin.plugin_type.launches_directly() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some({
|
||||||
|
let mut cmd_parts = vec![cp
|
||||||
|
.plugin
|
||||||
|
.executable()
|
||||||
|
.unwrap()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()];
|
||||||
|
cmd_parts.extend(cp.plugin.args.clone().unwrap_or_default());
|
||||||
|
cmd_parts
|
||||||
|
.iter()
|
||||||
|
.map(|part| format!("'{part}'"))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" ")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" & ");
|
||||||
|
if !plugins_cmd.is_empty() {
|
||||||
let mut jobs = VecDeque::new();
|
let mut jobs = VecDeque::new();
|
||||||
jobs.push_back(WorkerJob::new_cmd(
|
jobs.push_back(WorkerJob::new_cmd(
|
||||||
Some(prof.environment.clone()),
|
Some(prof.environment.clone()),
|
||||||
"sh".into(),
|
"sh".into(),
|
||||||
Some(vec!["-c".into(), autostart_cmd.clone()]),
|
Some(vec!["-c".into(), plugins_cmd]),
|
||||||
));
|
));
|
||||||
let autostart_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
|
let plugins_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
|
||||||
JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows),
|
JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows),
|
||||||
JobWorkerOut::Exit(code) => Msg::OnAutostartExit(code),
|
JobWorkerOut::Exit(code) => Msg::OnPluginsExit(code),
|
||||||
});
|
});
|
||||||
autostart_worker.start();
|
plugins_worker.start();
|
||||||
self.autostart_worker = Some(autostart_worker);
|
self.plugins_worker = Some(plugins_worker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restore_openxr_openvr_files(&self) {
|
pub fn restore_openxr_openvr_files(&self) {
|
||||||
restore_runtime_entrypoint();
|
restore_sniper_runtime_entrypoint();
|
||||||
if let Err(e) = set_current_active_runtime_to_steam() {
|
restore_soldier_runtime_entrypoint();
|
||||||
|
if let Err(e) = remove_current_active_runtime() {
|
||||||
alert(
|
alert(
|
||||||
"Could not restore Steam active runtime",
|
"Could not remove profile active runtime",
|
||||||
|
Some(&format!("{e}")),
|
||||||
|
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Err(e) = restore_active_runtime_backup() {
|
||||||
|
alert(
|
||||||
|
"Could not restore previous active runtime",
|
||||||
Some(&format!("{e}")),
|
Some(&format!("{e}")),
|
||||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||||
);
|
);
|
||||||
|
@ -262,27 +336,17 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shutdown_xrservice(&mut self) {
|
pub fn shutdown_xrservice(&mut self) {
|
||||||
if let Some(worker) = self.autostart_worker.as_ref() {
|
if let Some(w) = self.plugins_worker.as_ref() {
|
||||||
worker.stop();
|
w.stop();
|
||||||
}
|
}
|
||||||
self.xrservice_ready = false;
|
|
||||||
if let Some(w) = self.openxr_prober_worker.as_ref() {
|
if let Some(w) = self.openxr_prober_worker.as_ref() {
|
||||||
w.stop();
|
w.stop();
|
||||||
// this can cause threads to remain hanging...
|
// this can cause threads to remain hanging...
|
||||||
self.openxr_prober_worker = None;
|
self.openxr_prober_worker = None;
|
||||||
}
|
}
|
||||||
self.set_inhibit_session(false);
|
if let Some(w) = self.xrservice_worker.as_ref() {
|
||||||
if let Some(worker) = self.xrservice_worker.as_ref() {
|
w.stop();
|
||||||
worker.stop();
|
|
||||||
}
|
}
|
||||||
self.libmonado = None;
|
|
||||||
self.main_view
|
|
||||||
.sender()
|
|
||||||
.emit(MainViewMsg::XRServiceActiveChanged(false, None, false));
|
|
||||||
self.debug_view
|
|
||||||
.sender()
|
|
||||||
.emit(DebugViewMsg::XRServiceActiveChanged(false));
|
|
||||||
self.xr_devices = vec![];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,7 +380,7 @@ impl AsyncComponent for App {
|
||||||
set_content: Some(&adw::NavigationPage::new(model.debug_view.widget(), "Debug View")),
|
set_content: Some(&adw::NavigationPage::new(model.debug_view.widget(), "Debug View")),
|
||||||
set_show_content: false,
|
set_show_content: false,
|
||||||
set_collapsed: !model.config.debug_view_enabled,
|
set_collapsed: !model.config.debug_view_enabled,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
connect_close_request[sender] => move |win| {
|
connect_close_request[sender] => move |win| {
|
||||||
sender.input(Msg::SaveWinSize(win.width(), win.height()));
|
sender.input(Msg::SaveWinSize(win.width(), win.height()));
|
||||||
|
@ -340,6 +404,27 @@ impl AsyncComponent for App {
|
||||||
) {
|
) {
|
||||||
match message {
|
match message {
|
||||||
Msg::NoOp => {}
|
Msg::NoOp => {}
|
||||||
|
Msg::ShowThemeManager => {
|
||||||
|
let dialog = self
|
||||||
|
.theme_engine
|
||||||
|
.theme_chooser_dialog(Theme::default_themes().as_ref());
|
||||||
|
dialog.set_content_height(2000);
|
||||||
|
dialog.present(Some(&self.app_win));
|
||||||
|
dialog.connect_closed(clone!(
|
||||||
|
#[strong]
|
||||||
|
sender,
|
||||||
|
move |_| {
|
||||||
|
sender.input(Msg::SaveThemeConfig);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Msg::SaveThemeConfig => {
|
||||||
|
let name = self.theme_engine.current_theme_name();
|
||||||
|
if self.config.theme_name != name {
|
||||||
|
self.config.theme_name = name;
|
||||||
|
self.config.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
Msg::OnServiceLog(rows) => {
|
Msg::OnServiceLog(rows) => {
|
||||||
if !rows.is_empty() {
|
if !rows.is_empty() {
|
||||||
self.debug_view
|
self.debug_view
|
||||||
|
@ -348,6 +433,8 @@ impl AsyncComponent for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Msg::OnServiceExit(code) => {
|
Msg::OnServiceExit(code) => {
|
||||||
|
self.set_inhibit_session(false);
|
||||||
|
self.xrservice_ready = false;
|
||||||
self.restore_openxr_openvr_files();
|
self.restore_openxr_openvr_files();
|
||||||
self.main_view
|
self.main_view
|
||||||
.sender()
|
.sender()
|
||||||
|
@ -355,6 +442,8 @@ impl AsyncComponent for App {
|
||||||
self.debug_view
|
self.debug_view
|
||||||
.sender()
|
.sender()
|
||||||
.emit(DebugViewMsg::XRServiceActiveChanged(false));
|
.emit(DebugViewMsg::XRServiceActiveChanged(false));
|
||||||
|
self.libmonado = None;
|
||||||
|
self.xr_devices = vec![];
|
||||||
if code != 0 && code != 15 {
|
if code != 0 && code != 15 {
|
||||||
// 15 is SIGTERM
|
// 15 is SIGTERM
|
||||||
sender.input(Msg::OnServiceLog(vec![format!(
|
sender.input(Msg::OnServiceLog(vec![format!(
|
||||||
|
@ -369,7 +458,7 @@ impl AsyncComponent for App {
|
||||||
self.start_xrservice(sender, false);
|
self.start_xrservice(sender, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Msg::OnAutostartExit(_) => self.autostart_worker = None,
|
Msg::OnPluginsExit(_) => self.plugins_worker = None,
|
||||||
Msg::ClockTicking => {
|
Msg::ClockTicking => {
|
||||||
self.main_view.sender().emit(MainViewMsg::ClockTicking);
|
self.main_view.sender().emit(MainViewMsg::ClockTicking);
|
||||||
let xrservice_worker_is_alive = self
|
let xrservice_worker_is_alive = self
|
||||||
|
@ -409,7 +498,7 @@ impl AsyncComponent for App {
|
||||||
.emit(MainViewMsg::SetWivrnSupportsPairing(true));
|
.emit(MainViewMsg::SetWivrnSupportsPairing(true));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error: failed to get wivrn pairing mode: {e:?}");
|
error!("failed to get wivrn pairing mode: {e:?}");
|
||||||
self.main_view
|
self.main_view
|
||||||
.sender()
|
.sender()
|
||||||
.emit(MainViewMsg::SetWivrnSupportsPairing(false));
|
.emit(MainViewMsg::SetWivrnSupportsPairing(false));
|
||||||
|
@ -466,14 +555,24 @@ impl AsyncComponent for App {
|
||||||
jobs.extend(get_build_basalt_jobs(&profile, clean_build));
|
jobs.extend(get_build_basalt_jobs(&profile, clean_build));
|
||||||
}
|
}
|
||||||
if profile.features.mercury_enabled {
|
if profile.features.mercury_enabled {
|
||||||
jobs.extend(get_build_mercury_jobs(&profile));
|
jobs.extend(get_build_mercury_jobs());
|
||||||
}
|
}
|
||||||
jobs.extend(match profile.xrservice_type {
|
jobs.extend(match profile.xrservice_type {
|
||||||
XRServiceType::Monado => get_build_monado_jobs(&profile, clean_build),
|
XRServiceType::Monado => get_build_monado_jobs(&profile, clean_build),
|
||||||
XRServiceType::Wivrn => get_build_wivrn_jobs(&profile, clean_build),
|
XRServiceType::Wivrn => get_build_wivrn_jobs(&profile, clean_build),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
jobs.extend(get_build_opencomposite_jobs(&profile, clean_build));
|
jobs.extend(match profile.ovr_comp.mod_type {
|
||||||
|
OvrCompatibilityModuleType::Opencomposite => {
|
||||||
|
get_build_opencomposite_jobs(&profile, clean_build)
|
||||||
|
}
|
||||||
|
OvrCompatibilityModuleType::Xrizer => {
|
||||||
|
get_build_xrizer_jobs(&profile, clean_build)
|
||||||
|
}
|
||||||
|
OvrCompatibilityModuleType::Vapor => {
|
||||||
|
get_build_vapor_jobs(&profile, clean_build)
|
||||||
|
}
|
||||||
|
});
|
||||||
let missing_deps = profile.missing_dependencies();
|
let missing_deps = profile.missing_dependencies();
|
||||||
if !(self.skip_depcheck || profile.skip_dependency_check || missing_deps.is_empty())
|
if !(self.skip_depcheck || profile.skip_dependency_check || missing_deps.is_empty())
|
||||||
{
|
{
|
||||||
|
@ -562,6 +661,10 @@ impl AsyncComponent for App {
|
||||||
if dep_pkexec().check() {
|
if dep_pkexec().check() {
|
||||||
self.setcap_confirm_dialog.present(Some(&self.app_win));
|
self.setcap_confirm_dialog.present(Some(&self.app_win));
|
||||||
} else {
|
} else {
|
||||||
|
self.build_window
|
||||||
|
.sender()
|
||||||
|
.emit(BuildWindowMsg::UpdateContent(vec![TermColor::Red
|
||||||
|
.colorize("pkexec not found, cannot set capabilities\n")]));
|
||||||
alert_w_widget(
|
alert_w_widget(
|
||||||
"pkexec not found",
|
"pkexec not found",
|
||||||
Some(&format!(
|
Some(&format!(
|
||||||
|
@ -584,7 +687,7 @@ impl AsyncComponent for App {
|
||||||
self.build_window
|
self.build_window
|
||||||
.sender()
|
.sender()
|
||||||
.emit(BuildWindowMsg::UpdateBuildStatus(BuildStatus::Error(
|
.emit(BuildWindowMsg::UpdateBuildStatus(BuildStatus::Error(
|
||||||
format!("Exit status {}", errcode),
|
format!("Exit status {errcode}"),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -594,9 +697,16 @@ impl AsyncComponent for App {
|
||||||
w.stop();
|
w.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Msg::DeleteProfile => {
|
Msg::DeleteProfile(delete_files) => {
|
||||||
let todel = self.get_selected_profile();
|
let todel = self.get_selected_profile();
|
||||||
if todel.editable {
|
if todel.editable {
|
||||||
|
if delete_files {
|
||||||
|
for res in todel.delete_files() {
|
||||||
|
if let Err(e) = res {
|
||||||
|
error!("Error deleting profile directory: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
self.config.user_profiles.retain(|p| p.uuid != todel.uuid);
|
self.config.user_profiles.retain(|p| p.uuid != todel.uuid);
|
||||||
self.config.save();
|
self.config.save();
|
||||||
self.profiles = self.config.profiles();
|
self.profiles = self.config.profiles();
|
||||||
|
@ -630,13 +740,51 @@ impl AsyncComponent for App {
|
||||||
self.debug_view
|
self.debug_view
|
||||||
.sender()
|
.sender()
|
||||||
.emit(DebugViewMsg::UpdateSelectedProfile(prof.clone()));
|
.emit(DebugViewMsg::UpdateSelectedProfile(prof.clone()));
|
||||||
|
self.main_view
|
||||||
|
.sender()
|
||||||
|
.emit(MainViewMsg::QueryProfileRebuild);
|
||||||
}
|
}
|
||||||
Msg::RunSetCap => {
|
Msg::RunSetCap => {
|
||||||
if !dep_pkexec().check() {
|
if !dep_pkexec().check() {
|
||||||
println!("pkexec not found, skipping setcap");
|
// there's a precheck ahead of this, this should likely never happen
|
||||||
|
error!("pkexec not found, skipping setcap");
|
||||||
} else {
|
} else {
|
||||||
let profile = self.get_selected_profile();
|
let profile = self.get_selected_profile();
|
||||||
setcap_cap_sys_nice_eip(&profile).await;
|
let setcap_failed_dialog = || {
|
||||||
|
alert_w_widget(
|
||||||
|
"Setcap failed to run",
|
||||||
|
Some("Setting the capabilities automatically failed, you can still try manually using the command below."
|
||||||
|
),
|
||||||
|
Some(&copiable_code_snippet(
|
||||||
|
&format!("sudo {}", setcap_cap_sys_nice_eip_cmd(&profile).join(" "))
|
||||||
|
)),
|
||||||
|
Some(&self.app_win.clone().upcast())
|
||||||
|
);
|
||||||
|
};
|
||||||
|
if let Err(e) = setcap_cap_sys_nice_eip(&profile).await {
|
||||||
|
setcap_failed_dialog();
|
||||||
|
error!("failed running setcap: {e}");
|
||||||
|
self.build_window
|
||||||
|
.sender()
|
||||||
|
.emit(BuildWindowMsg::UpdateContent(vec![
|
||||||
|
TermColor::Red.colorize("Setting capabilities failed\n")
|
||||||
|
]));
|
||||||
|
} else if !verify_cap_sys_nice_eip(&profile).await {
|
||||||
|
setcap_failed_dialog();
|
||||||
|
error!("setcap succeeded but capabilities were reset");
|
||||||
|
self.build_window
|
||||||
|
.sender()
|
||||||
|
.emit(BuildWindowMsg::UpdateContent(vec![TermColor::Red
|
||||||
|
.colorize(
|
||||||
|
"Setting capabilities succeeded, but capabilities have been reset\n",
|
||||||
|
)]));
|
||||||
|
} else {
|
||||||
|
self.build_window
|
||||||
|
.sender()
|
||||||
|
.emit(BuildWindowMsg::UpdateContent(vec![
|
||||||
|
TermColor::Green.colorize("Capabilities set correctly\n")
|
||||||
|
]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Msg::ProfileSelected(prof) => {
|
Msg::ProfileSelected(prof) => {
|
||||||
|
@ -756,6 +904,21 @@ impl AsyncComponent for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Msg::OpenPluginStore => {
|
||||||
|
let pluginstore = PluginStore::builder()
|
||||||
|
.launch(PluginStoreInit {
|
||||||
|
config_plugins: self.config.plugins.clone(),
|
||||||
|
})
|
||||||
|
.forward(sender.input_sender(), move |msg| match msg {
|
||||||
|
PluginStoreOutMsg::UpdateConfigPlugins(cp) => Msg::UpdateConfigPlugins(cp),
|
||||||
|
});
|
||||||
|
pluginstore.sender().emit(PluginStoreMsg::Present);
|
||||||
|
self.pluginstore = Some(pluginstore);
|
||||||
|
}
|
||||||
|
Msg::UpdateConfigPlugins(cp) => {
|
||||||
|
self.config.plugins = cp;
|
||||||
|
self.config.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -857,6 +1020,28 @@ impl AsyncComponent for App {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
stateless_action!(
|
||||||
|
actions,
|
||||||
|
PluginStoreAction,
|
||||||
|
clone!(
|
||||||
|
#[strong]
|
||||||
|
sender,
|
||||||
|
move |_| {
|
||||||
|
sender.input(Msg::OpenPluginStore);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
stateless_action!(
|
||||||
|
actions,
|
||||||
|
ThemeManagerAction,
|
||||||
|
clone!(
|
||||||
|
#[strong]
|
||||||
|
sender,
|
||||||
|
move |_| {
|
||||||
|
sender.input(Msg::ShowThemeManager);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
// this bypasses the macro because I need the underlying gio action
|
// this bypasses the macro because I need the underlying gio action
|
||||||
// to enable/disable it in update()
|
// to enable/disable it in update()
|
||||||
let configure_wivrn_action = {
|
let configure_wivrn_action = {
|
||||||
|
@ -878,7 +1063,7 @@ impl AsyncComponent for App {
|
||||||
match VulkanInfo::get() {
|
match VulkanInfo::get() {
|
||||||
Ok(info) => Some(info),
|
Ok(info) => Some(info),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to get Vulkan info: {e:#?}");
|
error!("failed to get Vulkan info: {e:#?}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -893,15 +1078,15 @@ impl AsyncComponent for App {
|
||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
selected_profile: selected_profile.clone(),
|
selected_profile: selected_profile.clone(),
|
||||||
root_win: root.clone().into(),
|
root_win: root.clone().into(),
|
||||||
vkinfo: vkinfo.clone(),
|
|
||||||
})
|
})
|
||||||
.forward(sender.input_sender(), |message| match message {
|
.forward(sender.input_sender(), |message| match message {
|
||||||
MainViewOutMsg::DoStartStopXRService => Msg::DoStartStopXRService,
|
MainViewOutMsg::DoStartStopXRService => Msg::DoStartStopXRService,
|
||||||
MainViewOutMsg::RestartXRService => Msg::RestartXRService,
|
MainViewOutMsg::RestartXRService => Msg::RestartXRService,
|
||||||
MainViewOutMsg::ProfileSelected(uuid) => Msg::ProfileSelected(uuid),
|
MainViewOutMsg::ProfileSelected(uuid) => Msg::ProfileSelected(uuid),
|
||||||
MainViewOutMsg::DeleteProfile => Msg::DeleteProfile,
|
MainViewOutMsg::DeleteProfile(delete_files) => Msg::DeleteProfile(delete_files),
|
||||||
MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p),
|
MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p),
|
||||||
MainViewOutMsg::OpenLibsurviveSetup => Msg::OpenLibsurviveSetup,
|
MainViewOutMsg::OpenLibsurviveSetup => Msg::OpenLibsurviveSetup,
|
||||||
|
MainViewOutMsg::BuildProfile(clean) => Msg::BuildProfile(clean),
|
||||||
}),
|
}),
|
||||||
vkinfo,
|
vkinfo,
|
||||||
debug_view: DebugView::builder()
|
debug_view: DebugView::builder()
|
||||||
|
@ -925,10 +1110,21 @@ impl AsyncComponent for App {
|
||||||
.detach(),
|
.detach(),
|
||||||
split_view: None,
|
split_view: None,
|
||||||
setcap_confirm_dialog,
|
setcap_confirm_dialog,
|
||||||
|
theme_engine: ThemeEngine::new_with_theme(&{
|
||||||
|
if config.theme_name == "Follow system" {
|
||||||
|
Theme::default()
|
||||||
|
} else {
|
||||||
|
Theme::default_themes()
|
||||||
|
.into_iter()
|
||||||
|
.find(|t| t.name == config.theme_name)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
config,
|
config,
|
||||||
profiles,
|
profiles,
|
||||||
xrservice_worker: None,
|
xrservice_worker: None,
|
||||||
autostart_worker: None,
|
plugins_worker: None,
|
||||||
build_worker: None,
|
build_worker: None,
|
||||||
xr_devices: vec![],
|
xr_devices: vec![],
|
||||||
restart_xrservice: false,
|
restart_xrservice: false,
|
||||||
|
@ -939,6 +1135,7 @@ impl AsyncComponent for App {
|
||||||
openxr_prober_worker: None,
|
openxr_prober_worker: None,
|
||||||
xrservice_ready: false,
|
xrservice_ready: false,
|
||||||
inhibit_fail_notif: None,
|
inhibit_fail_notif: None,
|
||||||
|
pluginstore: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
|
@ -1011,6 +1208,8 @@ new_stateless_action!(pub BuildProfileCleanAction, AppActionGroup, "buildprofile
|
||||||
new_stateless_action!(pub QuitAction, AppActionGroup, "quit");
|
new_stateless_action!(pub QuitAction, AppActionGroup, "quit");
|
||||||
new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool);
|
new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool);
|
||||||
new_stateless_action!(pub ConfigureWivrnAction, AppActionGroup, "configurewivrn");
|
new_stateless_action!(pub ConfigureWivrnAction, AppActionGroup, "configurewivrn");
|
||||||
|
new_stateless_action!(pub PluginStoreAction, AppActionGroup, "store");
|
||||||
|
new_stateless_action!(pub ThemeManagerAction, AppActionGroup, "thememanager");
|
||||||
|
|
||||||
new_stateless_action!(pub DebugOpenDataAction, AppActionGroup, "debugopendata");
|
new_stateless_action!(pub DebugOpenDataAction, AppActionGroup, "debugopendata");
|
||||||
new_stateless_action!(pub DebugOpenPrefixAction, AppActionGroup, "debugopenprefix");
|
new_stateless_action!(pub DebugOpenPrefixAction, AppActionGroup, "debugopenprefix");
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::termcolor::TermColor;
|
||||||
|
|
||||||
use super::{term_widget::TermWidget, SENDER_IO_ERR_MSG};
|
use super::{term_widget::TermWidget, SENDER_IO_ERR_MSG};
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
|
@ -88,43 +90,54 @@ impl SimpleComponent for BuildWindow {
|
||||||
gtk::Label {
|
gtk::Label {
|
||||||
#[track = "model.changed(BuildWindow::build_status())"]
|
#[track = "model.changed(BuildWindow::build_status())"]
|
||||||
set_markup: match &model.build_status {
|
set_markup: match &model.build_status {
|
||||||
BuildStatus::Building => "Build in progress...".to_string(),
|
BuildStatus::Building => String::default(),
|
||||||
BuildStatus::Done => "Build done, you can close this window".to_string(),
|
BuildStatus::Done => "Build done, you can close this window".into(),
|
||||||
BuildStatus::Error(code) => {
|
BuildStatus::Error(code) => {
|
||||||
format!("Build failed: \"{c}\"", c = code)
|
format!("Build failed: \"{code}\"")
|
||||||
}
|
}
|
||||||
}.as_str(),
|
}.as_str(),
|
||||||
|
#[track = "model.changed(BuildWindow::build_status())"]
|
||||||
|
set_visible: match &model.build_status {
|
||||||
|
BuildStatus::Building => false,
|
||||||
|
BuildStatus::Done | BuildStatus::Error(_) => true,
|
||||||
|
},
|
||||||
add_css_class: "title-2",
|
add_css_class: "title-2",
|
||||||
set_wrap: true,
|
set_wrap: true,
|
||||||
set_wrap_mode: gtk::pango::WrapMode::Word,
|
set_wrap_mode: gtk::pango::WrapMode::Word,
|
||||||
set_justify: gtk::Justification::Center,
|
set_justify: gtk::Justification::Center,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
model.term.container.clone(),
|
||||||
|
},
|
||||||
|
add_bottom_bar: bottom_bar = >k::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
set_hexpand: true,
|
||||||
|
set_margin_bottom: 24,
|
||||||
|
set_spacing: 12,
|
||||||
|
gtk::Button {
|
||||||
|
add_css_class: "pill",
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
set_label: "Close",
|
||||||
|
#[track = "model.changed(BuildWindow::can_close())"]
|
||||||
|
set_visible: model.can_close,
|
||||||
|
connect_clicked[win] => move |_| {
|
||||||
|
win.close();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// this
|
||||||
gtk::Button {
|
gtk::Button {
|
||||||
#[track = "model.changed(BuildWindow::build_status())"]
|
#[track = "model.changed(BuildWindow::build_status())"]
|
||||||
set_visible: matches!(&model.build_status, BuildStatus::Building),
|
set_visible: matches!(&model.build_status, BuildStatus::Building),
|
||||||
add_css_class: "destructive-action",
|
add_css_class: "destructive-action",
|
||||||
add_css_class: "circular",
|
add_css_class: "pill",
|
||||||
set_icon_name: "window-close-symbolic",
|
set_label: "Cancel build",
|
||||||
set_tooltip_text: Some("Cancel build"),
|
|
||||||
connect_clicked[sender] => move |_| {
|
connect_clicked[sender] => move |_| {
|
||||||
sender.output(Self::Output::CancelBuild).expect(SENDER_IO_ERR_MSG);
|
sender.output(Self::Output::CancelBuild).expect(SENDER_IO_ERR_MSG);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// ^^^
|
||||||
},
|
},
|
||||||
model.term.container.clone(),
|
|
||||||
},
|
|
||||||
add_bottom_bar: bottom_bar = >k::Button {
|
|
||||||
add_css_class: "pill",
|
|
||||||
set_halign: gtk::Align::Center,
|
|
||||||
set_label: "Close",
|
|
||||||
set_margin_all: 12,
|
|
||||||
#[track = "model.changed(BuildWindow::can_close())"]
|
|
||||||
set_sensitive: model.can_close,
|
|
||||||
connect_clicked[win] => move |_| {
|
|
||||||
|
|
||||||
win.close();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,8 +166,18 @@ impl SimpleComponent for BuildWindow {
|
||||||
label.remove_css_class("success");
|
label.remove_css_class("success");
|
||||||
label.remove_css_class("error");
|
label.remove_css_class("error");
|
||||||
match status {
|
match status {
|
||||||
BuildStatus::Done => label.add_css_class("success"),
|
BuildStatus::Done => {
|
||||||
BuildStatus::Error(_) => label.add_css_class("error"),
|
label.add_css_class("success");
|
||||||
|
sender.input(BuildWindowMsg::UpdateContent(vec![
|
||||||
|
TermColor::Blue.colorize("Build completed!\n")
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
BuildStatus::Error(_) => {
|
||||||
|
label.add_css_class("error");
|
||||||
|
sender.input(BuildWindowMsg::UpdateContent(vec![
|
||||||
|
TermColor::Blue.colorize("Build failed!\n")
|
||||||
|
]));
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
if status != BuildStatus::Building {
|
if status != BuildStatus::Building {
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use crate::config::Config;
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
constants::{APP_NAME, VERSION},
|
||||||
|
};
|
||||||
use gtk::{
|
use gtk::{
|
||||||
gio::{
|
gio::{
|
||||||
prelude::{ApplicationCommandLineExt, ApplicationExt},
|
prelude::{ApplicationCommandLineExt, ApplicationExt},
|
||||||
|
@ -6,9 +9,11 @@ use gtk::{
|
||||||
},
|
},
|
||||||
glib::{self, prelude::IsA},
|
glib::{self, prelude::IsA},
|
||||||
};
|
};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CmdLineOpts {
|
pub struct CmdLineOpts {
|
||||||
|
pub version: bool,
|
||||||
pub start: bool,
|
pub start: bool,
|
||||||
pub list_profiles: bool,
|
pub list_profiles: bool,
|
||||||
pub profile_uuid: Option<String>,
|
pub profile_uuid: Option<String>,
|
||||||
|
@ -17,6 +22,7 @@ pub struct CmdLineOpts {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CmdLineOpts {
|
impl CmdLineOpts {
|
||||||
|
const OPT_VERSION: (&'static str, char) = ("version", 'v');
|
||||||
const OPT_START: (&'static str, char) = ("start", 'S');
|
const OPT_START: (&'static str, char) = ("start", 'S');
|
||||||
const OPT_LIST_PROFILES: (&'static str, char) = ("list-profiles", 'l');
|
const OPT_LIST_PROFILES: (&'static str, char) = ("list-profiles", 'l');
|
||||||
const OPT_PROFILE: (&'static str, char) = ("profile", 'p');
|
const OPT_PROFILE: (&'static str, char) = ("profile", 'p');
|
||||||
|
@ -24,6 +30,14 @@ impl CmdLineOpts {
|
||||||
const OPT_CHECK_DEPS_FOR: (&'static str, char) = ("check-deps-for", 'c');
|
const OPT_CHECK_DEPS_FOR: (&'static str, char) = ("check-deps-for", 'c');
|
||||||
|
|
||||||
pub fn init(app: &impl IsA<Application>) {
|
pub fn init(app: &impl IsA<Application>) {
|
||||||
|
app.add_main_option(
|
||||||
|
Self::OPT_VERSION.0,
|
||||||
|
glib::Char::try_from(Self::OPT_VERSION.1).unwrap(),
|
||||||
|
glib::OptionFlags::IN_MAIN,
|
||||||
|
glib::OptionArg::None,
|
||||||
|
"Print the version information",
|
||||||
|
None,
|
||||||
|
);
|
||||||
app.add_main_option(
|
app.add_main_option(
|
||||||
Self::OPT_START.0,
|
Self::OPT_START.0,
|
||||||
glib::Char::try_from(Self::OPT_START.1).unwrap(),
|
glib::Char::try_from(Self::OPT_START.1).unwrap(),
|
||||||
|
@ -68,6 +82,10 @@ impl CmdLineOpts {
|
||||||
|
|
||||||
/// returns an exit code if the application should quit immediately
|
/// returns an exit code if the application should quit immediately
|
||||||
pub fn handle_non_activating_opts(&self) -> Option<i32> {
|
pub fn handle_non_activating_opts(&self) -> Option<i32> {
|
||||||
|
if self.version {
|
||||||
|
println!("{APP_NAME} {VERSION}");
|
||||||
|
return Some(0);
|
||||||
|
}
|
||||||
if self.list_profiles {
|
if self.list_profiles {
|
||||||
println!("Available profiles\nUUID: \"name\"");
|
println!("Available profiles\nUUID: \"name\"");
|
||||||
let profiles = Config::get_config().profiles();
|
let profiles = Config::get_config().profiles();
|
||||||
|
@ -88,7 +106,7 @@ impl CmdLineOpts {
|
||||||
}
|
}
|
||||||
return Some(1);
|
return Some(1);
|
||||||
} else {
|
} else {
|
||||||
eprintln!("No profile found for uuid: `{prof_id}`");
|
error!("No profile found for uuid: `{prof_id}`");
|
||||||
return Some(404);
|
return Some(404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,6 +116,7 @@ impl CmdLineOpts {
|
||||||
pub fn from_cmdline(cmdline: &ApplicationCommandLine) -> Self {
|
pub fn from_cmdline(cmdline: &ApplicationCommandLine) -> Self {
|
||||||
let opts = cmdline.options_dict();
|
let opts = cmdline.options_dict();
|
||||||
Self {
|
Self {
|
||||||
|
version: opts.contains(Self::OPT_VERSION.0),
|
||||||
start: opts.contains(Self::OPT_START.0),
|
start: opts.contains(Self::OPT_START.0),
|
||||||
list_profiles: opts.contains(Self::OPT_LIST_PROFILES.0),
|
list_profiles: opts.contains(Self::OPT_LIST_PROFILES.0),
|
||||||
profile_uuid: opts
|
profile_uuid: opts
|
||||||
|
|
|
@ -61,9 +61,19 @@ impl SimpleComponent for DevicesBox {
|
||||||
}
|
}
|
||||||
if !has_left && dev.roles.contains(&XRDeviceRole::Left) {
|
if !has_left && dev.roles.contains(&XRDeviceRole::Left) {
|
||||||
has_left = true;
|
has_left = true;
|
||||||
|
if ["Qwerty Left Controller"].contains(&dev.name.as_str()) {
|
||||||
|
row_model.state = Some(DeviceRowState::Warning);
|
||||||
|
row_model.subtitle =
|
||||||
|
Some(format!("No left controller detected ({})", dev.name));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !has_right && dev.roles.contains(&XRDeviceRole::Right) {
|
if !has_right && dev.roles.contains(&XRDeviceRole::Right) {
|
||||||
has_right = true;
|
has_right = true;
|
||||||
|
if ["Qwerty Right Controller"].contains(&dev.name.as_str()) {
|
||||||
|
row_model.state = Some(DeviceRowState::Warning);
|
||||||
|
row_model.subtitle =
|
||||||
|
Some(format!("No right controller detected ({})", dev.name));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
models.push(row_model);
|
models.push(row_model);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::{
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use relm4::{new_action_group, new_stateless_action, prelude::*};
|
use relm4::{new_action_group, new_stateless_action, prelude::*};
|
||||||
use std::fs::remove_file;
|
use std::fs::remove_file;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
const WIVRN_LATEST_RELEASE_APK_URL: &str =
|
const WIVRN_LATEST_RELEASE_APK_URL: &str =
|
||||||
"https://github.com/WiVRn/WiVRn/releases/latest/download/WiVRn-standard-release.apk";
|
"https://github.com/WiVRn/WiVRn/releases/latest/download/WiVRn-standard-release.apk";
|
||||||
|
@ -113,7 +114,7 @@ impl AsyncComponent for InstallWivrnBox {
|
||||||
add_css_class: "dim-label",
|
add_css_class: "dim-label",
|
||||||
set_hexpand: true,
|
set_hexpand: true,
|
||||||
set_label: concat!(
|
set_label: concat!(
|
||||||
"Install the WiVRn APK on your standalong Android headset. ",
|
"Install the WiVRn APK on your standalone Android headset. ",
|
||||||
"You will need to enable Developer Mode on your headset, ",
|
"You will need to enable Developer Mode on your headset, ",
|
||||||
"then press the \"Install WiVRn\" button."
|
"then press the \"Install WiVRn\" button."
|
||||||
),
|
),
|
||||||
|
@ -172,7 +173,7 @@ impl AsyncComponent for InstallWivrnBox {
|
||||||
|
|
||||||
match get_wivrn_apk_ref(&self.selected_profile) {
|
match get_wivrn_apk_ref(&self.selected_profile) {
|
||||||
Err(GetWivrnApkRefErr::NotWivrn) => {
|
Err(GetWivrnApkRefErr::NotWivrn) => {
|
||||||
eprintln!("This is not a WiVRn profile, how did you get here?");
|
error!("this is not a WiVRn profile, how did you get here?");
|
||||||
}
|
}
|
||||||
Err(GetWivrnApkRefErr::RepoDirNotFound) => {
|
Err(GetWivrnApkRefErr::RepoDirNotFound) => {
|
||||||
self.set_install_wivrn_status(InstallWivrnStatus::Done(Some(
|
self.set_install_wivrn_status(InstallWivrnStatus::Done(Some(
|
||||||
|
@ -180,14 +181,11 @@ impl AsyncComponent for InstallWivrnBox {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
Err(GetWivrnApkRefErr::RepoManipulationFailed(giterr)) => {
|
Err(GetWivrnApkRefErr::RepoManipulationFailed(giterr)) => {
|
||||||
eprintln!("Error: failed to manipulate WiVRn repo: {giterr}, falling back to latest release APK");
|
error!("failed to manipulate WiVRn repo: {giterr}, falling back to latest release APK");
|
||||||
let existing = cache_file_path(WIVRN_LATEST_RELEASE_APK_URL, Some("apk"));
|
let existing = cache_file_path(WIVRN_LATEST_RELEASE_APK_URL, Some("apk"));
|
||||||
if existing.is_file() {
|
if existing.is_file() {
|
||||||
if let Err(e) = remove_file(&existing) {
|
if let Err(e) = remove_file(&existing) {
|
||||||
eprintln!(
|
error!("failed to remove file {}: {e}", existing.to_string_lossy());
|
||||||
"Failed to remove file {}: {e}",
|
|
||||||
existing.to_string_lossy()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sender.input(Self::Input::DoInstall(WIVRN_LATEST_RELEASE_APK_URL.into()));
|
sender.input(Self::Input::DoInstall(WIVRN_LATEST_RELEASE_APK_URL.into()));
|
||||||
|
@ -208,7 +206,7 @@ impl AsyncComponent for InstallWivrnBox {
|
||||||
// TODO: we gonna cache or just download async every time?
|
// TODO: we gonna cache or just download async every time?
|
||||||
match cache_file(&url, Some("apk")).await {
|
match cache_file(&url, Some("apk")).await {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to download apk: {e}");
|
error!("failed to download apk: {e}");
|
||||||
self.set_install_wivrn_status(InstallWivrnStatus::Done(Some(
|
self.set_install_wivrn_status(InstallWivrnStatus::Done(Some(
|
||||||
"Error downloading WiVRn client APK".into(),
|
"Error downloading WiVRn client APK".into(),
|
||||||
)));
|
)));
|
||||||
|
@ -236,14 +234,14 @@ impl AsyncComponent for InstallWivrnBox {
|
||||||
.into(),
|
.into(),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Error: ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr);
|
error!("ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr);
|
||||||
InstallWivrnStatus::Done(Some(
|
InstallWivrnStatus::Done(Some(
|
||||||
format!("ADB exited with code \"{}\"", out.exit_code)
|
format!("ADB exited with code \"{}\"", out.exit_code)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error: failed to run ADB: {e}");
|
error!("failed to run ADB: {e}");
|
||||||
InstallWivrnStatus::Done(Some(
|
InstallWivrnStatus::Done(Some(
|
||||||
"Failed to run ADB".into()
|
"Failed to run ADB".into()
|
||||||
))
|
))
|
||||||
|
|
|
@ -155,7 +155,7 @@ impl Worker for InternalJobWorker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const LAUNCH_OPTS_CMD_PLACEHOLDER: &str = "%command%";
|
pub const LAUNCH_OPTS_CMD_PLACEHOLDER: &str = "%command%";
|
||||||
|
|
||||||
impl InternalJobWorker {
|
impl InternalJobWorker {
|
||||||
pub fn xrservice_worker_from_profile(
|
pub fn xrservice_worker_from_profile(
|
||||||
|
@ -193,9 +193,6 @@ impl InternalJobWorker {
|
||||||
} else {
|
} else {
|
||||||
launch_opts
|
launch_opts
|
||||||
};
|
};
|
||||||
if !launch_opts.contains(" --no-instructions") {
|
|
||||||
launch_opts.push_str(" --no-instructions");
|
|
||||||
}
|
|
||||||
let (command, args) = match launch_opts.is_empty() {
|
let (command, args) = match launch_opts.is_empty() {
|
||||||
false => (
|
false => (
|
||||||
"sh".into(),
|
"sh".into(),
|
||||||
|
|
|
@ -15,6 +15,7 @@ use std::{
|
||||||
thread::{self, sleep},
|
thread::{self, sleep},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
use tracing::{error, warn};
|
||||||
|
|
||||||
pub mod internal_worker;
|
pub mod internal_worker;
|
||||||
pub mod job;
|
pub mod job;
|
||||||
|
@ -97,7 +98,7 @@ impl JobWorker {
|
||||||
self.state.lock().unwrap().stop_requested = true;
|
self.state.lock().unwrap().stop_requested = true;
|
||||||
if let Some(pid) = self.state.lock().unwrap().current_pid {
|
if let Some(pid) = self.state.lock().unwrap().current_pid {
|
||||||
if let Err(e) = kill(pid, SIGTERM) {
|
if let Err(e) = kill(pid, SIGTERM) {
|
||||||
eprintln!("Failed to send SIGTERM: {e:#?}");
|
error!("Failed to send SIGTERM: {e}");
|
||||||
}
|
}
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
|
@ -105,9 +106,9 @@ impl JobWorker {
|
||||||
if let Ok(s) = state.lock() {
|
if let Ok(s) = state.lock() {
|
||||||
if !s.exited {
|
if !s.exited {
|
||||||
// process is still alive
|
// process is still alive
|
||||||
eprintln!("Process is still alive 2 seconds after SIGTERM, proceeding to send SIGKILL...");
|
warn!("process is still alive 2 seconds after SIGTERM, proceeding to send SIGKILL...");
|
||||||
if let Err(e) = kill(pid, SIGKILL) {
|
if let Err(e) = kill(pid, SIGKILL) {
|
||||||
eprintln!("Failed to send SIGKILL: {e:#?}");
|
error!("failed to send SIGKILL: {e}");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use super::{
|
||||||
alert::alert,
|
alert::alert,
|
||||||
app::{
|
app::{
|
||||||
AboutAction, BuildProfileAction, BuildProfileCleanAction, ConfigureWivrnAction,
|
AboutAction, BuildProfileAction, BuildProfileCleanAction, ConfigureWivrnAction,
|
||||||
DebugViewToggleAction,
|
DebugViewToggleAction, PluginStoreAction,
|
||||||
},
|
},
|
||||||
devices_box::{DevicesBox, DevicesBoxMsg},
|
devices_box::{DevicesBox, DevicesBoxMsg},
|
||||||
install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg},
|
install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg},
|
||||||
|
@ -12,6 +12,7 @@ use super::{
|
||||||
steamvr_calibration_box::{SteamVrCalibrationBox, SteamVrCalibrationBoxMsg},
|
steamvr_calibration_box::{SteamVrCalibrationBox, SteamVrCalibrationBoxMsg},
|
||||||
util::{limit_dropdown_width, warning_heading},
|
util::{limit_dropdown_width, warning_heading},
|
||||||
wivrn_wired_start_box::{WivrnWiredStartBox, WivrnWiredStartBoxInit, WivrnWiredStartBoxMsg},
|
wivrn_wired_start_box::{WivrnWiredStartBox, WivrnWiredStartBoxInit, WivrnWiredStartBoxMsg},
|
||||||
|
SENDER_IO_ERR_MSG,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
|
@ -20,11 +21,11 @@ use crate::{
|
||||||
paths::{get_data_dir, get_home_dir},
|
paths::{get_data_dir, get_home_dir},
|
||||||
profile::{LighthouseDriver, Profile, XRServiceType},
|
profile::{LighthouseDriver, Profile, XRServiceType},
|
||||||
stateless_action,
|
stateless_action,
|
||||||
|
ui::app::ThemeManagerAction,
|
||||||
util::{
|
util::{
|
||||||
file_utils::{get_writer, mount_has_nosuid},
|
file_utils::{get_writer, mount_has_nosuid},
|
||||||
steamvr_utils::chaperone_info_exists,
|
steamvr_utils::chaperone_info_exists,
|
||||||
},
|
},
|
||||||
vulkaninfo::VulkanInfo,
|
|
||||||
wivrn_dbus,
|
wivrn_dbus,
|
||||||
xr_devices::XRDevice,
|
xr_devices::XRDevice,
|
||||||
};
|
};
|
||||||
|
@ -36,6 +37,7 @@ use relm4::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use std::{fs::read_to_string, io::Write};
|
use std::{fs::read_to_string, io::Write};
|
||||||
|
use tracing::{error, warn};
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
pub struct MainView {
|
pub struct MainView {
|
||||||
|
@ -59,6 +61,8 @@ pub struct MainView {
|
||||||
#[tracker::do_not_track]
|
#[tracker::do_not_track]
|
||||||
profile_delete_confirm_dialog: adw::AlertDialog,
|
profile_delete_confirm_dialog: adw::AlertDialog,
|
||||||
#[tracker::do_not_track]
|
#[tracker::do_not_track]
|
||||||
|
query_profile_rebuild_dialog: adw::AlertDialog,
|
||||||
|
#[tracker::do_not_track]
|
||||||
profile_editor: Option<Controller<ProfileEditor>>,
|
profile_editor: Option<Controller<ProfileEditor>>,
|
||||||
#[tracker::do_not_track]
|
#[tracker::do_not_track]
|
||||||
steamvr_calibration_box: Controller<SteamVrCalibrationBox>,
|
steamvr_calibration_box: Controller<SteamVrCalibrationBox>,
|
||||||
|
@ -71,8 +75,6 @@ pub struct MainView {
|
||||||
#[tracker::do_not_track]
|
#[tracker::do_not_track]
|
||||||
profile_export_action: gtk::gio::SimpleAction,
|
profile_export_action: gtk::gio::SimpleAction,
|
||||||
xrservice_ready: bool,
|
xrservice_ready: bool,
|
||||||
#[tracker::do_not_track]
|
|
||||||
vkinfo: Option<VulkanInfo>,
|
|
||||||
wivrn_pairing_mode: bool,
|
wivrn_pairing_mode: bool,
|
||||||
wivrn_pin: Option<String>,
|
wivrn_pin: Option<String>,
|
||||||
wivrn_supports_pairing: bool,
|
wivrn_supports_pairing: bool,
|
||||||
|
@ -103,6 +105,7 @@ pub enum MainViewMsg {
|
||||||
SetWivrnPairingMode(bool),
|
SetWivrnPairingMode(bool),
|
||||||
StopWivrnPairingMode,
|
StopWivrnPairingMode,
|
||||||
StartWivrnPairingMode,
|
StartWivrnPairingMode,
|
||||||
|
QueryProfileRebuild,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -110,16 +113,18 @@ pub enum MainViewOutMsg {
|
||||||
DoStartStopXRService,
|
DoStartStopXRService,
|
||||||
RestartXRService,
|
RestartXRService,
|
||||||
ProfileSelected(Profile),
|
ProfileSelected(Profile),
|
||||||
DeleteProfile,
|
/// bool param: delete files
|
||||||
|
DeleteProfile(bool),
|
||||||
SaveProfile(Profile),
|
SaveProfile(Profile),
|
||||||
OpenLibsurviveSetup,
|
OpenLibsurviveSetup,
|
||||||
|
/// params: clean
|
||||||
|
BuildProfile(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MainViewInit {
|
pub struct MainViewInit {
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
pub selected_profile: Profile,
|
pub selected_profile: Profile,
|
||||||
pub root_win: gtk::Window,
|
pub root_win: gtk::Window,
|
||||||
pub vkinfo: Option<VulkanInfo>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MainView {
|
impl MainView {
|
||||||
|
@ -147,6 +152,7 @@ impl AsyncComponent for MainView {
|
||||||
menu! {
|
menu! {
|
||||||
app_menu: {
|
app_menu: {
|
||||||
section! {
|
section! {
|
||||||
|
"Plugin_s" => PluginStoreAction,
|
||||||
// value inside action is ignored
|
// value inside action is ignored
|
||||||
"_Debug View" => DebugViewToggleAction,
|
"_Debug View" => DebugViewToggleAction,
|
||||||
"_Build Profile" => BuildProfileAction,
|
"_Build Profile" => BuildProfileAction,
|
||||||
|
@ -154,6 +160,7 @@ impl AsyncComponent for MainView {
|
||||||
"Configure _WiVRn" => ConfigureWivrnAction,
|
"Configure _WiVRn" => ConfigureWivrnAction,
|
||||||
},
|
},
|
||||||
section! {
|
section! {
|
||||||
|
"Change _Theme" => ThemeManagerAction,
|
||||||
"_About" => AboutAction,
|
"_About" => AboutAction,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -389,8 +396,8 @@ impl AsyncComponent for MainView {
|
||||||
set_visible: match mount_has_nosuid(&model.selected_profile.prefix) {
|
set_visible: match mount_has_nosuid(&model.selected_profile.prefix) {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!(
|
warn!(
|
||||||
"Warning (nosuid detection): could not get stat on path {}",
|
"nosuid detection: could not get stat on path {}",
|
||||||
model.selected_profile.prefix.to_string_lossy());
|
model.selected_profile.prefix.to_string_lossy());
|
||||||
false
|
false
|
||||||
},
|
},
|
||||||
|
@ -445,35 +452,7 @@ impl AsyncComponent for MainView {
|
||||||
set_label: concat!(
|
set_label: concat!(
|
||||||
"SteamVR room configuration not found.\n",
|
"SteamVR room configuration not found.\n",
|
||||||
"To use the SteamVR lighthouse driver, you ",
|
"To use the SteamVR lighthouse driver, you ",
|
||||||
"will need to run SteamVR and perform the room setup.",
|
"will need to run SteamVR Quick Calibration.",
|
||||||
),
|
|
||||||
add_css_class: "warning",
|
|
||||||
set_xalign: 0.0,
|
|
||||||
set_wrap: true,
|
|
||||||
set_wrap_mode: gtk::pango::WrapMode::Word,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
gtk::Box {
|
|
||||||
set_orientation: gtk::Orientation::Vertical,
|
|
||||||
set_hexpand: true,
|
|
||||||
set_vexpand: false,
|
|
||||||
set_spacing: 12,
|
|
||||||
add_css_class: "card",
|
|
||||||
add_css_class: "padded",
|
|
||||||
set_visible: model
|
|
||||||
.vkinfo
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(
|
|
||||||
|i| i.has_nvidia_gpu && !i.has_monado_vulkan_layers
|
|
||||||
),
|
|
||||||
warning_heading(),
|
|
||||||
gtk::Label {
|
|
||||||
set_label: concat!(
|
|
||||||
"An Nvidia GPU has been detected, but it ",
|
|
||||||
"seems you don't have the Monado Vulkan Layers ",
|
|
||||||
"installed on your system.\n\nInstall the ",
|
|
||||||
"Monado Vulkan Layers or your XR session will ",
|
|
||||||
"crash."
|
|
||||||
),
|
),
|
||||||
add_css_class: "warning",
|
add_css_class: "warning",
|
||||||
set_xalign: 0.0,
|
set_xalign: 0.0,
|
||||||
|
@ -627,7 +606,7 @@ impl AsyncComponent for MainView {
|
||||||
self.set_wivrn_pin(Some(pin));
|
self.set_wivrn_pin(Some(pin));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error: failed to get wivrn pairing pin: {e:?}");
|
error!("failed to get wivrn pairing pin: {e}");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
@ -637,12 +616,12 @@ impl AsyncComponent for MainView {
|
||||||
}
|
}
|
||||||
Self::Input::StopWivrnPairingMode => {
|
Self::Input::StopWivrnPairingMode => {
|
||||||
if let Err(e) = wivrn_dbus::disable_pairing().await {
|
if let Err(e) = wivrn_dbus::disable_pairing().await {
|
||||||
eprintln!("Error: failed to stop wivrn pairing mode: {e:?}");
|
error!("failed to stop wivrn pairing mode: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Input::StartWivrnPairingMode => {
|
Self::Input::StartWivrnPairingMode => {
|
||||||
if let Err(e) = wivrn_dbus::enable_pairing().await {
|
if let Err(e) = wivrn_dbus::enable_pairing().await {
|
||||||
eprintln!("Error: failed to start wivrn pairing mode: {e:?}");
|
error!("failed to start wivrn pairing mode: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Input::StartStopClicked => {
|
Self::Input::StartStopClicked => {
|
||||||
|
@ -720,6 +699,10 @@ impl AsyncComponent for MainView {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
Self::Input::QueryProfileRebuild => {
|
||||||
|
self.query_profile_rebuild_dialog
|
||||||
|
.present(Some(&self.root_win));
|
||||||
|
}
|
||||||
Self::Input::SetSelectedProfile(index) => {
|
Self::Input::SetSelectedProfile(index) => {
|
||||||
self.profiles_dropdown
|
self.profiles_dropdown
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -757,7 +740,7 @@ impl AsyncComponent for MainView {
|
||||||
Self::Input::SaveProfile(prof) => {
|
Self::Input::SaveProfile(prof) => {
|
||||||
sender
|
sender
|
||||||
.output(Self::Output::SaveProfile(prof))
|
.output(Self::Output::SaveProfile(prof))
|
||||||
.expect("Sender output failed");
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
}
|
}
|
||||||
Self::Input::DuplicateProfile => {
|
Self::Input::DuplicateProfile => {
|
||||||
if self.selected_profile.can_be_built {
|
if self.selected_profile.can_be_built {
|
||||||
|
@ -926,8 +909,38 @@ impl AsyncComponent for MainView {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let query_profile_rebuild_dialog = adw::AlertDialog::builder()
|
||||||
|
.heading("Do you want to build this profile now?")
|
||||||
|
.body("This will trigger a clean build")
|
||||||
|
.build();
|
||||||
|
query_profile_rebuild_dialog.add_response("no", "_No");
|
||||||
|
query_profile_rebuild_dialog.add_response("yes", "_Yes");
|
||||||
|
query_profile_rebuild_dialog.set_response_appearance("yes", ResponseAppearance::Suggested);
|
||||||
|
|
||||||
|
query_profile_rebuild_dialog.connect_response(
|
||||||
|
None,
|
||||||
|
clone!(
|
||||||
|
#[strong]
|
||||||
|
sender,
|
||||||
|
move |_, res| {
|
||||||
|
if res == "yes" {
|
||||||
|
sender
|
||||||
|
.output(Self::Output::BuildProfile(true))
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let profile_delete_confirm_dialog = adw::AlertDialog::builder()
|
let profile_delete_confirm_dialog = adw::AlertDialog::builder()
|
||||||
.heading("Are you sure you want to delete this profile?")
|
.heading("Are you sure you want to delete this profile?")
|
||||||
|
.extra_child(
|
||||||
|
>k::CheckButton::builder()
|
||||||
|
.label("Delete all files and folders associated with profile")
|
||||||
|
.halign(gtk::Align::Center)
|
||||||
|
.hexpand(true)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
.build();
|
.build();
|
||||||
profile_delete_confirm_dialog.add_response("no", "_No");
|
profile_delete_confirm_dialog.add_response("no", "_No");
|
||||||
profile_delete_confirm_dialog.add_response("yes", "_Yes");
|
profile_delete_confirm_dialog.add_response("yes", "_Yes");
|
||||||
|
@ -939,10 +952,19 @@ impl AsyncComponent for MainView {
|
||||||
clone!(
|
clone!(
|
||||||
#[strong]
|
#[strong]
|
||||||
sender,
|
sender,
|
||||||
move |_, res| {
|
move |dialog, res| {
|
||||||
|
let delete_files_checkbox = dialog
|
||||||
|
.extra_child()
|
||||||
|
.and_then(|child| child.downcast::<gtk::CheckButton>().ok());
|
||||||
|
let delete_files = delete_files_checkbox
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|c| c.is_active());
|
||||||
|
if let Some(check) = delete_files_checkbox {
|
||||||
|
check.set_active(false);
|
||||||
|
}
|
||||||
if res == "yes" {
|
if res == "yes" {
|
||||||
sender
|
sender
|
||||||
.output(Self::Output::DeleteProfile)
|
.output(Self::Output::DeleteProfile(delete_files))
|
||||||
.expect("Sender output failed");
|
.expect("Sender output failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1060,6 +1082,7 @@ impl AsyncComponent for MainView {
|
||||||
selected_profile: init.selected_profile.clone(),
|
selected_profile: init.selected_profile.clone(),
|
||||||
profile_not_editable_dialog,
|
profile_not_editable_dialog,
|
||||||
profile_delete_confirm_dialog,
|
profile_delete_confirm_dialog,
|
||||||
|
query_profile_rebuild_dialog,
|
||||||
root_win: init.root_win.clone(),
|
root_win: init.root_win.clone(),
|
||||||
steamvr_calibration_box,
|
steamvr_calibration_box,
|
||||||
openhmd_calibration_box,
|
openhmd_calibration_box,
|
||||||
|
@ -1067,7 +1090,6 @@ impl AsyncComponent for MainView {
|
||||||
xrservice_ready: false,
|
xrservice_ready: false,
|
||||||
profile_delete_action,
|
profile_delete_action,
|
||||||
profile_export_action,
|
profile_export_action,
|
||||||
vkinfo: init.vkinfo,
|
|
||||||
wivrn_pairing_mode: false,
|
wivrn_pairing_mode: false,
|
||||||
wivrn_supports_pairing: false,
|
wivrn_supports_pairing: false,
|
||||||
wivrn_pin: None,
|
wivrn_pin: None,
|
||||||
|
|
|
@ -13,6 +13,7 @@ mod libsurvive_setup_window;
|
||||||
mod macros;
|
mod macros;
|
||||||
mod main_view;
|
mod main_view;
|
||||||
mod openhmd_calibration_box;
|
mod openhmd_calibration_box;
|
||||||
|
pub mod plugins;
|
||||||
mod preference_rows;
|
mod preference_rows;
|
||||||
mod profile_editor;
|
mod profile_editor;
|
||||||
mod steam_launch_options_box;
|
mod steam_launch_options_box;
|
||||||
|
|
|
@ -3,6 +3,7 @@ use relm4::{
|
||||||
gtk::{self, prelude::*},
|
gtk::{self, prelude::*},
|
||||||
ComponentParts, ComponentSender, SimpleComponent,
|
ComponentParts, ComponentSender, SimpleComponent,
|
||||||
};
|
};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
pub struct OpenHmdCalibrationBox {
|
pub struct OpenHmdCalibrationBox {
|
||||||
|
@ -59,10 +60,10 @@ impl SimpleComponent for OpenHmdCalibrationBox {
|
||||||
let target = XDG.get_config_home().join("openhmd/rift-room-config.json");
|
let target = XDG.get_config_home().join("openhmd/rift-room-config.json");
|
||||||
if target.is_file() {
|
if target.is_file() {
|
||||||
if let Err(e) = std::fs::remove_file(target) {
|
if let Err(e) = std::fs::remove_file(target) {
|
||||||
eprintln!("Failed to remove openhmd config: {e}");
|
error!("Failed to remove openhmd config: {e}");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("info: trying to delete openhmd calibration config, but file is missing")
|
debug!("trying to delete openhmd calibration config, but file is missing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
199
src/ui/plugins/add_custom_plugin_win.rs
Normal file
199
src/ui/plugins/add_custom_plugin_win.rs
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::APP_ID,
|
||||||
|
ui::{
|
||||||
|
preference_rows::{entry_row, file_row},
|
||||||
|
SENDER_IO_ERR_MSG,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Plugin;
|
||||||
|
use adw::prelude::*;
|
||||||
|
use gtk::glib::clone;
|
||||||
|
use relm4::prelude::*;
|
||||||
|
|
||||||
|
#[tracker::track]
|
||||||
|
pub struct AddCustomPluginWin {
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
parent: gtk::Window,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
win: Option<adw::Dialog>,
|
||||||
|
/// this is true when enough fields are populated, allowing the creation
|
||||||
|
/// of the plugin object to add
|
||||||
|
can_add: bool,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
plugin: Plugin,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AddCustomPluginWinMsg {
|
||||||
|
Present,
|
||||||
|
Close,
|
||||||
|
OnNameChange(String),
|
||||||
|
OnArgsChange(String),
|
||||||
|
OnExecPathChange(Option<String>),
|
||||||
|
Add,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AddCustomPluginWinOutMsg {
|
||||||
|
Add(Plugin),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AddCustomPluginWinInit {
|
||||||
|
pub parent: gtk::Window,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::component(pub)]
|
||||||
|
impl SimpleComponent for AddCustomPluginWin {
|
||||||
|
type Init = AddCustomPluginWinInit;
|
||||||
|
type Input = AddCustomPluginWinMsg;
|
||||||
|
type Output = AddCustomPluginWinOutMsg;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
#[name(win)]
|
||||||
|
adw::Dialog {
|
||||||
|
set_can_close: true,
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child: inner = &adw::ToolbarView {
|
||||||
|
set_top_bar_style: adw::ToolbarStyle::Flat,
|
||||||
|
set_bottom_bar_style: adw::ToolbarStyle::Flat,
|
||||||
|
set_vexpand: true,
|
||||||
|
set_hexpand: true,
|
||||||
|
add_top_bar: top_bar = &adw::HeaderBar {
|
||||||
|
set_show_end_title_buttons: false,
|
||||||
|
set_show_start_title_buttons: false,
|
||||||
|
pack_start: cancel_btn = >k::Button {
|
||||||
|
set_label: "Cancel",
|
||||||
|
add_css_class: "destructive-action",
|
||||||
|
connect_clicked[sender] => move |_| {
|
||||||
|
sender.input(Self::Input::Close)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pack_end: add_btn = >k::Button {
|
||||||
|
set_label: "Add",
|
||||||
|
add_css_class: "suggested-action",
|
||||||
|
#[track = "model.changed(AddCustomPluginWin::can_add())"]
|
||||||
|
set_sensitive: model.can_add,
|
||||||
|
connect_clicked[sender] => move |_| {
|
||||||
|
sender.input(Self::Input::Add)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_title_widget: title_label = &adw::WindowTitle {
|
||||||
|
set_title: "Add Custom Plugin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_content: content = &adw::PreferencesPage {
|
||||||
|
set_hexpand: true,
|
||||||
|
set_vexpand: true,
|
||||||
|
add: grp = &adw::PreferencesGroup {
|
||||||
|
add: &entry_row(
|
||||||
|
"Plugin Name",
|
||||||
|
"",
|
||||||
|
clone!(
|
||||||
|
#[strong] sender,
|
||||||
|
move |row| sender.input(
|
||||||
|
Self::Input::OnNameChange(
|
||||||
|
row.text().to_string()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
add: &file_row(
|
||||||
|
"Plugin Executable",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(model.parent.clone()),
|
||||||
|
clone!(
|
||||||
|
#[strong] sender,
|
||||||
|
move |path_s| sender.input(
|
||||||
|
Self::Input::OnExecPathChange(path_s)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
add: &entry_row(
|
||||||
|
"Plugin Arguments",
|
||||||
|
"",
|
||||||
|
clone!(
|
||||||
|
#[strong] sender,
|
||||||
|
move |row| sender.input(
|
||||||
|
Self::Input::OnArgsChange(
|
||||||
|
row.text().to_string()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
|
||||||
|
self.reset();
|
||||||
|
|
||||||
|
match message {
|
||||||
|
Self::Input::Present => self.win.as_ref().unwrap().present(Some(&self.parent)),
|
||||||
|
Self::Input::Close => {
|
||||||
|
self.win.as_ref().unwrap().close();
|
||||||
|
}
|
||||||
|
Self::Input::Add => {
|
||||||
|
if self.plugin.validate() {
|
||||||
|
sender
|
||||||
|
.output(Self::Output::Add(self.plugin.clone()))
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
self.win.as_ref().unwrap().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Input::OnNameChange(name) => {
|
||||||
|
self.plugin.appid = if !name.is_empty() {
|
||||||
|
format!("{APP_ID}.customPlugin.{name}")
|
||||||
|
} else {
|
||||||
|
String::default()
|
||||||
|
};
|
||||||
|
self.plugin.name = name;
|
||||||
|
self.set_can_add(self.plugin.validate());
|
||||||
|
}
|
||||||
|
Self::Input::OnArgsChange(args) => {
|
||||||
|
let args = args.trim().to_string();
|
||||||
|
self.plugin.args = if args.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// it's fine to have them joined
|
||||||
|
// since they will ultimately be
|
||||||
|
// passed as a joined string
|
||||||
|
Some(vec![args])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Input::OnExecPathChange(ep) => {
|
||||||
|
self.plugin.exec_path = ep.map(PathBuf::from);
|
||||||
|
self.set_can_add(self.plugin.validate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
init: Self::Init,
|
||||||
|
root: Self::Root,
|
||||||
|
sender: ComponentSender<Self>,
|
||||||
|
) -> ComponentParts<Self> {
|
||||||
|
let mut model = Self {
|
||||||
|
tracker: 0,
|
||||||
|
win: None,
|
||||||
|
parent: init.parent,
|
||||||
|
can_add: false,
|
||||||
|
plugin: Plugin {
|
||||||
|
short_description: Some("Custom Plugin".into()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let widgets = view_output!();
|
||||||
|
model.win = Some(widgets.win.clone());
|
||||||
|
|
||||||
|
ComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
}
|
189
src/ui/plugins/mod.rs
Normal file
189
src/ui/plugins/mod.rs
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
pub mod add_custom_plugin_win;
|
||||||
|
pub mod store;
|
||||||
|
mod store_detail;
|
||||||
|
mod store_row_factory;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::APP_ID,
|
||||||
|
downloader::{cache_file_path, download_file_async},
|
||||||
|
file_builders::wayvr_dashboard_config::{
|
||||||
|
WayVrDashboardConfigFragment, WayVrDashboardConfigFragmentInner,
|
||||||
|
},
|
||||||
|
paths::get_plugins_dir,
|
||||||
|
util::file_utils::{get_writer, mark_as_executable},
|
||||||
|
xdg::XDG,
|
||||||
|
};
|
||||||
|
use anyhow::bail;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs::{create_dir_all, remove_file},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)]
|
||||||
|
pub enum PluginType {
|
||||||
|
#[default]
|
||||||
|
Executable,
|
||||||
|
WayVrApp,
|
||||||
|
WayVrDashboard,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginType {
|
||||||
|
pub fn launches_directly(&self) -> bool {
|
||||||
|
self == &Self::Executable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)]
|
||||||
|
pub struct Plugin {
|
||||||
|
pub appid: String,
|
||||||
|
pub name: String,
|
||||||
|
pub author: Option<String>,
|
||||||
|
pub icon_url: Option<String>,
|
||||||
|
pub version: Option<String>,
|
||||||
|
pub short_description: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub homepage_url: Option<String>,
|
||||||
|
pub screenshots: Vec<String>,
|
||||||
|
/// either one of exec_url or exec_path must be provided
|
||||||
|
pub exec_url: Option<String>,
|
||||||
|
/// either one of exec_url or exec_path must be provided
|
||||||
|
pub exec_path: Option<PathBuf>,
|
||||||
|
/// options and arguments that should be passed to the plugin executable
|
||||||
|
pub args: Option<Vec<String>>,
|
||||||
|
pub env_vars: Option<HashMap<String, String>>,
|
||||||
|
/// defined as a list of appids of other plugins
|
||||||
|
pub dependencies: Option<Vec<String>>,
|
||||||
|
/// defined as a list of appids of other plugins
|
||||||
|
/// all plugins of type WayVrDashboard should conflict with each other by default
|
||||||
|
pub conflicts: Option<Vec<String>>,
|
||||||
|
#[serde(default = "PluginType::default")]
|
||||||
|
pub plugin_type: PluginType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin {
|
||||||
|
fn wayvr_config_fragment_filename(&self) -> String {
|
||||||
|
format!("99-{APP_ID}-plugin.{}.yaml", self.appid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wayvr_conf_dir() -> PathBuf {
|
||||||
|
XDG.get_config_home().join("wlxoverlay/wayvr.conf.d")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable(&self) -> anyhow::Result<()> {
|
||||||
|
match self.plugin_type {
|
||||||
|
PluginType::Executable => {}
|
||||||
|
PluginType::WayVrApp => todo!(),
|
||||||
|
PluginType::WayVrDashboard => {
|
||||||
|
let wayvr_conf_dir = Self::wayvr_conf_dir();
|
||||||
|
if !wayvr_conf_dir.exists() {
|
||||||
|
create_dir_all(&wayvr_conf_dir)?;
|
||||||
|
} else if wayvr_conf_dir.is_file() {
|
||||||
|
bail!("wayvr.conf.d is a file and not a directory")
|
||||||
|
}
|
||||||
|
let config_fragment = WayVrDashboardConfigFragment {
|
||||||
|
dashboard: WayVrDashboardConfigFragmentInner {
|
||||||
|
exec: self
|
||||||
|
.executable()
|
||||||
|
.ok_or(anyhow::Error::msg("executable missing"))?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
args: self.args.as_ref().map(|args| args.join(" ")),
|
||||||
|
env: self
|
||||||
|
.env_vars
|
||||||
|
.as_ref()
|
||||||
|
.map(|vars| vars.iter().map(|(k, v)| format!("{k}={v}")).collect()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let writer =
|
||||||
|
get_writer(&wayvr_conf_dir.join(self.wayvr_config_fragment_filename()))?;
|
||||||
|
serde_yaml::to_writer(writer, &config_fragment)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable(&self) -> anyhow::Result<()> {
|
||||||
|
match self.plugin_type {
|
||||||
|
PluginType::Executable => {}
|
||||||
|
PluginType::WayVrApp => todo!(),
|
||||||
|
PluginType::WayVrDashboard => {
|
||||||
|
let wayvr_conf_dir = Self::wayvr_conf_dir();
|
||||||
|
remove_file(wayvr_conf_dir.join(self.wayvr_config_fragment_filename()))?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_enabled(&self, enabled: bool) -> anyhow::Result<()> {
|
||||||
|
if enabled {
|
||||||
|
self.enable()
|
||||||
|
} else {
|
||||||
|
self.disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn executable(&self) -> Option<PathBuf> {
|
||||||
|
if self.exec_path.is_some() {
|
||||||
|
self.exec_path.clone()
|
||||||
|
} else {
|
||||||
|
let canonical = self.canonical_exec_path();
|
||||||
|
if canonical.is_file() {
|
||||||
|
Some(canonical)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn canonical_exec_path(&self) -> PathBuf {
|
||||||
|
get_plugins_dir().join(&self.appid)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_installed(&self) -> bool {
|
||||||
|
self.executable().as_ref().is_some_and(|p| p.is_file())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_as_executable(&self) -> anyhow::Result<()> {
|
||||||
|
if let Some(p) = self.executable().as_ref() {
|
||||||
|
mark_as_executable(p)
|
||||||
|
} else {
|
||||||
|
bail!("no executable found for plugin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// validate if the plugin can be displayed correctly and run
|
||||||
|
pub fn validate(&self) -> bool {
|
||||||
|
!self.appid.is_empty()
|
||||||
|
&& !self.name.is_empty()
|
||||||
|
&& self.executable().as_ref().is_some_and(|p| p.is_file())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// urls to manifest json files representing plugins.
|
||||||
|
/// each manifest should be json and the link should always point to the latest version
|
||||||
|
const MANIFESTS: [&str;3] = [
|
||||||
|
"https://github.com/galister/wlx-overlay-s/raw/refs/heads/meta/com.github.galister.wlx-overlay-s.json",
|
||||||
|
"https://github.com/StardustXR/telescope/raw/refs/heads/main/envision/org.stardustxr.telescope.json",
|
||||||
|
"https://github.com/olekolek1000/wayvr-dashboard/raw/refs/heads/meta/dev.oo8.wayvr_dashboard.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub async fn refresh_plugins() -> anyhow::Result<Vec<anyhow::Result<Plugin>>> {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
for jh in MANIFESTS
|
||||||
|
.iter()
|
||||||
|
.map(|url| -> tokio::task::JoinHandle<anyhow::Result<Plugin>> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let path = cache_file_path(url, Some("json"));
|
||||||
|
download_file_async(url, &path).await?;
|
||||||
|
Ok(serde_json::from_str::<Plugin>(
|
||||||
|
&tokio::fs::read_to_string(path).await?,
|
||||||
|
)?)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
{
|
||||||
|
results.push(jh.await?);
|
||||||
|
}
|
||||||
|
Ok(results)
|
||||||
|
}
|
552
src/ui/plugins/store.rs
Normal file
552
src/ui/plugins/store.rs
Normal file
|
@ -0,0 +1,552 @@
|
||||||
|
use super::{
|
||||||
|
add_custom_plugin_win::{
|
||||||
|
AddCustomPluginWin, AddCustomPluginWinInit, AddCustomPluginWinMsg, AddCustomPluginWinOutMsg,
|
||||||
|
},
|
||||||
|
refresh_plugins,
|
||||||
|
store_detail::{StoreDetail, StoreDetailMsg, StoreDetailOutMsg},
|
||||||
|
store_row_factory::{StoreRowModel, StoreRowModelInit, StoreRowModelMsg, StoreRowModelOutMsg},
|
||||||
|
Plugin,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
config::PluginConfig,
|
||||||
|
downloader::download_file_async,
|
||||||
|
ui::{alert::alert, SENDER_IO_ERR_MSG},
|
||||||
|
};
|
||||||
|
use adw::prelude::*;
|
||||||
|
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
||||||
|
use std::{collections::HashMap, fs::remove_file};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
#[tracker::track]
|
||||||
|
pub struct PluginStore {
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
win: Option<adw::Window>,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
plugin_rows: Option<AsyncFactoryVecDeque<StoreRowModel>>,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
details: AsyncController<StoreDetail>,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
main_stack: Option<gtk::Stack>,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
config_plugins: HashMap<String, PluginConfig>,
|
||||||
|
refreshing: bool,
|
||||||
|
locked: bool,
|
||||||
|
plugins: Vec<Plugin>,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
add_custom_plugin_win: Option<Controller<AddCustomPluginWin>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum PluginStoreSignalSource {
|
||||||
|
Row,
|
||||||
|
Detail,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PluginStoreMsg {
|
||||||
|
Present,
|
||||||
|
/// sets state and calls DoRefresh
|
||||||
|
Refresh,
|
||||||
|
/// called by Refresh
|
||||||
|
DoRefresh,
|
||||||
|
Install(Plugin),
|
||||||
|
InstallDownload(Vec<Plugin>),
|
||||||
|
Remove(Plugin),
|
||||||
|
SetEnabled(PluginStoreSignalSource, Plugin, bool),
|
||||||
|
ShowDetails(usize),
|
||||||
|
ShowPluginList,
|
||||||
|
PresentAddCustomPluginWin,
|
||||||
|
AddCustomPlugin(Plugin),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PluginStoreInit {
|
||||||
|
pub config_plugins: HashMap<String, PluginConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PluginStoreOutMsg {
|
||||||
|
UpdateConfigPlugins(HashMap<String, PluginConfig>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginStore {
|
||||||
|
fn refresh_plugin_rows(&mut self) {
|
||||||
|
let mut guard = self.plugin_rows.as_mut().unwrap().guard();
|
||||||
|
guard.clear();
|
||||||
|
self.plugins.iter().for_each(|plugin| {
|
||||||
|
guard.push_back(StoreRowModelInit {
|
||||||
|
plugin: plugin.clone(),
|
||||||
|
enabled: self
|
||||||
|
.config_plugins
|
||||||
|
.get(&plugin.appid)
|
||||||
|
.is_some_and(|cp| cp.enabled),
|
||||||
|
needs_update: self
|
||||||
|
.config_plugins
|
||||||
|
.get(&plugin.appid)
|
||||||
|
.is_some_and(|cp| cp.plugin.version != plugin.version),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_plugin_to_config(&mut self, sender: &relm4::AsyncComponentSender<Self>, plugin: Plugin) {
|
||||||
|
self.config_plugins
|
||||||
|
.insert(plugin.appid.clone(), PluginConfig::from(&plugin));
|
||||||
|
sender
|
||||||
|
.output(PluginStoreOutMsg::UpdateConfigPlugins(
|
||||||
|
self.config_plugins.clone(),
|
||||||
|
))
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::component(pub async)]
|
||||||
|
impl AsyncComponent for PluginStore {
|
||||||
|
type Init = PluginStoreInit;
|
||||||
|
type Input = PluginStoreMsg;
|
||||||
|
type Output = PluginStoreOutMsg;
|
||||||
|
type CommandOutput = ();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
#[name(win)]
|
||||||
|
adw::Window {
|
||||||
|
set_title: Some("Plugins"),
|
||||||
|
#[name(main_stack)]
|
||||||
|
gtk::Stack {
|
||||||
|
add_child = &adw::ToolbarView {
|
||||||
|
set_top_bar_style: adw::ToolbarStyle::Flat,
|
||||||
|
add_top_bar: headerbar = &adw::HeaderBar {
|
||||||
|
pack_start: add_custom_plugin_btn = >k::Button {
|
||||||
|
set_icon_name: "list-add-symbolic",
|
||||||
|
set_tooltip_text: Some("Add custom plugin"),
|
||||||
|
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
|
||||||
|
set_sensitive: !(model.refreshing || model.locked),
|
||||||
|
connect_clicked[sender] => move |_| {
|
||||||
|
sender.input(Self::Input::PresentAddCustomPluginWin)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pack_end: refreshbtn = >k::Button {
|
||||||
|
set_icon_name: "view-refresh-symbolic",
|
||||||
|
set_tooltip_text: Some("Refresh"),
|
||||||
|
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
|
||||||
|
set_sensitive: !(model.refreshing || model.locked),
|
||||||
|
connect_clicked[sender] => move |_| {
|
||||||
|
sender.input(Self::Input::Refresh);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_content: inner = >k::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_hexpand: true,
|
||||||
|
set_vexpand: true,
|
||||||
|
gtk::Stack {
|
||||||
|
set_hexpand: true,
|
||||||
|
set_vexpand: true,
|
||||||
|
add_child = >k::ScrolledWindow {
|
||||||
|
set_hscrollbar_policy: gtk::PolicyType::Never,
|
||||||
|
set_hexpand: true,
|
||||||
|
set_vexpand: true,
|
||||||
|
adw::Clamp {
|
||||||
|
#[name(listbox)]
|
||||||
|
gtk::ListBox {
|
||||||
|
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
|
||||||
|
set_sensitive: !(model.refreshing || model.locked),
|
||||||
|
add_css_class: "boxed-list",
|
||||||
|
set_valign: gtk::Align::Start,
|
||||||
|
set_margin_all: 12,
|
||||||
|
set_selection_mode: gtk::SelectionMode::None,
|
||||||
|
connect_row_activated[sender] => move |_, row| {
|
||||||
|
sender.input(
|
||||||
|
Self::Input::ShowDetails(
|
||||||
|
row.index() as usize
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} -> {
|
||||||
|
set_name: "pluginlist"
|
||||||
|
},
|
||||||
|
add_child = >k::Spinner {
|
||||||
|
set_hexpand: true,
|
||||||
|
set_vexpand: true,
|
||||||
|
set_width_request: 32,
|
||||||
|
set_height_request: 32,
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
#[track = "model.changed(PluginStore::refreshing())"]
|
||||||
|
set_spinning: model.refreshing,
|
||||||
|
} -> {
|
||||||
|
set_name: "spinner"
|
||||||
|
},
|
||||||
|
add_child = &adw::StatusPage {
|
||||||
|
set_hexpand: true,
|
||||||
|
set_vexpand: true,
|
||||||
|
set_title: "No Plugins Found",
|
||||||
|
set_description: Some("Make sure you're connected to the internet and refresh"),
|
||||||
|
set_icon_name: Some("application-x-addon-symbolic"),
|
||||||
|
} -> {
|
||||||
|
set_name: "emptystate"
|
||||||
|
},
|
||||||
|
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::plugins())"]
|
||||||
|
set_visible_child_name: if model.refreshing {
|
||||||
|
"spinner"
|
||||||
|
} else if model.plugins.is_empty() {
|
||||||
|
"emptystate"
|
||||||
|
} else {
|
||||||
|
"pluginlist"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} -> {
|
||||||
|
set_name: "listview"
|
||||||
|
},
|
||||||
|
add_child = &adw::Bin {
|
||||||
|
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
|
||||||
|
set_sensitive: !(model.refreshing || model.locked),
|
||||||
|
set_child: Some(details_view),
|
||||||
|
} -> {
|
||||||
|
set_name: "detailsview",
|
||||||
|
},
|
||||||
|
set_visible_child_name: "listview",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
message: Self::Input,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
_root: &Self::Root,
|
||||||
|
) {
|
||||||
|
self.reset();
|
||||||
|
|
||||||
|
match message {
|
||||||
|
Self::Input::Present => {
|
||||||
|
self.win.as_ref().unwrap().present();
|
||||||
|
sender.input(Self::Input::Refresh);
|
||||||
|
}
|
||||||
|
Self::Input::AddCustomPlugin(plugin) => {
|
||||||
|
if self.config_plugins.contains_key(&plugin.appid) {
|
||||||
|
alert(
|
||||||
|
"Failed to add custom plugin",
|
||||||
|
Some("A plugin with the same name already exists"),
|
||||||
|
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.add_plugin_to_config(&sender, plugin);
|
||||||
|
sender.input(Self::Input::Refresh);
|
||||||
|
}
|
||||||
|
Self::Input::PresentAddCustomPluginWin => {
|
||||||
|
let add_win = AddCustomPluginWin::builder()
|
||||||
|
.launch(AddCustomPluginWinInit {
|
||||||
|
parent: self.win.as_ref().unwrap().clone().upcast(),
|
||||||
|
})
|
||||||
|
.forward(sender.input_sender(), |msg| match msg {
|
||||||
|
AddCustomPluginWinOutMsg::Add(plugin) => {
|
||||||
|
Self::Input::AddCustomPlugin(plugin)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add_win.sender().emit(AddCustomPluginWinMsg::Present);
|
||||||
|
self.add_custom_plugin_win = Some(add_win);
|
||||||
|
}
|
||||||
|
Self::Input::Refresh => {
|
||||||
|
self.set_refreshing(true);
|
||||||
|
sender.input(Self::Input::DoRefresh);
|
||||||
|
}
|
||||||
|
Self::Input::DoRefresh => {
|
||||||
|
let mut plugins = match refresh_plugins().await {
|
||||||
|
Err(e) => {
|
||||||
|
error!("failed to refresh plugins: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
Ok(results) => results
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|res| match res {
|
||||||
|
Ok(plugin) => Some(plugin),
|
||||||
|
Err(e) => {
|
||||||
|
error!("failed to refresh single plugin manifest: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let appids_from_web = plugins
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.appid.clone())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
// add all plugins that are in config but not retrieved
|
||||||
|
plugins.extend(self.config_plugins.values().filter_map(|cp| {
|
||||||
|
if appids_from_web.contains(&cp.plugin.appid) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Plugin::from(cp))
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
self.set_plugins(plugins);
|
||||||
|
self.refresh_plugin_rows();
|
||||||
|
self.set_refreshing(false);
|
||||||
|
}
|
||||||
|
Self::Input::Install(plugin) => {
|
||||||
|
self.set_locked(true);
|
||||||
|
let mut plugins = vec![plugin];
|
||||||
|
for dep in plugins[0].dependencies.clone().unwrap_or_default() {
|
||||||
|
if let Some(dep_plugin) = self
|
||||||
|
.plugins
|
||||||
|
.iter()
|
||||||
|
.find(|plugin| plugin.appid == dep)
|
||||||
|
.cloned()
|
||||||
|
{
|
||||||
|
plugins.push(dep_plugin);
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
"unable to find dependency for {}: {}",
|
||||||
|
plugins[0].appid, dep
|
||||||
|
);
|
||||||
|
alert(
|
||||||
|
"Missing dependencies",
|
||||||
|
Some(&format!(
|
||||||
|
"{} depends on unknown plugin {}",
|
||||||
|
plugins[0].name, dep
|
||||||
|
)),
|
||||||
|
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sender.input(Self::Input::InstallDownload(plugins))
|
||||||
|
}
|
||||||
|
Self::Input::InstallDownload(plugins) => {
|
||||||
|
for plugin in plugins {
|
||||||
|
let mut plugin = plugin.clone();
|
||||||
|
match plugin.exec_url.as_ref() {
|
||||||
|
Some(url) => {
|
||||||
|
let exec_path = plugin.canonical_exec_path();
|
||||||
|
if let Err(e) = download_file_async(url, &exec_path).await {
|
||||||
|
alert(
|
||||||
|
"Download failed",
|
||||||
|
Some(&format!(
|
||||||
|
"Downloading {} {} failed:\n\n{e}",
|
||||||
|
plugin.name,
|
||||||
|
plugin
|
||||||
|
.version
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&"(no version)".to_string())
|
||||||
|
)),
|
||||||
|
Some(
|
||||||
|
&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
plugin.exec_path = Some(exec_path);
|
||||||
|
if let Err(e) = plugin.enable() {
|
||||||
|
error!("failed to enable plugin {}: {e}", plugin.appid);
|
||||||
|
alert(
|
||||||
|
"Failed to enable plugin",
|
||||||
|
Some(&e.to_string()),
|
||||||
|
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.add_plugin_to_config(&sender, plugin.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
alert(
|
||||||
|
"Download failed",
|
||||||
|
Some(&format!(
|
||||||
|
"Downloading {} {} failed:\n\nNo executable url provided for this plugin, this is likely a bug!",
|
||||||
|
plugin.name,
|
||||||
|
plugin.version.as_ref().unwrap_or(&"(no version)".to_string()))
|
||||||
|
),
|
||||||
|
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.refresh_plugin_rows();
|
||||||
|
self.details
|
||||||
|
.emit(StoreDetailMsg::Refresh(plugin.appid, true, false));
|
||||||
|
}
|
||||||
|
self.set_locked(false);
|
||||||
|
}
|
||||||
|
Self::Input::Remove(plugin) => {
|
||||||
|
self.set_locked(true);
|
||||||
|
if let Err(e) = plugin.disable() {
|
||||||
|
error!("failed to disable plugin {}: {e}", plugin.appid);
|
||||||
|
alert(
|
||||||
|
"Failed to disable plugin",
|
||||||
|
Some(&e.to_string()),
|
||||||
|
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(exec) = plugin.executable() {
|
||||||
|
// delete executable only if it's not a custom plugin
|
||||||
|
if exec.is_file() && plugin.exec_url.is_some() {
|
||||||
|
if let Err(e) = remove_file(&exec) {
|
||||||
|
alert(
|
||||||
|
"Failed removing plugin",
|
||||||
|
Some(&format!(
|
||||||
|
"Could not remove plugin executable {}:\n\n{e}",
|
||||||
|
exec.to_string_lossy()
|
||||||
|
)),
|
||||||
|
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.config_plugins.remove(&plugin.appid);
|
||||||
|
self.set_plugins(
|
||||||
|
self.plugins
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|p| !(p.appid == plugin.appid && p.exec_url.is_none()))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
sender
|
||||||
|
.output(Self::Output::UpdateConfigPlugins(
|
||||||
|
self.config_plugins.clone(),
|
||||||
|
))
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
self.refresh_plugin_rows();
|
||||||
|
self.details
|
||||||
|
.emit(StoreDetailMsg::Refresh(plugin.appid, false, false));
|
||||||
|
self.set_locked(false);
|
||||||
|
}
|
||||||
|
Self::Input::SetEnabled(signal_sender, plugin, enabled) => {
|
||||||
|
if let Some(cp) = self.config_plugins.get_mut(&plugin.appid) {
|
||||||
|
if let Err(e) = plugin.set_enabled(enabled) {
|
||||||
|
error!(
|
||||||
|
"failed to {} plugin {}: {e}",
|
||||||
|
if enabled { "enable" } else { "disable" },
|
||||||
|
plugin.appid
|
||||||
|
);
|
||||||
|
alert(
|
||||||
|
&format!(
|
||||||
|
"Failed to {} plugin",
|
||||||
|
if enabled { "enable" } else { "disable" }
|
||||||
|
),
|
||||||
|
Some(&e.to_string()),
|
||||||
|
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cp.enabled = enabled;
|
||||||
|
if signal_sender == PluginStoreSignalSource::Detail {
|
||||||
|
if let Some(row) = self
|
||||||
|
.plugin_rows
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.guard()
|
||||||
|
.iter()
|
||||||
|
.find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid))
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
row.input_sender.emit(StoreRowModelMsg::Refresh(
|
||||||
|
enabled,
|
||||||
|
cp.plugin.version != plugin.version,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
error!("could not find corresponding listbox row")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if signal_sender == PluginStoreSignalSource::Row {
|
||||||
|
self.details.emit(StoreDetailMsg::Refresh(
|
||||||
|
plugin.appid,
|
||||||
|
enabled,
|
||||||
|
cp.plugin.version != plugin.version,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
"failed to set plugin {} enabled: could not find in hashmap",
|
||||||
|
plugin.appid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sender
|
||||||
|
.output(Self::Output::UpdateConfigPlugins(
|
||||||
|
self.config_plugins.clone(),
|
||||||
|
))
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
}
|
||||||
|
// we use index here because it's the listbox not the row that can
|
||||||
|
// send this signal, so I don't directly have the plugin object
|
||||||
|
Self::Input::ShowDetails(index) => {
|
||||||
|
if let Some(plugin) = self.plugins.get(index) {
|
||||||
|
self.details.sender().emit(StoreDetailMsg::SetPlugin(
|
||||||
|
plugin.clone(),
|
||||||
|
self.config_plugins
|
||||||
|
.get(&plugin.appid)
|
||||||
|
.is_some_and(|cp| cp.enabled),
|
||||||
|
self.config_plugins
|
||||||
|
.get(&plugin.appid)
|
||||||
|
.is_some_and(|cp| cp.plugin.version != plugin.version),
|
||||||
|
));
|
||||||
|
self.main_stack
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.set_visible_child_name("detailsview");
|
||||||
|
} else {
|
||||||
|
error!("plugins list index out of range!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Input::ShowPluginList => {
|
||||||
|
self.main_stack
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.set_visible_child_name("listview");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(
|
||||||
|
init: Self::Init,
|
||||||
|
root: Self::Root,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
) -> AsyncComponentParts<Self> {
|
||||||
|
let mut model = Self {
|
||||||
|
tracker: 0,
|
||||||
|
refreshing: false,
|
||||||
|
locked: false,
|
||||||
|
win: None,
|
||||||
|
plugins: Vec::default(),
|
||||||
|
plugin_rows: None,
|
||||||
|
details: StoreDetail::builder()
|
||||||
|
.launch(())
|
||||||
|
.forward(sender.input_sender(), move |msg| match msg {
|
||||||
|
StoreDetailOutMsg::GoBack => Self::Input::ShowPluginList,
|
||||||
|
StoreDetailOutMsg::Install(plugin) => Self::Input::Install(plugin),
|
||||||
|
StoreDetailOutMsg::Remove(plugin) => Self::Input::Remove(plugin),
|
||||||
|
StoreDetailOutMsg::SetEnabled(plugin, enabled) => {
|
||||||
|
Self::Input::SetEnabled(PluginStoreSignalSource::Detail, plugin, enabled)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
config_plugins: init.config_plugins,
|
||||||
|
main_stack: None,
|
||||||
|
add_custom_plugin_win: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let details_view = model.details.widget();
|
||||||
|
|
||||||
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
model.win = Some(widgets.win.clone());
|
||||||
|
model.plugin_rows = Some(
|
||||||
|
AsyncFactoryVecDeque::builder()
|
||||||
|
.launch(widgets.listbox.clone())
|
||||||
|
.forward(sender.input_sender(), move |msg| match msg {
|
||||||
|
StoreRowModelOutMsg::Install(appid) => Self::Input::Install(appid),
|
||||||
|
StoreRowModelOutMsg::Remove(appid) => Self::Input::Remove(appid),
|
||||||
|
StoreRowModelOutMsg::SetEnabled(plugin, enabled) => {
|
||||||
|
Self::Input::SetEnabled(PluginStoreSignalSource::Row, plugin, enabled)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
model.main_stack = Some(widgets.main_stack.clone());
|
||||||
|
|
||||||
|
AsyncComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
}
|
373
src/ui/plugins/store_detail.rs
Normal file
373
src/ui/plugins/store_detail.rs
Normal file
|
@ -0,0 +1,373 @@
|
||||||
|
use super::Plugin;
|
||||||
|
use crate::{downloader::cache_file, ui::SENDER_IO_ERR_MSG};
|
||||||
|
use adw::prelude::*;
|
||||||
|
use relm4::prelude::*;
|
||||||
|
use tracing::{error, warn};
|
||||||
|
|
||||||
|
#[tracker::track]
|
||||||
|
pub struct StoreDetail {
|
||||||
|
plugin: Option<Plugin>,
|
||||||
|
enabled: bool,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
carousel: Option<adw::Carousel>,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
icon: Option<gtk::Image>,
|
||||||
|
needs_update: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum StoreDetailMsg {
|
||||||
|
SetPlugin(Plugin, bool, bool),
|
||||||
|
SetIcon,
|
||||||
|
SetScreenshots,
|
||||||
|
Refresh(String, bool, bool),
|
||||||
|
Install,
|
||||||
|
Remove,
|
||||||
|
SetEnabled(bool),
|
||||||
|
OpenHomepage,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum StoreDetailOutMsg {
|
||||||
|
Install(Plugin),
|
||||||
|
Remove(Plugin),
|
||||||
|
GoBack,
|
||||||
|
SetEnabled(Plugin, bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::component(pub async)]
|
||||||
|
impl AsyncComponent for StoreDetail {
|
||||||
|
type Init = ();
|
||||||
|
type Input = StoreDetailMsg;
|
||||||
|
type Output = StoreDetailOutMsg;
|
||||||
|
type CommandOutput = ();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
adw::ToolbarView {
|
||||||
|
set_top_bar_style: adw::ToolbarStyle::Flat,
|
||||||
|
add_top_bar: headerbar = &adw::HeaderBar {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_title_widget: title = &adw::WindowTitle {
|
||||||
|
#[track = "model.changed(Self::plugin())"]
|
||||||
|
set_title: model
|
||||||
|
.plugin
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.name.as_str())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
},
|
||||||
|
pack_start: backbtn = >k::Button {
|
||||||
|
set_icon_name: "go-previous-symbolic",
|
||||||
|
set_tooltip_text: Some("Back"),
|
||||||
|
connect_clicked[sender] => move |_| {
|
||||||
|
sender.output(Self::Output::GoBack).expect(SENDER_IO_ERR_MSG);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_content: inner = >k::ScrolledWindow {
|
||||||
|
set_hscrollbar_policy: gtk::PolicyType::Never,
|
||||||
|
set_hexpand: true,
|
||||||
|
set_vexpand: true,
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_hexpand: true,
|
||||||
|
set_vexpand: true,
|
||||||
|
set_margin_top: 12,
|
||||||
|
set_margin_bottom: 48,
|
||||||
|
set_margin_start: 12,
|
||||||
|
set_margin_end: 12,
|
||||||
|
set_spacing: 24,
|
||||||
|
adw::Clamp { // icon, name, buttons
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_hexpand: true,
|
||||||
|
set_vexpand: false,
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_hexpand: true,
|
||||||
|
#[name(icon)]
|
||||||
|
gtk::Image {
|
||||||
|
set_icon_name: Some("application-x-addon-symbolic"),
|
||||||
|
set_margin_end: 12,
|
||||||
|
set_pixel_size: 96,
|
||||||
|
},
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_hexpand: true,
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
set_spacing: 6,
|
||||||
|
gtk::Label {
|
||||||
|
add_css_class: "title-2",
|
||||||
|
set_xalign: 0.0,
|
||||||
|
#[track = "model.changed(Self::plugin())"]
|
||||||
|
set_text: model
|
||||||
|
.plugin
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.name.as_str())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
set_ellipsize: gtk::pango::EllipsizeMode::None,
|
||||||
|
set_wrap: true,
|
||||||
|
},
|
||||||
|
gtk::Label {
|
||||||
|
add_css_class: "dim-label",
|
||||||
|
set_xalign: 0.0,
|
||||||
|
#[track = "model.changed(Self::plugin())"]
|
||||||
|
set_visible: model
|
||||||
|
.plugin
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|p| p.author.is_some()),
|
||||||
|
#[track = "model.changed(Self::plugin())"]
|
||||||
|
set_text: model
|
||||||
|
.plugin
|
||||||
|
.as_ref()
|
||||||
|
.and_then(
|
||||||
|
|p| p.author.as_deref()
|
||||||
|
)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
set_ellipsize: gtk::pango::EllipsizeMode::None,
|
||||||
|
set_wrap: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
set_spacing: 6,
|
||||||
|
gtk::Button {
|
||||||
|
#[track = "model.changed(Self::plugin())"]
|
||||||
|
set_visible: !model
|
||||||
|
.plugin
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|p| p.is_installed()),
|
||||||
|
set_label: "Install",
|
||||||
|
add_css_class: "suggested-action",
|
||||||
|
connect_clicked[sender] => move |_| {
|
||||||
|
sender.input(Self::Input::Install);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gtk::Button {
|
||||||
|
#[track = "model.changed(Self::plugin())"]
|
||||||
|
set_visible: model
|
||||||
|
.plugin
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|p| p.is_installed()),
|
||||||
|
set_label: "Remove",
|
||||||
|
add_css_class: "destructive-action",
|
||||||
|
connect_clicked[sender] => move |_| {
|
||||||
|
sender.input(Self::Input::Remove);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gtk::Button {
|
||||||
|
#[track = "model.changed(Self::plugin()) || model.changed(Self::needs_update())"]
|
||||||
|
set_visible: model
|
||||||
|
.plugin
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|p| p.is_installed()) && model.needs_update,
|
||||||
|
add_css_class: "suggested-action",
|
||||||
|
set_label: "Update",
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
connect_clicked[sender] => move |_| {
|
||||||
|
sender.input(Self::Input::Install);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gtk::Switch {
|
||||||
|
#[track = "model.changed(Self::plugin())"]
|
||||||
|
set_visible: model.plugin.as_ref()
|
||||||
|
.is_some_and(|p| p.is_installed()),
|
||||||
|
#[track = "model.changed(Self::enabled())"]
|
||||||
|
set_active: model.enabled,
|
||||||
|
set_tooltip_text: Some("Plugin enabled"),
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
connect_state_set[sender] => move |_, state| {
|
||||||
|
sender.input(Self::Input::SetEnabled(state));
|
||||||
|
gtk::glib::Propagation::Proceed
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gtk::Button {
|
||||||
|
#[track = "model.changed(Self::plugin())"]
|
||||||
|
set_visible: model.plugin.as_ref()
|
||||||
|
.is_some_and(|p| p.homepage_url.is_some()),
|
||||||
|
set_label: "Homepage",
|
||||||
|
connect_clicked[sender] => move |_| {
|
||||||
|
sender.input(Self::Input::OpenHomepage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gtk::Box { // screenshots
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_spacing: 12,
|
||||||
|
#[name(carousel)]
|
||||||
|
adw::Carousel {
|
||||||
|
set_allow_mouse_drag: true,
|
||||||
|
set_allow_scroll_wheel: false,
|
||||||
|
set_spacing: 24,
|
||||||
|
},
|
||||||
|
adw::CarouselIndicatorDots {
|
||||||
|
set_carousel: Some(&carousel),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adw::Clamp { // description
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
gtk::Label {
|
||||||
|
set_xalign: 0.0,
|
||||||
|
#[track = "model.changed(Self::plugin())"]
|
||||||
|
set_text: model
|
||||||
|
.plugin
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p
|
||||||
|
.description
|
||||||
|
.as_deref()
|
||||||
|
).unwrap_or(""),
|
||||||
|
set_ellipsize: gtk::pango::EllipsizeMode::None,
|
||||||
|
set_wrap: true,
|
||||||
|
set_justify: gtk::Justification::Fill,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
message: Self::Input,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
_root: &Self::Root,
|
||||||
|
) {
|
||||||
|
self.reset();
|
||||||
|
|
||||||
|
match message {
|
||||||
|
Self::Input::OpenHomepage => {
|
||||||
|
if let Some(plugin) = self.plugin.as_ref() {
|
||||||
|
if let Some(homepage) = plugin.homepage_url.as_ref() {
|
||||||
|
if let Err(e) = gtk::gio::AppInfo::launch_default_for_uri(
|
||||||
|
homepage,
|
||||||
|
gtk::gio::AppLaunchContext::NONE,
|
||||||
|
) {
|
||||||
|
error!("opening uri {homepage}: {e}");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Input::SetPlugin(p, enabled, needs_update) => {
|
||||||
|
self.set_plugin(Some(p));
|
||||||
|
self.set_enabled(enabled);
|
||||||
|
self.set_needs_update(needs_update);
|
||||||
|
sender.input(Self::Input::SetIcon);
|
||||||
|
sender.input(Self::Input::SetScreenshots);
|
||||||
|
}
|
||||||
|
Self::Input::SetIcon => {
|
||||||
|
if let Some(plugin) = self.plugin.as_ref() {
|
||||||
|
if let Some(url) = plugin.icon_url.as_ref() {
|
||||||
|
match cache_file(url, None).await {
|
||||||
|
Ok(dest) => {
|
||||||
|
self.icon.as_ref().unwrap().set_from_file(Some(dest));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed downloading icon '{url}': {e}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
self.icon
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.set_icon_name(Some("application-x-addon-symbolic"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Input::SetScreenshots => {
|
||||||
|
let carousel = self.carousel.as_ref().unwrap().clone();
|
||||||
|
while let Some(child) = carousel.first_child() {
|
||||||
|
carousel.remove(&child);
|
||||||
|
}
|
||||||
|
if let Some(plugin) = self.plugin.as_ref() {
|
||||||
|
carousel
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.set_visible(!plugin.screenshots.is_empty());
|
||||||
|
for url in plugin.screenshots.iter() {
|
||||||
|
match cache_file(url, None).await {
|
||||||
|
Ok(dest) => {
|
||||||
|
let pic = gtk::Picture::builder()
|
||||||
|
.height_request(300)
|
||||||
|
.css_classes(["card"])
|
||||||
|
.overflow(gtk::Overflow::Hidden)
|
||||||
|
.valign(gtk::Align::Center)
|
||||||
|
.build();
|
||||||
|
pic.set_filename(Some(dest));
|
||||||
|
let clamp = adw::Clamp::builder().child(&pic).build();
|
||||||
|
carousel.append(&clamp);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("failed downloading screenshot '{url}': {e}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Input::Refresh(appid, enabled, needs_update) => {
|
||||||
|
if self.plugin.as_ref().is_some_and(|p| p.appid == appid) {
|
||||||
|
self.mark_all_changed();
|
||||||
|
self.set_enabled(enabled);
|
||||||
|
self.set_needs_update(needs_update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Input::Install => {
|
||||||
|
if let Some(plugin) = self.plugin.as_ref() {
|
||||||
|
sender
|
||||||
|
.output(Self::Output::Install(plugin.clone()))
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Input::Remove => {
|
||||||
|
if let Some(plugin) = self.plugin.as_ref() {
|
||||||
|
sender
|
||||||
|
.output(Self::Output::Remove(plugin.clone()))
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
if plugin.exec_url.is_none() {
|
||||||
|
sender
|
||||||
|
.output(Self::Output::GoBack)
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Input::SetEnabled(enabled) => {
|
||||||
|
self.set_enabled(enabled);
|
||||||
|
if let Some(plugin) = self.plugin.as_ref() {
|
||||||
|
sender
|
||||||
|
.output(Self::Output::SetEnabled(plugin.clone(), enabled))
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(
|
||||||
|
_init: Self::Init,
|
||||||
|
root: Self::Root,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
) -> AsyncComponentParts<Self> {
|
||||||
|
let mut model = Self {
|
||||||
|
tracker: 0,
|
||||||
|
plugin: None,
|
||||||
|
enabled: false,
|
||||||
|
carousel: None,
|
||||||
|
icon: None,
|
||||||
|
needs_update: false,
|
||||||
|
};
|
||||||
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
model.carousel = Some(widgets.carousel.clone());
|
||||||
|
model.icon = Some(widgets.icon.clone());
|
||||||
|
|
||||||
|
AsyncComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
}
|
225
src/ui/plugins/store_row_factory.rs
Normal file
225
src/ui/plugins/store_row_factory.rs
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
use super::Plugin;
|
||||||
|
use crate::{downloader::cache_file, ui::SENDER_IO_ERR_MSG};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use relm4::{
|
||||||
|
factory::AsyncFactoryComponent, prelude::DynamicIndex, AsyncFactorySender, RelmWidgetExt,
|
||||||
|
};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[tracker::track]
|
||||||
|
pub struct StoreRowModel {
|
||||||
|
#[no_eq]
|
||||||
|
pub plugin: Plugin,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
icon: Option<gtk::Image>,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
pub input_sender: relm4::Sender<StoreRowModelMsg>,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub needs_update: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct StoreRowModelInit {
|
||||||
|
pub plugin: Plugin,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub needs_update: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum StoreRowModelMsg {
|
||||||
|
LoadIcon,
|
||||||
|
/// params: enabled, needs_update
|
||||||
|
Refresh(bool, bool),
|
||||||
|
SetEnabled(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum StoreRowModelOutMsg {
|
||||||
|
Install(Plugin),
|
||||||
|
Remove(Plugin),
|
||||||
|
SetEnabled(Plugin, bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::factory(async pub)]
|
||||||
|
impl AsyncFactoryComponent for StoreRowModel {
|
||||||
|
type Init = StoreRowModelInit;
|
||||||
|
type Input = StoreRowModelMsg;
|
||||||
|
type Output = StoreRowModelOutMsg;
|
||||||
|
type CommandOutput = ();
|
||||||
|
type ParentWidget = gtk::ListBox;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
root = gtk::ListBoxRow {
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_hexpand: true,
|
||||||
|
set_vexpand: false,
|
||||||
|
set_spacing: 12,
|
||||||
|
set_margin_all: 12,
|
||||||
|
#[name(icon)]
|
||||||
|
gtk::Image {
|
||||||
|
set_icon_name: Some("application-x-addon-symbolic"),
|
||||||
|
set_icon_size: gtk::IconSize::Large,
|
||||||
|
},
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_spacing: 6,
|
||||||
|
set_hexpand: true,
|
||||||
|
set_vexpand: true,
|
||||||
|
gtk::Label {
|
||||||
|
add_css_class: "title-3",
|
||||||
|
set_hexpand: true,
|
||||||
|
set_xalign: 0.0,
|
||||||
|
set_text: &self.plugin.name,
|
||||||
|
set_ellipsize: gtk::pango::EllipsizeMode::None,
|
||||||
|
set_wrap: true,
|
||||||
|
},
|
||||||
|
gtk::Label {
|
||||||
|
add_css_class: "dim-label",
|
||||||
|
set_hexpand: true,
|
||||||
|
set_xalign: 0.0,
|
||||||
|
set_text: self.plugin.short_description
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(""),
|
||||||
|
set_ellipsize: gtk::pango::EllipsizeMode::None,
|
||||||
|
set_wrap: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_spacing: 6,
|
||||||
|
set_vexpand: true,
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
gtk::Button {
|
||||||
|
#[track = "self.changed(StoreRowModel::plugin())"]
|
||||||
|
set_visible: !self.plugin.is_installed(),
|
||||||
|
set_icon_name: "folder-download-symbolic",
|
||||||
|
add_css_class: "suggested-action",
|
||||||
|
set_tooltip_text: Some("Install"),
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
connect_clicked[sender, plugin] => move |_| {
|
||||||
|
sender
|
||||||
|
.output(Self::Output::Install(
|
||||||
|
plugin.clone(),
|
||||||
|
))
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_spacing: 6,
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
gtk::Button {
|
||||||
|
#[track = "self.changed(StoreRowModel::plugin())"]
|
||||||
|
set_visible: self.plugin.is_installed(),
|
||||||
|
set_icon_name: "user-trash-symbolic",
|
||||||
|
add_css_class: "destructive-action",
|
||||||
|
set_tooltip_text: Some("Remove"),
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
connect_clicked[sender, plugin] => move |_| {
|
||||||
|
sender
|
||||||
|
.output(Self::Output::Remove(
|
||||||
|
plugin.clone(),
|
||||||
|
))
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gtk::Button {
|
||||||
|
#[track = "self.changed(StoreRowModel::plugin()) || self.changed(StoreRowModel::needs_update())"]
|
||||||
|
set_visible: self.plugin.is_installed() && self.needs_update,
|
||||||
|
set_icon_name: "view-refresh-symbolic",
|
||||||
|
add_css_class: "suggested-action",
|
||||||
|
set_tooltip_text: Some("Update"),
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
connect_clicked[sender, plugin] => move |_| {
|
||||||
|
sender
|
||||||
|
.output(Self::Output::Install(
|
||||||
|
plugin.clone(),
|
||||||
|
))
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gtk::Switch {
|
||||||
|
#[track = "self.changed(StoreRowModel::plugin())"]
|
||||||
|
set_visible: self.plugin.is_installed(),
|
||||||
|
#[track = "self.changed(StoreRowModel::enabled())"]
|
||||||
|
set_active: self.enabled,
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
set_tooltip_text: Some("Plugin enabled"),
|
||||||
|
connect_state_set[sender] => move |_, state| {
|
||||||
|
sender.input(Self::Input::SetEnabled(state));
|
||||||
|
gtk::glib::Propagation::Proceed
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&mut self, message: Self::Input, sender: AsyncFactorySender<Self>) {
|
||||||
|
self.reset();
|
||||||
|
|
||||||
|
match message {
|
||||||
|
Self::Input::LoadIcon => {
|
||||||
|
if let Some(url) = self.plugin.icon_url.as_ref() {
|
||||||
|
match cache_file(url, None).await {
|
||||||
|
Ok(dest) => {
|
||||||
|
self.icon.as_ref().unwrap().set_from_file(Some(dest));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("failed downloading icon '{url}': {e}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Input::SetEnabled(state) => {
|
||||||
|
self.set_enabled(state);
|
||||||
|
sender
|
||||||
|
.output(Self::Output::SetEnabled(self.plugin.clone(), state))
|
||||||
|
.expect(SENDER_IO_ERR_MSG);
|
||||||
|
}
|
||||||
|
Self::Input::Refresh(enabled, needs_update) => {
|
||||||
|
self.mark_all_changed();
|
||||||
|
self.set_enabled(enabled);
|
||||||
|
self.set_needs_update(needs_update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init_model(
|
||||||
|
init: Self::Init,
|
||||||
|
_index: &DynamicIndex,
|
||||||
|
sender: AsyncFactorySender<Self>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
tracker: 0,
|
||||||
|
plugin: init.plugin,
|
||||||
|
enabled: init.enabled,
|
||||||
|
icon: None,
|
||||||
|
input_sender: sender.input_sender().clone(),
|
||||||
|
needs_update: init.needs_update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_widgets(
|
||||||
|
&mut self,
|
||||||
|
_index: &DynamicIndex,
|
||||||
|
root: Self::Root,
|
||||||
|
_returned_widget: &<Self::ParentWidget as relm4::factory::FactoryView>::ReturnedWidget,
|
||||||
|
sender: AsyncFactorySender<Self>,
|
||||||
|
) -> Self::Widgets {
|
||||||
|
let plugin = self.plugin.clone(); // for use in a signal handler
|
||||||
|
let widgets = view_output!();
|
||||||
|
self.icon = Some(widgets.icon.clone());
|
||||||
|
sender.input(Self::Input::LoadIcon);
|
||||||
|
widgets
|
||||||
|
}
|
||||||
|
}
|
|
@ -156,13 +156,12 @@ pub fn spin_row<F: Fn(>k::Adjustment) + 'static>(
|
||||||
row
|
row
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
fn filedialog_row_base<F: Fn(Option<String>) + 'static + Clone>(
|
||||||
title: &str,
|
title: &str,
|
||||||
description: Option<&str>,
|
description: Option<&str>,
|
||||||
value: Option<String>,
|
value: Option<String>,
|
||||||
root_win: Option<gtk::Window>,
|
|
||||||
cb: F,
|
cb: F,
|
||||||
) -> adw::ActionRow {
|
) -> (adw::ActionRow, gtk::Label) {
|
||||||
let row = adw::ActionRow::builder()
|
let row = adw::ActionRow::builder()
|
||||||
.title(title)
|
.title(title)
|
||||||
.subtitle_lines(0)
|
.subtitle_lines(0)
|
||||||
|
@ -174,14 +173,14 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||||
row.set_subtitle(d);
|
row.set_subtitle(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
let path_label = >k::Label::builder()
|
let path_label = gtk::Label::builder()
|
||||||
.label(match value.as_ref() {
|
.label(match value.as_ref() {
|
||||||
None => "(None)",
|
None => "(None)",
|
||||||
Some(p) => p.as_str(),
|
Some(p) => p.as_str(),
|
||||||
})
|
})
|
||||||
.wrap(true)
|
.wrap(true)
|
||||||
.build();
|
.build();
|
||||||
row.add_suffix(path_label);
|
row.add_suffix(&path_label);
|
||||||
|
|
||||||
let clear_btn = gtk::Button::builder()
|
let clear_btn = gtk::Button::builder()
|
||||||
.icon_name("edit-clear-symbolic")
|
.icon_name("edit-clear-symbolic")
|
||||||
|
@ -200,9 +199,63 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||||
cb(None)
|
cb(None)
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
(row, path_label)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||||
|
title: &str,
|
||||||
|
description: Option<&str>,
|
||||||
|
value: Option<String>,
|
||||||
|
root_win: Option<gtk::Window>,
|
||||||
|
cb: F,
|
||||||
|
) -> adw::ActionRow {
|
||||||
|
let (row, path_label) = filedialog_row_base(title, description, value, cb.clone());
|
||||||
|
|
||||||
let filedialog = gtk::FileDialog::builder()
|
let filedialog = gtk::FileDialog::builder()
|
||||||
.modal(true)
|
.modal(true)
|
||||||
.title(format!("Select Path for {}", title))
|
.title(format!("Select {title}"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
row.connect_activated(clone!(
|
||||||
|
#[weak]
|
||||||
|
path_label,
|
||||||
|
move |_| {
|
||||||
|
filedialog.open(
|
||||||
|
root_win.as_ref(),
|
||||||
|
gio::Cancellable::NONE,
|
||||||
|
clone!(
|
||||||
|
#[weak]
|
||||||
|
path_label,
|
||||||
|
#[strong]
|
||||||
|
cb,
|
||||||
|
move |res| {
|
||||||
|
if let Ok(file) = res {
|
||||||
|
if let Some(path) = file.path() {
|
||||||
|
let path_s = path.to_string_lossy().to_string();
|
||||||
|
path_label.set_text(&path_s);
|
||||||
|
cb(Some(path_s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
row
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||||
|
title: &str,
|
||||||
|
description: Option<&str>,
|
||||||
|
value: Option<String>,
|
||||||
|
root_win: Option<gtk::Window>,
|
||||||
|
cb: F,
|
||||||
|
) -> adw::ActionRow {
|
||||||
|
let (row, path_label) = filedialog_row_base(title, description, value, cb.clone());
|
||||||
|
let filedialog = gtk::FileDialog::builder()
|
||||||
|
.modal(true)
|
||||||
|
.title(format!("Select Path for {title}"))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
row.connect_activated(clone!(
|
row.connect_activated(clone!(
|
||||||
|
@ -220,8 +273,8 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||||
move |res| {
|
move |res| {
|
||||||
if let Ok(file) = res {
|
if let Ok(file) = res {
|
||||||
if let Some(path) = file.path() {
|
if let Some(path) = file.path() {
|
||||||
let path_s = path.to_str().unwrap().to_string();
|
let path_s = path.to_string_lossy().to_string();
|
||||||
path_label.set_text(path_s.as_str());
|
path_label.set_text(&path_s);
|
||||||
cb(Some(path_s))
|
cb(Some(path_s))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,13 @@ use super::{
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
env_var_descriptions::ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH,
|
env_var_descriptions::ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH,
|
||||||
profile::{LighthouseDriver, Profile, XRServiceType},
|
profile::{LighthouseDriver, OvrCompatibilityModuleType, Profile, XRServiceType},
|
||||||
};
|
};
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use gtk::glib::{self, clone};
|
use gtk::glib::{self, clone};
|
||||||
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
||||||
use std::{cell::RefCell, path::PathBuf, rc::Rc};
|
use std::{cell::RefCell, path::PathBuf, rc::Rc};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
pub struct ProfileEditor {
|
pub struct ProfileEditor {
|
||||||
|
@ -129,14 +130,6 @@ impl SimpleComponent for ProfileEditor {
|
||||||
prof.borrow_mut().prefix = n_path.unwrap_or_default().into();
|
prof.borrow_mut().prefix = n_path.unwrap_or_default().into();
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
add: &entry_row("Autostart Command",
|
|
||||||
model.profile.borrow().autostart_command.as_ref().unwrap_or(&String::default()),
|
|
||||||
clone!(#[strong] prof, move |row| {
|
|
||||||
let txt = row.text().trim().to_string();
|
|
||||||
prof.borrow_mut().autostart_command =
|
|
||||||
if txt.is_empty() {None} else {Some(txt)};
|
|
||||||
})
|
|
||||||
),
|
|
||||||
add: &switch_row("Dependency Check",
|
add: &switch_row("Dependency Check",
|
||||||
Some("Warning: disabling dependency checks may result in build failures"),
|
Some("Warning: disabling dependency checks may result in build failures"),
|
||||||
!model.profile.borrow().skip_dependency_check,
|
!model.profile.borrow().skip_dependency_check,
|
||||||
|
@ -216,31 +209,43 @@ impl SimpleComponent for ProfileEditor {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
add: model.xrservice_cmake_flags_rows.widget(),
|
add: model.xrservice_cmake_flags_rows.widget(),
|
||||||
add: opencompgrp = &adw::PreferencesGroup {
|
add: ovr_comp_grp = &adw::PreferencesGroup {
|
||||||
set_title: "OpenComposite",
|
set_title: "OpenVR Compatibility",
|
||||||
set_description: Some("OpenVR driver built on top of OpenXR"),
|
set_description: Some("OpenVR compatibility module, translates between OpenXR and OpenVR to run legacy OpenVR apps"),
|
||||||
|
add: &combo_row(
|
||||||
|
"OpenVR Module Type",
|
||||||
|
None,
|
||||||
|
model.profile.borrow().ovr_comp.mod_type.to_string().as_str(),
|
||||||
|
OvrCompatibilityModuleType::iter()
|
||||||
|
.map(OvrCompatibilityModuleType::to_string)
|
||||||
|
.collect::<Vec<String>>(),
|
||||||
|
clone!(#[strong] prof, move |row| {
|
||||||
|
prof.borrow_mut().ovr_comp.mod_type =
|
||||||
|
OvrCompatibilityModuleType::from(row.selected());
|
||||||
|
}),
|
||||||
|
),
|
||||||
add: &path_row(
|
add: &path_row(
|
||||||
"OpenComposite Path", None,
|
"OpenVR Module Path", None,
|
||||||
Some(model.profile.borrow().opencomposite_path.clone().to_string_lossy().to_string()),
|
Some(model.profile.borrow().ovr_comp.path.clone().to_string_lossy().to_string()),
|
||||||
Some(init.root_win.clone()),
|
Some(init.root_win.clone()),
|
||||||
clone!(#[strong] prof, move |n_path| {
|
clone!(#[strong] prof, move |n_path| {
|
||||||
prof.borrow_mut().opencomposite_path = n_path.unwrap_or_default().into();
|
prof.borrow_mut().ovr_comp.path = n_path.unwrap_or_default().into();
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
add: &entry_row(
|
add: &entry_row(
|
||||||
"OpenComposite Repo",
|
"OpenVR Compatibility Repo",
|
||||||
model.profile.borrow().opencomposite_repo.clone().unwrap_or_default().as_str(),
|
model.profile.borrow().ovr_comp.repo.clone().unwrap_or_default().as_str(),
|
||||||
clone!(#[strong] prof, move |row| {
|
clone!(#[strong] prof, move |row| {
|
||||||
let n_val = row.text().to_string();
|
let n_val = row.text().to_string();
|
||||||
prof.borrow_mut().opencomposite_repo = (!n_val.is_empty()).then_some(n_val);
|
prof.borrow_mut().ovr_comp.repo = (!n_val.is_empty()).then_some(n_val);
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
add: &entry_row(
|
add: &entry_row(
|
||||||
"OpenComposite Branch",
|
"OpenVR Compatibility Branch",
|
||||||
model.profile.borrow().opencomposite_branch.clone().unwrap_or_default().as_str(),
|
model.profile.borrow().ovr_comp.branch.clone().unwrap_or_default().as_str(),
|
||||||
clone!(#[strong] prof, move |row| {
|
clone!(#[strong] prof, move |row| {
|
||||||
let n_val = row.text().to_string();
|
let n_val = row.text().to_string();
|
||||||
prof.borrow_mut().opencomposite_branch = (!n_val.is_empty()).then_some(n_val);
|
prof.borrow_mut().ovr_comp.branch = (!n_val.is_empty()).then_some(n_val);
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -499,14 +504,14 @@ impl SimpleComponent for ProfileEditor {
|
||||||
.halign(gtk::Align::End)
|
.halign(gtk::Align::End)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
add_btn.connect_clicked(clone!(
|
let on_add = clone!(
|
||||||
#[strong]
|
#[strong]
|
||||||
sender,
|
sender,
|
||||||
#[weak]
|
#[weak]
|
||||||
name_entry,
|
name_entry,
|
||||||
#[weak]
|
#[weak]
|
||||||
popover,
|
popover,
|
||||||
move |_| {
|
move || {
|
||||||
let key_gstr = name_entry.text();
|
let key_gstr = name_entry.text();
|
||||||
let key = key_gstr.trim();
|
let key = key_gstr.trim();
|
||||||
if !key.is_empty() {
|
if !key.is_empty() {
|
||||||
|
@ -515,7 +520,13 @@ impl SimpleComponent for ProfileEditor {
|
||||||
sender.input($event(key.to_string()));
|
sender.input($event(key.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
name_entry.connect_activate(clone!(
|
||||||
|
#[strong]
|
||||||
|
on_add,
|
||||||
|
move |_| on_add()
|
||||||
));
|
));
|
||||||
|
add_btn.connect_clicked(move |_| on_add());
|
||||||
btn
|
btn
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
@ -528,17 +539,30 @@ impl SimpleComponent for ProfileEditor {
|
||||||
let profile = Rc::new(RefCell::new(init.profile));
|
let profile = Rc::new(RefCell::new(init.profile));
|
||||||
let prof = profile.clone();
|
let prof = profile.clone();
|
||||||
|
|
||||||
|
let env_var_prefs_group = {
|
||||||
|
let pg = adw::PreferencesGroup::builder()
|
||||||
|
.title("Environment Variables")
|
||||||
|
.description(ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH.as_str())
|
||||||
|
.header_suffix(&add_env_var_btn)
|
||||||
|
.build();
|
||||||
|
if let Some(desc) = pg
|
||||||
|
.first_child()
|
||||||
|
.and_then(|c| c.first_child())
|
||||||
|
.and_then(|c| c.first_child())
|
||||||
|
.and_then(|c| c.last_child())
|
||||||
|
.and_downcast::<gtk::Label>()
|
||||||
|
{
|
||||||
|
desc.set_selectable(true);
|
||||||
|
} else {
|
||||||
|
warn!("failed to make env var preference group description selectable, please open a bug report");
|
||||||
|
}
|
||||||
|
pg
|
||||||
|
};
|
||||||
let mut model = Self {
|
let mut model = Self {
|
||||||
profile,
|
profile,
|
||||||
win: None,
|
win: None,
|
||||||
env_rows: AsyncFactoryVecDeque::builder()
|
env_rows: AsyncFactoryVecDeque::builder()
|
||||||
.launch(
|
.launch(env_var_prefs_group)
|
||||||
adw::PreferencesGroup::builder()
|
|
||||||
.title("Environment Variables")
|
|
||||||
.description(ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH.as_str())
|
|
||||||
.header_suffix(&add_env_var_btn)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.forward(sender.input_sender(), |msg| match msg {
|
.forward(sender.input_sender(), |msg| match msg {
|
||||||
EnvVarModelOutMsg::Changed(name, value) => {
|
EnvVarModelOutMsg::Changed(name, value) => {
|
||||||
ProfileEditorMsg::EnvVarChanged(name, value)
|
ProfileEditorMsg::EnvVarChanged(name, value)
|
||||||
|
|
|
@ -45,8 +45,7 @@ impl SimpleComponent for SteamLaunchOptionsBox {
|
||||||
add_css_class: "dim-label",
|
add_css_class: "dim-label",
|
||||||
set_hexpand: true,
|
set_hexpand: true,
|
||||||
set_label: format!(
|
set_label: format!(
|
||||||
"Set this string in the launch options of Steam games, so that they can pick up the {app} runtime correctly",
|
"Set this string in the launch options of Steam games, so that they can pick up the {APP_NAME} runtime correctly")
|
||||||
app = APP_NAME)
|
|
||||||
.as_str(),
|
.as_str(),
|
||||||
set_xalign: 0.0,
|
set_xalign: 0.0,
|
||||||
set_wrap: true,
|
set_wrap: true,
|
||||||
|
|
|
@ -10,10 +10,10 @@ use relm4::{
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, VecDeque},
|
collections::{HashMap, VecDeque},
|
||||||
path::Path,
|
|
||||||
thread::sleep,
|
thread::sleep,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
pub struct SteamVrCalibrationBox {
|
pub struct SteamVrCalibrationBox {
|
||||||
|
@ -144,12 +144,14 @@ impl SimpleComponent for SteamVrCalibrationBox {
|
||||||
}
|
}
|
||||||
Self::Input::RunCalibration => {
|
Self::Input::RunCalibration => {
|
||||||
self.set_calibration_result(None);
|
self.set_calibration_result(None);
|
||||||
let steamvr_bin_dir = get_steamvr_bin_dir_path().to_string_lossy().to_string();
|
match get_steamvr_bin_dir_path() {
|
||||||
if !Path::new(&steamvr_bin_dir).is_dir() {
|
Err(e) => {
|
||||||
|
error!("could not get SteamVR bin dir: {e}");
|
||||||
self.set_calibration_success(false);
|
self.set_calibration_success(false);
|
||||||
self.set_calibration_result(Some("SteamVR not found".into()));
|
self.set_calibration_result(Some("SteamVR not found".into()));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
Ok(bin_dir_p) => {
|
||||||
|
let steamvr_bin_dir = bin_dir_p.to_string_lossy().to_string();
|
||||||
let mut env: HashMap<String, String> = HashMap::new();
|
let mut env: HashMap<String, String> = HashMap::new();
|
||||||
env.insert("LD_LIBRARY_PATH".into(), steamvr_bin_dir.clone());
|
env.insert("LD_LIBRARY_PATH".into(), steamvr_bin_dir.clone());
|
||||||
let vrcmd = format!("{steamvr_bin_dir}/vrcmd");
|
let vrcmd = format!("{steamvr_bin_dir}/vrcmd");
|
||||||
|
@ -190,9 +192,11 @@ impl SimpleComponent for SteamVrCalibrationBox {
|
||||||
self.server_worker = Some(server_worker);
|
self.server_worker = Some(server_worker);
|
||||||
self.calibration_worker = Some(cal_worker);
|
self.calibration_worker = Some(cal_worker);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Self::Input::OnServerWorkerExit(code) => {
|
Self::Input::OnServerWorkerExit(code) => {
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
eprintln!("Calibration exited with code {code}");
|
error!("calibration exited with code {code}");
|
||||||
}
|
}
|
||||||
self.calibration_running = false;
|
self.calibration_running = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use gtk::{gdk, gio, glib::clone, prelude::*};
|
use gtk::{gdk, gio, glib::clone, prelude::*};
|
||||||
|
use tracing::{error, warn};
|
||||||
|
|
||||||
pub fn limit_dropdown_width(dd: >k::DropDown) {
|
pub fn limit_dropdown_width(dd: >k::DropDown) {
|
||||||
let mut dd_child = dd
|
let mut dd_child = dd
|
||||||
|
@ -46,14 +47,14 @@ pub fn warning_heading() -> gtk::Box {
|
||||||
|
|
||||||
pub fn open_with_default_handler(uri: &str) {
|
pub fn open_with_default_handler(uri: &str) {
|
||||||
if let Err(e) = gio::AppInfo::launch_default_for_uri(uri, gio::AppLaunchContext::NONE) {
|
if let Err(e) = gio::AppInfo::launch_default_for_uri(uri, gio::AppLaunchContext::NONE) {
|
||||||
eprintln!("Error opening uri {}: {}", uri, e)
|
error!("opening uri {uri}: {e}")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy_text(txt: &str) {
|
pub fn copy_text(txt: &str) {
|
||||||
match gdk::Display::default() {
|
match gdk::Display::default() {
|
||||||
None => {
|
None => {
|
||||||
eprintln!("Warning: could not get default gdk display")
|
warn!("could not get default gdk display")
|
||||||
}
|
}
|
||||||
Some(d) => {
|
Some(d) => {
|
||||||
d.clipboard().set_text(txt);
|
d.clipboard().set_text(txt);
|
||||||
|
|
|
@ -21,6 +21,7 @@ use crate::{
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use gtk::glib::clone;
|
use gtk::glib::clone;
|
||||||
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
pub struct WivrnConfEditor {
|
pub struct WivrnConfEditor {
|
||||||
|
@ -255,7 +256,7 @@ impl SimpleComponent for WivrnConfEditor {
|
||||||
if let Some(idx) = idx_opt {
|
if let Some(idx) = idx_opt {
|
||||||
self.encoder_models.as_mut().unwrap().guard().remove(idx);
|
self.encoder_models.as_mut().unwrap().guard().remove(idx);
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Couldn't find encoder model with id {id}");
|
error!("couldn't find encoder model with id {id}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
pub enum StartClientStatus {
|
pub enum StartClientStatus {
|
||||||
|
@ -111,7 +112,7 @@ impl AsyncComponent for WivrnWiredStartBox {
|
||||||
Self::Input::UpdateSelectedProfile(p) => self.set_selected_profile(p),
|
Self::Input::UpdateSelectedProfile(p) => self.set_selected_profile(p),
|
||||||
Self::Input::StartWivrnClient => {
|
Self::Input::StartWivrnClient => {
|
||||||
if !dep_adb().check() {
|
if !dep_adb().check() {
|
||||||
alert("ADB is not installed", Some(&format!("Please install ADB on your computer to start the WiVRn client from {}.", APP_NAME)), Some(&self.root_win));
|
alert("ADB is not installed", Some(&format!("Please install ADB on your computer to start the WiVRn client from {APP_NAME}.")), Some(&self.root_win));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.set_start_client_status(StartClientStatus::InProgress);
|
self.set_start_client_status(StartClientStatus::InProgress);
|
||||||
|
@ -153,14 +154,14 @@ impl AsyncComponent for WivrnWiredStartBox {
|
||||||
.into(),
|
.into(),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Error: ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr);
|
error!("ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr);
|
||||||
StartClientStatus::Done(Some(
|
StartClientStatus::Done(Some(
|
||||||
format!("ADB exited with code \"{}\"", out.exit_code)
|
format!("ADB exited with code \"{}\"", out.exit_code)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error: failed to run ADB: {e}");
|
error!("failed to run ADB: {e}");
|
||||||
StartClientStatus::Done(Some(
|
StartClientStatus::Done(Some(
|
||||||
"Failed to run ADB".into()
|
"Failed to run ADB".into()
|
||||||
))
|
))
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{async_process::async_process, profile::Profile};
|
use crate::{async_process::async_process, depcheck::common::dep_getcap_setcap, profile::Profile};
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use nix::{
|
use nix::{
|
||||||
errno::Errno,
|
errno::Errno,
|
||||||
|
@ -7,8 +7,10 @@ use nix::{
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, copy, create_dir_all, remove_dir_all, File, OpenOptions},
|
fs::{self, copy, create_dir_all, remove_dir_all, File, OpenOptions},
|
||||||
io::{BufReader, BufWriter},
|
io::{BufReader, BufWriter},
|
||||||
|
os::unix::fs::PermissionsExt,
|
||||||
path::Path,
|
path::Path,
|
||||||
};
|
};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
pub fn get_writer(path: &Path) -> anyhow::Result<BufWriter<std::fs::File>> {
|
pub fn get_writer(path: &Path) -> anyhow::Result<BufWriter<std::fs::File>> {
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
|
@ -36,7 +38,7 @@ pub fn get_reader(path: &Path) -> Option<BufReader<File>> {
|
||||||
}
|
}
|
||||||
match File::open(path) {
|
match File::open(path) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error opening {}: {}", path.to_string_lossy(), e);
|
error!("Error opening {}: {}", path.to_string_lossy(), e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Ok(fd) => Some(BufReader::new(fd)),
|
Ok(fd) => Some(BufReader::new(fd)),
|
||||||
|
@ -48,7 +50,7 @@ pub fn deserialize_file<T: serde::de::DeserializeOwned>(path: &Path) -> Option<T
|
||||||
None => None,
|
None => None,
|
||||||
Some(reader) => match serde_json::from_reader(reader) {
|
Some(reader) => match serde_json::from_reader(reader) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to deserialize {}: {}", path.to_string_lossy(), e);
|
error!("Failed to deserialize {}: {}", path.to_string_lossy(), e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Ok(res) => Some(res),
|
Ok(res) => Some(res),
|
||||||
|
@ -56,21 +58,50 @@ pub fn deserialize_file<T: serde::de::DeserializeOwned>(path: &Path) -> Option<T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_file_readonly(path: &Path, readonly: bool) -> Result<(), std::io::Error> {
|
pub fn set_file_readonly(path: &Path, readonly: bool) -> anyhow::Result<()> {
|
||||||
|
if path.is_symlink() {
|
||||||
|
bail!(
|
||||||
|
"path {} is a symlink, trying to change its write permission will only change the original file",
|
||||||
|
path.to_string_lossy()
|
||||||
|
);
|
||||||
|
}
|
||||||
if !path.is_file() {
|
if !path.is_file() {
|
||||||
eprintln!("WARN: trying to set readonly on a file that does not exist");
|
debug!(
|
||||||
|
"trying to set readonly on a file that does not exist: {}",
|
||||||
|
path.to_string_lossy()
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let mut perms = fs::metadata(path)
|
let mut perms = fs::metadata(path)
|
||||||
.expect("Could not get metadata for file")
|
.expect("Could not get metadata for file")
|
||||||
.permissions();
|
.permissions();
|
||||||
perms.set_readonly(readonly);
|
perms.set_readonly(readonly);
|
||||||
fs::set_permissions(path, perms)
|
Ok(fs::set_permissions(path, perms)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setcap_executable() -> Option<String> {
|
||||||
|
if dep_getcap_setcap().check() {
|
||||||
|
Some("setcap".into())
|
||||||
|
} else if Path::new("/sbin/setcap").try_exists().unwrap_or_default() {
|
||||||
|
Some("/sbin/setcap".into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getcap_executable() -> Option<String> {
|
||||||
|
if dep_getcap_setcap().check() {
|
||||||
|
Some("getcap".into())
|
||||||
|
} else if Path::new("/sbin/getcap").try_exists().unwrap_or_default() {
|
||||||
|
Some("/sbin/getcap".into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
|
pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
"setcap".into(),
|
setcap_executable().unwrap_or("setcap".into()),
|
||||||
"CAP_SYS_NICE=eip".into(),
|
"CAP_SYS_NICE=eip".into(),
|
||||||
profile
|
profile
|
||||||
.prefix
|
.prefix
|
||||||
|
@ -80,16 +111,42 @@ pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn setcap_cap_sys_nice_eip(profile: &Profile) {
|
pub async fn verify_cap_sys_nice_eip(profile: &Profile) -> bool {
|
||||||
if let Err(e) = async_process("pkexec", Some(&setcap_cap_sys_nice_eip_cmd(profile)), None).await
|
let xrservice_binary = profile.xrservice_binary().to_string_lossy().to_string();
|
||||||
{
|
if let Some(getcap_exec) = getcap_executable() {
|
||||||
eprintln!("Error: failed running setcap: {e}");
|
match async_process(&getcap_exec, Some(&[&xrservice_binary]), None).await {
|
||||||
|
Err(e) => {
|
||||||
|
error!("failed to run `getcap {xrservice_binary}`: {e:?}");
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
Ok(out) => {
|
||||||
|
debug!("getcap {xrservice_binary} stdout: {}", out.stdout);
|
||||||
|
debug!("getcap {xrservice_binary} stderr: {}", out.stderr);
|
||||||
|
if out.exit_code != 0 {
|
||||||
|
error!(
|
||||||
|
"command `getcap {xrservice_binary}` failed with status code {}",
|
||||||
|
out.exit_code
|
||||||
|
);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
out.stdout.to_lowercase().contains("cap_sys_nice=eip")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("getcap executable does not exist");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setcap_cap_sys_nice_eip(profile: &Profile) -> anyhow::Result<()> {
|
||||||
|
async_process("pkexec", Some(&setcap_cap_sys_nice_eip_cmd(profile)), None).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rm_rf(path: &Path) {
|
pub fn rm_rf(path: &Path) {
|
||||||
if remove_dir_all(path).is_err() {
|
if remove_dir_all(path).is_err() {
|
||||||
eprintln!("Failed to remove path {}", path.to_string_lossy());
|
error!("failed to remove path {}", path.to_string_lossy());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,11 +157,13 @@ pub fn copy_file(source: &Path, dest: &Path) {
|
||||||
.unwrap_or_else(|_| panic!("Failed to create dir {}", parent.to_str().unwrap()));
|
.unwrap_or_else(|_| panic!("Failed to create dir {}", parent.to_str().unwrap()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !dest.is_symlink() {
|
||||||
set_file_readonly(dest, false)
|
set_file_readonly(dest, false)
|
||||||
.unwrap_or_else(|_| panic!("Failed to set file {} as rw", dest.to_string_lossy()));
|
.unwrap_or_else(|_| panic!("Failed to set file {} as rw", dest.to_string_lossy()));
|
||||||
copy(source, dest).unwrap_or_else(|_| {
|
}
|
||||||
|
copy(source, dest).unwrap_or_else(|e| {
|
||||||
panic!(
|
panic!(
|
||||||
"Failed to copy {} to {}",
|
"Failed to copy {} to {}: {e}",
|
||||||
source.to_string_lossy(),
|
source.to_string_lossy(),
|
||||||
dest.to_string_lossy()
|
dest.to_string_lossy()
|
||||||
)
|
)
|
||||||
|
@ -118,6 +177,17 @@ pub fn mount_has_nosuid(path: &Path) -> Result<bool, Errno> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mark_as_executable(path: &Path) -> anyhow::Result<()> {
|
||||||
|
if !path.is_file() {
|
||||||
|
bail!("Path '{}' is not a file", path.to_string_lossy())
|
||||||
|
} else {
|
||||||
|
let mut perms = fs::metadata(path)?.permissions();
|
||||||
|
perms.set_mode(perms.mode() | 0o111);
|
||||||
|
fs::set_permissions(path, perms)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::mount_has_nosuid;
|
use super::mount_has_nosuid;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod file_utils;
|
pub mod file_utils;
|
||||||
pub mod hash;
|
pub mod hash;
|
||||||
|
pub mod steam_library_folder;
|
||||||
pub mod steamvr_utils;
|
pub mod steamvr_utils;
|
||||||
|
|
69
src/util/steam_library_folder.rs
Normal file
69
src/util/steam_library_folder.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use crate::paths::get_home_dir;
|
||||||
|
use anyhow::bail;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs::read_to_string,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SteamLibraryFolder {
|
||||||
|
pub path: String,
|
||||||
|
pub apps: HashMap<u32, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_steam_main_dir_path() -> anyhow::Result<PathBuf> {
|
||||||
|
let steam_root: PathBuf = get_home_dir().join(".steam/root").canonicalize()?;
|
||||||
|
|
||||||
|
if steam_root.is_dir() {
|
||||||
|
Ok(steam_root)
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
"Canonical steam root '{}' is not a dir nor a symlink!",
|
||||||
|
steam_root.to_string_lossy()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SteamLibraryFolder {
|
||||||
|
pub fn get_folders() -> anyhow::Result<HashMap<u32, Self>> {
|
||||||
|
let libraryfolders_path = get_steam_main_dir_path()?
|
||||||
|
.join("steamapps/libraryfolders.vdf")
|
||||||
|
.canonicalize()?;
|
||||||
|
if !libraryfolders_path.is_file() {
|
||||||
|
bail!(
|
||||||
|
"Steam libraryfolders.vdf does not exist in its canonical location {}",
|
||||||
|
libraryfolders_path.to_string_lossy()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self::get_folders_from_path(&libraryfolders_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do not use this: use get_folders() instead as it always uses Steam's
|
||||||
|
/// canonical root path. This is intended to be directly used only for
|
||||||
|
/// unit tests
|
||||||
|
pub fn get_folders_from_path(p: &Path) -> anyhow::Result<HashMap<u32, Self>> {
|
||||||
|
Ok(keyvalues_serde::from_str(read_to_string(p)?.as_str())?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::SteamLibraryFolder;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_steam_libraryfolders_vdf() {
|
||||||
|
let lf = SteamLibraryFolder::get_folders_from_path(Path::new(
|
||||||
|
"./test/files/steam_libraryfolders.vdf",
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(lf.len(), 1);
|
||||||
|
let first = lf.get(&0).unwrap();
|
||||||
|
assert_eq!(first.path, "/home/gabmus/.local/share/Steam");
|
||||||
|
assert_eq!(first.apps.len(), 10);
|
||||||
|
assert_eq!(first.apps.get(&228980).unwrap(), &29212173);
|
||||||
|
assert_eq!(first.apps.get(&632360).unwrap(), &0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,12 +5,10 @@ use ash::{
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct VulkanInfo {
|
pub struct VulkanInfo {
|
||||||
pub has_nvidia_gpu: bool,
|
|
||||||
pub has_monado_vulkan_layers: bool,
|
|
||||||
pub gpu_names: Vec<String>,
|
pub gpu_names: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const NVIDIA_VENDOR_ID: u32 = 0x10de;
|
// const NVIDIA_VENDOR_ID: u32 = 0x10de;
|
||||||
|
|
||||||
impl VulkanInfo {
|
impl VulkanInfo {
|
||||||
/// # Safety
|
/// # Safety
|
||||||
|
@ -25,40 +23,19 @@ impl VulkanInfo {
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
}?;
|
}?;
|
||||||
let mut has_nvidia_gpu = false;
|
|
||||||
let mut has_monado_vulkan_layers = false;
|
|
||||||
let gpu_names = unsafe { instance.enumerate_physical_devices() }?
|
let gpu_names = unsafe { instance.enumerate_physical_devices() }?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|d| {
|
.filter_map(|d| {
|
||||||
let props = unsafe { instance.get_physical_device_properties(d) };
|
Some(
|
||||||
if props.vendor_id == NVIDIA_VENDOR_ID {
|
unsafe { instance.get_physical_device_properties(d) }
|
||||||
has_nvidia_gpu = true;
|
|
||||||
}
|
|
||||||
if !has_monado_vulkan_layers {
|
|
||||||
has_monado_vulkan_layers =
|
|
||||||
unsafe { instance.enumerate_device_layer_properties(d) }
|
|
||||||
.ok()
|
|
||||||
.map(|layerprops| {
|
|
||||||
layerprops.iter().any(|lp| {
|
|
||||||
lp.layer_name_as_c_str().is_ok_and(|name| {
|
|
||||||
name.to_string_lossy()
|
|
||||||
== "VK_LAYER_MND_enable_timeline_semaphore"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
== Some(true);
|
|
||||||
}
|
|
||||||
props
|
|
||||||
.device_name_as_c_str()
|
.device_name_as_c_str()
|
||||||
.ok()
|
.ok()?
|
||||||
.map(|cs| cs.to_string_lossy().to_string())
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
unsafe { instance.destroy_instance(None) };
|
unsafe { instance.destroy_instance(None) };
|
||||||
Ok(Self {
|
Ok(Self { gpu_names })
|
||||||
gpu_names,
|
|
||||||
has_nvidia_gpu,
|
|
||||||
has_monado_vulkan_layers,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use libmonado::{self, BatteryStatus, DeviceRole};
|
use libmonado::{self, BatteryStatus, DeviceRole};
|
||||||
use std::{collections::HashMap, fmt::Display, slice::Iter};
|
use std::{collections::HashMap, fmt::Display, slice::Iter};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum XRDeviceRole {
|
pub enum XRDeviceRole {
|
||||||
|
@ -280,8 +281,8 @@ impl XRDevice {
|
||||||
if let Some(target) = devs.get_mut(&index) {
|
if let Some(target) = devs.get_mut(&index) {
|
||||||
target.roles.push(role.into());
|
target.roles.push(role.into());
|
||||||
} else {
|
} else {
|
||||||
eprintln!(
|
error!(
|
||||||
"Could not find device index {index} for role {}",
|
"could not find device index {index} for role {}",
|
||||||
XRDeviceRole::from(role)
|
XRDeviceRole::from(role)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,7 @@
|
||||||
"xrservice_path": "/home/user/monado",
|
"xrservice_path": "/home/user/monado",
|
||||||
"xrservice_repo": null,
|
"xrservice_repo": null,
|
||||||
"xrservice_branch": null,
|
"xrservice_branch": null,
|
||||||
"opencomposite_path": "/home/user/opencomposite",
|
"ovr_comp": { "mod_type": "Opencomposite", "path": "/home/user/opencomposite", "repo": null, "branch": null },
|
||||||
"opencomposite_repo": null,
|
|
||||||
"opencomposite_branch": null,
|
|
||||||
"features": {
|
"features": {
|
||||||
"libsurvive": {
|
"libsurvive": {
|
||||||
"feature_type": "Libsurvive",
|
"feature_type": "Libsurvive",
|
||||||
|
|
|
@ -12,5 +12,5 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"application": ["foobar", "baz"],
|
"application": ["foobar", "baz"],
|
||||||
"tcp_only": true
|
"tcp-only": true
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue