mirror of
https://gitlab.com/gabmus/envision.git
synced 2025-04-20 03:24:52 +00:00
Compare commits
79 commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
f1e8a010c8 | ||
|
d8ca8cf961 |
74 changed files with 3346 additions and 699 deletions
|
@ -1,4 +1,4 @@
|
|||
image: "debian:unstable"
|
||||
image: "ubuntu:24.04"
|
||||
|
||||
stages:
|
||||
- check
|
||||
|
@ -13,52 +13,6 @@ commitcheck:
|
|||
# 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
|
||||
|
||||
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:
|
||||
stage: deploy
|
||||
script:
|
||||
|
@ -68,6 +22,7 @@ appimage:
|
|||
- chmod +x /tmp/rustup.sh
|
||||
- /tmp/rustup.sh -y
|
||||
- source "$HOME/.cargo/env"
|
||||
- rustup component add clippy
|
||||
- bash ./dist/appimage/build_appimage.sh
|
||||
artifacts:
|
||||
paths:
|
||||
|
|
174
Cargo.lock
generated
174
Cargo.lock
generated
|
@ -1,6 +1,6 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
|
@ -420,6 +420,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
|
@ -554,7 +563,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "envision"
|
||||
version = "1.1.0"
|
||||
version = "3.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ash",
|
||||
|
@ -574,8 +583,12 @@ dependencies = [
|
|||
"rusb",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"tracker",
|
||||
"uuid",
|
||||
"vte4",
|
||||
|
@ -1698,6 +1711,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
|
@ -1820,6 +1842,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
|
@ -1945,6 +1977,12 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.20.6"
|
||||
|
@ -2180,8 +2218,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
|||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
"regex-automata 0.4.9",
|
||||
"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]]
|
||||
|
@ -2192,9 +2239,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
|||
dependencies = [
|
||||
"aho-corasick",
|
||||
"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]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
|
@ -2503,6 +2556,19 @@ dependencies = [
|
|||
"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]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
|
@ -2525,6 +2591,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
|
@ -2713,6 +2788,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "time"
|
||||
version = "0.3.36"
|
||||
|
@ -2720,10 +2805,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2732,6 +2819,16 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
|
@ -2844,6 +2941,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.28"
|
||||
|
@ -2862,6 +2971,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
|
||||
dependencies = [
|
||||
"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]]
|
||||
|
@ -2925,6 +3077,12 @@ version = "1.12.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
|
@ -2964,6 +3122,12 @@ dependencies = [
|
|||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
|
15
Cargo.toml
15
Cargo.toml
|
@ -1,7 +1,16 @@
|
|||
[package]
|
||||
name = "envision"
|
||||
version = "1.1.0"
|
||||
version = "3.1.0"
|
||||
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
|
||||
|
||||
|
@ -31,3 +40,7 @@ sha2 = "0.10.8"
|
|||
tokio = { version = "1.39.3", features = ["process"] }
|
||||
notify-rust = "4.11.3"
|
||||
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"
|
||||
|
|
|
@ -60,6 +60,10 @@ cd envision
|
|||
|
||||
</details>
|
||||
|
||||
# Debugging
|
||||
|
||||
To view all the logs you need to run envision with the env var `RUST_LOG=trace`.
|
||||
|
||||
# Common issues
|
||||
|
||||
## NOSUID with systemd-homed
|
||||
|
|
|
@ -2,22 +2,173 @@
|
|||
<component type="desktop-application">
|
||||
<id>@APP_ID@</id>
|
||||
<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>
|
||||
<summary>GUI for Monado</summary>
|
||||
<summary>Orchestrator for the free XR stack</summary>
|
||||
<description>
|
||||
<p>GUI for Monado</p> <!-- temporary -->
|
||||
<p>Orchestrator for the free XR stack</p>
|
||||
</description>
|
||||
<!--screenshots>
|
||||
<screenshots>
|
||||
<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>
|
||||
</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="bugtracker">@REPO_URL@/issues</url>
|
||||
<content_rating type="oars-1.0" />
|
||||
<releases>
|
||||
<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">
|
||||
<description>
|
||||
<p>Fixes</p>
|
||||
<ul>
|
||||
<li>remove wivrn pairing mode timer</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.1.0" date="2024-11-28">
|
||||
<description>
|
||||
<p>What's new</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
|
||||
|
||||
meson setup appimage_build -Dprefix=/usr -Dprofile=default
|
||||
meson test -C appimage_build --print-errorlogs
|
||||
DESTDIR="$PWD/AppDir" ninja -C appimage_build install
|
||||
curl -SsLO https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/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=(
|
||||
'libudev0-shim: steamvr_lh lighthouse driver support'
|
||||
'monado-vulkan-layers-git: Vulkan layers for NVIDIA users'
|
||||
)
|
||||
provides=(envision)
|
||||
conflicts=(envision)
|
||||
|
|
33
meson.build
33
meson.build
|
@ -1,9 +1,9 @@
|
|||
project(
|
||||
'envision',
|
||||
'rust',
|
||||
version: '1.1.0', # version number row
|
||||
version: '3.1.0', # version number row
|
||||
meson_version: '>= 0.59',
|
||||
license: 'AGPL-3.0',
|
||||
license: 'AGPL-3.0-or-later',
|
||||
)
|
||||
|
||||
i18n = import('i18n')
|
||||
|
@ -38,17 +38,30 @@ iconsdir = datadir / 'icons'
|
|||
podir = meson.project_source_root() / 'po'
|
||||
gettext_package = meson.project_name()
|
||||
|
||||
# are we building a tagged version?
|
||||
if run_command('git', 'describe', '--tags', '--exact-match').returncode() != 0
|
||||
profile = 'Devel'
|
||||
vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip()
|
||||
if vcs_tag == ''
|
||||
version_suffix = '-devel'
|
||||
opt_profile = get_option('profile')
|
||||
|
||||
# if a profile isn't specified infer from git
|
||||
if opt_profile == 'default'
|
||||
# are we building a tagged version?
|
||||
if run_command('git', 'describe', '--tags', '--exact-match').returncode() != 0
|
||||
profile = 'Devel'
|
||||
vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip()
|
||||
if vcs_tag == ''
|
||||
version_suffix = '-devel'
|
||||
else
|
||||
version_suffix = '-@0@'.format(vcs_tag)
|
||||
endif
|
||||
application_id = '@0@.@1@'.format(base_id, profile)
|
||||
else
|
||||
version_suffix = '-@0@'.format(vcs_tag)
|
||||
profile = ''
|
||||
version_suffix = ''
|
||||
application_id = base_id
|
||||
endif
|
||||
elif opt_profile == 'development'
|
||||
profile = 'Devel'
|
||||
version_suffix = '-devel'
|
||||
application_id = '@0@.@1@'.format(base_id, profile)
|
||||
else
|
||||
elif opt_profile == 'release'
|
||||
profile = ''
|
||||
version_suffix = ''
|
||||
application_id = base_id
|
||||
|
|
|
@ -3,6 +3,7 @@ option(
|
|||
type: 'combo',
|
||||
choices: [
|
||||
'default',
|
||||
'release',
|
||||
'development'
|
||||
],
|
||||
value: 'default',
|
||||
|
|
|
@ -11,7 +11,22 @@ if [[ -z $PREFIX ]] || [[ -z $CACHE_DIR ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
ONNX_VER=$(curl -sSL "https://api.github.com/repos/microsoft/onnxruntime/releases/latest" | jq -r .tag_name | tr -d v)
|
||||
ONNX_RELEASES=$(curl -sSL "https://api.github.com/repos/microsoft/onnxruntime/releases")
|
||||
NUM_RELEASES=$(echo "$ONNX_RELEASES" | jq -r '[ select (.[]!=null) ] | length')
|
||||
|
||||
for (( IDX=0; IDX<NUM_RELEASES; IDX++ )); do
|
||||
ASSETS_LEN=$(echo "$ONNX_RELEASES" | jq -r ".[$IDX].assets_url" | xargs -n 1 curl -sSL | jq -r '[ select (.[]!=null) ] | length')
|
||||
if [[ $ASSETS_LEN -gt 0 ]]; then
|
||||
ONNX_VER=$(echo "$ONNX_RELEASES" | jq -r ".[$IDX].tag_name" | tr -d v)
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z $ONNX_VER ]]; then
|
||||
echo "Failed to find a suitable ONNX Runtime release."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SYS_ARCH=$(uname -m)
|
||||
|
||||
if [[ $SYS_ARCH == x*64 ]]; then
|
||||
|
|
|
@ -35,28 +35,39 @@ pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<W
|
|||
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
||||
|
||||
let build_dir = profile.features.basalt.path.as_ref().unwrap().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());
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
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_INSTALL_LIBDIR".into(),
|
||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||
);
|
||||
|
||||
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),
|
||||
env: Some({
|
||||
let mut cmake_env: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("CMAKE_BUILD_PARALLEL_LEVEL", "2"),
|
||||
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||
("BUILD_TESTS", "off"),
|
||||
] {
|
||||
cmake_env.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
cmake_env
|
||||
}),
|
||||
vars: Some({
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("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_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_LIBDIR".into(),
|
||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars
|
||||
}),
|
||||
source_dir: profile.features.basalt.path.as_ref().unwrap().clone(),
|
||||
build_dir: build_dir.clone(),
|
||||
};
|
||||
|
|
|
@ -44,24 +44,30 @@ pub fn get_build_libsurvive_jobs(profile: &Profile, clean_build: bool) -> VecDeq
|
|||
.as_ref()
|
||||
.unwrap()
|
||||
.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());
|
||||
cmake_vars.insert("ENABLE_api_example".into(), "OFF".into());
|
||||
cmake_vars.insert("USE_HIDAPI".into(), "ON".into());
|
||||
cmake_vars.insert("CMAKE_SKIP_INSTALL_RPATH".into(), "YES".into());
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_LIBDIR".into(),
|
||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||
);
|
||||
|
||||
let cmake = Cmake {
|
||||
env: None,
|
||||
vars: Some(cmake_vars),
|
||||
vars: Some({
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||
("ENABLE_api_example", "OFF"),
|
||||
("USE_HIDAPI", "ON"),
|
||||
("CMAKE_SKIP_INSTALL_RPATH", "YES"),
|
||||
] {
|
||||
cmake_vars.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_LIBDIR".into(),
|
||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars
|
||||
}),
|
||||
source_dir: profile.features.libsurvive.path.as_ref().unwrap().clone(),
|
||||
build_dir: build_dir.clone(),
|
||||
};
|
||||
|
|
|
@ -43,37 +43,43 @@ pub fn get_build_monado_jobs(profile: &Profile, clean_build: bool) -> VecDeque<W
|
|||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
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());
|
||||
cmake_vars.insert("XRT_HAVE_SYSTEM_CJSON".into(), "NO".into());
|
||||
cmake_vars.insert(
|
||||
"CMAKE_LIBDIR".into(),
|
||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_C_FLAGS".into(),
|
||||
format!("-Wl,-rpath='{}/lib'", profile.prefix.to_string_lossy(),),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_CXX_FLAGS".into(),
|
||||
format!("-Wl,-rpath='{}/lib'", profile.prefix.to_string_lossy(),),
|
||||
);
|
||||
profile.xrservice_cmake_flags.iter().for_each(|(k, v)| {
|
||||
if k == "CMAKE_C_FLAGS" || k == "CMAKE_CXX_FLAGS" {
|
||||
cmake_vars.insert(k.clone(), format!("{} {}", cmake_vars.get(k).unwrap(), v));
|
||||
} else {
|
||||
cmake_vars.insert(k.clone(), v.clone());
|
||||
}
|
||||
});
|
||||
|
||||
let cmake = Cmake {
|
||||
env: Some(env),
|
||||
vars: Some(cmake_vars),
|
||||
vars: Some({
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||
("XRT_HAVE_SYSTEM_CJSON", "NO"),
|
||||
] {
|
||||
cmake_vars.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
cmake_vars.insert(
|
||||
"CMAKE_LIBDIR".into(),
|
||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_C_FLAGS".into(),
|
||||
format!("-Wl,-rpath='{}/lib'", profile.prefix.to_string_lossy(),),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_CXX_FLAGS".into(),
|
||||
format!("-Wl,-rpath='{}/lib'", profile.prefix.to_string_lossy(),),
|
||||
);
|
||||
profile.xrservice_cmake_flags.iter().for_each(|(k, v)| {
|
||||
if k == "CMAKE_C_FLAGS" || k == "CMAKE_CXX_FLAGS" {
|
||||
cmake_vars.insert(k.clone(), format!("{} {}", cmake_vars.get(k).unwrap(), v));
|
||||
} else {
|
||||
cmake_vars.insert(k.clone(), v.clone());
|
||||
}
|
||||
});
|
||||
cmake_vars
|
||||
}),
|
||||
source_dir: profile.xrservice_path.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 {
|
||||
repo: profile
|
||||
.opencomposite_repo
|
||||
.ovr_comp
|
||||
.repo
|
||||
.as_ref()
|
||||
.unwrap_or(&"https://gitlab.com/znixian/OpenOVR.git".into())
|
||||
.clone(),
|
||||
dir: profile.opencomposite_path.clone(),
|
||||
dir: profile.ovr_comp.path.clone(),
|
||||
branch: profile
|
||||
.opencomposite_branch
|
||||
.ovr_comp
|
||||
.branch
|
||||
.as_ref()
|
||||
.unwrap_or(&"openxr".into())
|
||||
.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));
|
||||
|
||||
let build_dir = profile.opencomposite_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 build_dir = profile.ovr_comp.path.join("build");
|
||||
let cmake = Cmake {
|
||||
env: None,
|
||||
vars: Some(cmake_vars),
|
||||
source_dir: profile.opencomposite_path.clone(),
|
||||
vars: Some({
|
||||
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(),
|
||||
};
|
||||
if !Path::new(&build_dir).is_dir() || clean_build {
|
||||
|
|
85
src/builders/build_vapor.rs
Normal file
85
src/builders/build_vapor.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use crate::{
|
||||
build_tools::{cmake::Cmake, git::Git},
|
||||
profile::Profile,
|
||||
termcolor::TermColor,
|
||||
ui::job_worker::job::{FuncWorkerData, FuncWorkerOut, WorkerJob},
|
||||
util::file_utils::{copy_file, rm_rf},
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
fs::create_dir_all,
|
||||
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 cmake = Cmake {
|
||||
env: None,
|
||||
vars: Some({
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("VAPOR_LOG_SILENT=ON", "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(),
|
||||
};
|
||||
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(WorkerJob::Func(FuncWorkerData {
|
||||
func: Box::new(move || {
|
||||
let dest_dir = build_dir.join("bin/linux64");
|
||||
if let Err(e) = create_dir_all(&dest_dir) {
|
||||
return FuncWorkerOut {
|
||||
success: false,
|
||||
out: vec![format!(
|
||||
"failed to create dir {}: {e}",
|
||||
dest_dir.to_string_lossy()
|
||||
)],
|
||||
};
|
||||
}
|
||||
copy_file(
|
||||
&build_dir.join("src/vrclient.so"),
|
||||
&dest_dir.join("vrclient.so"),
|
||||
);
|
||||
|
||||
FuncWorkerOut {
|
||||
success: true,
|
||||
out: Vec::default(),
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
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));
|
||||
|
||||
let build_dir = profile.xrservice_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());
|
||||
cmake_vars.insert("XRT_HAVE_SYSTEM_CJSON".into(), "NO".into());
|
||||
cmake_vars.insert("WIVRN_BUILD_CLIENT".into(), "OFF".into());
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
|
||||
profile.xrservice_cmake_flags.iter().for_each(|(k, v)| {
|
||||
cmake_vars.insert(k.clone(), v.clone());
|
||||
});
|
||||
|
||||
let cmake = Cmake {
|
||||
env: None,
|
||||
vars: Some(cmake_vars),
|
||||
vars: Some({
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||
("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_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
profile.xrservice_cmake_flags.iter().for_each(|(k, v)| {
|
||||
cmake_vars.insert(k.clone(), v.clone());
|
||||
});
|
||||
cmake_vars
|
||||
}),
|
||||
source_dir: profile.xrservice_path.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_opencomposite;
|
||||
pub mod build_openhmd;
|
||||
pub mod build_vapor;
|
||||
pub mod build_wivrn;
|
||||
pub mod build_xrizer;
|
||||
|
|
|
@ -7,15 +7,38 @@ use crate::{
|
|||
lighthouse::lighthouse_profile, openhmd::openhmd_profile, simulated::simulated_profile,
|
||||
survive::survive_profile, wivrn::wivrn_profile, wmr::wmr_profile,
|
||||
},
|
||||
ui::plugins::Plugin,
|
||||
util::file_utils::get_writer,
|
||||
};
|
||||
use serde::{de::Error, Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::BufReader,
|
||||
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 fn default_win_size() -> [i32; 2] {
|
||||
|
@ -29,6 +52,8 @@ pub struct Config {
|
|||
pub user_profiles: Vec<Profile>,
|
||||
#[serde(default = "default_win_size")]
|
||||
pub win_size: [i32; 2],
|
||||
#[serde(default)]
|
||||
pub plugins: HashMap<String, PluginConfig>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
@ -37,8 +62,9 @@ impl Default for Config {
|
|||
// TODO: using an empty string here is ugly
|
||||
selected_profile_uuid: "".to_string(),
|
||||
debug_view_enabled: false,
|
||||
user_profiles: vec![],
|
||||
user_profiles: Vec::default(),
|
||||
win_size: DEFAULT_WIN_SIZE,
|
||||
plugins: HashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,10 +91,42 @@ impl Config {
|
|||
}
|
||||
|
||||
fn from_path(path: &Path) -> Self {
|
||||
File::open(path)
|
||||
let mut this: Self = File::open(path)
|
||||
.ok()
|
||||
.and_then(|file| serde_json::from_reader(BufReader::new(file)).ok())
|
||||
.unwrap_or_default()
|
||||
.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()
|
||||
.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> {
|
||||
|
|
|
@ -16,10 +16,6 @@ pub const LOCALE_DIR: &str = "@LOCALEDIR@";
|
|||
pub const BUILD_PROFILE: &str = "@PROFILE@";
|
||||
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> {
|
||||
vec!["App Icon: Yannick (@Yandr)".into()]
|
||||
}
|
||||
|
|
|
@ -52,8 +52,8 @@ pub fn boost_deps() -> Vec<Dependency> {
|
|||
packages: HashMap::from([
|
||||
(LinuxDistro::Arch, "boost".into()),
|
||||
(LinuxDistro::Debian, "libboost-all-dev".into()),
|
||||
(LinuxDistro::Fedora, "boost".into()),
|
||||
(LinuxDistro::Alpine, "boost".into()),
|
||||
(LinuxDistro::Fedora, "boost-devel".into()),
|
||||
(LinuxDistro::Alpine, "boost-dev".into()),
|
||||
(LinuxDistro::Gentoo, "dev-libs/boost".into()),
|
||||
(LinuxDistro::Suse, package.into()),
|
||||
]),
|
||||
|
|
|
@ -16,6 +16,7 @@ pub enum DepType {
|
|||
Executable,
|
||||
Include,
|
||||
UdevRule,
|
||||
Share,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
@ -49,6 +50,7 @@ impl Dependency {
|
|||
.collect(),
|
||||
DepType::Include => include_paths(),
|
||||
DepType::UdevRule => udev_rules_paths(),
|
||||
DepType::Share => share_paths(),
|
||||
} {
|
||||
let path_s = &format!("{dir}/{fname}", dir = dir, fname = self.filename);
|
||||
let path = Path::new(&path_s);
|
||||
|
@ -145,6 +147,10 @@ fn udev_rules_paths() -> Vec<String> {
|
|||
vec!["/usr/lib/udev/rules.d".into()]
|
||||
}
|
||||
|
||||
fn share_paths() -> Vec<String> {
|
||||
vec!["/usr/share".into()]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DepType, Dependency};
|
||||
|
|
|
@ -30,6 +30,30 @@ fn monado_deps() -> Vec<Dependency> {
|
|||
(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_eigen(),
|
||||
dep_git(),
|
||||
|
|
|
@ -78,15 +78,15 @@ fn wivrn_deps() -> Vec<Dependency> {
|
|||
]),
|
||||
},
|
||||
Dependency {
|
||||
name: "libpulse-dev".into(),
|
||||
name: "libpipewire-dev".into(),
|
||||
dep_type: DepType::Include,
|
||||
filename: "pulse/context.h".into(),
|
||||
filename: "pipewire-0.3/pipewire/pipewire.h".into(),
|
||||
packages: HashMap::from([
|
||||
(LinuxDistro::Arch, "libpulse".into()),
|
||||
(LinuxDistro::Debian, "libpulse-dev".into()),
|
||||
(LinuxDistro::Fedora, "pulseaudio-libs-devel".into()),
|
||||
(LinuxDistro::Gentoo, "media-libs/libpulse".into()),
|
||||
(LinuxDistro::Suse, "libpulse-devel".into()),
|
||||
(LinuxDistro::Arch, "libpipewire".into()),
|
||||
(LinuxDistro::Debian, "libpipewire-0.3-dev".into()),
|
||||
(LinuxDistro::Fedora, "pipewire-devel".into()),
|
||||
(LinuxDistro::Gentoo, "media-video/pipewire".into()),
|
||||
(LinuxDistro::Suse, "pipewire-devel".into()),
|
||||
]),
|
||||
},
|
||||
dep_eigen(),
|
||||
|
@ -169,7 +169,10 @@ fn wivrn_deps() -> Vec<Dependency> {
|
|||
filename: "pkgconfig/gstreamer-app-1.0.pc".into(),
|
||||
packages: HashMap::from([
|
||||
(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::Gentoo, "media-libs/gst-plugins-base".into()),
|
||||
(LinuxDistro::Suse, "gstreamer-plugins-base-devel".into()),
|
||||
|
@ -235,6 +238,30 @@ fn wivrn_deps() -> Vec<Dependency> {
|
|||
(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()),
|
||||
]),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ use lazy_static::lazy_static;
|
|||
fn env_var_descriptions() -> Vec<(&'static str, &'static str)> {
|
||||
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."
|
||||
),
|
||||
(
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
use crate::{
|
||||
paths::{get_backup_dir, SYSTEM_PREFIX},
|
||||
paths::SYSTEM_PREFIX,
|
||||
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,
|
||||
};
|
||||
use anyhow::bail;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs::remove_file,
|
||||
fs::{create_dir_all, remove_file, rename},
|
||||
os::unix::fs::symlink,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ActiveRuntimeInnerRuntime {
|
||||
|
@ -34,29 +37,6 @@ fn get_active_runtime_json_path() -> PathBuf {
|
|||
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> {
|
||||
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())
|
||||
}
|
||||
|
||||
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> {
|
||||
let Some(libopenxr_path) = profile.libopenxr_so() else {
|
||||
anyhow::bail!(
|
||||
|
@ -137,18 +94,67 @@ fn relativize_active_runtime_lib_path(ar: &ActiveRuntime, path: &Path) -> Active
|
|||
res
|
||||
}
|
||||
|
||||
const ACTIVE_RUNTIME_BAK: &str = "active_runtime.json.envision.bak";
|
||||
|
||||
pub fn set_current_active_runtime_to_profile(profile: &Profile) -> anyhow::Result<()> {
|
||||
let dest = get_active_runtime_json_path();
|
||||
set_file_readonly(&dest, false)?;
|
||||
backup_steam_active_runtime();
|
||||
let pfx = profile.clone().prefix;
|
||||
let mut ar = build_profile_active_runtime(profile)?;
|
||||
// hack: relativize libopenxr_monado.so path for system installs
|
||||
if pfx == PathBuf::from(SYSTEM_PREFIX) {
|
||||
ar = relativize_active_runtime_lib_path(&ar, &dest);
|
||||
if dest.is_dir() {
|
||||
bail!("{} is a directory", dest.to_string_lossy());
|
||||
}
|
||||
dump_current_active_runtime(&ar)?;
|
||||
set_file_readonly(&dest, true)?;
|
||||
if !dest.is_symlink() {
|
||||
set_file_readonly(&dest, false)?;
|
||||
}
|
||||
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 mut ar = build_profile_active_runtime(profile)?;
|
||||
// hack: relativize libopenxr_monado.so path for system installs
|
||||
if pfx == PathBuf::from(SYSTEM_PREFIX) {
|
||||
ar = relativize_active_runtime_lib_path(&ar, &dest);
|
||||
}
|
||||
dump_current_active_runtime(&ar)?;
|
||||
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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
pub mod active_runtime_json;
|
||||
pub mod monado_autorun;
|
||||
pub mod openvrpaths_vrpath;
|
||||
pub mod wayvr_dashboard_config;
|
||||
pub mod wivrn_config;
|
||||
pub mod wivrn_encoder_presets;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{
|
||||
paths::get_backup_dir,
|
||||
profile::Profile,
|
||||
|
@ -7,6 +5,7 @@ use crate::{
|
|||
xdg::XDG,
|
||||
};
|
||||
use serde::{ser::Error, Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OpenVrPaths {
|
||||
|
@ -86,6 +85,7 @@ fn build_steam_openvrpaths() -> OpenVrPaths {
|
|||
}
|
||||
|
||||
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)?;
|
||||
dump_current_openvrpaths(&build_steam_openvrpaths())?;
|
||||
Ok(())
|
||||
|
@ -98,25 +98,24 @@ pub fn build_profile_openvrpaths(profile: &Profile) -> OpenVrPaths {
|
|||
external_drivers: None,
|
||||
jsonid: "vrpathreg".into(),
|
||||
log: vec![datadir.join("Steam/logs")],
|
||||
runtime: vec![profile.opencomposite_path.join("build")],
|
||||
runtime: vec![profile.ovr_comp.runtime_dir()],
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current_openvrpaths_to_profile(profile: &Profile) -> anyhow::Result<()> {
|
||||
let dest = get_openvrpaths_vrpath_path();
|
||||
// removing readonly flag just in case, remove this line in the future
|
||||
set_file_readonly(&dest, false)?;
|
||||
backup_steam_openvrpaths();
|
||||
dump_current_openvrpaths(&build_profile_openvrpaths(profile))?;
|
||||
set_file_readonly(&dest, true)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::{dump_openvrpaths_to_path, get_openvrpaths_from_path, OpenVrPaths};
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
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,
|
||||
}
|
|
@ -49,13 +49,13 @@ impl LinuxDistro {
|
|||
Ok(_) if buf.starts_with("PRETTY_NAME=\"") => {
|
||||
return buf
|
||||
.split('=')
|
||||
.last()
|
||||
.next_back()
|
||||
.map(|b| b.trim().trim_matches('"').trim().to_string());
|
||||
}
|
||||
Ok(_) if buf.starts_with("NAME=\"") => {
|
||||
name = buf
|
||||
.split('=')
|
||||
.last()
|
||||
.next_back()
|
||||
.map(|b| b.trim().trim_matches('"').trim().to_string());
|
||||
}
|
||||
_ => {}
|
||||
|
@ -79,7 +79,7 @@ impl LinuxDistro {
|
|||
{
|
||||
let name = buf
|
||||
.split('=')
|
||||
.last()
|
||||
.next_back()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
|
|
43
src/main.rs
43
src/main.rs
|
@ -1,10 +1,11 @@
|
|||
use anyhow::Result;
|
||||
use constants::{resources, APP_ID, APP_NAME, GETTEXT_PACKAGE, LOCALE_DIR, RESOURCES_BASE_PATH};
|
||||
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},
|
||||
};
|
||||
use gettextrs::LocaleCategory;
|
||||
use paths::get_logs_dir;
|
||||
use relm4::{
|
||||
adw,
|
||||
gtk::{self, gdk, gio, glib, prelude::*},
|
||||
|
@ -12,6 +13,10 @@ use relm4::{
|
|||
};
|
||||
use std::env;
|
||||
use steam_linux_runtime_injector::restore_runtime_entrypoint;
|
||||
use tracing::warn;
|
||||
use tracing_subscriber::{
|
||||
filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
|
||||
};
|
||||
use ui::{
|
||||
app::{App, AppInit, Msg},
|
||||
cmdline_opts::CmdLineOpts,
|
||||
|
@ -22,6 +27,7 @@ pub mod build_tools;
|
|||
pub mod builders;
|
||||
pub mod cmd_runner;
|
||||
pub mod config;
|
||||
#[rustfmt::skip]
|
||||
pub mod constants;
|
||||
pub mod depcheck;
|
||||
pub mod device_prober;
|
||||
|
@ -46,22 +52,15 @@ pub mod xdg;
|
|||
pub mod xr_devices;
|
||||
|
||||
fn restore_steam_xr_files() {
|
||||
let active_runtime = get_current_active_runtime();
|
||||
let openvrpaths = get_current_openvrpaths();
|
||||
if let Some(ar) = active_runtime {
|
||||
if !file_builders::active_runtime_json::is_steam(&ar) {
|
||||
match set_current_active_runtime_to_steam() {
|
||||
Ok(_) => {}
|
||||
Err(e) => eprintln!("Warning: failed to restore active runtime to steam: {e}"),
|
||||
};
|
||||
}
|
||||
if let Err(e) = restore_active_runtime_backup() {
|
||||
warn!("failed to restore active runtime to steam: {e}");
|
||||
}
|
||||
if let Some(ovrp) = openvrpaths {
|
||||
if !file_builders::openvrpaths_vrpath::is_steam(&ovrp) {
|
||||
match set_current_openvrpaths_to_steam() {
|
||||
Ok(_) => {}
|
||||
Err(e) => eprintln!("Warning: failed to restore openvrpaths to steam: {e}"),
|
||||
};
|
||||
if let Err(e) = set_current_openvrpaths_to_steam() {
|
||||
warn!("failed to restore openvrpaths to steam: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
restore_runtime_entrypoint();
|
||||
|
@ -73,6 +72,24 @@ fn main() -> Result<()> {
|
|||
}
|
||||
restore_steam_xr_files();
|
||||
|
||||
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();
|
||||
|
||||
// Prepare i18n
|
||||
gettextrs::setlocale(LocaleCategory::LcAll, "");
|
||||
gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALE_DIR).expect("Unable to bind the text domain");
|
||||
|
|
|
@ -3,7 +3,7 @@ config = configure_file(
|
|||
output: 'constants.rs',
|
||||
configuration: global_conf
|
||||
)
|
||||
# Copy the config.rs output to the source directory.
|
||||
# Copy the constants.rs output to the source directory.
|
||||
run_command(
|
||||
'cp',
|
||||
meson.project_build_root() / 'src' / 'constants.rs',
|
||||
|
@ -43,3 +43,24 @@ cargo_build = custom_target(
|
|||
'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,
|
||||
)
|
||||
|
|
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::{
|
||||
env,
|
||||
fs::create_dir_all,
|
||||
|
@ -54,6 +56,10 @@ pub fn get_cache_dir() -> PathBuf {
|
|||
XDG.get_cache_home().join(CMD_NAME)
|
||||
}
|
||||
|
||||
pub fn get_logs_dir() -> PathBuf {
|
||||
get_cache_dir().join("logs")
|
||||
}
|
||||
|
||||
pub fn get_backup_dir() -> PathBuf {
|
||||
let p = get_data_dir().join("backups");
|
||||
if !p.is_dir() {
|
||||
|
@ -83,7 +89,26 @@ pub fn get_exec_prefix() -> PathBuf {
|
|||
.into()
|
||||
}
|
||||
|
||||
pub fn get_steamvr_bin_dir_path() -> PathBuf {
|
||||
XDG.get_data_home()
|
||||
.join("Steam/steamapps/common/SteamVR/bin/linux64")
|
||||
const STEAMVR_STEAM_APPID: u32 = 250820;
|
||||
|
||||
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")
|
||||
}
|
||||
|
|
182
src/profile.rs
182
src/profile.rs
|
@ -4,8 +4,9 @@ use crate::{
|
|||
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,
|
||||
},
|
||||
file_builders::active_runtime_json::ActiveRuntime,
|
||||
paths::{get_data_dir, BWRAP_SYSTEM_PREFIX, SYSTEM_PREFIX},
|
||||
util::file_utils::get_writer,
|
||||
util::file_utils::{deserialize_file, get_writer},
|
||||
xdg::XDG,
|
||||
};
|
||||
use nix::NixPath;
|
||||
|
@ -17,6 +18,7 @@ use std::{
|
|||
io::BufReader,
|
||||
path::{Path, PathBuf},
|
||||
slice::Iter,
|
||||
str::FromStr,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -43,7 +45,14 @@ impl XRServiceType {
|
|||
pub fn libmonado_path(&self) -> &'static str {
|
||||
match self {
|
||||
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 +260,97 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
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 | OvrCompatibilityModuleType::Vapor => {
|
||||
self.path.join("build")
|
||||
}
|
||||
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)]
|
||||
pub struct Profile {
|
||||
pub uuid: String,
|
||||
|
@ -261,9 +361,15 @@ pub struct Profile {
|
|||
pub xrservice_branch: Option<String>,
|
||||
#[serde(default = "HashMap::<String, String>::default")]
|
||||
pub xrservice_cmake_flags: HashMap<String, String>,
|
||||
#[deprecated]
|
||||
#[serde(default)]
|
||||
pub opencomposite_path: PathBuf,
|
||||
#[deprecated]
|
||||
pub opencomposite_repo: Option<String>,
|
||||
#[deprecated]
|
||||
pub opencomposite_branch: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ovr_comp: ProfileOvrCompatibilityModule,
|
||||
pub features: ProfileFeatures,
|
||||
pub environment: HashMap<String, String>,
|
||||
/// Install prefix
|
||||
|
@ -276,7 +382,6 @@ pub struct Profile {
|
|||
pub lighthouse_driver: LighthouseDriver,
|
||||
#[serde(default = "String::default")]
|
||||
pub xrservice_launch_options: String,
|
||||
pub autostart_command: Option<String>,
|
||||
#[serde(default)]
|
||||
pub skip_dependency_check: bool,
|
||||
}
|
||||
|
@ -288,6 +393,7 @@ impl Display for Profile {
|
|||
}
|
||||
|
||||
impl Default for Profile {
|
||||
#[allow(deprecated)]
|
||||
fn default() -> Self {
|
||||
let uuid = Self::new_uuid();
|
||||
let profile_dir = get_data_dir().join(&uuid);
|
||||
|
@ -323,23 +429,27 @@ impl Default for Profile {
|
|||
mercury_enabled: false,
|
||||
},
|
||||
environment: HashMap::new(),
|
||||
prefix: get_data_dir().join("prefixes").join(&uuid),
|
||||
prefix: Self::default_prefix_path(&uuid),
|
||||
can_be_built: true,
|
||||
pull_on_build: true,
|
||||
opencomposite_path: profile_dir.join("opencomposite"),
|
||||
opencomposite_repo: None,
|
||||
opencomposite_branch: None,
|
||||
ovr_comp: ProfileOvrCompatibilityModule::default_for_uuid(&uuid),
|
||||
editable: true,
|
||||
lighthouse_driver: LighthouseDriver::default(),
|
||||
xrservice_launch_options: String::default(),
|
||||
uuid,
|
||||
autostart_command: None,
|
||||
skip_dependency_check: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
fn default_prefix_path(uuid: &str) -> PathBuf {
|
||||
get_data_dir().join("prefixes").join(uuid)
|
||||
}
|
||||
|
||||
pub fn xr_runtime_json_env_var(&self) -> String {
|
||||
format!(
|
||||
"XR_RUNTIME_JSON=\"{prefix}/share/openxr/1/openxr_{runtime}.json\"",
|
||||
|
@ -358,8 +468,8 @@ impl Profile {
|
|||
pub fn env_vars_full(&self) -> Vec<String> {
|
||||
vec![
|
||||
// format!(
|
||||
// "VR_OVERRIDE={opencomp}/build",
|
||||
// opencomp = self.opencomposite_path,
|
||||
// "VR_OVERRIDE={}",
|
||||
// self.ovr_comp.runtime_dir(),
|
||||
// ),
|
||||
self.xr_runtime_json_env_var(),
|
||||
format!(
|
||||
|
@ -417,8 +527,8 @@ impl Profile {
|
|||
}
|
||||
let uuid = Self::new_uuid();
|
||||
let profile_dir = get_data_dir().join(&uuid);
|
||||
#[allow(deprecated)]
|
||||
let mut dup = Self {
|
||||
uuid,
|
||||
name: format!("Duplicate of {}", self.name),
|
||||
xrservice_type: self.xrservice_type.clone(),
|
||||
xrservice_repo: self.xrservice_repo.clone(),
|
||||
|
@ -450,7 +560,6 @@ impl Profile {
|
|||
mercury_enabled: self.features.mercury_enabled,
|
||||
},
|
||||
environment: self.environment.clone(),
|
||||
autostart_command: self.autostart_command.clone(),
|
||||
pull_on_build: self.pull_on_build,
|
||||
lighthouse_driver: self.lighthouse_driver,
|
||||
opencomposite_repo: self.opencomposite_repo.clone(),
|
||||
|
@ -458,7 +567,16 @@ impl Profile {
|
|||
opencomposite_path: profile_dir.join("opencomposite"),
|
||||
skip_dependency_check: self.skip_dependency_check,
|
||||
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") {
|
||||
dup.environment.insert(
|
||||
|
@ -544,21 +662,37 @@ impl Profile {
|
|||
}
|
||||
|
||||
/// 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"]
|
||||
.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())
|
||||
}
|
||||
|
||||
/// absolute path to the libmonado shared object
|
||||
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
|
||||
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> {
|
||||
|
@ -586,6 +720,12 @@ impl Profile {
|
|||
missing_deps.dedup(); // dedup only works if sorted, hence the above
|
||||
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 {
|
||||
|
@ -599,7 +739,10 @@ mod tests {
|
|||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::profile::{ProfileFeature, ProfileFeatureType, ProfileFeatures, XRServiceType};
|
||||
use crate::profile::{
|
||||
OvrCompatibilityModuleType, ProfileFeature, ProfileFeatureType, ProfileFeatures,
|
||||
ProfileOvrCompatibilityModule, XRServiceType,
|
||||
};
|
||||
|
||||
use super::Profile;
|
||||
|
||||
|
@ -609,7 +752,7 @@ mod tests {
|
|||
assert_eq!(profile.name, "Demo profile");
|
||||
assert_eq!(profile.xrservice_path, PathBuf::from("/home/user/monado"));
|
||||
assert_eq!(
|
||||
profile.opencomposite_path,
|
||||
profile.ovr_comp.path,
|
||||
PathBuf::from("/home/user/opencomposite")
|
||||
);
|
||||
assert_eq!(profile.prefix, PathBuf::from("/home/user/envisionprefix"));
|
||||
|
@ -640,7 +783,12 @@ mod tests {
|
|||
name: "Demo profile".into(),
|
||||
xrservice_path: PathBuf::from("/home/user/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 {
|
||||
libsurvive: ProfileFeature {
|
||||
feature_type: ProfileFeatureType::Libsurvive,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use crate::{
|
||||
constants::APP_NAME,
|
||||
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;
|
||||
|
||||
|
@ -21,7 +24,10 @@ pub fn lighthouse_profile() -> Profile {
|
|||
name: format!("Lighthouse Driver - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_monado_path(),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: data_opencomposite_path(),
|
||||
..Default::default()
|
||||
},
|
||||
features: ProfileFeatures::default(),
|
||||
environment,
|
||||
prefix,
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
paths::{data_monado_path, data_opencomposite_path, data_openhmd_path, get_data_dir},
|
||||
profile::{
|
||||
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
|
||||
ProfileFeatures, XRServiceType,
|
||||
ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType,
|
||||
},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
@ -24,7 +24,10 @@ pub fn openhmd_profile() -> Profile {
|
|||
name: format!("OpenHMD - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_monado_path(),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: data_opencomposite_path(),
|
||||
..Default::default()
|
||||
},
|
||||
features: ProfileFeatures {
|
||||
openhmd: ProfileFeature {
|
||||
feature_type: ProfileFeatureType::OpenHmd,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
constants::APP_NAME,
|
||||
paths::{data_monado_path, data_opencomposite_path, get_data_dir},
|
||||
profile::{Profile, ProfileFeatures, XRServiceType},
|
||||
profile::{Profile, ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -25,7 +25,10 @@ pub fn simulated_profile() -> Profile {
|
|||
name: format!("Simulated Driver - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_monado_path(),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: data_opencomposite_path(),
|
||||
..Default::default()
|
||||
},
|
||||
features: ProfileFeatures::default(),
|
||||
environment,
|
||||
prefix,
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
paths::{data_libsurvive_path, data_monado_path, data_opencomposite_path, get_data_dir},
|
||||
profile::{
|
||||
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
|
||||
ProfileFeatures, XRServiceType,
|
||||
ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType,
|
||||
},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
@ -26,7 +26,10 @@ pub fn survive_profile() -> Profile {
|
|||
name: format!("Survive - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_monado_path(),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: data_opencomposite_path(),
|
||||
..Default::default()
|
||||
},
|
||||
features: ProfileFeatures {
|
||||
libsurvive: ProfileFeature {
|
||||
feature_type: ProfileFeatureType::Libsurvive,
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
use crate::{
|
||||
constants::APP_NAME,
|
||||
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;
|
||||
|
||||
|
@ -18,10 +22,16 @@ pub fn wivrn_profile() -> Profile {
|
|||
name: format!("WiVRn - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_wivrn_path(),
|
||||
xrservice_type: XRServiceType::Wivrn,
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: data_opencomposite_path(),
|
||||
..Default::default()
|
||||
},
|
||||
features: ProfileFeatures {
|
||||
..Default::default()
|
||||
},
|
||||
xrservice_launch_options: format!(
|
||||
"{LAUNCH_OPTS_CMD_PLACEHOLDER} --no-instructions --no-manage-active-runtime"
|
||||
),
|
||||
environment,
|
||||
prefix,
|
||||
can_be_built: true,
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
paths::{data_basalt_path, data_monado_path, data_opencomposite_path, get_data_dir},
|
||||
profile::{
|
||||
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
|
||||
ProfileFeatures, XRServiceType,
|
||||
ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType,
|
||||
},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
@ -24,7 +24,10 @@ pub fn wmr_profile() -> Profile {
|
|||
name: format!("WMR - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_monado_path(),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: data_opencomposite_path(),
|
||||
..Default::default()
|
||||
},
|
||||
features: ProfileFeatures {
|
||||
basalt: ProfileFeature {
|
||||
feature_type: ProfileFeatureType::Basalt,
|
||||
|
|
|
@ -1,75 +1,33 @@
|
|||
use crate::{
|
||||
paths::{get_backup_dir, get_home_dir},
|
||||
paths::get_backup_dir,
|
||||
profile::Profile,
|
||||
util::file_utils::{copy_file, get_writer},
|
||||
util::{
|
||||
file_utils::{copy_file, get_writer},
|
||||
steam_library_folder::SteamLibraryFolder,
|
||||
},
|
||||
};
|
||||
use anyhow::bail;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::read_to_string,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LibraryFolder {
|
||||
pub path: String,
|
||||
pub apps: HashMap<u32, usize>,
|
||||
}
|
||||
use tracing::error;
|
||||
|
||||
pub const PRESSURE_VESSEL_STEAM_APPID: u32 = 1628350;
|
||||
|
||||
fn get_steam_main_dir_path() -> anyhow::Result<PathBuf> {
|
||||
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()
|
||||
.find(|(_, libraryfolder)| {
|
||||
libraryfolder
|
||||
.apps
|
||||
.contains_key(&PRESSURE_VESSEL_STEAM_APPID)
|
||||
})
|
||||
.map(|(_, libraryfolder)| {
|
||||
PathBuf::from(&libraryfolder.path)
|
||||
.join("steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point")
|
||||
})
|
||||
}
|
||||
match SteamLibraryFolder::get_folders() {
|
||||
Ok(libraryfolders) => libraryfolders
|
||||
.iter()
|
||||
.find(|(_, folder)| folder.apps.contains_key(&PRESSURE_VESSEL_STEAM_APPID))
|
||||
.map(|(_, folder)| {
|
||||
PathBuf::from(&folder.path)
|
||||
.join("steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point")
|
||||
}),
|
||||
Err(e) => {
|
||||
eprintln!("Error getting steam root path: {e}");
|
||||
error!("unable to get runtime entrypoint path: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -125,22 +83,3 @@ pub fn set_runtime_entrypoint_launch_opts_from_profile(profile: &Profile) -> any
|
|||
}
|
||||
bail!("Could not find valid runtime entrypoint");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::parse_steam_libraryfolders_vdf;
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
constants::{
|
||||
get_artists, get_developers, APP_ID, APP_NAME, BUILD_DATETIME, ISSUES_URL, REPO_URL,
|
||||
SINGLE_DEVELOPER, VERSION,
|
||||
get_artists, APP_ID, APP_NAME, BUILD_DATETIME, ISSUES_URL, REPO_URL, SINGLE_DEVELOPER,
|
||||
VERSION,
|
||||
},
|
||||
device_prober::PhysicalXRDevice,
|
||||
linux_distro::LinuxDistro,
|
||||
|
@ -20,13 +20,20 @@ pub fn create_about_dialog() -> adw::AboutDialog {
|
|||
.website(REPO_URL)
|
||||
.issue_url(ISSUES_URL)
|
||||
.developer_name(SINGLE_DEVELOPER)
|
||||
.developers(get_developers())
|
||||
.developers(
|
||||
env!("CARGO_PKG_AUTHORS")
|
||||
.split(':')
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.artists(get_artists())
|
||||
.build()
|
||||
}
|
||||
|
||||
const UNKNOWN: &str = "UNKNOWN";
|
||||
|
||||
pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo>) {
|
||||
if dialog.debug_info().len() > 0 {
|
||||
if !dialog.debug_info().is_empty() {
|
||||
return;
|
||||
}
|
||||
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!(
|
||||
"Operating system: {d} ({f})",
|
||||
d = distro.unwrap_or("unknown".into()),
|
||||
d = distro.unwrap_or(UNKNOWN.into()),
|
||||
f = distro_family
|
||||
.map(|f| f.to_string())
|
||||
.unwrap_or("unknown".into())
|
||||
.unwrap_or(UNKNOWN.into())
|
||||
),
|
||||
format!(
|
||||
"Kernel: {}",
|
||||
|
@ -50,23 +57,29 @@ pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo
|
|||
),
|
||||
format!(
|
||||
"Session type: {}",
|
||||
env::var("XDG_SESSION_TYPE").unwrap_or("unknown".into())
|
||||
env::var("XDG_SESSION_TYPE").unwrap_or(UNKNOWN.into())
|
||||
),
|
||||
format!(
|
||||
"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!(
|
||||
"GPUs: {}",
|
||||
vkinfo
|
||||
.map(|i| i.gpu_names.join(", "))
|
||||
.unwrap_or("unknown".into())
|
||||
),
|
||||
format!(
|
||||
"Monado Vulkan Layers: {}",
|
||||
vkinfo
|
||||
.map(|i| i.has_monado_vulkan_layers.to_string())
|
||||
.unwrap_or("unknown".into())
|
||||
.unwrap_or(UNKNOWN.into())
|
||||
),
|
||||
format!("Detected XR Devices: {}", {
|
||||
let devs = PhysicalXRDevice::from_usb();
|
||||
|
|
295
src/ui/app.rs
295
src/ui/app.rs
|
@ -11,6 +11,7 @@ use super::{
|
|||
},
|
||||
libsurvive_setup_window::{LibsurviveSetupMsg, LibsurviveSetupWindow},
|
||||
main_view::{MainView, MainViewInit, MainViewMsg, MainViewOutMsg},
|
||||
plugins::store::{PluginStore, PluginStoreInit, PluginStoreMsg, PluginStoreOutMsg},
|
||||
util::{copiable_code_snippet, copy_text, open_with_default_handler},
|
||||
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_mercury::get_build_mercury_jobs, build_monado::get_build_monado_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,
|
||||
depcheck::common::dep_pkexec,
|
||||
file_builders::{
|
||||
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::{
|
||||
set_current_openvrpaths_to_profile, set_current_openvrpaths_to_steam,
|
||||
|
@ -35,12 +38,14 @@ use crate::{
|
|||
linux_distro::LinuxDistro,
|
||||
openxr_prober::is_openxr_ready,
|
||||
paths::get_data_dir,
|
||||
profile::{Profile, XRServiceType},
|
||||
profile::{OvrCompatibilityModuleType, Profile, XRServiceType},
|
||||
stateless_action,
|
||||
steam_linux_runtime_injector::{
|
||||
restore_runtime_entrypoint, set_runtime_entrypoint_launch_opts_from_profile,
|
||||
},
|
||||
util::file_utils::{setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd},
|
||||
util::file_utils::{
|
||||
setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd, verify_cap_sys_nice_eip,
|
||||
},
|
||||
vulkaninfo::VulkanInfo,
|
||||
wivrn_dbus,
|
||||
xr_devices::XRDevice,
|
||||
|
@ -53,7 +58,12 @@ use relm4::{
|
|||
new_action_group, new_stateful_action, new_stateless_action,
|
||||
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 {
|
||||
application: adw::Application,
|
||||
|
@ -70,7 +80,7 @@ pub struct App {
|
|||
|
||||
config: Config,
|
||||
xrservice_worker: Option<JobWorker>,
|
||||
autostart_worker: Option<JobWorker>,
|
||||
plugins_worker: Option<JobWorker>,
|
||||
restart_xrservice: bool,
|
||||
build_worker: Option<JobWorker>,
|
||||
profiles: Vec<Profile>,
|
||||
|
@ -85,13 +95,14 @@ pub struct App {
|
|||
vkinfo: Option<VulkanInfo>,
|
||||
|
||||
inhibit_fail_notif: Option<NotificationHandle>,
|
||||
pluginstore: Option<AsyncController<PluginStore>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
OnServiceLog(Vec<String>),
|
||||
OnServiceExit(i32),
|
||||
OnAutostartExit(i32),
|
||||
OnPluginsExit(i32),
|
||||
OnBuildLog(Vec<String>),
|
||||
OnBuildExit(i32),
|
||||
ClockTicking,
|
||||
|
@ -116,6 +127,8 @@ pub enum Msg {
|
|||
StartProber,
|
||||
OnProberExit(bool),
|
||||
WivrnCheckPairMode,
|
||||
OpenPluginStore,
|
||||
UpdateConfigPlugins(HashMap<String, PluginConfig>),
|
||||
NoOp,
|
||||
}
|
||||
|
||||
|
@ -147,7 +160,7 @@ impl App {
|
|||
} {
|
||||
Ok(n) => Some(n),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to send desktop notification: {e:?}");
|
||||
error!("failed to send desktop notification: {e:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -163,57 +176,7 @@ impl App {
|
|||
pub fn start_xrservice(&mut self, sender: AsyncComponentSender<Self>, debug: bool) {
|
||||
self.xrservice_ready = false;
|
||||
let prof = self.get_selected_profile();
|
||||
if prof.can_start() {
|
||||
if let Err(e) = set_current_active_runtime_to_profile(&prof) {
|
||||
alert(
|
||||
"Failed to start XR Service",
|
||||
Some(&format!(
|
||||
"Error setting current active runtime to profile: {e}"
|
||||
)),
|
||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if let Err(e) = set_current_openvrpaths_to_profile(&prof) {
|
||||
alert(
|
||||
"Failed to start XR Service",
|
||||
Some(&format!(
|
||||
"Error setting current openvrpaths file to profile: {e}"
|
||||
)),
|
||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
};
|
||||
self.debug_view.sender().emit(DebugViewMsg::ClearLog);
|
||||
self.xr_devices = vec![];
|
||||
remove_file(prof.xrservice_type.ipc_file_path())
|
||||
.is_err()
|
||||
.then(|| println!("Failed to remove xrservice IPC file"));
|
||||
let worker = JobWorker::xrservice_worker_wrap_from_profile(
|
||||
&prof,
|
||||
sender.input_sender(),
|
||||
|msg| match msg {
|
||||
JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows),
|
||||
JobWorkerOut::Exit(code) => Msg::OnServiceExit(code),
|
||||
},
|
||||
debug,
|
||||
);
|
||||
worker.start();
|
||||
self.xrservice_worker = Some(worker);
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::XRServiceActiveChanged(
|
||||
true,
|
||||
Some(self.get_selected_profile()),
|
||||
// show launch opts only if setting the runtime entrypoint fails
|
||||
set_runtime_entrypoint_launch_opts_from_profile(&prof).is_err(),
|
||||
));
|
||||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::XRServiceActiveChanged(true));
|
||||
self.set_inhibit_session(true);
|
||||
sender.input(Msg::StartProber);
|
||||
} else {
|
||||
if !prof.can_start() {
|
||||
alert(
|
||||
"Failed to start profile",
|
||||
Some(concat!(
|
||||
|
@ -222,32 +185,133 @@ impl App {
|
|||
)),
|
||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if let Err(e) = set_current_active_runtime_to_profile(&prof) {
|
||||
alert(
|
||||
"Failed to start XR Service",
|
||||
Some(&format!(
|
||||
"Error setting current active runtime to profile: {e}"
|
||||
)),
|
||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if let Err(e) = set_current_openvrpaths_to_profile(&prof) {
|
||||
alert(
|
||||
"Failed to start XR Service",
|
||||
Some(&format!(
|
||||
"Error setting current openvrpaths file to profile: {e}"
|
||||
)),
|
||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
};
|
||||
self.debug_view.sender().emit(DebugViewMsg::ClearLog);
|
||||
self.xr_devices = vec![];
|
||||
{
|
||||
let ipc_file = prof.xrservice_type.ipc_file_path();
|
||||
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(
|
||||
&prof,
|
||||
sender.input_sender(),
|
||||
|msg| match msg {
|
||||
JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows),
|
||||
JobWorkerOut::Exit(code) => Msg::OnServiceExit(code),
|
||||
},
|
||||
debug,
|
||||
);
|
||||
worker.start();
|
||||
self.xrservice_worker = Some(worker);
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::XRServiceActiveChanged(
|
||||
true,
|
||||
Some(self.get_selected_profile()),
|
||||
// show launch opts only if setting the runtime entrypoint fails
|
||||
set_runtime_entrypoint_launch_opts_from_profile(&prof).is_err(),
|
||||
));
|
||||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::XRServiceActiveChanged(true));
|
||||
self.set_inhibit_session(true);
|
||||
sender.input(Msg::StartProber);
|
||||
}
|
||||
|
||||
pub fn run_autostart(&mut self, sender: AsyncComponentSender<Self>) {
|
||||
let prof = self.get_selected_profile();
|
||||
if let Some(autostart_cmd) = &prof.autostart_command {
|
||||
let plugins_cmd = self
|
||||
.config
|
||||
.plugins
|
||||
.values()
|
||||
.filter_map(|cp| {
|
||||
// disable potentially unsafe wayvr_dashboard
|
||||
if cp.plugin.appid.contains("wayvr_dashboard") {
|
||||
return None;
|
||||
}
|
||||
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();
|
||||
jobs.push_back(WorkerJob::new_cmd(
|
||||
Some(prof.environment.clone()),
|
||||
"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::Exit(code) => Msg::OnAutostartExit(code),
|
||||
JobWorkerOut::Exit(code) => Msg::OnPluginsExit(code),
|
||||
});
|
||||
autostart_worker.start();
|
||||
self.autostart_worker = Some(autostart_worker);
|
||||
plugins_worker.start();
|
||||
self.plugins_worker = Some(plugins_worker);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restore_openxr_openvr_files(&self) {
|
||||
restore_runtime_entrypoint();
|
||||
if let Err(e) = set_current_active_runtime_to_steam() {
|
||||
if let Err(e) = remove_current_active_runtime() {
|
||||
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(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
|
@ -262,27 +326,17 @@ impl App {
|
|||
}
|
||||
|
||||
pub fn shutdown_xrservice(&mut self) {
|
||||
if let Some(worker) = self.autostart_worker.as_ref() {
|
||||
worker.stop();
|
||||
if let Some(w) = self.plugins_worker.as_ref() {
|
||||
w.stop();
|
||||
}
|
||||
self.xrservice_ready = false;
|
||||
if let Some(w) = self.openxr_prober_worker.as_ref() {
|
||||
w.stop();
|
||||
// this can cause threads to remain hanging...
|
||||
self.openxr_prober_worker = None;
|
||||
}
|
||||
self.set_inhibit_session(false);
|
||||
if let Some(worker) = self.xrservice_worker.as_ref() {
|
||||
worker.stop();
|
||||
if let Some(w) = self.xrservice_worker.as_ref() {
|
||||
w.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![];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -348,6 +402,8 @@ impl AsyncComponent for App {
|
|||
}
|
||||
}
|
||||
Msg::OnServiceExit(code) => {
|
||||
self.set_inhibit_session(false);
|
||||
self.xrservice_ready = false;
|
||||
self.restore_openxr_openvr_files();
|
||||
self.main_view
|
||||
.sender()
|
||||
|
@ -355,6 +411,8 @@ impl AsyncComponent for App {
|
|||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::XRServiceActiveChanged(false));
|
||||
self.libmonado = None;
|
||||
self.xr_devices = vec![];
|
||||
if code != 0 && code != 15 {
|
||||
// 15 is SIGTERM
|
||||
sender.input(Msg::OnServiceLog(vec![format!(
|
||||
|
@ -369,7 +427,7 @@ impl AsyncComponent for App {
|
|||
self.start_xrservice(sender, false);
|
||||
}
|
||||
}
|
||||
Msg::OnAutostartExit(_) => self.autostart_worker = None,
|
||||
Msg::OnPluginsExit(_) => self.plugins_worker = None,
|
||||
Msg::ClockTicking => {
|
||||
self.main_view.sender().emit(MainViewMsg::ClockTicking);
|
||||
let xrservice_worker_is_alive = self
|
||||
|
@ -409,7 +467,7 @@ impl AsyncComponent for App {
|
|||
.emit(MainViewMsg::SetWivrnSupportsPairing(true));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: failed to get wivrn pairing mode: {e:?}");
|
||||
error!("failed to get wivrn pairing mode: {e:?}");
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::SetWivrnSupportsPairing(false));
|
||||
|
@ -473,7 +531,17 @@ impl AsyncComponent for App {
|
|||
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();
|
||||
if !(self.skip_depcheck || profile.skip_dependency_check || missing_deps.is_empty())
|
||||
{
|
||||
|
@ -630,13 +698,32 @@ impl AsyncComponent for App {
|
|||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::UpdateSelectedProfile(prof.clone()));
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::QueryProfileRebuild);
|
||||
}
|
||||
Msg::RunSetCap => {
|
||||
if !dep_pkexec().check() {
|
||||
println!("pkexec not found, skipping setcap");
|
||||
error!("pkexec not found, skipping setcap");
|
||||
} else {
|
||||
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}");
|
||||
} else if !verify_cap_sys_nice_eip(&profile).await {
|
||||
setcap_failed_dialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::ProfileSelected(prof) => {
|
||||
|
@ -756,6 +843,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 +959,17 @@ impl AsyncComponent for App {
|
|||
}
|
||||
)
|
||||
);
|
||||
stateless_action!(
|
||||
actions,
|
||||
PluginStoreAction,
|
||||
clone!(
|
||||
#[strong]
|
||||
sender,
|
||||
move |_| {
|
||||
sender.input(Msg::OpenPluginStore);
|
||||
}
|
||||
)
|
||||
);
|
||||
// this bypasses the macro because I need the underlying gio action
|
||||
// to enable/disable it in update()
|
||||
let configure_wivrn_action = {
|
||||
|
@ -878,7 +991,7 @@ impl AsyncComponent for App {
|
|||
match VulkanInfo::get() {
|
||||
Ok(info) => Some(info),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get Vulkan info: {e:#?}");
|
||||
error!("failed to get Vulkan info: {e:#?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -893,7 +1006,6 @@ impl AsyncComponent for App {
|
|||
config: config.clone(),
|
||||
selected_profile: selected_profile.clone(),
|
||||
root_win: root.clone().into(),
|
||||
vkinfo: vkinfo.clone(),
|
||||
})
|
||||
.forward(sender.input_sender(), |message| match message {
|
||||
MainViewOutMsg::DoStartStopXRService => Msg::DoStartStopXRService,
|
||||
|
@ -902,6 +1014,7 @@ impl AsyncComponent for App {
|
|||
MainViewOutMsg::DeleteProfile => Msg::DeleteProfile,
|
||||
MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p),
|
||||
MainViewOutMsg::OpenLibsurviveSetup => Msg::OpenLibsurviveSetup,
|
||||
MainViewOutMsg::BuildProfile(clean) => Msg::BuildProfile(clean),
|
||||
}),
|
||||
vkinfo,
|
||||
debug_view: DebugView::builder()
|
||||
|
@ -928,7 +1041,7 @@ impl AsyncComponent for App {
|
|||
config,
|
||||
profiles,
|
||||
xrservice_worker: None,
|
||||
autostart_worker: None,
|
||||
plugins_worker: None,
|
||||
build_worker: None,
|
||||
xr_devices: vec![],
|
||||
restart_xrservice: false,
|
||||
|
@ -939,6 +1052,7 @@ impl AsyncComponent for App {
|
|||
openxr_prober_worker: None,
|
||||
xrservice_ready: false,
|
||||
inhibit_fail_notif: None,
|
||||
pluginstore: None,
|
||||
};
|
||||
|
||||
let widgets = view_output!();
|
||||
|
@ -1011,6 +1125,7 @@ new_stateless_action!(pub BuildProfileCleanAction, AppActionGroup, "buildprofile
|
|||
new_stateless_action!(pub QuitAction, AppActionGroup, "quit");
|
||||
new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool);
|
||||
new_stateless_action!(pub ConfigureWivrnAction, AppActionGroup, "configurewivrn");
|
||||
new_stateless_action!(pub PluginStoreAction, AppActionGroup, "store");
|
||||
|
||||
new_stateless_action!(pub DebugOpenDataAction, AppActionGroup, "debugopendata");
|
||||
new_stateless_action!(pub DebugOpenPrefixAction, AppActionGroup, "debugopenprefix");
|
||||
|
|
|
@ -88,43 +88,54 @@ impl SimpleComponent for BuildWindow {
|
|||
gtk::Label {
|
||||
#[track = "model.changed(BuildWindow::build_status())"]
|
||||
set_markup: match &model.build_status {
|
||||
BuildStatus::Building => "Build in progress...".to_string(),
|
||||
BuildStatus::Done => "Build done, you can close this window".to_string(),
|
||||
BuildStatus::Building => String::default(),
|
||||
BuildStatus::Done => "Build done, you can close this window".into(),
|
||||
BuildStatus::Error(code) => {
|
||||
format!("Build failed: \"{c}\"", c = code)
|
||||
}
|
||||
}.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",
|
||||
set_wrap: true,
|
||||
set_wrap_mode: gtk::pango::WrapMode::Word,
|
||||
set_justify: gtk::Justification::Center,
|
||||
},
|
||||
gtk::Button {
|
||||
#[track = "model.changed(BuildWindow::build_status())"]
|
||||
set_visible: matches!(&model.build_status, BuildStatus::Building),
|
||||
add_css_class: "destructive-action",
|
||||
add_css_class: "circular",
|
||||
set_icon_name: "window-close-symbolic",
|
||||
set_tooltip_text: Some("Cancel build"),
|
||||
connect_clicked[sender] => move |_| {
|
||||
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",
|
||||
add_bottom_bar: bottom_bar = >k::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
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();
|
||||
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 {
|
||||
#[track = "model.changed(BuildWindow::build_status())"]
|
||||
set_visible: matches!(&model.build_status, BuildStatus::Building),
|
||||
add_css_class: "destructive-action",
|
||||
add_css_class: "pill",
|
||||
set_label: "Cancel build",
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.output(Self::Output::CancelBuild).expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
},
|
||||
// ^^^
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use crate::config::Config;
|
||||
use crate::{
|
||||
config::Config,
|
||||
constants::{APP_NAME, VERSION},
|
||||
};
|
||||
use gtk::{
|
||||
gio::{
|
||||
prelude::{ApplicationCommandLineExt, ApplicationExt},
|
||||
|
@ -6,9 +9,11 @@ use gtk::{
|
|||
},
|
||||
glib::{self, prelude::IsA},
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CmdLineOpts {
|
||||
pub version: bool,
|
||||
pub start: bool,
|
||||
pub list_profiles: bool,
|
||||
pub profile_uuid: Option<String>,
|
||||
|
@ -17,6 +22,7 @@ pub struct CmdLineOpts {
|
|||
}
|
||||
|
||||
impl CmdLineOpts {
|
||||
const OPT_VERSION: (&'static str, char) = ("version", 'v');
|
||||
const OPT_START: (&'static str, char) = ("start", 'S');
|
||||
const OPT_LIST_PROFILES: (&'static str, char) = ("list-profiles", 'l');
|
||||
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');
|
||||
|
||||
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(
|
||||
Self::OPT_START.0,
|
||||
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
|
||||
pub fn handle_non_activating_opts(&self) -> Option<i32> {
|
||||
if self.version {
|
||||
println!("{APP_NAME} {VERSION}");
|
||||
return Some(0);
|
||||
}
|
||||
if self.list_profiles {
|
||||
println!("Available profiles\nUUID: \"name\"");
|
||||
let profiles = Config::get_config().profiles();
|
||||
|
@ -88,7 +106,7 @@ impl CmdLineOpts {
|
|||
}
|
||||
return Some(1);
|
||||
} else {
|
||||
eprintln!("No profile found for uuid: `{prof_id}`");
|
||||
error!("No profile found for uuid: `{prof_id}`");
|
||||
return Some(404);
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +116,7 @@ impl CmdLineOpts {
|
|||
pub fn from_cmdline(cmdline: &ApplicationCommandLine) -> Self {
|
||||
let opts = cmdline.options_dict();
|
||||
Self {
|
||||
version: opts.contains(Self::OPT_VERSION.0),
|
||||
start: opts.contains(Self::OPT_START.0),
|
||||
list_profiles: opts.contains(Self::OPT_LIST_PROFILES.0),
|
||||
profile_uuid: opts
|
||||
|
|
|
@ -61,9 +61,19 @@ impl SimpleComponent for DevicesBox {
|
|||
}
|
||||
if !has_left && dev.roles.contains(&XRDeviceRole::Left) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use crate::{
|
|||
use gtk::prelude::*;
|
||||
use relm4::{new_action_group, new_stateless_action, prelude::*};
|
||||
use std::fs::remove_file;
|
||||
use tracing::error;
|
||||
|
||||
const WIVRN_LATEST_RELEASE_APK_URL: &str =
|
||||
"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",
|
||||
set_hexpand: true,
|
||||
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, ",
|
||||
"then press the \"Install WiVRn\" button."
|
||||
),
|
||||
|
@ -172,7 +173,7 @@ impl AsyncComponent for InstallWivrnBox {
|
|||
|
||||
match get_wivrn_apk_ref(&self.selected_profile) {
|
||||
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) => {
|
||||
self.set_install_wivrn_status(InstallWivrnStatus::Done(Some(
|
||||
|
@ -180,14 +181,11 @@ impl AsyncComponent for InstallWivrnBox {
|
|||
)));
|
||||
}
|
||||
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"));
|
||||
if existing.is_file() {
|
||||
if let Err(e) = remove_file(&existing) {
|
||||
eprintln!(
|
||||
"Failed to remove file {}: {e}",
|
||||
existing.to_string_lossy()
|
||||
);
|
||||
error!("failed to remove file {}: {e}", existing.to_string_lossy());
|
||||
}
|
||||
}
|
||||
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?
|
||||
match cache_file(&url, Some("apk")).await {
|
||||
Err(e) => {
|
||||
eprintln!("Failed to download apk: {e}");
|
||||
error!("failed to download apk: {e}");
|
||||
self.set_install_wivrn_status(InstallWivrnStatus::Done(Some(
|
||||
"Error downloading WiVRn client APK".into(),
|
||||
)));
|
||||
|
@ -236,14 +234,14 @@ impl AsyncComponent for InstallWivrnBox {
|
|||
.into(),
|
||||
))
|
||||
} 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(
|
||||
format!("ADB exited with code \"{}\"", out.exit_code)
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: failed to run ADB: {e}");
|
||||
error!("failed to run ADB: {e}");
|
||||
InstallWivrnStatus::Done(Some(
|
||||
"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 {
|
||||
pub fn xrservice_worker_from_profile(
|
||||
|
@ -193,9 +193,6 @@ impl InternalJobWorker {
|
|||
} else {
|
||||
launch_opts
|
||||
};
|
||||
if !launch_opts.contains(" --no-instructions") {
|
||||
launch_opts.push_str(" --no-instructions");
|
||||
}
|
||||
let (command, args) = match launch_opts.is_empty() {
|
||||
false => (
|
||||
"sh".into(),
|
||||
|
|
|
@ -15,6 +15,7 @@ use std::{
|
|||
thread::{self, sleep},
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::{error, warn};
|
||||
|
||||
pub mod internal_worker;
|
||||
pub mod job;
|
||||
|
@ -97,7 +98,7 @@ impl JobWorker {
|
|||
self.state.lock().unwrap().stop_requested = true;
|
||||
if let Some(pid) = self.state.lock().unwrap().current_pid {
|
||||
if let Err(e) = kill(pid, SIGTERM) {
|
||||
eprintln!("Failed to send SIGTERM: {e:#?}");
|
||||
error!("Failed to send SIGTERM: {e}");
|
||||
}
|
||||
let state = self.state.clone();
|
||||
thread::spawn(move || {
|
||||
|
@ -105,9 +106,9 @@ impl JobWorker {
|
|||
if let Ok(s) = state.lock() {
|
||||
if !s.exited {
|
||||
// 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) {
|
||||
eprintln!("Failed to send SIGKILL: {e:#?}");
|
||||
error!("failed to send SIGKILL: {e}");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::{
|
|||
alert::alert,
|
||||
app::{
|
||||
AboutAction, BuildProfileAction, BuildProfileCleanAction, ConfigureWivrnAction,
|
||||
DebugViewToggleAction,
|
||||
DebugViewToggleAction, PluginStoreAction,
|
||||
},
|
||||
devices_box::{DevicesBox, DevicesBoxMsg},
|
||||
install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg},
|
||||
|
@ -12,6 +12,7 @@ use super::{
|
|||
steamvr_calibration_box::{SteamVrCalibrationBox, SteamVrCalibrationBoxMsg},
|
||||
util::{limit_dropdown_width, warning_heading},
|
||||
wivrn_wired_start_box::{WivrnWiredStartBox, WivrnWiredStartBoxInit, WivrnWiredStartBoxMsg},
|
||||
SENDER_IO_ERR_MSG,
|
||||
};
|
||||
use crate::{
|
||||
config::Config,
|
||||
|
@ -24,7 +25,6 @@ use crate::{
|
|||
file_utils::{get_writer, mount_has_nosuid},
|
||||
steamvr_utils::chaperone_info_exists,
|
||||
},
|
||||
vulkaninfo::VulkanInfo,
|
||||
wivrn_dbus,
|
||||
xr_devices::XRDevice,
|
||||
};
|
||||
|
@ -36,6 +36,7 @@ use relm4::{
|
|||
prelude::*,
|
||||
};
|
||||
use std::{fs::read_to_string, io::Write};
|
||||
use tracing::{error, warn};
|
||||
|
||||
#[tracker::track]
|
||||
pub struct MainView {
|
||||
|
@ -59,6 +60,8 @@ pub struct MainView {
|
|||
#[tracker::do_not_track]
|
||||
profile_delete_confirm_dialog: adw::AlertDialog,
|
||||
#[tracker::do_not_track]
|
||||
query_profile_rebuild_dialog: adw::AlertDialog,
|
||||
#[tracker::do_not_track]
|
||||
profile_editor: Option<Controller<ProfileEditor>>,
|
||||
#[tracker::do_not_track]
|
||||
steamvr_calibration_box: Controller<SteamVrCalibrationBox>,
|
||||
|
@ -71,8 +74,6 @@ pub struct MainView {
|
|||
#[tracker::do_not_track]
|
||||
profile_export_action: gtk::gio::SimpleAction,
|
||||
xrservice_ready: bool,
|
||||
#[tracker::do_not_track]
|
||||
vkinfo: Option<VulkanInfo>,
|
||||
wivrn_pairing_mode: bool,
|
||||
wivrn_pin: Option<String>,
|
||||
wivrn_supports_pairing: bool,
|
||||
|
@ -103,6 +104,7 @@ pub enum MainViewMsg {
|
|||
SetWivrnPairingMode(bool),
|
||||
StopWivrnPairingMode,
|
||||
StartWivrnPairingMode,
|
||||
QueryProfileRebuild,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -113,13 +115,14 @@ pub enum MainViewOutMsg {
|
|||
DeleteProfile,
|
||||
SaveProfile(Profile),
|
||||
OpenLibsurviveSetup,
|
||||
/// params: clean
|
||||
BuildProfile(bool),
|
||||
}
|
||||
|
||||
pub struct MainViewInit {
|
||||
pub config: Config,
|
||||
pub selected_profile: Profile,
|
||||
pub root_win: gtk::Window,
|
||||
pub vkinfo: Option<VulkanInfo>,
|
||||
}
|
||||
|
||||
impl MainView {
|
||||
|
@ -147,6 +150,7 @@ impl AsyncComponent for MainView {
|
|||
menu! {
|
||||
app_menu: {
|
||||
section! {
|
||||
"Plugin_s" => PluginStoreAction,
|
||||
// value inside action is ignored
|
||||
"_Debug View" => DebugViewToggleAction,
|
||||
"_Build Profile" => BuildProfileAction,
|
||||
|
@ -389,8 +393,8 @@ impl AsyncComponent for MainView {
|
|||
set_visible: match mount_has_nosuid(&model.selected_profile.prefix) {
|
||||
Ok(b) => b,
|
||||
Err(_) => {
|
||||
eprintln!(
|
||||
"Warning (nosuid detection): could not get stat on path {}",
|
||||
warn!(
|
||||
"nosuid detection: could not get stat on path {}",
|
||||
model.selected_profile.prefix.to_string_lossy());
|
||||
false
|
||||
},
|
||||
|
@ -445,35 +449,7 @@ impl AsyncComponent for MainView {
|
|||
set_label: concat!(
|
||||
"SteamVR room configuration not found.\n",
|
||||
"To use the SteamVR lighthouse driver, you ",
|
||||
"will need to run SteamVR and perform the room setup.",
|
||||
),
|
||||
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."
|
||||
"will need to run SteamVR Quick Calibration.",
|
||||
),
|
||||
add_css_class: "warning",
|
||||
set_xalign: 0.0,
|
||||
|
@ -627,7 +603,7 @@ impl AsyncComponent for MainView {
|
|||
self.set_wivrn_pin(Some(pin));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: failed to get wivrn pairing pin: {e:?}");
|
||||
error!("failed to get wivrn pairing pin: {e}");
|
||||
}
|
||||
};
|
||||
} else {
|
||||
|
@ -637,12 +613,12 @@ impl AsyncComponent for MainView {
|
|||
}
|
||||
Self::Input::StopWivrnPairingMode => {
|
||||
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 => {
|
||||
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 => {
|
||||
|
@ -720,6 +696,10 @@ impl AsyncComponent for MainView {
|
|||
}
|
||||
}));
|
||||
}
|
||||
Self::Input::QueryProfileRebuild => {
|
||||
self.query_profile_rebuild_dialog
|
||||
.present(Some(&self.root_win));
|
||||
}
|
||||
Self::Input::SetSelectedProfile(index) => {
|
||||
self.profiles_dropdown
|
||||
.as_ref()
|
||||
|
@ -757,7 +737,7 @@ impl AsyncComponent for MainView {
|
|||
Self::Input::SaveProfile(prof) => {
|
||||
sender
|
||||
.output(Self::Output::SaveProfile(prof))
|
||||
.expect("Sender output failed");
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
Self::Input::DuplicateProfile => {
|
||||
if self.selected_profile.can_be_built {
|
||||
|
@ -926,6 +906,29 @@ 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()
|
||||
.heading("Are you sure you want to delete this profile?")
|
||||
.build();
|
||||
|
@ -1060,6 +1063,7 @@ impl AsyncComponent for MainView {
|
|||
selected_profile: init.selected_profile.clone(),
|
||||
profile_not_editable_dialog,
|
||||
profile_delete_confirm_dialog,
|
||||
query_profile_rebuild_dialog,
|
||||
root_win: init.root_win.clone(),
|
||||
steamvr_calibration_box,
|
||||
openhmd_calibration_box,
|
||||
|
@ -1067,7 +1071,6 @@ impl AsyncComponent for MainView {
|
|||
xrservice_ready: false,
|
||||
profile_delete_action,
|
||||
profile_export_action,
|
||||
vkinfo: init.vkinfo,
|
||||
wivrn_pairing_mode: false,
|
||||
wivrn_supports_pairing: false,
|
||||
wivrn_pin: None,
|
||||
|
|
|
@ -13,6 +13,7 @@ mod libsurvive_setup_window;
|
|||
mod macros;
|
||||
mod main_view;
|
||||
mod openhmd_calibration_box;
|
||||
pub mod plugins;
|
||||
mod preference_rows;
|
||||
mod profile_editor;
|
||||
mod steam_launch_options_box;
|
||||
|
|
|
@ -3,6 +3,7 @@ use relm4::{
|
|||
gtk::{self, prelude::*},
|
||||
ComponentParts, ComponentSender, SimpleComponent,
|
||||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[tracker::track]
|
||||
pub struct OpenHmdCalibrationBox {
|
||||
|
@ -59,10 +60,10 @@ impl SimpleComponent for OpenHmdCalibrationBox {
|
|||
let target = XDG.get_config_home().join("openhmd/rift-room-config.json");
|
||||
if target.is_file() {
|
||||
if let Err(e) = std::fs::remove_file(target) {
|
||||
eprintln!("Failed to remove openhmd config: {e}");
|
||||
error!("Failed to remove openhmd config: {e}");
|
||||
}
|
||||
} else {
|
||||
println!("info: trying to delete openhmd calibration config, but file is missing")
|
||||
debug!("trying to delete openhmd calibration config, but file is missing")
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
169
src/ui/plugins/add_custom_plugin_win.rs
Normal file
169
src/ui/plugins/add_custom_plugin_win.rs
Normal file
|
@ -0,0 +1,169 @@
|
|||
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),
|
||||
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))
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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::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;2] = [
|
||||
"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",
|
||||
// wayvr dashboard potentially unsafe
|
||||
];
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||
fn filedialog_row_base<F: Fn(Option<String>) + 'static + Clone>(
|
||||
title: &str,
|
||||
description: Option<&str>,
|
||||
value: Option<String>,
|
||||
root_win: Option<gtk::Window>,
|
||||
cb: F,
|
||||
) -> adw::ActionRow {
|
||||
) -> (adw::ActionRow, gtk::Label) {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(title)
|
||||
.subtitle_lines(0)
|
||||
|
@ -174,14 +173,14 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
|||
row.set_subtitle(d);
|
||||
}
|
||||
|
||||
let path_label = >k::Label::builder()
|
||||
let path_label = gtk::Label::builder()
|
||||
.label(match value.as_ref() {
|
||||
None => "(None)",
|
||||
Some(p) => p.as_str(),
|
||||
})
|
||||
.wrap(true)
|
||||
.build();
|
||||
row.add_suffix(path_label);
|
||||
row.add_suffix(&path_label);
|
||||
|
||||
let clear_btn = gtk::Button::builder()
|
||||
.icon_name("edit-clear-symbolic")
|
||||
|
@ -200,6 +199,60 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
|||
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()
|
||||
.modal(true)
|
||||
.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))
|
||||
|
@ -220,8 +273,8 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
|||
move |res| {
|
||||
if let Ok(file) = res {
|
||||
if let Some(path) = file.path() {
|
||||
let path_s = path.to_str().unwrap().to_string();
|
||||
path_label.set_text(path_s.as_str());
|
||||
let path_s = path.to_string_lossy().to_string();
|
||||
path_label.set_text(&path_s);
|
||||
cb(Some(path_s))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,13 @@ use super::{
|
|||
};
|
||||
use crate::{
|
||||
env_var_descriptions::ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH,
|
||||
profile::{LighthouseDriver, Profile, XRServiceType},
|
||||
profile::{LighthouseDriver, OvrCompatibilityModuleType, Profile, XRServiceType},
|
||||
};
|
||||
use adw::prelude::*;
|
||||
use gtk::glib::{self, clone};
|
||||
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
||||
use std::{cell::RefCell, path::PathBuf, rc::Rc};
|
||||
use tracing::warn;
|
||||
|
||||
#[tracker::track]
|
||||
pub struct ProfileEditor {
|
||||
|
@ -129,14 +130,6 @@ impl SimpleComponent for ProfileEditor {
|
|||
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",
|
||||
Some("Warning: disabling dependency checks may result in build failures"),
|
||||
!model.profile.borrow().skip_dependency_check,
|
||||
|
@ -216,31 +209,43 @@ impl SimpleComponent for ProfileEditor {
|
|||
),
|
||||
},
|
||||
add: model.xrservice_cmake_flags_rows.widget(),
|
||||
add: opencompgrp = &adw::PreferencesGroup {
|
||||
set_title: "OpenComposite",
|
||||
set_description: Some("OpenVR driver built on top of OpenXR"),
|
||||
add: ovr_comp_grp = &adw::PreferencesGroup {
|
||||
set_title: "OpenVR Compatibility",
|
||||
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(
|
||||
"OpenComposite Path", None,
|
||||
Some(model.profile.borrow().opencomposite_path.clone().to_string_lossy().to_string()),
|
||||
"OpenVR Module Path", None,
|
||||
Some(model.profile.borrow().ovr_comp.path.clone().to_string_lossy().to_string()),
|
||||
Some(init.root_win.clone()),
|
||||
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(
|
||||
"OpenComposite Repo",
|
||||
model.profile.borrow().opencomposite_repo.clone().unwrap_or_default().as_str(),
|
||||
"OpenVR Compatibility Repo",
|
||||
model.profile.borrow().ovr_comp.repo.clone().unwrap_or_default().as_str(),
|
||||
clone!(#[strong] prof, move |row| {
|
||||
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(
|
||||
"OpenComposite Branch",
|
||||
model.profile.borrow().opencomposite_branch.clone().unwrap_or_default().as_str(),
|
||||
"OpenVR Compatibility Branch",
|
||||
model.profile.borrow().ovr_comp.branch.clone().unwrap_or_default().as_str(),
|
||||
clone!(#[strong] prof, move |row| {
|
||||
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)
|
||||
.build();
|
||||
|
||||
add_btn.connect_clicked(clone!(
|
||||
let on_add = clone!(
|
||||
#[strong]
|
||||
sender,
|
||||
#[weak]
|
||||
name_entry,
|
||||
#[weak]
|
||||
popover,
|
||||
move |_| {
|
||||
move || {
|
||||
let key_gstr = name_entry.text();
|
||||
let key = key_gstr.trim();
|
||||
if !key.is_empty() {
|
||||
|
@ -515,7 +520,13 @@ impl SimpleComponent for ProfileEditor {
|
|||
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
|
||||
}};
|
||||
}
|
||||
|
@ -528,17 +539,30 @@ impl SimpleComponent for ProfileEditor {
|
|||
let profile = Rc::new(RefCell::new(init.profile));
|
||||
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 {
|
||||
profile,
|
||||
win: None,
|
||||
env_rows: AsyncFactoryVecDeque::builder()
|
||||
.launch(
|
||||
adw::PreferencesGroup::builder()
|
||||
.title("Environment Variables")
|
||||
.description(ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH.as_str())
|
||||
.header_suffix(&add_env_var_btn)
|
||||
.build(),
|
||||
)
|
||||
.launch(env_var_prefs_group)
|
||||
.forward(sender.input_sender(), |msg| match msg {
|
||||
EnvVarModelOutMsg::Changed(name, value) => {
|
||||
ProfileEditorMsg::EnvVarChanged(name, value)
|
||||
|
|
|
@ -10,10 +10,10 @@ use relm4::{
|
|||
};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
path::Path,
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
#[tracker::track]
|
||||
pub struct SteamVrCalibrationBox {
|
||||
|
@ -144,55 +144,59 @@ impl SimpleComponent for SteamVrCalibrationBox {
|
|||
}
|
||||
Self::Input::RunCalibration => {
|
||||
self.set_calibration_result(None);
|
||||
let steamvr_bin_dir = get_steamvr_bin_dir_path().to_string_lossy().to_string();
|
||||
if !Path::new(&steamvr_bin_dir).is_dir() {
|
||||
self.set_calibration_success(false);
|
||||
self.set_calibration_result(Some("SteamVR not found".into()));
|
||||
return;
|
||||
}
|
||||
let mut env: HashMap<String, String> = HashMap::new();
|
||||
env.insert("LD_LIBRARY_PATH".into(), steamvr_bin_dir.clone());
|
||||
let vrcmd = format!("{steamvr_bin_dir}/vrcmd");
|
||||
let server_worker = {
|
||||
let mut jobs: VecDeque<WorkerJob> = VecDeque::new();
|
||||
jobs.push_back(WorkerJob::new_cmd(
|
||||
Some(env.clone()),
|
||||
vrcmd.clone(),
|
||||
Some(vec!["--pollposes".into()]),
|
||||
));
|
||||
JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
|
||||
JobWorkerOut::Log(_) => Self::Input::NoOp,
|
||||
JobWorkerOut::Exit(code) => Self::Input::OnServerWorkerExit(code),
|
||||
})
|
||||
};
|
||||
let cal_worker = {
|
||||
let mut jobs: VecDeque<WorkerJob> = VecDeque::new();
|
||||
jobs.push_back(WorkerJob::new_func(Box::new(move || {
|
||||
sleep(Duration::from_secs(2));
|
||||
FuncWorkerOut {
|
||||
success: true,
|
||||
out: vec![],
|
||||
}
|
||||
})));
|
||||
jobs.push_back(WorkerJob::new_cmd(
|
||||
Some(env),
|
||||
vrcmd,
|
||||
Some(vec!["--resetroomsetup".into()]),
|
||||
));
|
||||
JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
|
||||
JobWorkerOut::Log(_) => Self::Input::NoOp,
|
||||
JobWorkerOut::Exit(code) => Self::Input::OnCalWorkerExit(code),
|
||||
})
|
||||
};
|
||||
match get_steamvr_bin_dir_path() {
|
||||
Err(e) => {
|
||||
error!("could not get SteamVR bin dir: {e}");
|
||||
self.set_calibration_success(false);
|
||||
self.set_calibration_result(Some("SteamVR not found".into()));
|
||||
}
|
||||
Ok(bin_dir_p) => {
|
||||
let steamvr_bin_dir = bin_dir_p.to_string_lossy().to_string();
|
||||
let mut env: HashMap<String, String> = HashMap::new();
|
||||
env.insert("LD_LIBRARY_PATH".into(), steamvr_bin_dir.clone());
|
||||
let vrcmd = format!("{steamvr_bin_dir}/vrcmd");
|
||||
let server_worker = {
|
||||
let mut jobs: VecDeque<WorkerJob> = VecDeque::new();
|
||||
jobs.push_back(WorkerJob::new_cmd(
|
||||
Some(env.clone()),
|
||||
vrcmd.clone(),
|
||||
Some(vec!["--pollposes".into()]),
|
||||
));
|
||||
JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
|
||||
JobWorkerOut::Log(_) => Self::Input::NoOp,
|
||||
JobWorkerOut::Exit(code) => Self::Input::OnServerWorkerExit(code),
|
||||
})
|
||||
};
|
||||
let cal_worker = {
|
||||
let mut jobs: VecDeque<WorkerJob> = VecDeque::new();
|
||||
jobs.push_back(WorkerJob::new_func(Box::new(move || {
|
||||
sleep(Duration::from_secs(2));
|
||||
FuncWorkerOut {
|
||||
success: true,
|
||||
out: vec![],
|
||||
}
|
||||
})));
|
||||
jobs.push_back(WorkerJob::new_cmd(
|
||||
Some(env),
|
||||
vrcmd,
|
||||
Some(vec!["--resetroomsetup".into()]),
|
||||
));
|
||||
JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
|
||||
JobWorkerOut::Log(_) => Self::Input::NoOp,
|
||||
JobWorkerOut::Exit(code) => Self::Input::OnCalWorkerExit(code),
|
||||
})
|
||||
};
|
||||
|
||||
server_worker.start();
|
||||
cal_worker.start();
|
||||
self.server_worker = Some(server_worker);
|
||||
self.calibration_worker = Some(cal_worker);
|
||||
server_worker.start();
|
||||
cal_worker.start();
|
||||
self.server_worker = Some(server_worker);
|
||||
self.calibration_worker = Some(cal_worker);
|
||||
}
|
||||
};
|
||||
}
|
||||
Self::Input::OnServerWorkerExit(code) => {
|
||||
if code != 0 {
|
||||
eprintln!("Calibration exited with code {code}");
|
||||
error!("calibration exited with code {code}");
|
||||
}
|
||||
self.calibration_running = false;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use gtk::{gdk, gio, glib::clone, prelude::*};
|
||||
use tracing::{error, warn};
|
||||
|
||||
pub fn limit_dropdown_width(dd: >k::DropDown) {
|
||||
let mut dd_child = dd
|
||||
|
@ -46,14 +47,14 @@ pub fn warning_heading() -> gtk::Box {
|
|||
|
||||
pub fn open_with_default_handler(uri: &str) {
|
||||
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) {
|
||||
match gdk::Display::default() {
|
||||
None => {
|
||||
eprintln!("Warning: could not get default gdk display")
|
||||
warn!("could not get default gdk display")
|
||||
}
|
||||
Some(d) => {
|
||||
d.clipboard().set_text(txt);
|
||||
|
|
|
@ -21,6 +21,7 @@ use crate::{
|
|||
use adw::prelude::*;
|
||||
use gtk::glib::clone;
|
||||
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
||||
use tracing::error;
|
||||
|
||||
#[tracker::track]
|
||||
pub struct WivrnConfEditor {
|
||||
|
@ -255,7 +256,7 @@ impl SimpleComponent for WivrnConfEditor {
|
|||
if let Some(idx) = idx_opt {
|
||||
self.encoder_models.as_mut().unwrap().guard().remove(idx);
|
||||
} 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 relm4::prelude::*;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum StartClientStatus {
|
||||
|
@ -153,14 +154,14 @@ impl AsyncComponent for WivrnWiredStartBox {
|
|||
.into(),
|
||||
))
|
||||
} 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(
|
||||
format!("ADB exited with code \"{}\"", out.exit_code)
|
||||
))
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Error: failed to run ADB: {e}");
|
||||
error!("failed to run ADB: {e}");
|
||||
StartClientStatus::Done(Some(
|
||||
"Failed to run ADB".into()
|
||||
))
|
||||
|
|
|
@ -7,8 +7,10 @@ use nix::{
|
|||
use std::{
|
||||
fs::{self, copy, create_dir_all, remove_dir_all, File, OpenOptions},
|
||||
io::{BufReader, BufWriter},
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::Path,
|
||||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
pub fn get_writer(path: &Path) -> anyhow::Result<BufWriter<std::fs::File>> {
|
||||
if let Some(parent) = path.parent() {
|
||||
|
@ -36,7 +38,7 @@ pub fn get_reader(path: &Path) -> Option<BufReader<File>> {
|
|||
}
|
||||
match File::open(path) {
|
||||
Err(e) => {
|
||||
eprintln!("Error opening {}: {}", path.to_string_lossy(), e);
|
||||
error!("Error opening {}: {}", path.to_string_lossy(), e);
|
||||
None
|
||||
}
|
||||
Ok(fd) => Some(BufReader::new(fd)),
|
||||
|
@ -48,7 +50,7 @@ pub fn deserialize_file<T: serde::de::DeserializeOwned>(path: &Path) -> Option<T
|
|||
None => None,
|
||||
Some(reader) => match serde_json::from_reader(reader) {
|
||||
Err(e) => {
|
||||
eprintln!("Failed to deserialize {}: {}", path.to_string_lossy(), e);
|
||||
error!("Failed to deserialize {}: {}", path.to_string_lossy(), e);
|
||||
None
|
||||
}
|
||||
Ok(res) => Some(res),
|
||||
|
@ -56,16 +58,25 @@ 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() {
|
||||
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(());
|
||||
}
|
||||
let mut perms = fs::metadata(path)
|
||||
.expect("Could not get metadata for file")
|
||||
.permissions();
|
||||
perms.set_readonly(readonly);
|
||||
fs::set_permissions(path, perms)
|
||||
Ok(fs::set_permissions(path, perms)?)
|
||||
}
|
||||
|
||||
pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
|
||||
|
@ -80,16 +91,37 @@ pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
|
|||
]
|
||||
}
|
||||
|
||||
pub async fn setcap_cap_sys_nice_eip(profile: &Profile) {
|
||||
if let Err(e) = async_process("pkexec", Some(&setcap_cap_sys_nice_eip_cmd(profile)), None).await
|
||||
{
|
||||
eprintln!("Error: failed running setcap: {e}");
|
||||
pub async fn verify_cap_sys_nice_eip(profile: &Profile) -> bool {
|
||||
let xrservice_binary = profile.xrservice_binary().to_string_lossy().to_string();
|
||||
match async_process("getcap", 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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 +132,13 @@ pub fn copy_file(source: &Path, dest: &Path) {
|
|||
.unwrap_or_else(|_| panic!("Failed to create dir {}", parent.to_str().unwrap()));
|
||||
}
|
||||
}
|
||||
set_file_readonly(dest, false)
|
||||
.unwrap_or_else(|_| panic!("Failed to set file {} as rw", dest.to_string_lossy()));
|
||||
copy(source, dest).unwrap_or_else(|_| {
|
||||
if !dest.is_symlink() {
|
||||
set_file_readonly(dest, false)
|
||||
.unwrap_or_else(|_| panic!("Failed to set file {} as rw", dest.to_string_lossy()));
|
||||
}
|
||||
copy(source, dest).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"Failed to copy {} to {}",
|
||||
"Failed to copy {} to {}: {e}",
|
||||
source.to_string_lossy(),
|
||||
dest.to_string_lossy()
|
||||
)
|
||||
|
@ -118,6 +152,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)]
|
||||
mod tests {
|
||||
use super::mount_has_nosuid;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod file_utils;
|
||||
pub mod hash;
|
||||
pub mod steam_library_folder;
|
||||
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)]
|
||||
pub struct VulkanInfo {
|
||||
pub has_nvidia_gpu: bool,
|
||||
pub has_monado_vulkan_layers: bool,
|
||||
pub gpu_names: Vec<String>,
|
||||
}
|
||||
|
||||
const NVIDIA_VENDOR_ID: u32 = 0x10de;
|
||||
// const NVIDIA_VENDOR_ID: u32 = 0x10de;
|
||||
|
||||
impl VulkanInfo {
|
||||
/// # Safety
|
||||
|
@ -25,40 +23,19 @@ impl VulkanInfo {
|
|||
None,
|
||||
)
|
||||
}?;
|
||||
let mut has_nvidia_gpu = false;
|
||||
let mut has_monado_vulkan_layers = false;
|
||||
let gpu_names = unsafe { instance.enumerate_physical_devices() }?
|
||||
.into_iter()
|
||||
.filter_map(|d| {
|
||||
let props = unsafe { instance.get_physical_device_properties(d) };
|
||||
if props.vendor_id == NVIDIA_VENDOR_ID {
|
||||
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()
|
||||
.ok()
|
||||
.map(|cs| cs.to_string_lossy().to_string())
|
||||
Some(
|
||||
unsafe { instance.get_physical_device_properties(d) }
|
||||
.device_name_as_c_str()
|
||||
.ok()?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
unsafe { instance.destroy_instance(None) };
|
||||
Ok(Self {
|
||||
gpu_names,
|
||||
has_nvidia_gpu,
|
||||
has_monado_vulkan_layers,
|
||||
})
|
||||
Ok(Self { gpu_names })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,6 @@
|
|||
#[allow(non_snake_case)]
|
||||
mod internal;
|
||||
|
||||
/// timeout for dbus methods in seconds
|
||||
const TIMEOUT: i32 = 10;
|
||||
|
||||
async fn proxy<'a>() -> zbus::Result<internal::ServerProxy<'a>> {
|
||||
let connection = zbus::Connection::session().await?;
|
||||
let proxy = internal::ServerProxy::new(&connection).await?;
|
||||
|
@ -25,7 +22,7 @@ pub async fn is_pairing_mode() -> zbus::Result<bool> {
|
|||
}
|
||||
|
||||
pub async fn enable_pairing() -> zbus::Result<String> {
|
||||
proxy().await?.enable_pairing(TIMEOUT).await
|
||||
proxy().await?.enable_pairing(0).await
|
||||
}
|
||||
|
||||
pub async fn disable_pairing() -> zbus::Result<()> {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use libmonado::{self, BatteryStatus, DeviceRole};
|
||||
use std::{collections::HashMap, fmt::Display, slice::Iter};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum XRDeviceRole {
|
||||
|
@ -280,8 +281,8 @@ impl XRDevice {
|
|||
if let Some(target) = devs.get_mut(&index) {
|
||||
target.roles.push(role.into());
|
||||
} else {
|
||||
eprintln!(
|
||||
"Could not find device index {index} for role {}",
|
||||
error!(
|
||||
"could not find device index {index} for role {}",
|
||||
XRDeviceRole::from(role)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
"xrservice_path": "/home/user/monado",
|
||||
"xrservice_repo": null,
|
||||
"xrservice_branch": null,
|
||||
"opencomposite_path": "/home/user/opencomposite",
|
||||
"opencomposite_repo": null,
|
||||
"opencomposite_branch": null,
|
||||
"ovr_comp": { "mod_type": "Opencomposite", "path": "/home/user/opencomposite", "repo": null, "branch": null },
|
||||
"features": {
|
||||
"libsurvive": {
|
||||
"feature_type": "Libsurvive",
|
||||
|
@ -34,4 +32,4 @@
|
|||
"can_be_built": true,
|
||||
"editable": true,
|
||||
"pull_on_build": true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue