Compare commits

..

77 commits
1.1.1 ... main

Author SHA1 Message Date
Gabriele Musco
139f72e294
chore: update version to 3.1.0
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2025-04-08 15:49:55 +02:00
Gabriele Musco
7a02fcc5d1
fix: disable and blacklist wayvr dashboard plugin 2025-04-08 15:38:20 +02:00
Gabriele Musco
2f5ec57a0a
feat: don't set openvrpaths as read only during profile startup
Some checks are pending
/ cargo-test (push) Waiting to run
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ appimage (push) Waiting to run
2025-04-07 08:12:18 +02:00
Gabriele Musco
8742a27b7c
feat: small design changes to build window ui
Some checks are pending
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
2025-04-07 08:12:08 +02:00
Gabriele Musco
25c90d175f
chore: clippy
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2025-04-06 13:30:30 +02:00
Gabriele Musco
2fc33b10b0
feat: add support for vapor openvr compatibility module 2025-04-06 13:26:48 +02:00
Gabriele Musco
f38199601e
feat: remove monado vulkan layers check for nvidia
fixes #208
2025-04-06 12:33:26 +02:00
hypevhs
db45103d1b fix(monado dependencies): use wayland-protocols-devel on Fedora
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2025-03-13 11:30:35 -05:00
Bones
e117986715 chore: update version to 3.0.1
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2025-03-02 13:51:58 -05:00
Etch9
1ac253ecbf fix: libnotify headers path in wivrn depcheck 2025-03-02 18:28:28 +00:00
Bones
92d17512a4 chore: update version to 3.0.0 2025-03-02 12:50:07 -05:00
Jonathan Steffan
40503d0895 Use serde_yaml
- Switch back to serde_yaml due to concerns #199
2025-03-02 10:13:18 -07:00
Sapphire
96717d193f
fix: onnxruntime build error when latest release has no artifacts 2025-03-01 17:14:04 -06:00
Faith Connors
9c6bfe110a
fix: typo in install wivrn box
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2025-02-23 17:36:42 +01:00
Aleksander
39ace1d8db feat: Add WayVR Dashboard to the plugin list 2025-02-21 17:27:55 +01:00
Gabriele Musco
33db18bd62
feat(wivrn): replace pulse dependency with pipewire
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2025-02-14 21:48:55 +01:00
williamvds
1ed031a2bf
fix: typo in XRT_COMPOSITOR_SCALE_PERCENTAGE
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
XRT_COMPOSITOR_SCALE_PECENTAGE -> XRT_COMPOSITOR_SCALE_PECENTAGE
2025-02-13 22:01:19 +01:00
Gabriele Musco
338e711455
feat: set wivrn launch options in the default profile 2025-02-05 07:59:12 +01:00
Gabriele Musco
3680e305a9
fix: add plugin to config via function instead of signal
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2025-01-26 11:41:43 +01:00
Gabriele Musco
9bdda7d63d
fix: refresh all rows on plugin install (fixes dependencies showing up as not installed) 2025-01-26 11:31:17 +01:00
Gabriele Musco
67e2ade501
fix: always mark plugin executable as executable 2025-01-26 11:22:01 +01:00
Gabriele Musco
35d268e01b
fix: wrap single plugin cmd parts in single quotes 2025-01-26 11:03:31 +01:00
Nova
2bec37ee24 refactor(plugins): point to stardust hosted manifest
Some checks failed
/ appimage (push) Has been cancelled
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
2025-01-20 17:07:28 -08:00
Gabriele Musco
160d733054
feat: support for plugin dependencies and wayvr dashboards (using unreleased api)
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2025-01-19 23:13:05 +01:00
Etch9
eda2105566
fix: use correct wayland-protocols package name for open suse
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2025-01-19 17:24:18 +01:00
Gabriele Musco
18e5670d90
feat: launch options for plugins
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2025-01-12 10:05:51 +01:00
Gabriele Musco
879637115c feat: homepage and author in plugin details
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2025-01-08 07:50:44 +01:00
Gabriele Musco
b24c8e4c0b fix: typo in plugin schema 2025-01-08 07:40:05 +01:00
Gabriele Musco
5187a00971 fix: remove canonicalize from get steamvr bin dir path function 2025-01-08 07:25:02 +01:00
Sapphire
1a71c82d1a
fix: actually return steamvr dir in get_steamvr_base_dir 2025-01-07 21:24:44 -06:00
Gabriele Musco
96e1a20eda feat: write rolling logs to file
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2025-01-07 07:38:11 +01:00
Gabriele Musco
869927bb5c fix: canonicalize some steamvr related paths to hopefully resolve symlinks 2025-01-07 07:06:46 +01:00
Gabriele Musco
e62d0ced36 fix: get ovr compatibility module runtime dir from profile ovr compatibility module struct
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2025-01-06 16:51:26 +01:00
Gabriele Musco
1c3b4decb5 feat: add xrizer as an option for openvr compatibility module
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2025-01-04 19:39:42 +01:00
Gabriele Musco
8ffb44aa11 fix: use exists() to verify existance of socket file
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2025-01-04 10:57:06 +01:00
galister
a651b87cc3 feat: switch wlx manifest
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2025-01-02 23:58:52 +01:00
BabbleBones
cfb874fa35 fix: correct wording of lighthouse calibration 2025-01-02 15:19:43 -05:00
Gabriele Musco
69eba0153b feat: fetch plugins manifests online
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2025-01-02 19:37:21 +01:00
Nova King
aa9bd09372 feat: add telescope to plugin store 2025-01-02 15:25:11 +00:00
Gabriele Musco
6fa7d1e2a3 feat: ask to build profile after editing it
fixes #166
2025-01-02 12:18:32 +01:00
Gabriele Musco
e5a59ebf62 fix: get steamvr bin dir by parsing libraryfolders.vdf
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
fixes #171
2025-01-02 11:42:43 +01:00
Gabriele Musco
eef963793d feat: add cpu to debug info 2025-01-02 11:22:13 +01:00
Bones
4767a4eb13 fix: switch to searching for the xml for deb based distros
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2025-01-01 19:15:53 -05:00
Bones
0adf894b45 fix: Include not shared object wayland-protocols 2025-01-01 17:07:39 -05:00
Bones
31b22b59f3 fix: add libbsd deps for monado 2025-01-01 15:49:23 -05:00
Bones
db5c295435 fix: add wayland drm-lease protocols dep for monado 2025-01-01 14:05:58 -05:00
GabMus
d38acf0a7e feat!: plugin store 2025-01-01 19:02:28 +00:00
BabbleBones
e5435d0aa3 fix: use boost dev packages
Some checks failed
/ appimage (push) Has been cancelled
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
2024-12-30 09:47:38 -05:00
Gabriele Musco
696c541598 fix: debian package name for gstreamer plugins base
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2024-12-29 10:09:02 +01:00
Gabriele Musco
e69a7a9bd6 feat: make env var description selectable
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2024-12-21 11:35:38 +01:00
Gabriele Musco
ca813d6168 feat: press enter on env var entry to add 2024-12-21 11:05:22 +01:00
Gabriele Musco
0020dcf3d4 feat: clearer messaging around setcap failures; getcap after setcap
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2024-12-18 23:19:24 +01:00
Gabriele Musco
36322b3b2c feat: version command line option
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2024-12-18 07:36:22 +01:00
Gabriele Musco
bc5c4a4a40 fix: print active runtime related informative logs as debug 2024-12-18 07:31:53 +01:00
Gabriele Musco
e781736ffa feat: single stage ci with tests, clippy and fmt check all in one
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2024-12-17 07:17:51 +01:00
Gabriele Musco
f04723c1c4 feat: use ubuntu for the ci
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2024-12-14 18:14:55 +01:00
Gabriele Musco
67172df567 chore: update version to 2.0.1
Some checks failed
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
/ cargo-fmtcheck (push) Has been cancelled
2024-12-11 07:47:33 +01:00
Gabriele Musco
b61f2d963f fix: add screenshots to appdata 2024-12-11 07:46:08 +01:00
Gabriele Musco
9711c257a6 chore: update version to 2.0.0
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2024-12-09 18:06:51 +01:00
Gabriele Musco
380f800fa8 chore: format
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2024-12-08 15:18:36 +01:00
Gabriele Musco
46df6d36e5 fix: build profile can be specified manually
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2024-12-08 12:15:00 +01:00
Gabriele Musco
68d7757aa4 feat: add metadata to Cargo.toml; get developers from Cargo.toml authors; rectify SPDX id for license as AGPL-3.0-or-later 2024-12-08 12:02:51 +01:00
Gabriele Musco
ce5f486596 feat: refactor builders cmake vars and env to use inner blocks 2024-12-08 11:44:51 +01:00
Gabriele Musco
4f80aed3c2 feat: disable wivrnctl; refactor cmake vars in wivrn builder 2024-12-08 11:34:10 +01:00
Gabriele Musco
7f05d696c4 fix: update wivrn libmonado path to wirvn/libmonado_wivrn.so 2024-12-07 14:21:27 -08:00
Gabriele Musco
9a4ef01ed9 feat: make left and right qwerty controllers appear as no controller detected
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2024-12-05 07:54:42 +01:00
Gabriele Musco
92cd8f6a94 feat: try to find libmonado and openxr shared objects by reading openxr config 2024-12-05 07:50:26 +01:00
Gabriele Musco
4905c8fed1 fix: create openxr config dir when starting profile 2024-12-05 07:09:00 +01:00
Gabriele Musco
e685cf757d feat!: enable support for different openvr compatibility modules other than opencomposite
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2024-12-04 20:32:36 +01:00
GabMus
4ea0ce53b0 feat: prefer symlinks over generating files for openxr active runtime json file 2024-12-04 19:14:14 +00:00
Gabriele Musco
a9fa4f8cf4 feat: move steam library folders parser to own module; function to find steam openxr json; format
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2024-12-02 18:25:00 +01:00
Gabriele Musco
61f13dbd8f feat: proper logging framework
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2024-12-02 15:22:46 +01:00
Gabriele Musco
c78b844b60 fix: add libnotify-dev dependency for wivrn
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run
2024-12-01 13:01:58 +01:00
Gabriele Musco
592709ab56 fix: openssl dep is an include 2024-12-01 11:02:56 +01:00
Gabriele Musco
448b97469e fix: add openssl-devel dep for wivrn
Some checks are pending
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ cargo-fmtcheck (push) Waiting to run
/ appimage (push) Waiting to run
2024-12-01 08:27:37 +01:00
Gabriele Musco
3f846b26e0
fix: negative logic and early return in start xrservice func
Some checks failed
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-clippy (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2024-11-29 19:40:16 +01:00
Gabriele Musco
2217f84ff4
fix: use let err instead of match in restore xr files func 2024-11-29 19:37:20 +01:00
73 changed files with 3337 additions and 695 deletions

View file

@ -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
View file

@ -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.1"
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"

View file

@ -1,7 +1,16 @@
[package]
name = "envision"
version = "1.1.1"
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"

View file

@ -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

View file

@ -2,22 +2,165 @@
<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&#x27;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&#x2F;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>

BIN
data/screenshots/01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
data/screenshots/02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
data/screenshots/03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
data/screenshots/04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View file

@ -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
View file

@ -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)

View file

@ -1,9 +1,9 @@
project(
'envision',
'rust',
version: '1.1.1', # 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

View file

@ -3,6 +3,7 @@ option(
type: 'combo',
choices: [
'default',
'release',
'development'
],
value: 'default',

View file

@ -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

View file

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

View file

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

View file

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

View file

@ -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 {

View 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
}

View file

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

View 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
}

View file

@ -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;

View file

@ -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> {

View file

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

View file

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

View file

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

View file

@ -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(),

View file

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

View file

@ -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."
),
(

View file

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

View file

@ -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;

View file

@ -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() {

View 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,
}

View file

@ -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('"')

View file

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

View file

@ -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,
)

View file

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

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

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

View file

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

View file

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

View file

@ -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 = &gtk::Button {
add_css_class: "pill",
add_bottom_bar: bottom_bar = &gtk::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);
}
},
// ^^^
},
}
}
}

View file

@ -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

View file

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

View file

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

View file

@ -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(),

View file

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

View file

@ -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,

View file

@ -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;

View file

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

View 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 = &gtk::Button {
set_label: "Cancel",
add_css_class: "destructive-action",
connect_clicked[sender] => move |_| {
sender.input(Self::Input::Close)
},
},
pack_end: add_btn = &gtk::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
View 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
View 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 = &gtk::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 = &gtk::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 = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_vexpand: true,
gtk::Stack {
set_hexpand: true,
set_vexpand: true,
add_child = &gtk::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 = &gtk::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 }
}
}

View 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 = &gtk::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 = &gtk::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 }
}
}

View 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
}
}

View file

@ -156,13 +156,12 @@ pub fn spin_row<F: Fn(&gtk::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 = &gtk::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))
}
}

View file

@ -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)

View file

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

View file

@ -1,4 +1,5 @@
use gtk::{gdk, gio, glib::clone, prelude::*};
use tracing::{error, warn};
pub fn limit_dropdown_width(dd: &gtk::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);

View file

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

View file

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

View file

@ -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;

View file

@ -1,3 +1,4 @@
pub mod file_utils;
pub mod hash;
pub mod steam_library_folder;
pub mod steamvr_utils;

View 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);
}
}

View file

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

View file

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

View file

@ -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
}
}