Compare commits

..

99 commits
0.0.2 ... 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
Gabriele Musco
f1e8a010c8
chore: update version to 1.1.1 2024-11-29 18:16:44 +01:00
Gabriele Musco
d8ca8cf961
fix: remove wivrn pairing mode timer 2024-11-29 18:16:02 +01:00
Gabriele Musco
356b42b056
chore: update version to 1.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
2024-11-28 07:28:29 +01:00
Gabriele Musco
88dea2aa43
chore: in tagging script add cargo lock as well 2024-11-28 07:27:44 +01:00
Gabriele Musco
1bd34b9ad0
fix: profile context menu binding 2024-11-28 07:25:23 +01:00
Gabriele Musco
802337a8f1
fix: profile context menu should prefer opening towards the top 2024-11-28 07:20:58 +01:00
Gabriele Musco
e514e55008
feat: check missing deps command line option; handle non activating opts is a proper method and all opts are part of the struct
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
2024-11-27 21:41:13 +01:00
Gabriele Musco
afbebfc2ec
feat: move dependency collection to profile method 2024-11-27 21:39:37 +01:00
Gabriele Musco
00322492bd
fix: exit on listing profiles 2024-11-27 21:12:28 +01:00
Gabriele Musco
355ad050f0
chore: update version to 1.0.0 2024-11-27 19:48:44 +01:00
GabMus
4638ac1bf4 feat!: wivrn pairing support 2024-11-27 18:47:54 +00:00
GabMus
3e9c4bed80 feat: send desktop notification instead of showing alert when failing to inhibit screen lock 2024-11-27 18:12:27 +00:00
Sapphire
c5460758c5
fix: disable wivrn launch options instructions in debug view
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-23 01:04:07 -06:00
Sapphire
e65c4a8d9d
fix: lower wivrn default foveation to 0.5
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-20 15:54:57 -06:00
Gabriele Musco
af5c57f0f8
feat: split cmake var into name and value when it contains =
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-18 13:39:11 +01:00
Gabriele Musco
7e27614fb8
fix: separate debugbuild option to build in debug mode
Some checks failed
/ cargo-clippy (push) Has been cancelled
/ cargo-fmtcheck (push) Has been cancelled
/ cargo-test (push) Has been cancelled
/ appimage (push) Has been cancelled
2024-11-14 07:05:32 +01:00
Gabriele Musco
09172d6f6c
fix(openhmd): add meson as a dependency
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-04 14:29:20 +01:00
Gabriele Musco
e19df22cce chore: 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-11-03 10:51:51 +01:00
Gabriele Musco
a813015885 fix(dependencies): correct packages for GL/gl.h 2024-11-03 10:51:02 +01:00
Gabriele Musco
0b54808d20 fix(appimage): build for devel releases 2024-11-03 09:27:42 +00:00
Gabriele Musco
3318ad4cc6 chore(ci): conventional commit check 2024-11-03 09:27:42 +00:00
Gabriele Musco
2c642b489c fix(monado dependencies): add libxrandr 2024-11-03 10:23:16 +01:00
83 changed files with 5425 additions and 1361 deletions

View file

@ -1,54 +1,17 @@
image: "debian:unstable"
image: "ubuntu:24.04"
stages:
- check
- deploy
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:
commitcheck:
image: "python"
stage: check
variables:
RUSTFLAGS: "-Dwarnings"
GIT_STRATEGY: clone
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
# 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
appimage:
stage: deploy
@ -59,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:

2125
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,16 @@
[package]
name = "envision"
version = "0.0.2"
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
@ -29,3 +38,9 @@ openxr = { version = "0.19.0", features = ["linked"] }
ash = "0.38.0"
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

@ -11,3 +11,4 @@ Keywords=vr;virtual;reality;monado;
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
Icon=@APP_ID@
StartupNotify=true
X-GNOME-UsesNotifications=true

View file

@ -2,22 +2,220 @@
<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>
<ul>
<li>remove wivrn pairing mode timer</li>
</ul>
</description>
</release>
<release version="1.1.0" date="2024-11-28">
<description>
<p>What's new</p>
<ul>
<li>check missing deps command line option; handle non activating opts is a proper method and all opts are part of the struct</li>
<li>move dependency collection to profile method</li>
</ul>
<p>Fixes</p>
<ul>
<li>profile context menu binding</li>
<li>profile context menu should prefer opening towards the top</li>
<li>exit on listing profiles</li>
</ul>
<p>Other changes</p>
<ul>
<li>in tagging script add cargo lock as well</li>
</ul>
</description>
</release>
<release version="1.0.0" date="2024-11-27">
<description>
<p>Breaking changes</p>
<ul>
<li>wivrn pairing support</li>
</ul>
<p>What's new</p>
<ul>
<li>send desktop notification instead of showing alert when failing to inhibit screen lock</li>
<li>split cmake var into name and value when it contains =</li>
</ul>
<p>Fixes</p>
<ul>
<li>disable wivrn launch options instructions in debug view</li>
<li>lower wivrn default foveation to 0.5</li>
<li>separate debugbuild option to build in debug mode</li>
<li>openhmd: add meson as a dependency</li>
<li>dependencies: correct packages for GL&#x2F;gl.h</li>
<li>appimage: build for devel releases</li>
<li>monado dependencies: add libxrandr</li>
</ul>
<p>Other changes</p>
<ul>
<li>format</li>
<li>ci: conventional commit check</li>
</ul>
</description>
</release>
<release version="0.0.2" date="2024-11-02">
<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,12 +8,13 @@ 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
cp dist/appimage/linuxdeploy-plugin-gtk.sh ./
if [ -f "AppDir/usr/share/icons/hicolod/scalable/apps/org.gabmus.envision.Devel.svg" ]; then
if [ -f "AppDir/usr/share/icons/hicolor/scalable/apps/org.gabmus.envision.Devel.svg" ]; then
APPID="org.gabmus.envision.Devel"
else
APPID="org.gabmus.envision"

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)

37
dist/tagging/check_conventional_commit.py vendored Executable file
View file

@ -0,0 +1,37 @@
#!/usr/bin/env python3
from subprocess import Popen, PIPE
from typing import List
from sys import argv, stderr
import re
CONVENTIONAL_COMMIT_RE = re.compile(r"(feat|fix|chore)(\([\w_\-\/ ]+\))?: .+")
def eprint(*args, **kwargs):
print(*args, file=stderr, **kwargs)
def cmd(args: List[str]) -> List[str]:
proc = Popen(args, stdout=PIPE)
(stdout, _) = proc.communicate()
retcode = proc.returncode
if retcode != 0:
raise ValueError(f"Command {" ".join(args)} failed with code {retcode}")
return stdout.decode().splitlines()
if __name__ == "__main__":
target_branch = argv[1]
if target_branch is None:
eprint(f"Usage: {argv[0]} CURRENT_COMMIT TARGET_BRANCH")
exit(1)
cmd(["git", "fetch", "origin", f"{target_branch}:{target_branch}"])
cmsgs = cmd(["git", "show", "-s", "--format=%s", f"{target_branch}..HEAD"])
success = True
for cmsg in cmsgs:
if CONVENTIONAL_COMMIT_RE.match(cmsg) is None:
eprint(f"Error: commit message '{cmsg}' does not follow the conventional commit standard")
if not success:
exit(1)

View file

@ -109,7 +109,7 @@ if __name__ == "__main__":
print(f"Will commit with the following message: '{commitmsg}'")
if not yes_no():
sys.exit(0)
cmd(["git", "add", "meson.build", "Cargo.toml", METAINFO_PATH])
cmd(["git", "add", "meson.build", "Cargo.toml", "Cargo.lock", METAINFO_PATH])
cmd(["git", "commit", "-m", commitmsg])
print(f"Will add tag '{tag}'")
if not yes_no():

View file

@ -1,9 +1,9 @@
project(
'envision',
'rust',
version: '0.0.2', # 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,8 +3,16 @@ option(
type: 'combo',
choices: [
'default',
'release',
'development'
],
value: 'default',
description: 'The build profile. One of "default" or "development".'
)
option(
'debugbuild',
type: 'boolean',
value: false,
description: 'Build in debug mode, false by 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

@ -1,6 +1,6 @@
use super::{
boost_deps::boost_deps,
common::{dep_cmake, dep_eigen, dep_gpp, dep_libglvnd, dep_ninja, dep_opencv},
common::{dep_cmake, dep_eigen, dep_gpp, dep_libgl, dep_ninja, dep_opencv},
DepType, Dependency, DependencyCheckResult,
};
use crate::linux_distro::LinuxDistro;
@ -11,7 +11,7 @@ fn basalt_deps() -> Vec<Dependency> {
dep_gpp(),
dep_cmake(),
dep_ninja(),
dep_libglvnd(),
dep_libgl(),
Dependency {
name: "lz4-dev".into(),
dep_type: DepType::Include,

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

@ -223,19 +223,20 @@ pub fn dep_libudev() -> Dependency {
}
}
pub fn dep_libglvnd() -> Dependency {
pub fn dep_libgl() -> Dependency {
Dependency {
name: "libglvnd-dev".into(),
dep_type: DepType::Include,
filename: "GL/gl.h".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "libglvnd".into()),
(LinuxDistro::Debian, "libglvnd-dev".into()),
(LinuxDistro::Fedora, "libglvnd-devel".into()),
// WARN: can't find anything exact for alpine, mesa-dev offers
// GL/gl.h hopefully that's the only one needed
// the right debian package would be libgl-dev but the mesa one
// has it as a dependency
(LinuxDistro::Debian, "libgl1-mesa-dev".into()),
// as above, the right package would be libglvnd-devel
(LinuxDistro::Fedora, "mesa-libGL-devel".into()),
(LinuxDistro::Alpine, "mesa-dev".into()),
(LinuxDistro::Suse, "libglvnd-devel".into()),
(LinuxDistro::Suse, "Mesa-libGL-devel".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

@ -1,12 +1,12 @@
use super::{
common::{
dep_cmake, dep_eigen, dep_gcc, dep_git, dep_glslang_validator, dep_gpp, dep_libdrm,
dep_libglvnd, dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr,
dep_vulkan_headers, dep_vulkan_icd_loader,
dep_libgl, dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr, dep_vulkan_headers,
dep_vulkan_icd_loader,
},
DepType, Dependency, DependencyCheckResult,
};
use crate::linux_distro::LinuxDistro;
use crate::{depcheck::common::dep_libxrandr, linux_distro::LinuxDistro};
use std::collections::HashMap;
fn monado_deps() -> Vec<Dependency> {
@ -17,6 +17,7 @@ fn monado_deps() -> Vec<Dependency> {
dep_vulkan_headers(),
dep_libxcb(),
dep_libx11(),
dep_libxrandr(),
Dependency {
name: "wayland".into(),
dep_type: DepType::SharedObject,
@ -29,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(),
@ -73,7 +98,7 @@ fn monado_deps() -> Vec<Dependency> {
(LinuxDistro::Suse, "Mesa-dri-devel".into()),
]),
},
dep_libglvnd(),
dep_libgl(),
]
}

View file

@ -1,10 +1,29 @@
use super::{
common::{dep_cmake, dep_gcc, dep_git, dep_gpp, dep_ninja},
common::{dep_gcc, dep_git, dep_gpp, dep_ninja},
Dependency, DependencyCheckResult,
};
use crate::linux_distro::LinuxDistro;
use std::collections::HashMap;
fn openhmd_deps() -> Vec<Dependency> {
vec![dep_gcc(), dep_gpp(), dep_cmake(), dep_ninja(), dep_git()]
vec![
dep_gcc(),
dep_gpp(),
dep_ninja(),
dep_git(),
Dependency {
name: "meson".into(),
filename: "meson".into(),
dep_type: crate::depcheck::DepType::Executable,
packages: HashMap::from([
(LinuxDistro::Arch, "meson".into()),
(LinuxDistro::Debian, "meson".into()),
(LinuxDistro::Fedora, "meson".into()),
(LinuxDistro::Alpine, "meson".into()),
(LinuxDistro::Suse, "meson".into()),
]),
},
]
}
pub fn check_openhmd_deps() -> Vec<DependencyCheckResult> {

View file

@ -6,7 +6,10 @@ use super::{
},
DepType, Dependency, DependencyCheckResult,
};
use crate::{depcheck::common::dep_libxrandr, linux_distro::LinuxDistro};
use crate::{
depcheck::common::{dep_libgl, dep_libxrandr},
linux_distro::LinuxDistro,
};
use std::collections::HashMap;
fn wivrn_deps() -> Vec<Dependency> {
@ -23,6 +26,7 @@ fn wivrn_deps() -> Vec<Dependency> {
dep_libxcb(),
dep_libx11(),
dep_libxrandr(),
dep_libgl(),
Dependency {
name: "patch".into(),
dep_type: DepType::Executable,
@ -74,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(),
@ -165,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()),
@ -231,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

@ -153,7 +153,7 @@ pub struct WivrnConfig {
impl Default for WivrnConfig {
fn default() -> Self {
Self {
scale: Some([0.8, 0.8]),
scale: Some([0.5, 0.5]),
bitrate: Some(50000000),
encoders: vec![],
application: None,

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;
@ -41,26 +47,20 @@ pub mod termcolor;
pub mod ui;
pub mod util;
pub mod vulkaninfo;
pub mod wivrn_dbus;
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();
@ -72,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");
@ -109,11 +127,13 @@ fn main() -> Result<()> {
CmdLineOpts::init(&main_app);
let sender = BROKER.sender();
main_app.connect_command_line(move |this, cmdline| {
if CmdLineOpts::handle_non_activating_opts(cmdline) {
return 0;
let opts = CmdLineOpts::from_cmdline(cmdline);
if let Some(exit_code) = opts.handle_non_activating_opts() {
this.quit();
return exit_code;
}
this.activate();
sender.emit(Msg::HandleCommandLine(CmdLineOpts::from_cmdline(cmdline)));
sender.emit(Msg::HandleCommandLine(opts));
0
});
let app = RelmApp::from_app(main_app.clone()).with_broker(&BROKER);

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',
@ -14,13 +14,13 @@ run_command(
cargo_options = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ]
cargo_options += [ '--target-dir', meson.project_build_root() / 'src' ]
if get_option('profile') == 'default'
if get_option('debugbuild')
rust_target = 'debug'
message('Building in debug mode')
else
cargo_options += [ '--release' ]
rust_target = 'release'
message('Building in release mode')
else
rust_target = 'debug'
message('Building in debug mode')
endif
cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ]
@ -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

@ -1,6 +1,12 @@
use crate::{
depcheck::{
basalt_deps::get_missing_basalt_deps, libsurvive_deps::get_missing_libsurvive_deps,
mercury_deps::get_missing_mercury_deps, monado_deps::get_missing_monado_deps,
openhmd_deps::get_missing_openhmd_deps, wivrn_deps::get_missing_wivrn_deps, Dependency,
},
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;
@ -12,6 +18,7 @@ use std::{
io::BufReader,
path::{Path, PathBuf},
slice::Iter,
str::FromStr,
};
use uuid::Uuid;
@ -38,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",
}
}
@ -246,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,
@ -256,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
@ -271,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,
}
@ -283,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);
@ -318,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\"",
@ -353,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!(
@ -412,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(),
@ -445,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(),
@ -453,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(
@ -539,21 +662,69 @@ 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> {
let mut missing_deps = Vec::new();
if self.can_be_built {
missing_deps.extend(match self.xrservice_type {
XRServiceType::Monado => get_missing_monado_deps(),
XRServiceType::Wivrn => get_missing_wivrn_deps(),
});
if self.features.libsurvive.enabled {
missing_deps.extend(get_missing_libsurvive_deps());
}
if self.features.openhmd.enabled {
missing_deps.extend(get_missing_openhmd_deps());
}
if self.features.basalt.enabled {
missing_deps.extend(get_missing_basalt_deps());
}
if self.features.mercury_enabled {
missing_deps.extend(get_missing_mercury_deps());
}
// no listed deps for opencomp
}
missing_deps.sort_unstable();
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())
}
}
@ -568,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;
@ -578,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"));
@ -609,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

@ -1,4 +1,6 @@
use crate::constants::APP_NAME;
use gtk::prelude::GtkApplicationExt;
use notify_rust::Notification;
use relm4::{adw::prelude::*, prelude::*};
fn alert_base(title: &str, msg: Option<&str>) -> adw::AlertDialog {
@ -36,3 +38,12 @@ pub fn alert_w_widget(
}
present_alert(d, parent);
}
pub fn notification(title: &str, msg: &str) -> Notification {
Notification::new()
.summary(title)
.body(msg)
.icon("org.gabmus.envision-symbolic")
.appname(APP_NAME)
.finalize()
}

View file

@ -1,6 +1,6 @@
use super::{
about_dialog::{create_about_dialog, populate_debug_info},
alert::{alert, alert_w_widget},
alert::{alert, alert_w_widget, notification},
build_window::{BuildStatus, BuildWindow, BuildWindowInit, BuildWindowMsg, BuildWindowOutMsg},
cmdline_opts::CmdLineOpts,
debug_view::{DebugView, DebugViewInit, DebugViewMsg, DebugViewOutMsg},
@ -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,19 +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::{
basalt_deps::get_missing_basalt_deps, common::dep_pkexec,
libsurvive_deps::get_missing_libsurvive_deps, mercury_deps::get_missing_mercury_deps,
monado_deps::get_missing_monado_deps, openhmd_deps::get_missing_openhmd_deps,
wivrn_deps::get_missing_wivrn_deps,
},
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,
@ -40,30 +38,39 @@ 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,
};
use adw::{prelude::*, ResponseAppearance};
use gtk::glib::{self, clone};
use notify_rust::NotificationHandle;
use relm4::{
actions::{AccelsPlus, ActionGroupName, RelmAction, RelmActionGroup},
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,
app_win: adw::ApplicationWindow,
inhibit_id: Option<u32>,
main_view: Controller<MainView>,
main_view: AsyncController<MainView>,
debug_view: Controller<DebugView>,
split_view: Option<adw::NavigationSplitView>,
about_dialog: adw::AboutDialog,
@ -73,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>,
@ -86,13 +93,16 @@ pub struct App {
openxr_prober_worker: Option<JobWorker>,
xrservice_ready: bool,
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 +126,9 @@ pub enum Msg {
HandleCommandLine(CmdLineOpts),
StartProber,
OnProberExit(bool),
WivrnCheckPairMode,
OpenPluginStore,
UpdateConfigPlugins(HashMap<String, PluginConfig>),
NoOp,
}
@ -135,11 +148,22 @@ impl App {
Some("XR session running"),
);
if inhibit_id == 0 {
alert(
"Failed to inhibit desktop locking",
Some(&format!("{APP_NAME} tries to inhibit desktop locking to avoid automatic suspension or screen locking kicking in while the XR session is active, but this process failed.\n\nThe session is still running but you might want to manually disable automatic suspension and screen locking.")),
Some(&self.app_win.clone().upcast())
);
self.inhibit_fail_notif = match if let Some(notif) =
self.inhibit_fail_notif.as_ref()
{
notif.show()
} else {
notification(
"Failed to inhibit desktop locking",
&format!("{APP_NAME} tries to inhibit desktop locking to avoid automatic suspension or screen locking kicking in while the XR session is active, but this process failed.\n\nThe session is still running but you might want to manually disable automatic suspension and screen locking."),
).show()
} {
Ok(n) => Some(n),
Err(e) => {
error!("failed to send desktop notification: {e:?}");
None
}
}
} else {
self.inhibit_id = Some(inhibit_id);
}
@ -152,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!(
@ -211,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>()),
);
@ -251,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![];
}
}
@ -337,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()
@ -344,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!(
@ -358,14 +427,14 @@ 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 should_poll_for_devices = self.xrservice_ready
&& self
.xrservice_worker
.as_ref()
.is_some_and(JobWorker::is_alive);
let xrservice_worker_is_alive = self
.xrservice_worker
.as_ref()
.is_some_and(JobWorker::is_alive);
let should_poll_for_devices = self.xrservice_ready && xrservice_worker_is_alive;
if should_poll_for_devices {
if let Some(monado) = self.libmonado.as_ref() {
self.xr_devices = XRDevice::from_libmonado(monado);
@ -379,6 +448,32 @@ impl AsyncComponent for App {
}
}
}
if xrservice_worker_is_alive
&& self.get_selected_profile().xrservice_type == XRServiceType::Wivrn
{
// is in pairing mode?
sender.input(Msg::WivrnCheckPairMode);
}
}
Msg::WivrnCheckPairMode => {
if self.get_selected_profile().xrservice_type == XRServiceType::Wivrn {
match wivrn_dbus::is_pairing_mode().await {
Ok(state) => {
self.main_view
.sender()
.emit(MainViewMsg::SetWivrnPairingMode(state));
self.main_view
.sender()
.emit(MainViewMsg::SetWivrnSupportsPairing(true));
}
Err(e) => {
error!("failed to get wivrn pairing mode: {e:?}");
self.main_view
.sender()
.emit(MainViewMsg::SetWivrnSupportsPairing(false));
}
};
}
}
Msg::EnableDebugViewChanged(val) => {
self.config.debug_view_enabled = val;
@ -416,41 +511,40 @@ impl AsyncComponent for App {
},
Msg::BuildProfile(clean_build) => {
let profile = self.get_selected_profile();
let mut missing_deps = vec![];
let mut jobs = VecDeque::<WorkerJob>::new();
// profile per se can't be built, but we still need opencomp
if profile.can_be_built {
missing_deps.extend(match profile.xrservice_type {
XRServiceType::Monado => get_missing_monado_deps(),
XRServiceType::Wivrn => get_missing_wivrn_deps(),
});
if profile.features.libsurvive.enabled {
missing_deps.extend(get_missing_libsurvive_deps());
jobs.extend(get_build_libsurvive_jobs(&profile, clean_build));
}
if profile.features.openhmd.enabled {
missing_deps.extend(get_missing_openhmd_deps());
jobs.extend(get_build_openhmd_jobs(&profile, clean_build));
}
if profile.features.basalt.enabled {
missing_deps.extend(get_missing_basalt_deps());
jobs.extend(get_build_basalt_jobs(&profile, clean_build));
}
if profile.features.mercury_enabled {
missing_deps.extend(get_missing_mercury_deps());
jobs.extend(get_build_mercury_jobs(&profile));
}
jobs.extend(match profile.xrservice_type {
XRServiceType::Monado => get_build_monado_jobs(&profile, clean_build),
XRServiceType::Wivrn => get_build_wivrn_jobs(&profile, clean_build),
});
// no listed deps for opencomp
}
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())
{
missing_deps.sort_unstable();
missing_deps.dedup(); // dedup only works if sorted, hence the above
let distro = LinuxDistro::get();
let (missing_package_list, install_missing_widget): (
String,
@ -604,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) => {
@ -730,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();
}
}
}
@ -831,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 = {
@ -852,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
}
}
@ -867,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,
@ -876,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()
@ -902,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,
@ -912,6 +1051,8 @@ impl AsyncComponent for App {
configure_wivrn_action,
openxr_prober_worker: None,
xrservice_ready: false,
inhibit_fail_notif: None,
pluginstore: None,
};
let widgets = view_output!();
@ -984,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,21 +9,35 @@ 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>,
pub skip_depcheck: bool,
pub check_dependencies_for: Option<String>,
}
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');
const OPT_SKIP_DEPCHECK: (&'static str, char) = ("skip-dependency-check", 'd');
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(),
@ -53,31 +70,62 @@ impl CmdLineOpts {
"Skip dependency checks when building profiles",
None,
);
app.add_main_option(
Self::OPT_CHECK_DEPS_FOR.0,
glib::Char::try_from(Self::OPT_CHECK_DEPS_FOR.1).unwrap(),
glib::OptionFlags::IN_MAIN,
glib::OptionArg::String,
"Prints missing dependencies for given profile id; returns nothing if all dependencies are satisfied",
None,
);
}
/// returns true if the application should quit
pub fn handle_non_activating_opts(cmdline: &ApplicationCommandLine) -> bool {
if cmdline.options_dict().contains(Self::OPT_LIST_PROFILES.0) {
/// 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();
profiles.iter().for_each(|p| {
println!("{}: \"{}\"", p.uuid, p.name);
});
return true;
return Some(0);
}
false
if let Some(prof_id) = self.check_dependencies_for.as_ref() {
let profiles = Config::get_config().profiles();
if let Some(prof) = profiles.iter().find(|p| &p.uuid == prof_id) {
let deps = prof.missing_dependencies();
if deps.is_empty() {
return Some(0);
}
for dep in deps {
println!("{}", dep.name);
}
return Some(1);
} else {
error!("No profile found for uuid: `{prof_id}`");
return Some(404);
}
}
None
}
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),
profile_uuid: match opts.lookup::<String>(Self::OPT_PROFILE.0) {
Err(_) => None,
Ok(None) => None,
Ok(Some(variant)) => Some(variant),
},
list_profiles: opts.contains(Self::OPT_LIST_PROFILES.0),
profile_uuid: opts
.lookup::<String>(Self::OPT_PROFILE.0)
.unwrap_or_default(),
skip_depcheck: opts.contains(Self::OPT_SKIP_DEPCHECK.0),
check_dependencies_for: opts
.lookup::<String>(Self::OPT_CHECK_DEPS_FOR.0)
.unwrap_or_default(),
}
}
}

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(
@ -175,7 +175,7 @@ impl InternalJobWorker {
}
}
}
let mut launch_opts = prof.xrservice_launch_options.trim();
let mut launch_opts = prof.xrservice_launch_options.trim().to_string();
let debug_launch_opts = if debug {
if launch_opts.contains(LAUNCH_OPTS_CMD_PLACEHOLDER) {
format!("{} {}", "gdbserver localhost:9000", launch_opts)
@ -189,7 +189,7 @@ impl InternalJobWorker {
String::default()
};
launch_opts = if debug {
debug_launch_opts.as_str()
debug_launch_opts
} else {
launch_opts
};

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,7 @@ use crate::{
file_utils::{get_writer, mount_has_nosuid},
steamvr_utils::chaperone_info_exists,
},
vulkaninfo::VulkanInfo,
wivrn_dbus,
xr_devices::XRDevice,
};
use adw::{prelude::*, ResponseAppearance};
@ -33,9 +34,9 @@ use relm4::{
actions::{ActionGroupName, RelmAction, RelmActionGroup},
new_action_group, new_stateless_action,
prelude::*,
ComponentParts, ComponentSender, SimpleComponent,
};
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,9 @@ 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,
}
#[derive(Debug)]
@ -96,6 +100,11 @@ pub enum MainViewMsg {
ExportProfile,
ImportProfile,
OpenProfileEditor(Profile),
SetWivrnSupportsPairing(bool),
SetWivrnPairingMode(bool),
StopWivrnPairingMode,
StartWivrnPairingMode,
QueryProfileRebuild,
}
#[derive(Debug)]
@ -106,17 +115,18 @@ 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 {
fn create_profile_editor(&mut self, sender: ComponentSender<MainView>, prof: Profile) {
fn create_profile_editor(&mut self, sender: AsyncComponentSender<Self>, prof: Profile) {
self.profile_editor = Some(
ProfileEditor::builder()
.launch(ProfileEditorInit {
@ -130,15 +140,17 @@ impl MainView {
}
}
#[relm4::component(pub)]
impl SimpleComponent for MainView {
#[relm4::component(pub async)]
impl AsyncComponent for MainView {
type Init = MainViewInit;
type Input = MainViewMsg;
type Output = MainViewOutMsg;
type CommandOutput = ();
menu! {
app_menu: {
section! {
"Plugin_s" => PluginStoreAction,
// value inside action is ignored
"_Debug View" => DebugViewToggleAction,
"_Build Profile" => BuildProfileAction,
@ -240,28 +252,136 @@ impl SimpleComponent for MainView {
set_visible: model.xrservice_active,
add_css_class: "card",
gtk::Label {
#[track = "model.changed(Self::xrservice_active()) || model.changed(Self::xrservice_ready())"]
set_label: if model.xrservice_ready {
"Service ready, you can launch XR apps"
} else {
#[track = "model.changed(Self::xrservice_active()) || model.changed(Self::xrservice_ready()) || model.changed(Self::wivrn_pairing_mode())"]
set_label: {
match model.selected_profile.xrservice_type {
XRServiceType::Monado =>
"Starting…",
if model.xrservice_ready {
"Service ready, you can launch XR apps"
} else {
"Starting…"
}
XRServiceType::Wivrn =>
"Starting, please connect your client device…",
if model.wivrn_pairing_mode {
"Pairing mode"
} else {
"Starting, connect your client device…"
}
}
},
set_margin_all: 18,
add_css_class: "heading",
add_css_class: "success",
add_css_class: "warning",
#[track = "model.changed(Self::xrservice_active()) || model.changed(Self::xrservice_ready())"]
set_class_active: ("warning", !model.xrservice_ready),
#[track = "model.changed(Self::xrservice_active()) || model.changed(Self::xrservice_ready()) || model.changed(Self::wivrn_pairing_mode())"]
set_class_active: (
"success",
model.xrservice_ready
&& (
model.selected_profile.xrservice_type != XRServiceType::Wivrn
|| !model.wivrn_pairing_mode
)
),
set_wrap: true,
set_justify: gtk::Justification::Center,
},
},
model.devices_box.widget(),
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_vexpand: false,
set_spacing: 12,
add_css_class: "card",
add_css_class: "padded",
#[track = "model.changed(Self::wivrn_supports_pairing()) || model.changed(Self::xrservice_active()) || model.changed(Self::selected_profile()) || model.changed(Self::wivrn_pairing_mode()) || model.changed(Self::wivrn_pin())"]
set_visible: model.wivrn_supports_pairing
&& model.xrservice_active
&& model.selected_profile.xrservice_type == XRServiceType::Wivrn
&& !model.wivrn_pairing_mode,
gtk::Label {
add_css_class: "heading",
set_hexpand: true,
set_xalign: 0.0,
set_label: "Pairing mode",
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
},
gtk::Label {
add_css_class: "dim-label",
set_hexpand: true,
set_label: concat!(
"To connect a new device to WiVRn, you ",
"will need to pair it first.\n\n",
"You can do so by starting the pairing mode ",
"with the button below."
),
set_xalign: 0.0,
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
},
gtk::Button {
add_css_class: "suggested-action",
set_label: "Start pairing mode",
set_halign: gtk::Align::Start,
connect_clicked[sender] => move |_| {
sender.input(Self::Input::StartWivrnPairingMode);
}
},
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_vexpand: false,
set_spacing: 12,
add_css_class: "card",
add_css_class: "padded",
#[track = "model.changed(Self::wivrn_supports_pairing()) || model.changed(Self::xrservice_active()) || model.changed(Self::selected_profile()) || model.changed(Self::wivrn_pairing_mode()) || model.changed(Self::wivrn_pin())"]
set_visible: model.wivrn_supports_pairing
&& model.xrservice_active
&& model.selected_profile.xrservice_type == XRServiceType::Wivrn
&& model.wivrn_pairing_mode && model.wivrn_pin.is_some(),
gtk::Label {
add_css_class: "heading",
set_hexpand: true,
set_xalign: 0.0,
set_label: "Pairing mode",
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
},
gtk::Label {
add_css_class: "dim-label",
set_hexpand: true,
set_label: concat!(
"WiVRn is in pairing mode. Pair your client ",
"device with the following PIN:"
),
set_xalign: 0.0,
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
},
gtk::Label {
add_css_class: "title-2",
add_css_class: "monospace",
set_hexpand: true,
set_selectable: true,
#[track = "model.changed(Self::wivrn_pin())"]
set_label: model.wivrn_pin
.as_deref().unwrap_or(""),
set_xalign: 0.5,
set_justify: gtk::Justification::Center,
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
},
gtk::Button {
add_css_class: "destructive-action",
set_label: "Stop pairing mode",
set_halign: gtk::Align::Start,
connect_clicked[sender] => move |_| {
sender.input(Self::Input::StopWivrnPairingMode);
}
},
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
@ -273,8 +393,8 @@ impl SimpleComponent 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
},
@ -329,35 +449,7 @@ impl SimpleComponent 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,
@ -480,17 +572,55 @@ impl SimpleComponent for MainView {
set_icon_name: "view-more-symbolic",
set_tooltip_text: Some("Menu"),
set_menu_model: Some(&profile_actions_menu),
set_direction: gtk::ArrowType::Up,
},
},
}
}
}
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
async fn update(
&mut self,
message: Self::Input,
sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
self.reset();
match message {
Self::Input::ClockTicking => {}
Self::Input::SetWivrnSupportsPairing(supported) => {
if self.wivrn_supports_pairing != supported {
self.set_wivrn_supports_pairing(supported)
}
}
Self::Input::SetWivrnPairingMode(enabled) => {
if self.wivrn_pairing_mode != enabled {
self.set_wivrn_pairing_mode(enabled);
if enabled {
match wivrn_dbus::pairing_pin().await {
Ok(pin) => {
self.set_wivrn_pin(Some(pin));
}
Err(e) => {
error!("failed to get wivrn pairing pin: {e}");
}
};
} else {
self.set_wivrn_pin(None);
}
}
}
Self::Input::StopWivrnPairingMode => {
if let Err(e) = wivrn_dbus::disable_pairing().await {
error!("failed to stop wivrn pairing mode: {e}");
}
}
Self::Input::StartWivrnPairingMode => {
if let Err(e) = wivrn_dbus::enable_pairing().await {
error!("failed to start wivrn pairing mode: {e}");
}
}
Self::Input::StartStopClicked => {
sender
.output(Self::Output::DoStartStopXRService)
@ -504,6 +634,7 @@ impl SimpleComponent for MainView {
Self::Input::XRServiceActiveChanged(active, profile, show_launch_opts) => {
if !active {
self.set_xrservice_ready(false);
sender.input(Self::Input::SetWivrnPairingMode(false));
}
self.set_xrservice_active(active);
self.steamvr_calibration_box
@ -565,6 +696,10 @@ impl SimpleComponent for MainView {
}
}));
}
Self::Input::QueryProfileRebuild => {
self.query_profile_rebuild_dialog
.present(Some(&self.root_win));
}
Self::Input::SetSelectedProfile(index) => {
self.profiles_dropdown
.as_ref()
@ -602,7 +737,7 @@ impl SimpleComponent 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 {
@ -742,11 +877,11 @@ impl SimpleComponent for MainView {
}
}
fn init(
async fn init(
init: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let profile_not_editable_dialog = adw::AlertDialog::builder()
.heading("This profile is not editable")
.body(concat!(
@ -771,6 +906,29 @@ impl SimpleComponent 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();
@ -836,42 +994,6 @@ impl SimpleComponent for MainView {
ret
};
let mut model = Self {
xrservice_active: false,
enable_debug_view: init.config.debug_view_enabled,
profiles_dropdown: None,
profiles: vec![],
steam_launch_options_box: SteamLaunchOptionsBox::builder().launch(()).detach(),
install_wivrn_box: InstallWivrnBox::builder()
.launch(InstallWivrnBoxInit {
selected_profile: init.selected_profile.clone(),
root_win: init.root_win.clone(),
})
.detach(),
wivrn_wired_start_box: WivrnWiredStartBox::builder()
.launch(WivrnWiredStartBoxInit {
selected_profile: init.selected_profile.clone(),
root_win: init.root_win.clone(),
})
.detach(),
devices_box: DevicesBox::builder().launch(()).detach(),
selected_profile: init.selected_profile.clone(),
profile_not_editable_dialog,
profile_delete_confirm_dialog,
root_win: init.root_win.clone(),
steamvr_calibration_box,
openhmd_calibration_box,
profile_editor: None,
xrservice_ready: false,
profile_delete_action,
profile_export_action,
vkinfo: init.vkinfo,
tracker: 0,
};
let widgets = view_output!();
model.profiles_dropdown = Some(widgets.profiles_dropdown.clone());
stateless_action!(
actions,
ProfileMenuNewAction,
@ -919,7 +1041,46 @@ impl SimpleComponent for MainView {
root.insert_action_group(ProfileActionGroup::NAME, Some(&actions.into_action_group()));
ComponentParts { model, widgets }
let mut model = Self {
xrservice_active: false,
enable_debug_view: init.config.debug_view_enabled,
profiles_dropdown: None,
profiles: vec![],
steam_launch_options_box: SteamLaunchOptionsBox::builder().launch(()).detach(),
install_wivrn_box: InstallWivrnBox::builder()
.launch(InstallWivrnBoxInit {
selected_profile: init.selected_profile.clone(),
root_win: init.root_win.clone(),
})
.detach(),
wivrn_wired_start_box: WivrnWiredStartBox::builder()
.launch(WivrnWiredStartBoxInit {
selected_profile: init.selected_profile.clone(),
root_win: init.root_win.clone(),
})
.detach(),
devices_box: DevicesBox::builder().launch(()).detach(),
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,
profile_editor: None,
xrservice_ready: false,
profile_delete_action,
profile_export_action,
wivrn_pairing_mode: false,
wivrn_supports_pairing: false,
wivrn_pin: None,
tracker: 0,
};
let widgets = view_output!();
model.profiles_dropdown = Some(widgets.profiles_dropdown.clone());
AsyncComponentParts { model, widgets }
}
}

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 {
@ -48,6 +49,21 @@ pub struct ProfileEditorInit {
pub profile: Profile,
}
/// This parses a var (either env var or cmake var) and if it contains
/// a '=' char, it assumes it's key and value, otherwise it's just going to
/// be the key and the value is gonna be an empty string
fn parse_var(input: &str) -> (String, String) {
if input.contains('=') {
let mut sp = input.split('=');
(
sp.next().unwrap().to_string(),
sp.next().unwrap().to_string(),
)
} else {
(input.to_string(), "".to_string())
}
}
#[relm4::component(pub)]
impl SimpleComponent for ProfileEditor {
type Init = ProfileEditorInit;
@ -114,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,
@ -201,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);
})
),
},
@ -428,33 +448,23 @@ impl SimpleComponent for ProfileEditor {
}
Self::Input::AddEnvVar(var) => {
let mut prof = self.profile.borrow_mut();
if !prof.environment.contains_key(&var) {
let (name, value) = if var.contains('=') {
let mut sp = var.split('=');
(
sp.next().unwrap().to_string(),
sp.next().unwrap().to_string(),
)
} else {
(var, "".to_string())
};
let (name, value) = parse_var(&var);
if !prof.environment.contains_key(&name) {
prof.environment.insert(name.clone(), value.clone());
self.env_rows
.guard()
.push_back(EnvVarModelInit { name, value });
}
}
Self::Input::AddXrServiceCmakeFlag(name) => {
Self::Input::AddXrServiceCmakeFlag(var) => {
let mut prof = self.profile.borrow_mut();
let (name, value) = parse_var(&var);
if !prof.xrservice_cmake_flags.contains_key(&name) {
prof.xrservice_cmake_flags
.insert(name.clone(), "".to_string());
.insert(name.clone(), value.clone());
self.xrservice_cmake_flags_rows
.guard()
.push_back(EnvVarModelInit {
name,
value: "".to_string(),
});
.push_back(EnvVarModelInit { name, value });
}
}
}
@ -494,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() {
@ -510,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
}};
}
@ -523,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 })
}
}

122
src/wivrn_dbus/internal.rs Normal file
View file

@ -0,0 +1,122 @@
//! # D-Bus interface proxy for: `io.github.wivrn.Server`
//!
//! This code was generated by `zbus-xmlgen` `5.0.1` from D-Bus introspection data.
//! Source: `io.github.wivrn.Server.xml`.
//!
//! You may prefer to adapt it, instead of using it verbatim.
//!
//! More information can be found in the [Writing a client proxy] section of the zbus
//! documentation.
//!
//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
//! following zbus API can be used:
//!
//! * [`zbus::fdo::PropertiesProxy`]
//!
//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
//!
//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
use zbus::proxy;
#[proxy(
interface = "io.github.wivrn.Server",
default_path = "/io/github/wivrn/Server",
default_service = "io.github.wivrn.Server"
)]
pub trait Server {
/// DisablePairing method
fn disable_pairing(&self) -> zbus::Result<()>;
/// Disconnect method
fn disconnect(&self) -> zbus::Result<()>;
/// EnablePairing method
fn enable_pairing(&self, TimeoutSecs: i32) -> zbus::Result<String>;
/// Quit method
fn quit(&self) -> zbus::Result<()>;
/// RenameKey method
fn rename_key(&self, PublicKey: &str, Name: &str) -> zbus::Result<()>;
/// RevokeKey method
fn revoke_key(&self, PublicKey: &str) -> zbus::Result<()>;
/// AvailableRefreshRates property
#[zbus(property)]
fn available_refresh_rates(&self) -> zbus::Result<Vec<f64>>;
/// EncryptionEnabled property
#[zbus(property)]
fn encryption_enabled(&self) -> zbus::Result<bool>;
/// EyeGaze property
#[zbus(property)]
fn eye_gaze(&self) -> zbus::Result<bool>;
/// FaceTracking property
#[zbus(property)]
fn face_tracking(&self) -> zbus::Result<bool>;
/// FieldOfView property
#[zbus(property)]
fn field_of_view(&self) -> zbus::Result<Vec<(f64, f64, f64, f64)>>;
/// HandTracking property
#[zbus(property)]
fn hand_tracking(&self) -> zbus::Result<bool>;
/// HeadsetConnected property
#[zbus(property)]
fn headset_connected(&self) -> zbus::Result<bool>;
/// JsonConfiguration property
#[zbus(property)]
fn json_configuration(&self) -> zbus::Result<String>;
#[zbus(property)]
fn set_json_configuration(&self, value: &str) -> zbus::Result<()>;
/// KnownKeys property
#[zbus(property)]
fn known_keys(&self) -> zbus::Result<Vec<(String, String)>>;
/// MicChannels property
#[zbus(property)]
fn mic_channels(&self) -> zbus::Result<u32>;
/// MicSampleRate property
#[zbus(property)]
fn mic_sample_rate(&self) -> zbus::Result<u32>;
/// PairingEnabled property
#[zbus(property)]
fn pairing_enabled(&self) -> zbus::Result<bool>;
/// Pin property
#[zbus(property)]
fn pin(&self) -> zbus::Result<String>;
/// PreferredRefreshRate property
#[zbus(property)]
fn preferred_refresh_rate(&self) -> zbus::Result<f64>;
/// RecommendedEyeSize property
#[zbus(property)]
fn recommended_eye_size(&self) -> zbus::Result<(u32, u32)>;
/// SpeakerChannels property
#[zbus(property)]
fn speaker_channels(&self) -> zbus::Result<u32>;
/// SpeakerSampleRate property
#[zbus(property)]
fn speaker_sample_rate(&self) -> zbus::Result<u32>;
/// SteamCommand property
#[zbus(property)]
fn steam_command(&self) -> zbus::Result<String>;
/// SupportedCodecs property
#[zbus(property)]
fn supported_codecs(&self) -> zbus::Result<Vec<String>>;
}

34
src/wivrn_dbus/mod.rs Normal file
View file

@ -0,0 +1,34 @@
// how to regenerate this one:
//
// ```bash
// cargo install zbus_xmlgen
// curl -sSLO https://github.com/WiVRn/WiVRn/blob/master/dbus/io.github.wivrn.Server.xml
// zbus-xmlgen file io.github.wivrn.Server.xml
// ```
//
// it should output a file called server.rs, move it accordingly
#[rustfmt::skip]
#[allow(non_snake_case)]
mod internal;
async fn proxy<'a>() -> zbus::Result<internal::ServerProxy<'a>> {
let connection = zbus::Connection::session().await?;
let proxy = internal::ServerProxy::new(&connection).await?;
Ok(proxy)
}
pub async fn is_pairing_mode() -> zbus::Result<bool> {
proxy().await?.pairing_enabled().await
}
pub async fn enable_pairing() -> zbus::Result<String> {
proxy().await?.enable_pairing(0).await
}
pub async fn disable_pairing() -> zbus::Result<()> {
proxy().await?.disable_pairing().await
}
pub async fn pairing_pin() -> zbus::Result<String> {
proxy().await?.pin().await
}

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