Compare commits

..

110 commits

Author SHA1 Message Date
Gabriele Musco
0a46a7d332 chore: fix unit test
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-07-22 07:56:26 +02:00
Gabriele Musco
ec4d3d2f57 fix: rename WivrnConfig tcp_only to tcp-only 2025-07-22 07:40:59 +02:00
Gabriele Musco
1ad8a29df1 feat: inject env vars in soldier runtime 2025-07-22 07:25:18 +02:00
Gabriele Musco
8311adc3dd feat: allow specifying arguments for custom 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-07-18 07:46:34 +02:00
Gabriele Musco
bdb19b5738 fix: rely on system packages for onnxruntime 2025-07-18 07:33:10 +02:00
Sapphire
b0f0f4647c fix: ensure _v2-entry-point is marked as executable
As far as I can tell, the file should always be executable, but one person was having issues
with both the backup and injected entry point file not having the execute bit.
2025-07-18 06:57:15 +02:00
Gabriele Musco
830344d665 chore: fix clippy 2025-07-18 06:56:11 +02:00
Gabriele Musco
1a1d1682fe fix: specify cmake policy version in basalt build
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-06-14 17:53:20 +02:00
Gabriele Musco
8f3f9b8759 feat: add some messages related to setcap output to build window; add build completed message to build window
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-06-14 17:53:05 +02:00
Gabriele Musco
bd0cc9e2b1 chore: format 2025-06-14 17:28:39 +02:00
Gabriele Musco
1cad5c4d1b fix: add cargo as xrizer dependency
fixes #218
2025-06-14 17:27:59 +02:00
micheal65536
754395586e fix: properly build and install vapor
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
Co-authored-by: Gabriele Musco <gabmus@disroot.org>
2025-06-12 07:57:54 +02:00
Sapphire
5139ed7ba3 fix(builders/basalt): limit to at most 6 build processes
The Basalt build is quite memory hungry, causing people's systems to
lock up and trigger the OOM killer during build.
2025-06-12 05:36:13 +00:00
Sapphire
eed85abb2a
fix: detect cachyos as arch 2025-06-04 02:45:31 -05:00
Sapphire
d42de840a2
fix: add more libclang library paths
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-05-18 15:35:48 -05:00
Gabriele Musco
b174fab6bf fix: add wayland-dev to xrizer dependencies
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-05-14 07:25:26 +02:00
Sapphire
93ea2501b4
fix: add gentoo library paths for libclang to search paths 2025-05-10 09:27:17 -05:00
Gabriele Musco
fc4a2d3993 fix: deduplicate glslc 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
2025-05-10 10:39:53 +02:00
Gabriele Musco
27d37198c7 fix: refactor and optimize missing dependency filtering 2025-05-10 10:35:19 +02:00
Sapphire
4709a50483 fix: xrizer deps 2025-05-10 10:27:14 +02:00
Gabriele Musco
d0df943e48 fix: opensuse dep libusb-1_0 is now libusb-1_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
2025-05-02 11:16:49 +02:00
Gabriele Musco
99af59056d fix: opensuse dep SDL2-devel is now sdl2-compat-devel 2025-05-02 11:14:11 +02:00
Gabriele Musco
e0eae7c13a feat: theme manager 2025-05-02 09:40:16 +02:00
Gabriele Musco
743dbfa3a1 feat: account for getcap/setcap being found in /sbin and not in $PATH 2025-05-02 09:35:41 +02:00
Gabriele Musco
8ffac63e7e fix: account for opi packages for opensuse 2025-05-02 09:18:54 +02:00
Gabriele Musco
c794037377 feat: checkbox to delete profile dirs along with profile
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-29 14:22:27 +02:00
Gabriele Musco
9d85f1c24f fix: remove 2 thread limit when building basalt
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-22 09:04:13 +02:00
Gabriele Musco
3e23073f4c feat: remove old logs on startup, keep a max of 1GB and 3 files 2025-04-22 09:01:40 +02:00
Gabriele Musco
71a8223ce8 chore: update version to 3.1.1 2025-04-22 08:14:23 +02:00
Sapphire
e4d3980b14 fix: add libusb and libusb-dev deps 2025-04-22 08:11:25 +02:00
Aleksander
9ea754bb2e fix: Revert "disable and blacklist wayvr dashboard plugin" 2025-04-13 19:59:15 +02:00
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
88 changed files with 3991 additions and 851 deletions

View file

@ -1,4 +1,4 @@
image: "debian:unstable" image: "ubuntu:24.04"
stages: stages:
- check - check
@ -13,52 +13,6 @@ commitcheck:
# only run for merge requests # only run for merge requests
- if [ -z "$CI_MERGE_REQUEST_TITLTE" ]; then true; else python ./dist/tagging/check_conventional_commit.py "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"; fi - if [ -z "$CI_MERGE_REQUEST_TITLTE" ]; then true; else python ./dist/tagging/check_conventional_commit.py "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"; fi
cargo:fmtcheck:
image: "rust:slim"
stage: check
script:
- rustup component add rustfmt
# Create blank versions of our configured files
# so rustfmt does not yell about non-existent files or completely empty files
- echo -e "" >> src/constants.rs
- rustc -Vv && cargo -Vv
- cargo fmt --version
- cargo fmt --all -- --check
cargo:clippy:
stage: check
variables:
RUSTFLAGS: "-Dwarnings"
script:
- apt-get update
- apt-get install libgtk-4-dev libadwaita-1-dev libssl-dev libjxl-dev libvte-2.91-gtk4-dev meson ninja-build git desktop-file-utils gettext file libusb-dev libusb-1.0-0-dev libopenxr-dev curl -y
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup.sh
- chmod +x /tmp/rustup.sh
- /tmp/rustup.sh -y
- source "$HOME/.cargo/env"
- rustup component add clippy
- rustc -Vv && cargo -Vv
- cp src/constants.rs.in src/constants.rs
- cargo clippy --version
- cargo clippy --all-targets --all-features
cargo:test:
stage: check
script:
- apt-get update
- apt-get install libgtk-4-dev libadwaita-1-dev libssl-dev libjxl-dev libvte-2.91-gtk4-dev meson ninja-build git desktop-file-utils gettext file libusb-dev libusb-1.0-0-dev libopenxr-dev curl -y
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup.sh
- chmod +x /tmp/rustup.sh
- /tmp/rustup.sh -y
- source "$HOME/.cargo/env"
- rustc --version && cargo --version # Print version info for debugging
- meson setup build -Dprefix="$PWD/build/localprefix" -Dprofile=development
- ninja -C build
- cargo test --workspace --verbose
cache:
paths:
- /var/cache/apt
appimage: appimage:
stage: deploy stage: deploy
script: script:
@ -68,6 +22,7 @@ appimage:
- chmod +x /tmp/rustup.sh - chmod +x /tmp/rustup.sh
- /tmp/rustup.sh -y - /tmp/rustup.sh -y
- source "$HOME/.cargo/env" - source "$HOME/.cargo/env"
- rustup component add clippy
- bash ./dist/appimage/build_appimage.sh - bash ./dist/appimage/build_appimage.sh
artifacts: artifacts:
paths: paths:

197
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@ -420,6 +420,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crossbeam-channel"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.20" version = "0.8.20"
@ -436,6 +445,16 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "delicious-adwaita"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53548c789a95211e0ce6d26c213067002b9b4360f8de69046d84de78ad9da3f"
dependencies = [
"gtk4",
"libadwaita",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@ -554,10 +573,11 @@ dependencies = [
[[package]] [[package]]
name = "envision" name = "envision"
version = "1.1.0" version = "3.1.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ash", "ash",
"delicious-adwaita",
"gettext-rs", "gettext-rs",
"git2", "git2",
"gtk4", "gtk4",
@ -574,8 +594,12 @@ dependencies = [
"rusb", "rusb",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml",
"sha2", "sha2",
"tokio", "tokio",
"tracing",
"tracing-appender",
"tracing-subscriber",
"tracker", "tracker",
"uuid", "uuid",
"vte4", "vte4",
@ -1060,9 +1084,9 @@ dependencies = [
[[package]] [[package]]
name = "gtk4" name = "gtk4"
version = "0.9.4" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9376d14d7e33486c54823a42bef296e882b9f25cb4c52b52f4d1d57bbadb5b6d" checksum = "af1c491051f030994fd0cde6f3c44f3f5640210308cff1298c7673c47408091d"
dependencies = [ dependencies = [
"cairo-rs", "cairo-rs",
"field-offset", "field-offset",
@ -1093,9 +1117,9 @@ dependencies = [
[[package]] [[package]]
name = "gtk4-sys" name = "gtk4-sys"
version = "0.9.4" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e653b0a9001ba9be1ffddb9373bfe9a111f688222f5aeee2841481300d91b55a" checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6"
dependencies = [ dependencies = [
"cairo-sys-rs", "cairo-sys-rs",
"gdk-pixbuf-sys", "gdk-pixbuf-sys",
@ -1510,9 +1534,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "libadwaita" name = "libadwaita"
version = "0.7.1" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8611ee9fb85e7606c362b513afcaf5b59853f79e4d98caaaf581d99465014247" checksum = "500135d29c16aabf67baafd3e7741d48e8b8978ca98bac39e589165c8dc78191"
dependencies = [ dependencies = [
"gdk4", "gdk4",
"gio", "gio",
@ -1698,6 +1722,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@ -1820,6 +1853,16 @@ dependencies = [
"zbus 4.4.0", "zbus 4.4.0",
] ]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
@ -1945,6 +1988,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "pango" name = "pango"
version = "0.20.6" version = "0.20.6"
@ -2180,8 +2229,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata", "regex-automata 0.4.9",
"regex-syntax", "regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
] ]
[[package]] [[package]]
@ -2192,9 +2250,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax", "regex-syntax 0.8.5",
] ]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.5" version = "0.8.5"
@ -2503,6 +2567,19 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"
@ -2525,6 +2602,15 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@ -2713,6 +2799,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.36" version = "0.3.36"
@ -2720,10 +2816,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa",
"num-conv", "num-conv",
"powerfmt", "powerfmt",
"serde", "serde",
"time-core", "time-core",
"time-macros",
] ]
[[package]] [[package]]
@ -2732,6 +2830,16 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.7.6" version = "0.7.6"
@ -2844,6 +2952,18 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-appender"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
dependencies = [
"crossbeam-channel",
"thiserror",
"time",
"tracing-subscriber",
]
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.28" version = "0.1.28"
@ -2862,6 +2982,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-serde"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
dependencies = [
"serde",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
] ]
[[package]] [[package]]
@ -2925,6 +3088,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@ -2964,6 +3133,12 @@ dependencies = [
"rand", "rand",
] ]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"

View file

@ -1,7 +1,16 @@
[package] [package]
name = "envision" name = "envision"
version = "1.1.0" version = "3.1.1"
edition = "2021" edition = "2021"
authors = [
"Gabriele Musco <gabmus@disroot.org>",
]
description = "Orchestrator for the free XR stack"
repository = "https://gitlab.com/gabmus/envision"
documentation = "https://gitlab.com/gabmus/envision"
license = "AGPL-3.0-or-later"
keywords = ["desktop", "linux", "vr", "xr", "gtk"]
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -31,3 +40,8 @@ sha2 = "0.10.8"
tokio = { version = "1.39.3", features = ["process"] } tokio = { version = "1.39.3", features = ["process"] }
notify-rust = "4.11.3" notify-rust = "4.11.3"
zbus = { version = "5.1.1", features = ["tokio"] } zbus = { version = "5.1.1", features = ["tokio"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
tracing = "0.1.41"
tracing-appender = "0.2.3"
serde_yaml = "0.9.34"
delicious-adwaita = { version = "0.3.0", features = ["all_themes"] }

View file

@ -60,6 +60,10 @@ cd envision
</details> </details>
# Debugging
To view all the logs you need to run envision with the env var `RUST_LOG=trace`.
# Common issues # Common issues
## NOSUID with systemd-homed ## NOSUID with systemd-homed

View file

@ -2,22 +2,182 @@
<component type="desktop-application"> <component type="desktop-application">
<id>@APP_ID@</id> <id>@APP_ID@</id>
<metadata_license>CC0</metadata_license> <metadata_license>CC0</metadata_license>
<project_license>AGPL-3.0</project_license> <project_license>AGPL-3.0-or-later</project_license>
<name translatable="no">@PRETTY_NAME@</name> <name translatable="no">@PRETTY_NAME@</name>
<summary>GUI for Monado</summary> <summary>Orchestrator for the free XR stack</summary>
<description> <description>
<p>GUI for Monado</p> <!-- temporary --> <p>Orchestrator for the free XR stack</p>
</description> </description>
<!--screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://gitlab.com/gabmus/envision/raw/main/misc/screenshots/screenshot1.png</image> <image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/01.png</image>
<caption>Main window</caption> <caption>Main window</caption>
</screenshot> </screenshot>
</screenshots--> <screenshot type="default">
<image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/02.png</image>
<caption>Profile editor</caption>
</screenshot>
<screenshot type="default">
<image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/03.png</image>
<caption>Profile running</caption>
</screenshot>
<screenshot type="default">
<image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/04.png</image>
<caption>Profile running with debug view open</caption>
</screenshot>
</screenshots>
<url type="homepage">@REPO_URL@</url> <url type="homepage">@REPO_URL@</url>
<url type="bugtracker">@REPO_URL@/issues</url> <url type="bugtracker">@REPO_URL@/issues</url>
<content_rating type="oars-1.0" /> <content_rating type="oars-1.0" />
<releases> <releases>
<release version="3.1.1" date="2025-04-22">
<description>
<p>Fixes</p>
<ul>
<li>add libusb and libusb-dev deps</li>
<li>Revert &quot;disable and blacklist wayvr dashboard plugin&quot;</li>
</ul>
</description>
</release>
<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"> <release version="1.1.0" date="2024-11-28">
<description> <description>
<p>What's new</p> <p>What's new</p>

BIN
data/screenshots/01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
data/screenshots/02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
data/screenshots/03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
data/screenshots/04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View file

@ -8,6 +8,7 @@ if [[ ! -f Cargo.toml ]]; then
fi fi
meson setup appimage_build -Dprefix=/usr -Dprofile=default meson setup appimage_build -Dprefix=/usr -Dprofile=default
meson test -C appimage_build --print-errorlogs
DESTDIR="$PWD/AppDir" ninja -C appimage_build install DESTDIR="$PWD/AppDir" ninja -C appimage_build install
curl -SsLO https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage curl -SsLO https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage chmod +x linuxdeploy-x86_64.AppImage

1
dist/arch/PKGBUILD vendored
View file

@ -33,7 +33,6 @@ makedepends=(
) )
optdepends=( optdepends=(
'libudev0-shim: steamvr_lh lighthouse driver support' 'libudev0-shim: steamvr_lh lighthouse driver support'
'monado-vulkan-layers-git: Vulkan layers for NVIDIA users'
) )
provides=(envision) provides=(envision)
conflicts=(envision) conflicts=(envision)

View file

@ -1,9 +1,9 @@
project( project(
'envision', 'envision',
'rust', 'rust',
version: '1.1.0', # version number row version: '3.1.1', # version number row
meson_version: '>= 0.59', meson_version: '>= 0.59',
license: 'AGPL-3.0', license: 'AGPL-3.0-or-later',
) )
i18n = import('i18n') i18n = import('i18n')
@ -38,17 +38,30 @@ iconsdir = datadir / 'icons'
podir = meson.project_source_root() / 'po' podir = meson.project_source_root() / 'po'
gettext_package = meson.project_name() gettext_package = meson.project_name()
# are we building a tagged version? opt_profile = get_option('profile')
if run_command('git', 'describe', '--tags', '--exact-match').returncode() != 0
profile = 'Devel' # if a profile isn't specified infer from git
vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip() if opt_profile == 'default'
if vcs_tag == '' # are we building a tagged version?
version_suffix = '-devel' 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 else
version_suffix = '-@0@'.format(vcs_tag) profile = ''
version_suffix = ''
application_id = base_id
endif endif
elif opt_profile == 'development'
profile = 'Devel'
version_suffix = '-devel'
application_id = '@0@.@1@'.format(base_id, profile) application_id = '@0@.@1@'.format(base_id, profile)
else elif opt_profile == 'release'
profile = '' profile = ''
version_suffix = '' version_suffix = ''
application_id = base_id application_id = base_id

View file

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

View file

@ -2,42 +2,6 @@
set -ev set -ev
PREFIX=$1
CACHE_DIR=$2
if [[ -z $PREFIX ]] || [[ -z $CACHE_DIR ]]; then
echo "Usage: $0 PREFIX CACHE_DIR"
exit 1
fi
ONNX_VER=$(curl -sSL "https://api.github.com/repos/microsoft/onnxruntime/releases/latest" | jq -r .tag_name | tr -d v)
SYS_ARCH=$(uname -m)
if [[ $SYS_ARCH == x*64 ]]; then
ARCH="x64"
elif [[ $SYS_ARCH == arm64 ]] || [[ $ARCH == aarch64 ]]; then
ARCH="aarch64"
else
echo "CPU architecture '$SYS_ARCH' is not supported"
exit 1
fi
ONNX="onnxruntime-linux-${ARCH}-${ONNX_VER}"
ONNX_URL="https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VER}/${ONNX}.tgz"
mkdir -p "$CACHE_DIR"
curl -sSL "$ONNX_URL" -o "${CACHE_DIR}/onnxruntime.tgz"
tar xf "${CACHE_DIR}/onnxruntime.tgz" --directory="${CACHE_DIR}"
mkdir -p "${PREFIX}/lib"
mkdir -p "${PREFIX}/include"
cp -r "${CACHE_DIR}/${ONNX}/include/"* "${PREFIX}/include/"
cp -r "${CACHE_DIR}/${ONNX}/lib/"* "${PREFIX}/lib/"
if [[ -z $XDG_DATA_HOME ]]; then if [[ -z $XDG_DATA_HOME ]]; then
DATA_HOME=$HOME/.local/share DATA_HOME=$HOME/.local/share
else else

View file

@ -22,7 +22,7 @@ impl Cmake {
if k.contains(' ') { if k.contains(' ') {
panic!("Cmake vars cannot contain spaces!"); panic!("Cmake vars cannot contain spaces!");
} }
args.push(format!("-D{k}={v}", k = k, v = v)); args.push(format!("-D{k}={v}"));
} }
} }
args.push(self.source_dir.to_string_lossy().to_string()); args.push(self.source_dir.to_string_lossy().to_string());

View file

@ -5,7 +5,10 @@ use crate::{
ui::job_worker::job::WorkerJob, ui::job_worker::job::WorkerJob,
util::file_utils::rm_rf, util::file_utils::rm_rf,
}; };
use std::collections::{HashMap, VecDeque}; use std::{
collections::{HashMap, VecDeque},
num::NonZero,
};
pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> { pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> {
let mut jobs = VecDeque::<WorkerJob>::new(); let mut jobs = VecDeque::<WorkerJob>::new();
@ -35,28 +38,52 @@ pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<W
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build)); jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
let build_dir = profile.features.basalt.path.as_ref().unwrap().join("build"); let build_dir = profile.features.basalt.path.as_ref().unwrap().join("build");
let 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 { let cmake = Cmake {
env: Some(cmake_env), env: Some({
vars: Some(cmake_vars), let mut cmake_env: HashMap<String, String> = HashMap::new();
for (k, v) in [
// The basalt build uses a lot of RAM, so we have to limit the number of
// build processes to not starve the system of memory
// Limit to 6 build processes at most
(
"CMAKE_BUILD_PARALLEL_LEVEL",
std::cmp::min(
6,
std::thread::available_parallelism()
.map(NonZero::get)
.unwrap_or(2),
)
.to_string(),
),
("CMAKE_BUILD_TYPE", "RelWithDebInfo".into()),
("CMAKE_POLICY_VERSION_MINIMUM", "3.5".into()),
("BUILD_TESTS", "off".into()),
] {
cmake_env.insert(k.to_string(), v);
}
cmake_env
}),
vars: Some({
let mut cmake_vars: HashMap<String, String> = HashMap::new();
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(), source_dir: profile.features.basalt.path.as_ref().unwrap().clone(),
build_dir: build_dir.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() .as_ref()
.unwrap() .unwrap()
.join("build"); .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 { let cmake = Cmake {
env: None, 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(), source_dir: profile.features.libsurvive.path.as_ref().unwrap().clone(),
build_dir: build_dir.clone(), build_dir: build_dir.clone(),
}; };

View file

@ -1,11 +1,8 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use crate::{ use crate::{constants::pkg_data_dir, termcolor::TermColor, ui::job_worker::job::WorkerJob};
constants::pkg_data_dir, paths::get_cache_dir, profile::Profile, termcolor::TermColor,
ui::job_worker::job::WorkerJob,
};
pub fn get_build_mercury_jobs(profile: &Profile) -> VecDeque<WorkerJob> { pub fn get_build_mercury_jobs() -> VecDeque<WorkerJob> {
let mut jobs = VecDeque::new(); let mut jobs = VecDeque::new();
jobs.push_back(WorkerJob::new_printer( jobs.push_back(WorkerJob::new_printer(
"Building Mercury...", "Building Mercury...",
@ -17,10 +14,7 @@ pub fn get_build_mercury_jobs(profile: &Profile) -> VecDeque<WorkerJob> {
.join("scripts/build_mercury.sh") .join("scripts/build_mercury.sh")
.to_string_lossy() .to_string_lossy()
.to_string(), .to_string(),
Some(vec![ None,
profile.prefix.to_string_lossy().to_string(),
get_cache_dir().to_string_lossy().to_string(),
]),
)); ));
jobs jobs

View file

@ -43,37 +43,43 @@ pub fn get_build_monado_jobs(profile: &Profile, clean_build: bool) -> VecDeque<W
.to_string_lossy() .to_string_lossy()
.to_string(), .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 { let cmake = Cmake {
env: Some(env), 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(), source_dir: profile.xrservice_path.clone(),
build_dir: build_dir.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 { let git = Git {
repo: profile repo: profile
.opencomposite_repo .ovr_comp
.repo
.as_ref() .as_ref()
.unwrap_or(&"https://gitlab.com/znixian/OpenOVR.git".into()) .unwrap_or(&"https://gitlab.com/znixian/OpenOVR.git".into())
.clone(), .clone(),
dir: profile.opencomposite_path.clone(), dir: profile.ovr_comp.path.clone(),
branch: profile branch: profile
.opencomposite_branch .ovr_comp
.branch
.as_ref() .as_ref()
.unwrap_or(&"openxr".into()) .unwrap_or(&"openxr".into())
.clone(), .clone(),
@ -33,14 +35,20 @@ pub fn get_build_opencomposite_jobs(profile: &Profile, clean_build: bool) -> Vec
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build)); jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
let build_dir = profile.opencomposite_path.join("build"); let build_dir = profile.ovr_comp.path.join("build");
let mut cmake_vars: HashMap<String, String> = HashMap::new();
cmake_vars.insert("CMAKE_EXPORT_COMPILE_COMMANDS".into(), "ON".into());
cmake_vars.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
let cmake = Cmake { let cmake = Cmake {
env: None, env: None,
vars: Some(cmake_vars), vars: Some({
source_dir: profile.opencomposite_path.clone(), let mut cmake_vars: HashMap<String, String> = HashMap::new();
for (k, v) in [
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
] {
cmake_vars.insert(k.to_string(), v.to_string());
}
cmake_vars
}),
source_dir: profile.ovr_comp.path.clone(),
build_dir: build_dir.clone(), build_dir: build_dir.clone(),
}; };
if !Path::new(&build_dir).is_dir() || clean_build { if !Path::new(&build_dir).is_dir() || clean_build {

View file

@ -0,0 +1,68 @@
use crate::{
build_tools::{cmake::Cmake, git::Git},
profile::Profile,
termcolor::TermColor,
ui::job_worker::job::WorkerJob,
util::file_utils::rm_rf,
};
use std::{
collections::{HashMap, VecDeque},
path::Path,
};
pub fn get_build_vapor_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> {
let mut jobs = VecDeque::<WorkerJob>::new();
jobs.push_back(WorkerJob::new_printer(
"Building VapoR...",
Some(TermColor::Blue),
));
let git = Git {
repo: profile
.ovr_comp
.repo
.as_ref()
.unwrap_or(&"https://github.com/micheal65536/VapoR.git".into())
.clone(),
dir: profile.ovr_comp.path.clone(),
branch: profile
.ovr_comp
.branch
.as_ref()
.unwrap_or(&"master".into())
.clone(),
};
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
let build_dir = profile.ovr_comp.path.join("build");
let install_dir = build_dir.join("install_pfx");
let cmake = Cmake {
env: None,
vars: Some({
let mut cmake_vars: HashMap<String, String> = HashMap::new();
for (k, v) in [
("VAPOR_LOG_SILENT", "ON"),
("USE_SYSTEM_OPENXR", "OFF"),
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
] {
cmake_vars.insert(k.to_string(), v.to_string());
}
cmake_vars.insert(
"CMAKE_INSTALL_PREFIX".into(),
install_dir.to_string_lossy().to_string(),
);
cmake_vars
}),
source_dir: profile.ovr_comp.path.clone(),
build_dir: build_dir.clone(),
};
if !Path::new(&build_dir).is_dir() || clean_build {
rm_rf(&build_dir);
jobs.push_back(cmake.get_prepare_job());
}
jobs.push_back(cmake.get_build_job());
jobs.push_back(cmake.get_install_job());
jobs
}

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)); jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
let build_dir = profile.xrservice_path.join("build"); let build_dir = profile.xrservice_path.join("build");
let 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 { let cmake = Cmake {
env: None, 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(), source_dir: profile.xrservice_path.clone(),
build_dir: build_dir.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_monado;
pub mod build_opencomposite; pub mod build_opencomposite;
pub mod build_openhmd; pub mod build_openhmd;
pub mod build_vapor;
pub mod build_wivrn; pub mod build_wivrn;
pub mod build_xrizer;

View file

@ -7,21 +7,48 @@ use crate::{
lighthouse::lighthouse_profile, openhmd::openhmd_profile, simulated::simulated_profile, lighthouse::lighthouse_profile, openhmd::openhmd_profile, simulated::simulated_profile,
survive::survive_profile, wivrn::wivrn_profile, wmr::wmr_profile, survive::survive_profile, wivrn::wivrn_profile, wmr::wmr_profile,
}, },
ui::plugins::Plugin,
util::file_utils::get_writer, util::file_utils::get_writer,
}; };
use serde::{de::Error, Deserialize, Serialize}; use serde::{de::Error, Deserialize, Serialize};
use std::{ use std::{
collections::HashMap,
fs::File, fs::File,
io::BufReader, io::BufReader,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginConfig {
pub plugin: Plugin,
pub enabled: bool,
}
impl From<&Plugin> for PluginConfig {
fn from(p: &Plugin) -> Self {
Self {
plugin: p.clone(),
enabled: true,
}
}
}
impl From<&PluginConfig> for Plugin {
fn from(cp: &PluginConfig) -> Self {
cp.plugin.clone()
}
}
const DEFAULT_WIN_SIZE: [i32; 2] = [360, 400]; const DEFAULT_WIN_SIZE: [i32; 2] = [360, 400];
const fn default_win_size() -> [i32; 2] { const fn default_win_size() -> [i32; 2] {
DEFAULT_WIN_SIZE DEFAULT_WIN_SIZE
} }
fn default_theme_name() -> String {
"Follow system".into()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub selected_profile_uuid: String, pub selected_profile_uuid: String,
@ -29,6 +56,10 @@ pub struct Config {
pub user_profiles: Vec<Profile>, pub user_profiles: Vec<Profile>,
#[serde(default = "default_win_size")] #[serde(default = "default_win_size")]
pub win_size: [i32; 2], pub win_size: [i32; 2],
#[serde(default)]
pub plugins: HashMap<String, PluginConfig>,
#[serde(default = "default_theme_name")]
pub theme_name: String,
} }
impl Default for Config { impl Default for Config {
@ -37,8 +68,10 @@ impl Default for Config {
// TODO: using an empty string here is ugly // TODO: using an empty string here is ugly
selected_profile_uuid: "".to_string(), selected_profile_uuid: "".to_string(),
debug_view_enabled: false, debug_view_enabled: false,
user_profiles: vec![], user_profiles: Vec::default(),
win_size: DEFAULT_WIN_SIZE, win_size: DEFAULT_WIN_SIZE,
plugins: HashMap::default(),
theme_name: default_theme_name(),
} }
} }
} }
@ -65,10 +98,42 @@ impl Config {
} }
fn from_path(path: &Path) -> Self { fn from_path(path: &Path) -> Self {
File::open(path) let mut this: Self = File::open(path)
.ok() .ok()
.and_then(|file| serde_json::from_reader(BufReader::new(file)).ok()) .and_then(|file| serde_json::from_reader(BufReader::new(file)).ok())
.unwrap_or_default() .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> { 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_PROFILE: &str = "@PROFILE@";
pub const BUILD_DATETIME: &str = "@BUILD_DATETIME@"; pub const BUILD_DATETIME: &str = "@BUILD_DATETIME@";
pub fn get_developers() -> Vec<String> {
vec!["Gabriele Musco <gabmus@disroot.org>".into()]
}
pub fn get_artists() -> Vec<String> { pub fn get_artists() -> Vec<String> {
vec!["App Icon: Yannick (@Yandr)".into()] vec!["App Icon: Yannick (@Yandr)".into()]
} }

View file

@ -1,7 +1,7 @@
use super::{ use super::{
boost_deps::boost_deps, boost_deps::boost_deps,
common::{dep_cmake, dep_eigen, dep_gpp, dep_libgl, dep_ninja, dep_opencv}, common::{dep_cmake, dep_eigen, dep_gpp, dep_libgl, dep_ninja, dep_opencv},
DepType, Dependency, DependencyCheckResult, DepType, DepcheckResultGetMissing, Dependency, DependencyCheckResult,
}; };
use crate::linux_distro::LinuxDistro; use crate::linux_distro::LinuxDistro;
use std::collections::HashMap; use std::collections::HashMap;
@ -181,9 +181,5 @@ pub fn check_basalt_deps() -> Vec<DependencyCheckResult> {
} }
pub fn get_missing_basalt_deps() -> Vec<Dependency> { pub fn get_missing_basalt_deps() -> Vec<Dependency> {
check_basalt_deps() check_basalt_deps().filter_missing_deps()
.iter()
.filter(|res| !res.found)
.map(|res| res.dependency.clone())
.collect()
} }

View file

@ -52,8 +52,8 @@ pub fn boost_deps() -> Vec<Dependency> {
packages: HashMap::from([ packages: HashMap::from([
(LinuxDistro::Arch, "boost".into()), (LinuxDistro::Arch, "boost".into()),
(LinuxDistro::Debian, "libboost-all-dev".into()), (LinuxDistro::Debian, "libboost-all-dev".into()),
(LinuxDistro::Fedora, "boost".into()), (LinuxDistro::Fedora, "boost-devel".into()),
(LinuxDistro::Alpine, "boost".into()), (LinuxDistro::Alpine, "boost-dev".into()),
(LinuxDistro::Gentoo, "dev-libs/boost".into()), (LinuxDistro::Gentoo, "dev-libs/boost".into()),
(LinuxDistro::Suse, package.into()), (LinuxDistro::Suse, package.into()),
]), ]),

View file

@ -303,3 +303,35 @@ pub fn dep_adb() -> Dependency {
]), ]),
} }
} }
pub fn dep_getcap_setcap() -> Dependency {
Dependency {
name: "libcap".into(),
dep_type: DepType::Executable,
filename: "setcap".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "libcap".into()),
(LinuxDistro::Debian, "libcap2-bin".into()),
(LinuxDistro::Fedora, "libcap".into()),
(LinuxDistro::Alpine, "libcap".into()),
(LinuxDistro::Gentoo, "sys-libs/libcap".into()),
(LinuxDistro::Suse, "libcap-progs".into()),
]),
}
}
pub fn dep_glslc() -> Dependency {
Dependency {
name: "glslc".into(),
dep_type: DepType::Executable,
filename: "glslc".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "shaderc".into()),
(LinuxDistro::Debian, "glslc".into()),
(LinuxDistro::Fedora, "glslc".into()),
(LinuxDistro::Alpine, "shaderc".into()),
(LinuxDistro::Gentoo, "media-libs/shaderc".into()),
(LinuxDistro::Suse, "shaderc".into()),
]),
}
}

View file

@ -1,6 +1,6 @@
use super::{ use super::{
common::{dep_cmake, dep_eigen, dep_gcc, dep_git, dep_gpp, dep_ninja}, common::{dep_cmake, dep_eigen, dep_gcc, dep_git, dep_gpp, dep_ninja},
Dependency, DependencyCheckResult, DepcheckResultGetMissing, Dependency, DependencyCheckResult,
}; };
fn libsurvive_deps() -> Vec<Dependency> { fn libsurvive_deps() -> Vec<Dependency> {
@ -19,9 +19,5 @@ pub fn check_libsurvive_deps() -> Vec<DependencyCheckResult> {
} }
pub fn get_missing_libsurvive_deps() -> Vec<Dependency> { pub fn get_missing_libsurvive_deps() -> Vec<Dependency> {
check_libsurvive_deps() check_libsurvive_deps().filter_missing_deps()
.iter()
.filter(|res| !res.found)
.map(|res| res.dependency.clone())
.collect()
} }

View file

@ -1,10 +1,27 @@
use super::{common::dep_opencv, DepType, Dependency, DependencyCheckResult}; use super::{
common::dep_opencv, DepType, DepcheckResultGetMissing, Dependency, DependencyCheckResult,
};
use crate::linux_distro::LinuxDistro; use crate::linux_distro::LinuxDistro;
use std::collections::HashMap; use std::collections::HashMap;
fn mercury_deps() -> Vec<Dependency> { fn mercury_deps() -> Vec<Dependency> {
vec![ vec![
dep_opencv(), dep_opencv(),
Dependency {
name: "onnxruntime-dev".into(),
dep_type: DepType::Include,
filename: "onnxruntime/onnxruntime_c_api.h".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "onnxruntime".into()),
(LinuxDistro::Debian, "libonnxruntime-dev".into()),
(LinuxDistro::Fedora, "onnxruntime-devel".into()),
// alpine doesn't seem to have the package
// (LinuxDistro::Alpine, "".into()),
(LinuxDistro::Gentoo, "sci-ml/onnx".into()),
// opensuse doesn't seem to have the package
// (LinuxDistro::Suse, "".into()),
]),
},
Dependency { Dependency {
name: "jq".into(), name: "jq".into(),
dep_type: DepType::Executable, dep_type: DepType::Executable,
@ -39,9 +56,5 @@ pub fn check_mercury_deps() -> Vec<DependencyCheckResult> {
} }
pub fn get_missing_mercury_deps() -> Vec<Dependency> { pub fn get_missing_mercury_deps() -> Vec<Dependency> {
check_mercury_deps() check_mercury_deps().filter_missing_deps()
.iter()
.filter(|res| !res.found)
.map(|res| res.dependency.clone())
.collect()
} }

View file

@ -6,6 +6,7 @@ pub mod mercury_deps;
pub mod monado_deps; pub mod monado_deps;
pub mod openhmd_deps; pub mod openhmd_deps;
pub mod wivrn_deps; pub mod wivrn_deps;
pub mod xrizer_deps;
use crate::linux_distro::LinuxDistro; use crate::linux_distro::LinuxDistro;
use std::{collections::HashMap, env, fmt::Display, path::Path}; use std::{collections::HashMap, env, fmt::Display, path::Path};
@ -16,6 +17,7 @@ pub enum DepType {
Executable, Executable,
Include, Include,
UdevRule, UdevRule,
Share,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -49,6 +51,7 @@ impl Dependency {
.collect(), .collect(),
DepType::Include => include_paths(), DepType::Include => include_paths(),
DepType::UdevRule => udev_rules_paths(), DepType::UdevRule => udev_rules_paths(),
DepType::Share => share_paths(),
} { } {
let path_s = &format!("{dir}/{fname}", dir = dir, fname = self.filename); let path_s = &format!("{dir}/{fname}", dir = dir, fname = self.filename);
let path = Path::new(&path_s); let path = Path::new(&path_s);
@ -106,6 +109,24 @@ impl Display for DependencyCheckResult {
} }
} }
pub trait DepcheckResultGetMissing {
fn filter_missing_deps(self) -> Vec<Dependency>;
}
impl DepcheckResultGetMissing for Vec<DependencyCheckResult> {
fn filter_missing_deps(self) -> Vec<Dependency> {
self.into_iter()
.filter_map(|res| {
if !res.found {
Some(res.dependency)
} else {
None
}
})
.collect()
}
}
fn shared_obj_paths() -> Vec<String> { fn shared_obj_paths() -> Vec<String> {
vec![ vec![
"/lib".into(), "/lib".into(),
@ -115,6 +136,24 @@ fn shared_obj_paths() -> Vec<String> {
"/usr/local/lib64".into(), "/usr/local/lib64".into(),
"/usr/lib/x86_64-linux-gnu".into(), "/usr/lib/x86_64-linux-gnu".into(),
"/usr/lib/aarch64-linux-gnu".into(), "/usr/lib/aarch64-linux-gnu".into(),
// Debian puts libclang in /usr/lib/llvm-[llvm major version]/lib.
"/usr/lib/llvm-15/lib".into(),
"/usr/lib/llvm-16/lib".into(),
"/usr/lib/llvm-19/lib".into(),
// Fedora puts libclang in /usr/lib64/llvm[llvm major version]/lib as well as /usr/lib64.
"/usr/lib64/llvm15/lib".into(),
"/usr/lib64/llvm16/lib".into(),
"/usr/lib64/llvm17/lib".into(),
"/usr/lib64/llvm18/lib".into(),
"/usr/lib64/llvm19/lib".into(),
"/usr/lib64/llvm20/lib".into(),
// Gentoo puts libclang in /usr/lib/llvm/[llvm major version]/lib64.
"/usr/lib/llvm/15/lib64".into(),
"/usr/lib/llvm/16/lib64".into(),
"/usr/lib/llvm/17/lib64".into(),
"/usr/lib/llvm/18/lib64".into(),
"/usr/lib/llvm/19/lib64".into(),
"/usr/lib/llvm/20/lib64".into(),
"/lib/x86_64-linux-gnu".into(), "/lib/x86_64-linux-gnu".into(),
"/lib/aarch64-linux-gnu".into(), "/lib/aarch64-linux-gnu".into(),
"/app/lib".into(), "/app/lib".into(),
@ -138,6 +177,8 @@ fn include_paths() -> Vec<String> {
"/usr/include/ffmpeg/libpostproc".into(), "/usr/include/ffmpeg/libpostproc".into(),
"/usr/include/ffmpeg/libswresample".into(), "/usr/include/ffmpeg/libswresample".into(),
"/usr/include/ffmpeg/libswscale".into(), "/usr/include/ffmpeg/libswscale".into(),
// opensuse puts wayland-client.h here
"/usr/include/wayland".into(),
] ]
} }
@ -145,6 +186,10 @@ fn udev_rules_paths() -> Vec<String> {
vec!["/usr/lib/udev/rules.d".into()] vec!["/usr/lib/udev/rules.d".into()]
} }
fn share_paths() -> Vec<String> {
vec!["/usr/share".into()]
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{DepType, Dependency}; use super::{DepType, Dependency};

View file

@ -4,9 +4,12 @@ use super::{
dep_libgl, dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr, dep_vulkan_headers, dep_libgl, dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr, dep_vulkan_headers,
dep_vulkan_icd_loader, dep_vulkan_icd_loader,
}, },
DepType, Dependency, DependencyCheckResult, DepType, DepcheckResultGetMissing, Dependency, DependencyCheckResult,
};
use crate::{
depcheck::common::{dep_glslc, dep_libxrandr},
linux_distro::LinuxDistro,
}; };
use crate::{depcheck::common::dep_libxrandr, linux_distro::LinuxDistro};
use std::collections::HashMap; use std::collections::HashMap;
fn monado_deps() -> Vec<Dependency> { fn monado_deps() -> Vec<Dependency> {
@ -30,25 +33,37 @@ fn monado_deps() -> Vec<Dependency> {
(LinuxDistro::Suse, "wayland-devel".into()), (LinuxDistro::Suse, "wayland-devel".into()),
]), ]),
}, },
Dependency {
name: "wayland-protocols".into(),
dep_type: DepType::Share,
filename: "wayland-protocols/staging/drm-lease/drm-lease-v1.xml".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "wayland-protocols".into()),
(LinuxDistro::Debian, "wayland-protocols".into()),
(LinuxDistro::Fedora, "wayland-protocols-devel".into()),
(LinuxDistro::Gentoo, "dev-libs/wayland-protocols".into()),
(LinuxDistro::Suse, "wayland-protocols-devel".into()),
]),
},
Dependency {
name: "libbsd".into(),
dep_type: DepType::SharedObject,
filename: "libbsd.so".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "libbsd".into()),
(LinuxDistro::Debian, "libbsd-dev".into()),
(LinuxDistro::Fedora, "libbsd-devel".into()),
(LinuxDistro::Gentoo, "dev-libs/libbsd".into()),
(LinuxDistro::Suse, "libbsd-devel".into()),
]),
},
dep_cmake(), dep_cmake(),
dep_eigen(), dep_eigen(),
dep_git(), dep_git(),
dep_ninja(), dep_ninja(),
dep_gcc(), dep_gcc(),
dep_gpp(), dep_gpp(),
Dependency { dep_glslc(),
name: "glslc".into(),
dep_type: DepType::Executable,
filename: "glslc".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "shaderc".into()),
(LinuxDistro::Debian, "glslc".into()),
(LinuxDistro::Fedora, "glslc".into()),
(LinuxDistro::Alpine, "shaderc".into()),
(LinuxDistro::Gentoo, "media-libs/shaderc".into()),
(LinuxDistro::Suse, "shaderc".into()),
]),
},
dep_glslang_validator(), dep_glslang_validator(),
Dependency { Dependency {
name: "sdl2".into(), name: "sdl2".into(),
@ -59,10 +74,34 @@ fn monado_deps() -> Vec<Dependency> {
(LinuxDistro::Debian, "libsdl2-dev".into()), (LinuxDistro::Debian, "libsdl2-dev".into()),
(LinuxDistro::Fedora, "SDL2-devel".into()), (LinuxDistro::Fedora, "SDL2-devel".into()),
(LinuxDistro::Gentoo, "media-libs/libsdl2".into()), (LinuxDistro::Gentoo, "media-libs/libsdl2".into()),
(LinuxDistro::Suse, "SDL2-devel".into()), (LinuxDistro::Suse, "sdl2-compat-devel".into()),
]), ]),
}, },
dep_libudev(), dep_libudev(),
Dependency {
name: "libusb".into(),
dep_type: DepType::SharedObject,
filename: "libusb-1.0.so".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "libusb".into()),
(LinuxDistro::Debian, "libusb-1.0-0".into()),
(LinuxDistro::Fedora, "libusb1".into()),
(LinuxDistro::Gentoo, "dev-libs/libusb".into()),
(LinuxDistro::Suse, "libusb-1_0-0".into()),
]),
},
Dependency {
name: "libusb-dev".into(),
dep_type: DepType::Include,
filename: "libusb-1.0/libusb.h".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "libusb".into()),
(LinuxDistro::Debian, "libusb-1.0-0-dev".into()),
(LinuxDistro::Fedora, "libusb1-devel".into()),
(LinuxDistro::Gentoo, "dev-libs/libusb".into()),
(LinuxDistro::Suse, "libusb-1_0-devel".into()),
]),
},
Dependency { Dependency {
name: "mesa-common-dev".into(), name: "mesa-common-dev".into(),
dep_type: DepType::Include, dep_type: DepType::Include,
@ -83,9 +122,5 @@ pub fn check_monado_deps() -> Vec<DependencyCheckResult> {
} }
pub fn get_missing_monado_deps() -> Vec<Dependency> { pub fn get_missing_monado_deps() -> Vec<Dependency> {
check_monado_deps() check_monado_deps().filter_missing_deps()
.iter()
.filter(|res| !res.found)
.map(|res| res.dependency.clone())
.collect()
} }

View file

@ -1,6 +1,6 @@
use super::{ use super::{
common::{dep_gcc, dep_git, dep_gpp, dep_ninja}, common::{dep_gcc, dep_git, dep_gpp, dep_ninja},
Dependency, DependencyCheckResult, DepcheckResultGetMissing, Dependency, DependencyCheckResult,
}; };
use crate::linux_distro::LinuxDistro; use crate::linux_distro::LinuxDistro;
use std::collections::HashMap; use std::collections::HashMap;
@ -31,9 +31,5 @@ pub fn check_openhmd_deps() -> Vec<DependencyCheckResult> {
} }
pub fn get_missing_openhmd_deps() -> Vec<Dependency> { pub fn get_missing_openhmd_deps() -> Vec<Dependency> {
check_openhmd_deps() check_openhmd_deps().filter_missing_deps()
.iter()
.filter(|res| !res.found)
.map(|res| res.dependency.clone())
.collect()
} }

View file

@ -4,7 +4,7 @@ use super::{
dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr, dep_vulkan_headers, dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr, dep_vulkan_headers,
dep_vulkan_icd_loader, dep_vulkan_icd_loader,
}, },
DepType, Dependency, DependencyCheckResult, DepType, DepcheckResultGetMissing, Dependency, DependencyCheckResult,
}; };
use crate::{ use crate::{
depcheck::common::{dep_libgl, dep_libxrandr}, depcheck::common::{dep_libgl, dep_libxrandr},
@ -78,15 +78,15 @@ fn wivrn_deps() -> Vec<Dependency> {
]), ]),
}, },
Dependency { Dependency {
name: "libpulse-dev".into(), name: "libpipewire-dev".into(),
dep_type: DepType::Include, dep_type: DepType::Include,
filename: "pulse/context.h".into(), filename: "pipewire-0.3/pipewire/pipewire.h".into(),
packages: HashMap::from([ packages: HashMap::from([
(LinuxDistro::Arch, "libpulse".into()), (LinuxDistro::Arch, "libpipewire".into()),
(LinuxDistro::Debian, "libpulse-dev".into()), (LinuxDistro::Debian, "libpipewire-0.3-dev".into()),
(LinuxDistro::Fedora, "pulseaudio-libs-devel".into()), (LinuxDistro::Fedora, "pipewire-devel".into()),
(LinuxDistro::Gentoo, "media-libs/libpulse".into()), (LinuxDistro::Gentoo, "media-video/pipewire".into()),
(LinuxDistro::Suse, "libpulse-devel".into()), (LinuxDistro::Suse, "pipewire-devel".into()),
]), ]),
}, },
dep_eigen(), dep_eigen(),
@ -169,7 +169,10 @@ fn wivrn_deps() -> Vec<Dependency> {
filename: "pkgconfig/gstreamer-app-1.0.pc".into(), filename: "pkgconfig/gstreamer-app-1.0.pc".into(),
packages: HashMap::from([ packages: HashMap::from([
(LinuxDistro::Arch, "gst-plugins-base-libs".into()), (LinuxDistro::Arch, "gst-plugins-base-libs".into()),
(LinuxDistro::Debian, "libgstreamer1.0-dev".into()), (
LinuxDistro::Debian,
"libgstreamer-plugins-base1.0-dev".into(),
),
(LinuxDistro::Fedora, "gstreamer1-plugins-base-devel".into()), (LinuxDistro::Fedora, "gstreamer1-plugins-base-devel".into()),
(LinuxDistro::Gentoo, "media-libs/gst-plugins-base".into()), (LinuxDistro::Gentoo, "media-libs/gst-plugins-base".into()),
(LinuxDistro::Suse, "gstreamer-plugins-base-devel".into()), (LinuxDistro::Suse, "gstreamer-plugins-base-devel".into()),
@ -235,6 +238,30 @@ fn wivrn_deps() -> Vec<Dependency> {
(LinuxDistro::Suse, "glib2-devel".into()), (LinuxDistro::Suse, "glib2-devel".into()),
]), ]),
}, },
Dependency {
name: "openssl-dev".into(),
dep_type: DepType::Include,
filename: "openssl/ssl3.h".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "openssl".into()),
(LinuxDistro::Alpine, "openssl-dev".into()),
(LinuxDistro::Debian, "libssl-dev".into()),
(LinuxDistro::Fedora, "openssl-devel".into()),
(LinuxDistro::Suse, "openssl-devel".into()),
]),
},
Dependency {
name: "libnotify-dev".into(),
dep_type: DepType::Include,
filename: "libnotify/notify.h".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "libnotify".into()),
(LinuxDistro::Alpine, "libnotify-dev".into()),
(LinuxDistro::Debian, "libnotify-dev".into()),
(LinuxDistro::Fedora, "libnotify-devel".into()),
(LinuxDistro::Suse, "libnotify-devel".into()),
]),
},
] ]
} }
@ -243,9 +270,5 @@ pub fn check_wivrn_deps() -> Vec<DependencyCheckResult> {
} }
pub fn get_missing_wivrn_deps() -> Vec<Dependency> { pub fn get_missing_wivrn_deps() -> Vec<Dependency> {
check_wivrn_deps() check_wivrn_deps().filter_missing_deps()
.iter()
.filter(|res| !res.found)
.map(|res| res.dependency.clone())
.collect()
} }

View file

@ -0,0 +1,65 @@
use super::{DepType, DepcheckResultGetMissing, Dependency, DependencyCheckResult};
use crate::{depcheck::common::dep_glslc, linux_distro::LinuxDistro};
use std::collections::HashMap;
fn xrizer_deps() -> Vec<Dependency> {
vec![
dep_glslc(),
Dependency {
name: "cargo".into(),
dep_type: DepType::Executable,
filename: "cargo".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "rust".into()),
(LinuxDistro::Debian, "cargo".into()),
(LinuxDistro::Fedora, "cargo".into()),
(LinuxDistro::Alpine, "cargo".into()),
(LinuxDistro::Suse, "cargo".into()),
]),
},
Dependency {
name: "libxcb-glx".into(),
dep_type: DepType::Include,
filename: "xcb/glx.h".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "libxcb".into()),
(LinuxDistro::Debian, "libxcb-glx0-dev".into()),
(LinuxDistro::Fedora, "libxcb-devel".into()),
(LinuxDistro::Gentoo, "x11-libs/libxcb".into()),
(LinuxDistro::Suse, "libxcb-devel".into()),
]),
},
Dependency {
name: "libclang".into(),
dep_type: DepType::SharedObject,
filename: "libclang.so".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "clang".into()),
(LinuxDistro::Debian, "libclang-19-dev".into()),
(LinuxDistro::Fedora, "clang19-devel".into()),
(LinuxDistro::Gentoo, "llvm-core/clang-runtime".into()),
(LinuxDistro::Suse, "clang19-devel".into()),
]),
},
Dependency {
name: "wayland-dev".into(),
dep_type: DepType::Include,
filename: "wayland-client.h".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "wayland".into()),
(LinuxDistro::Debian, "libwayland-dev".into()),
(LinuxDistro::Fedora, "wayland-devel".into()),
(LinuxDistro::Gentoo, "dev-libs/wayland".into()),
(LinuxDistro::Suse, "wayland-devel".into()),
]),
},
]
}
pub fn check_xrizer_deps() -> Vec<DependencyCheckResult> {
Dependency::check_many(xrizer_deps())
}
pub fn get_missing_xrizer_deps() -> Vec<Dependency> {
check_xrizer_deps().filter_missing_deps()
}

View file

@ -13,7 +13,7 @@ const CHUNK_SIZE: usize = 1024;
fn headers() -> HeaderMap { fn headers() -> HeaderMap {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, format!("{}/1.0", APP_ID).parse().unwrap()); headers.insert(USER_AGENT, format!("{APP_ID}/1.0").parse().unwrap());
headers headers
} }

View file

@ -3,7 +3,7 @@ use lazy_static::lazy_static;
fn env_var_descriptions() -> Vec<(&'static str, &'static str)> { fn env_var_descriptions() -> Vec<(&'static str, &'static str)> {
vec![ vec![
( (
"XRT_COMPOSITOR_SCALE_PECENTAGE", "XRT_COMPOSITOR_SCALE_PERCENTAGE",
"Render resolution percentage. A percentage higher than the native resolution (>100) will help with antialiasing and image clarity." "Render resolution percentage. A percentage higher than the native resolution (>100) will help with antialiasing and image clarity."
), ),
( (
@ -65,7 +65,7 @@ fn env_var_descriptions() -> Vec<(&'static str, &'static str)> {
fn env_var_descriptions_as_paragraph() -> String { fn env_var_descriptions_as_paragraph() -> String {
ENV_VAR_DESCRIPTIONS ENV_VAR_DESCRIPTIONS
.iter() .iter()
.map(|(k, v)| format!("<span size=\"large\" weight=\"bold\">{}</span>\n{}", k, v)) .map(|(k, v)| format!("<span size=\"large\" weight=\"bold\">{k}</span>\n{v}"))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n\n") .join("\n\n")
} }

View file

@ -1,14 +1,17 @@
use crate::{ use crate::{
paths::{get_backup_dir, SYSTEM_PREFIX}, paths::SYSTEM_PREFIX,
profile::Profile, profile::Profile,
util::file_utils::{copy_file, deserialize_file, get_writer, set_file_readonly}, util::file_utils::{deserialize_file, get_writer, set_file_readonly},
xdg::XDG, xdg::XDG,
}; };
use anyhow::bail;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
fs::remove_file, fs::{create_dir_all, remove_file, rename},
os::unix::fs::symlink,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use tracing::{debug, warn};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActiveRuntimeInnerRuntime { pub struct ActiveRuntimeInnerRuntime {
@ -34,29 +37,6 @@ fn get_active_runtime_json_path() -> PathBuf {
get_openxr_conf_dir().join("1/active_runtime.json") get_openxr_conf_dir().join("1/active_runtime.json")
} }
pub fn is_steam(active_runtime: &ActiveRuntime) -> bool {
matches!(active_runtime.runtime.valve_runtime_is_steamvr, Some(true))
}
fn get_backup_steam_active_runtime_path() -> PathBuf {
get_backup_dir().join("active_runtime.json.steam.bak")
}
fn get_backed_up_steam_active_runtime() -> Option<ActiveRuntime> {
get_active_runtime_from_path(&get_backup_steam_active_runtime_path())
}
fn backup_steam_active_runtime() {
if let Some(ar) = get_current_active_runtime() {
if is_steam(&ar) {
copy_file(
&get_active_runtime_json_path(),
&get_backup_steam_active_runtime_path(),
);
}
}
}
fn get_active_runtime_from_path(path: &Path) -> Option<ActiveRuntime> { fn get_active_runtime_from_path(path: &Path) -> Option<ActiveRuntime> {
deserialize_file(path) deserialize_file(path)
} }
@ -78,29 +58,6 @@ pub fn dump_current_active_runtime(active_runtime: &ActiveRuntime) -> anyhow::Re
dump_active_runtime_to_path(active_runtime, &get_active_runtime_json_path()) dump_active_runtime_to_path(active_runtime, &get_active_runtime_json_path())
} }
fn build_steam_active_runtime() -> ActiveRuntime {
if let Some(backup) = get_backed_up_steam_active_runtime() {
return backup;
}
ActiveRuntime {
file_format_version: "1.0.0".into(),
runtime: ActiveRuntimeInnerRuntime {
valve_runtime_is_steamvr: Some(true),
libmonado_path: None,
library_path: XDG
.get_data_home()
.join("Steam/steamapps/common/SteamVR/bin/linux64/vrclient.so"),
name: Some("SteamVR".into()),
},
}
}
pub fn set_current_active_runtime_to_steam() -> anyhow::Result<()> {
set_file_readonly(&get_active_runtime_json_path(), false)?;
dump_current_active_runtime(&build_steam_active_runtime())?;
Ok(())
}
pub fn build_profile_active_runtime(profile: &Profile) -> anyhow::Result<ActiveRuntime> { pub fn build_profile_active_runtime(profile: &Profile) -> anyhow::Result<ActiveRuntime> {
let Some(libopenxr_path) = profile.libopenxr_so() else { let Some(libopenxr_path) = profile.libopenxr_so() else {
anyhow::bail!( anyhow::bail!(
@ -137,18 +94,67 @@ fn relativize_active_runtime_lib_path(ar: &ActiveRuntime, path: &Path) -> Active
res res
} }
const ACTIVE_RUNTIME_BAK: &str = "active_runtime.json.envision.bak";
pub fn set_current_active_runtime_to_profile(profile: &Profile) -> anyhow::Result<()> { pub fn set_current_active_runtime_to_profile(profile: &Profile) -> anyhow::Result<()> {
let dest = get_active_runtime_json_path(); let dest = get_active_runtime_json_path();
set_file_readonly(&dest, false)?; if dest.is_dir() {
backup_steam_active_runtime(); bail!("{} is a directory", dest.to_string_lossy());
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)?; if !dest.is_symlink() {
set_file_readonly(&dest, true)?; 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(()) Ok(())
} }

View file

@ -1,5 +1,6 @@
pub mod active_runtime_json; pub mod active_runtime_json;
pub mod monado_autorun; pub mod monado_autorun;
pub mod openvrpaths_vrpath; pub mod openvrpaths_vrpath;
pub mod wayvr_dashboard_config;
pub mod wivrn_config; pub mod wivrn_config;
pub mod wivrn_encoder_presets; pub mod wivrn_encoder_presets;

View file

@ -1,5 +1,3 @@
use std::path::{Path, PathBuf};
use crate::{ use crate::{
paths::get_backup_dir, paths::get_backup_dir,
profile::Profile, profile::Profile,
@ -7,6 +5,7 @@ use crate::{
xdg::XDG, xdg::XDG,
}; };
use serde::{ser::Error, Deserialize, Serialize}; use serde::{ser::Error, Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OpenVrPaths { pub struct OpenVrPaths {
@ -86,6 +85,7 @@ fn build_steam_openvrpaths() -> OpenVrPaths {
} }
pub fn set_current_openvrpaths_to_steam() -> anyhow::Result<()> { pub fn set_current_openvrpaths_to_steam() -> anyhow::Result<()> {
// removing readonly flag just in case, remove this line in the future
set_file_readonly(&get_openvrpaths_vrpath_path(), false)?; set_file_readonly(&get_openvrpaths_vrpath_path(), false)?;
dump_current_openvrpaths(&build_steam_openvrpaths())?; dump_current_openvrpaths(&build_steam_openvrpaths())?;
Ok(()) Ok(())
@ -98,25 +98,24 @@ pub fn build_profile_openvrpaths(profile: &Profile) -> OpenVrPaths {
external_drivers: None, external_drivers: None,
jsonid: "vrpathreg".into(), jsonid: "vrpathreg".into(),
log: vec![datadir.join("Steam/logs")], log: vec![datadir.join("Steam/logs")],
runtime: vec![profile.opencomposite_path.join("build")], runtime: vec![profile.ovr_comp.runtime_dir()],
version: 1, version: 1,
} }
} }
pub fn set_current_openvrpaths_to_profile(profile: &Profile) -> anyhow::Result<()> { pub fn set_current_openvrpaths_to_profile(profile: &Profile) -> anyhow::Result<()> {
let dest = get_openvrpaths_vrpath_path(); let dest = get_openvrpaths_vrpath_path();
// removing readonly flag just in case, remove this line in the future
set_file_readonly(&dest, false)?; set_file_readonly(&dest, false)?;
backup_steam_openvrpaths(); backup_steam_openvrpaths();
dump_current_openvrpaths(&build_profile_openvrpaths(profile))?; dump_current_openvrpaths(&build_profile_openvrpaths(profile))?;
set_file_readonly(&dest, true)?;
Ok(()) Ok(())
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::path::Path;
use super::{dump_openvrpaths_to_path, get_openvrpaths_from_path, OpenVrPaths}; use super::{dump_openvrpaths_to_path, get_openvrpaths_from_path, OpenVrPaths};
use std::path::Path;
#[test] #[test]
fn can_read_openvrpaths_vrpath_steamvr() { fn can_read_openvrpaths_vrpath_steamvr() {

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

@ -143,7 +143,7 @@ pub struct WivrnConfig {
pub encoders: Vec<WivrnConfEncoder>, pub encoders: Vec<WivrnConfEncoder>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub application: Option<WivrnConfigApplication>, pub application: Option<WivrnConfigApplication>,
#[serde(default)] #[serde(default, rename = "tcp-only")]
pub tcp_only: bool, pub tcp_only: bool,
/// contains unknown fields /// contains unknown fields
#[serde(flatten)] #[serde(flatten)]

View file

@ -116,7 +116,7 @@ fn list_gpus() -> Vec<GpuSysDrm> {
for i in 0..5 { for i in 0..5 {
// arbitrary range, find a better way // arbitrary range, find a better way
let card_dir = PathBuf::from(format!("/sys/class/drm/card{}", i)); let card_dir = PathBuf::from(format!("/sys/class/drm/card{i}"));
let vendor_file = card_dir.join("device/vendor"); let vendor_file = card_dir.join("device/vendor");
if let Some(mut reader) = get_reader(&vendor_file) { if let Some(mut reader) = get_reader(&vendor_file) {
let mut buf = String::new(); let mut buf = String::new();

View file

@ -49,13 +49,13 @@ impl LinuxDistro {
Ok(_) if buf.starts_with("PRETTY_NAME=\"") => { Ok(_) if buf.starts_with("PRETTY_NAME=\"") => {
return buf return buf
.split('=') .split('=')
.last() .next_back()
.map(|b| b.trim().trim_matches('"').trim().to_string()); .map(|b| b.trim().trim_matches('"').trim().to_string());
} }
Ok(_) if buf.starts_with("NAME=\"") => { Ok(_) if buf.starts_with("NAME=\"") => {
name = buf name = buf
.split('=') .split('=')
.last() .next_back()
.map(|b| b.trim().trim_matches('"').trim().to_string()); .map(|b| b.trim().trim_matches('"').trim().to_string());
} }
_ => {} _ => {}
@ -79,7 +79,7 @@ impl LinuxDistro {
{ {
let name = buf let name = buf
.split('=') .split('=')
.last() .next_back()
.unwrap_or_default() .unwrap_or_default()
.trim() .trim()
.trim_matches('"') .trim_matches('"')
@ -115,6 +115,7 @@ impl LinuxDistro {
|| s.contains("steamos") || s.contains("steamos")
|| s.contains("steam os") || s.contains("steam os")
|| s.contains("endeavour") || s.contains("endeavour")
|| s.contains("cachyos")
|| s.contains("garuda") || s.contains("garuda")
{ {
return Some(Self::Arch); return Some(Self::Arch);
@ -150,7 +151,33 @@ impl LinuxDistro {
Self::Alpine => format!("sudo apk add {}", packages.join(" ")), Self::Alpine => format!("sudo apk add {}", packages.join(" ")),
Self::Debian => format!("sudo apt install {}", packages.join(" ")), Self::Debian => format!("sudo apt install {}", packages.join(" ")),
Self::Gentoo => format!("sudo emerge -av {}", packages.join(" ")), Self::Gentoo => format!("sudo emerge -av {}", packages.join(" ")),
Self::Suse => format!("sudo zypper install {}", packages.join(" ")), Self::Suse => {
let mut opi_pkgs = Vec::new();
let mut zypper_pkgs = Vec::new();
for pkg in packages {
if ["OpenXR-SDK-devel"].contains(&pkg.as_str()) {
opi_pkgs.push(pkg.clone());
} else {
zypper_pkgs.push(pkg.clone());
}
}
[
if opi_pkgs.is_empty() {
None
} else {
Some(format!("opi {}", opi_pkgs.join(" ")))
},
if zypper_pkgs.is_empty() {
None
} else {
Some(format!("sudo zypper install {}", zypper_pkgs.join(" ")))
},
]
.iter()
.filter_map(|c| c.clone())
.collect::<Vec<String>>()
.join(" && ")
}
Self::Fedora => { Self::Fedora => {
let mut install_rpmfusion_cmd: Option<String> = None; let mut install_rpmfusion_cmd: Option<String> = None;
let mut swap_ffmpeg_cmd: Option<String> = None; let mut swap_ffmpeg_cmd: Option<String> = None;
@ -190,9 +217,9 @@ impl LinuxDistro {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::path::Path;
use super::LinuxDistro; use super::LinuxDistro;
use crate::depcheck::common::{dep_openxr, dep_pkexec, dep_vulkan_icd_loader};
use std::path::Path;
#[test] #[test]
fn can_detect_arch_linux_from_etc_os_release() { fn can_detect_arch_linux_from_etc_os_release() {
@ -203,4 +230,34 @@ mod tests {
Some(LinuxDistro::Arch) Some(LinuxDistro::Arch)
) )
} }
#[test]
fn can_account_for_opensuse_opi_packages() {
assert_eq!(
LinuxDistro::Suse
.install_command(
&[dep_openxr(), dep_vulkan_icd_loader()]
.iter()
.map(|dep| dep.package_name_for_distro(Some(&LinuxDistro::Suse)))
.collect::<Vec<String>>()
)
.as_str(),
"opi OpenXR-SDK-devel && sudo zypper install vulkan-devel"
)
}
#[test]
fn opensuse_opi_does_not_interfere_if_not_needed() {
assert_eq!(
LinuxDistro::Suse
.install_command(
&[dep_pkexec(), dep_vulkan_icd_loader()]
.iter()
.map(|dep| dep.package_name_for_distro(Some(&LinuxDistro::Suse)))
.collect::<Vec<String>>()
)
.as_str(),
"sudo zypper install polkit vulkan-devel"
)
}
} }

View file

@ -1,17 +1,29 @@
use anyhow::Result; use anyhow::Result;
use constants::{resources, APP_ID, APP_NAME, GETTEXT_PACKAGE, LOCALE_DIR, RESOURCES_BASE_PATH}; use constants::{resources, APP_ID, APP_NAME, GETTEXT_PACKAGE, LOCALE_DIR, RESOURCES_BASE_PATH};
use file_builders::{ use file_builders::{
active_runtime_json::{get_current_active_runtime, set_current_active_runtime_to_steam}, active_runtime_json::restore_active_runtime_backup,
openvrpaths_vrpath::{get_current_openvrpaths, set_current_openvrpaths_to_steam}, openvrpaths_vrpath::{get_current_openvrpaths, set_current_openvrpaths_to_steam},
}; };
use gettextrs::LocaleCategory; use gettextrs::LocaleCategory;
use paths::get_logs_dir;
use relm4::{ use relm4::{
adw, adw,
gtk::{self, gdk, gio, glib, prelude::*}, gtk::{self, gdk, gio, glib, prelude::*},
MessageBroker, RelmApp, MessageBroker, RelmApp,
}; };
use std::env; use std::{
use steam_linux_runtime_injector::restore_runtime_entrypoint; env,
fs::{read_dir, remove_file},
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
};
use steam_linux_runtime_injector::{
restore_sniper_runtime_entrypoint, restore_soldier_runtime_entrypoint,
};
use tracing::{error, warn};
use tracing_subscriber::{
filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
};
use ui::{ use ui::{
app::{App, AppInit, Msg}, app::{App, AppInit, Msg},
cmdline_opts::CmdLineOpts, cmdline_opts::CmdLineOpts,
@ -22,6 +34,7 @@ pub mod build_tools;
pub mod builders; pub mod builders;
pub mod cmd_runner; pub mod cmd_runner;
pub mod config; pub mod config;
#[rustfmt::skip]
pub mod constants; pub mod constants;
pub mod depcheck; pub mod depcheck;
pub mod device_prober; pub mod device_prober;
@ -46,25 +59,61 @@ pub mod xdg;
pub mod xr_devices; pub mod xr_devices;
fn restore_steam_xr_files() { fn restore_steam_xr_files() {
let active_runtime = get_current_active_runtime();
let openvrpaths = get_current_openvrpaths(); let openvrpaths = get_current_openvrpaths();
if let Some(ar) = active_runtime { if let Err(e) = restore_active_runtime_backup() {
if !file_builders::active_runtime_json::is_steam(&ar) { warn!("failed to restore active runtime to steam: {e}");
match set_current_active_runtime_to_steam() {
Ok(_) => {}
Err(e) => eprintln!("Warning: failed to restore active runtime to steam: {e}"),
};
}
} }
if let Some(ovrp) = openvrpaths { if let Some(ovrp) = openvrpaths {
if !file_builders::openvrpaths_vrpath::is_steam(&ovrp) { if !file_builders::openvrpaths_vrpath::is_steam(&ovrp) {
match set_current_openvrpaths_to_steam() { if let Err(e) = set_current_openvrpaths_to_steam() {
Ok(_) => {} warn!("failed to restore openvrpaths to steam: {e}");
Err(e) => eprintln!("Warning: failed to restore openvrpaths to steam: {e}"), }
};
} }
} }
restore_runtime_entrypoint(); restore_sniper_runtime_entrypoint();
restore_soldier_runtime_entrypoint();
}
const LOGS_MAX_SIZE_BYTES: u64 = 1000000000; // 1GB
fn remove_old_logs(dir: &Path, log_files: Option<Vec<PathBuf>>) -> anyhow::Result<()> {
let log_files: Vec<PathBuf> = log_files
.map::<anyhow::Result<Vec<PathBuf>>, _>(Ok)
.unwrap_or_else(|| {
let mut files: Vec<PathBuf> = read_dir(dir)?
.filter_map(|de| {
let p = de.ok()?.path();
if p.is_file() && !p.is_symlink() {
Some(p)
} else {
None
}
})
.collect();
files.sort_unstable();
Ok(files)
})?;
let total_size = log_files
.iter()
.filter_map(|p| Some(p.metadata().ok()?.size()))
.reduce(u64::saturating_add)
.unwrap_or(0);
// if size is under threshold, finish
if total_size < LOGS_MAX_SIZE_BYTES {
return Ok(());
}
// keep a minimum of 3 logs
if log_files.len() <= 3 {
return Ok(());
}
remove_file(log_files.first().ok_or_else(||
anyhow::Error::msg(
"Could not get first item in log files list, but they should be more than 3! This is a bug!"
)
)?)?;
remove_old_logs(dir, Some(log_files))
} }
fn main() -> Result<()> { fn main() -> Result<()> {
@ -72,6 +121,30 @@ fn main() -> Result<()> {
panic!("{APP_NAME} cannot run as root"); panic!("{APP_NAME} cannot run as root");
} }
restore_steam_xr_files(); restore_steam_xr_files();
// deferring error logging for this since tracing isn't initialized yet
let old_logs_removal_res = remove_old_logs(&get_logs_dir(), None);
let rolling_log_writer = tracing_appender::rolling::daily(get_logs_dir(), "log");
let (non_blocking_appender, _appender_guard) =
tracing_appender::non_blocking(rolling_log_writer);
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer().pretty().with_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
),
)
.with(
tracing_subscriber::fmt::layer()
.json()
.with_writer(non_blocking_appender),
)
.init();
if let Err(e) = old_logs_removal_res {
error!("Failed to remove old log files: {e}");
}
// Prepare i18n // Prepare i18n
gettextrs::setlocale(LocaleCategory::LcAll, ""); gettextrs::setlocale(LocaleCategory::LcAll, "");
@ -87,7 +160,7 @@ fn main() -> Result<()> {
} }
let provider = gtk::CssProvider::new(); let provider = gtk::CssProvider::new();
provider.load_from_resource(&format!("{}/style.css", RESOURCES_BASE_PATH)); provider.load_from_resource(&format!("{RESOURCES_BASE_PATH}/style.css"));
if let Some(display) = gdk::Display::default() { if let Some(display) = gdk::Display::default() {
gtk::style_context_add_provider_for_display( gtk::style_context_add_provider_for_display(
&display, &display,

View file

@ -3,7 +3,7 @@ config = configure_file(
output: 'constants.rs', output: 'constants.rs',
configuration: global_conf configuration: global_conf
) )
# Copy the config.rs output to the source directory. # Copy the constants.rs output to the source directory.
run_command( run_command(
'cp', 'cp',
meson.project_build_root() / 'src' / 'constants.rs', meson.project_build_root() / 'src' / 'constants.rs',
@ -43,3 +43,24 @@ cargo_build = custom_target(
'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@', 'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@',
] ]
) )
test(
'cargo-fmt-check',
cargo,
args: ['fmt', '--all', '--check']
)
test(
'cargo-clippy',
cargo,
env: ['RUSTFLAGS=-Dwarnings'],
args: ['clippy', '--all-targets', '--all-features'],
timeout: 0,
)
test(
'cargo-test',
cargo,
args: ['test'],
timeout: 0,
)

View file

@ -17,7 +17,7 @@ pub fn is_openxr_ready() -> bool {
let Ok(xr_instance) = entry.create_instance( let Ok(xr_instance) = entry.create_instance(
&xr::ApplicationInfo { &xr::ApplicationInfo {
application_name: &format!("{}-openxr-prober", CMD_NAME), application_name: &format!("{CMD_NAME}-openxr-prober"),
application_version: 0, application_version: 0,
engine_name: CMD_NAME, engine_name: CMD_NAME,
engine_version: 0, engine_version: 0,

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::{ use std::{
env, env,
fs::create_dir_all, fs::create_dir_all,
@ -54,6 +56,10 @@ pub fn get_cache_dir() -> PathBuf {
XDG.get_cache_home().join(CMD_NAME) XDG.get_cache_home().join(CMD_NAME)
} }
pub fn get_logs_dir() -> PathBuf {
get_cache_dir().join("logs")
}
pub fn get_backup_dir() -> PathBuf { pub fn get_backup_dir() -> PathBuf {
let p = get_data_dir().join("backups"); let p = get_data_dir().join("backups");
if !p.is_dir() { if !p.is_dir() {
@ -83,7 +89,26 @@ pub fn get_exec_prefix() -> PathBuf {
.into() .into()
} }
pub fn get_steamvr_bin_dir_path() -> PathBuf { const STEAMVR_STEAM_APPID: u32 = 250820;
XDG.get_data_home()
.join("Steam/steamapps/common/SteamVR/bin/linux64") fn get_steamvr_base_dir() -> anyhow::Result<PathBuf> {
SteamLibraryFolder::get_folders()?
.into_iter()
.find(|(_, lf)| lf.apps.contains_key(&STEAMVR_STEAM_APPID))
.map(|(_, lf)| PathBuf::from(lf.path).join("steamapps/common/SteamVR"))
.ok_or(anyhow::Error::msg(
"Could not find SteamVR in Steam libraryfolders.vdf",
))
}
pub fn get_steamvr_bin_dir_path() -> anyhow::Result<PathBuf> {
let res = get_steamvr_base_dir()?.join("bin/linux64");
if !res.is_dir() {
bail!("SteamVR bin dir `{}` does not exist", res.to_string_lossy());
}
Ok(res)
}
pub fn get_plugins_dir() -> PathBuf {
get_data_dir().join("plugins")
} }

View file

@ -2,10 +2,12 @@ use crate::{
depcheck::{ depcheck::{
basalt_deps::get_missing_basalt_deps, libsurvive_deps::get_missing_libsurvive_deps, basalt_deps::get_missing_basalt_deps, libsurvive_deps::get_missing_libsurvive_deps,
mercury_deps::get_missing_mercury_deps, monado_deps::get_missing_monado_deps, mercury_deps::get_missing_mercury_deps, monado_deps::get_missing_monado_deps,
openhmd_deps::get_missing_openhmd_deps, wivrn_deps::get_missing_wivrn_deps, Dependency, openhmd_deps::get_missing_openhmd_deps, wivrn_deps::get_missing_wivrn_deps,
xrizer_deps::get_missing_xrizer_deps, Dependency,
}, },
file_builders::active_runtime_json::ActiveRuntime,
paths::{get_data_dir, BWRAP_SYSTEM_PREFIX, SYSTEM_PREFIX}, paths::{get_data_dir, BWRAP_SYSTEM_PREFIX, SYSTEM_PREFIX},
util::file_utils::get_writer, util::file_utils::{deserialize_file, get_writer},
xdg::XDG, xdg::XDG,
}; };
use nix::NixPath; use nix::NixPath;
@ -13,10 +15,11 @@ use serde::{Deserialize, Serialize};
use std::{ use std::{
collections::HashMap, collections::HashMap,
fmt::Display, fmt::Display,
fs::File, fs::{remove_dir_all, File},
io::BufReader, io::BufReader,
path::{Path, PathBuf}, path::{Path, PathBuf},
slice::Iter, slice::Iter,
str::FromStr,
}; };
use uuid::Uuid; use uuid::Uuid;
@ -43,7 +46,14 @@ impl XRServiceType {
pub fn libmonado_path(&self) -> &'static str { pub fn libmonado_path(&self) -> &'static str {
match self { match self {
Self::Monado => "libmonado.so", Self::Monado => "libmonado.so",
Self::Wivrn => "wivrn/libmonado.so", Self::Wivrn => "wivrn/libmonado_wivrn.so",
}
}
pub fn openxr_json_rel_path(&self) -> &'static str {
match self {
Self::Monado => "share/openxr/1/openxr_monado.json",
Self::Wivrn => "share/openxr/1/openxr_wivrn.json",
} }
} }
@ -251,6 +261,105 @@ impl Display for LighthouseDriver {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum OvrCompatibilityModuleType {
#[default]
Opencomposite,
Xrizer,
Vapor,
}
impl Display for OvrCompatibilityModuleType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Opencomposite => "OpenComposite",
Self::Xrizer => "xrizer",
Self::Vapor => "VapoR",
})
}
}
impl OvrCompatibilityModuleType {
pub fn iter() -> Iter<'static, Self> {
[Self::Opencomposite, Self::Xrizer, Self::Vapor].iter()
}
pub fn get_missing_deps(&self) -> Vec<Dependency> {
match self {
OvrCompatibilityModuleType::Xrizer => get_missing_xrizer_deps(),
OvrCompatibilityModuleType::Opencomposite | OvrCompatibilityModuleType::Vapor => {
Vec::default()
}
}
}
}
impl FromStr for OvrCompatibilityModuleType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().trim() {
"opencomposite" => Ok(Self::Opencomposite),
"xrizer" => Ok(Self::Xrizer),
"vapor" => Ok(Self::Vapor),
_ => Err(format!("no match for ovr compatibility module `{s}`")),
}
}
}
impl From<u32> for OvrCompatibilityModuleType {
fn from(value: u32) -> Self {
match value {
0 => Self::Opencomposite,
1 => Self::Xrizer,
2 => Self::Vapor,
_ => panic!("OvrCompatibilityModuleType index out of bounds"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileOvrCompatibilityModule {
pub mod_type: OvrCompatibilityModuleType,
pub repo: Option<String>,
pub branch: Option<String>,
pub path: PathBuf,
}
impl ProfileOvrCompatibilityModule {
pub fn default_for_uuid(uuid: &str) -> Self {
let mod_type = OvrCompatibilityModuleType::default();
Self {
mod_type,
repo: None,
branch: None,
path: get_data_dir().join(uuid).join(mod_type.to_string()),
}
}
/// get the directory corresponding to the openvr runtime.
/// this should correspond to the build output directory
pub fn runtime_dir(&self) -> PathBuf {
match self.mod_type {
OvrCompatibilityModuleType::Opencomposite => self.path.join("build"),
OvrCompatibilityModuleType::Vapor => self.path.join("build/install_pfx/lib/VapoR"),
OvrCompatibilityModuleType::Xrizer => self.path.join("target/release"),
}
}
}
impl Default for ProfileOvrCompatibilityModule {
fn default() -> Self {
let mod_type = OvrCompatibilityModuleType::default();
Self {
mod_type,
repo: None,
branch: None,
path: get_data_dir().join("__envision__fallbackovrcomp"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Profile { pub struct Profile {
pub uuid: String, pub uuid: String,
@ -261,9 +370,15 @@ pub struct Profile {
pub xrservice_branch: Option<String>, pub xrservice_branch: Option<String>,
#[serde(default = "HashMap::<String, String>::default")] #[serde(default = "HashMap::<String, String>::default")]
pub xrservice_cmake_flags: HashMap<String, String>, pub xrservice_cmake_flags: HashMap<String, String>,
#[deprecated]
#[serde(default)]
pub opencomposite_path: PathBuf, pub opencomposite_path: PathBuf,
#[deprecated]
pub opencomposite_repo: Option<String>, pub opencomposite_repo: Option<String>,
#[deprecated]
pub opencomposite_branch: Option<String>, pub opencomposite_branch: Option<String>,
#[serde(default)]
pub ovr_comp: ProfileOvrCompatibilityModule,
pub features: ProfileFeatures, pub features: ProfileFeatures,
pub environment: HashMap<String, String>, pub environment: HashMap<String, String>,
/// Install prefix /// Install prefix
@ -276,7 +391,6 @@ pub struct Profile {
pub lighthouse_driver: LighthouseDriver, pub lighthouse_driver: LighthouseDriver,
#[serde(default = "String::default")] #[serde(default = "String::default")]
pub xrservice_launch_options: String, pub xrservice_launch_options: String,
pub autostart_command: Option<String>,
#[serde(default)] #[serde(default)]
pub skip_dependency_check: bool, pub skip_dependency_check: bool,
} }
@ -288,6 +402,7 @@ impl Display for Profile {
} }
impl Default for Profile { impl Default for Profile {
#[allow(deprecated)]
fn default() -> Self { fn default() -> Self {
let uuid = Self::new_uuid(); let uuid = Self::new_uuid();
let profile_dir = get_data_dir().join(&uuid); let profile_dir = get_data_dir().join(&uuid);
@ -323,23 +438,50 @@ impl Default for Profile {
mercury_enabled: false, mercury_enabled: false,
}, },
environment: HashMap::new(), environment: HashMap::new(),
prefix: get_data_dir().join("prefixes").join(&uuid), prefix: Self::default_prefix_path(&uuid),
can_be_built: true, can_be_built: true,
pull_on_build: true, pull_on_build: true,
opencomposite_path: profile_dir.join("opencomposite"), opencomposite_path: profile_dir.join("opencomposite"),
opencomposite_repo: None, opencomposite_repo: None,
opencomposite_branch: None, opencomposite_branch: None,
ovr_comp: ProfileOvrCompatibilityModule::default_for_uuid(&uuid),
editable: true, editable: true,
lighthouse_driver: LighthouseDriver::default(), lighthouse_driver: LighthouseDriver::default(),
xrservice_launch_options: String::default(), xrservice_launch_options: String::default(),
uuid, uuid,
autostart_command: None,
skip_dependency_check: false, skip_dependency_check: false,
} }
} }
} }
impl Profile { impl Profile {
fn default_prefix_path(uuid: &str) -> PathBuf {
get_data_dir().join("prefixes").join(uuid)
}
/// deletes files and folders associated to this profile (mostly repo clones)
pub fn delete_files(&self) -> Vec<std::io::Result<()>> {
[
Some(&self.xrservice_path),
Some(&self.ovr_comp.path),
self.features.libsurvive.path.as_ref(),
self.features.basalt.path.as_ref(),
self.features.openhmd.path.as_ref(),
]
.iter()
.map(|dir| match dir {
Some(dir) => {
if dir.try_exists().unwrap_or_default() {
remove_dir_all(dir)
} else {
Ok(())
}
}
None => Ok(()),
})
.collect()
}
pub fn xr_runtime_json_env_var(&self) -> String { pub fn xr_runtime_json_env_var(&self) -> String {
format!( format!(
"XR_RUNTIME_JSON=\"{prefix}/share/openxr/1/openxr_{runtime}.json\"", "XR_RUNTIME_JSON=\"{prefix}/share/openxr/1/openxr_{runtime}.json\"",
@ -358,8 +500,8 @@ impl Profile {
pub fn env_vars_full(&self) -> Vec<String> { pub fn env_vars_full(&self) -> Vec<String> {
vec![ vec![
// format!( // format!(
// "VR_OVERRIDE={opencomp}/build", // "VR_OVERRIDE={}",
// opencomp = self.opencomposite_path, // self.ovr_comp.runtime_dir(),
// ), // ),
self.xr_runtime_json_env_var(), self.xr_runtime_json_env_var(),
format!( format!(
@ -417,8 +559,8 @@ impl Profile {
} }
let uuid = Self::new_uuid(); let uuid = Self::new_uuid();
let profile_dir = get_data_dir().join(&uuid); let profile_dir = get_data_dir().join(&uuid);
#[allow(deprecated)]
let mut dup = Self { let mut dup = Self {
uuid,
name: format!("Duplicate of {}", self.name), name: format!("Duplicate of {}", self.name),
xrservice_type: self.xrservice_type.clone(), xrservice_type: self.xrservice_type.clone(),
xrservice_repo: self.xrservice_repo.clone(), xrservice_repo: self.xrservice_repo.clone(),
@ -450,7 +592,6 @@ impl Profile {
mercury_enabled: self.features.mercury_enabled, mercury_enabled: self.features.mercury_enabled,
}, },
environment: self.environment.clone(), environment: self.environment.clone(),
autostart_command: self.autostart_command.clone(),
pull_on_build: self.pull_on_build, pull_on_build: self.pull_on_build,
lighthouse_driver: self.lighthouse_driver, lighthouse_driver: self.lighthouse_driver,
opencomposite_repo: self.opencomposite_repo.clone(), opencomposite_repo: self.opencomposite_repo.clone(),
@ -458,7 +599,16 @@ impl Profile {
opencomposite_path: profile_dir.join("opencomposite"), opencomposite_path: profile_dir.join("opencomposite"),
skip_dependency_check: self.skip_dependency_check, skip_dependency_check: self.skip_dependency_check,
xrservice_launch_options: self.xrservice_launch_options.clone(), xrservice_launch_options: self.xrservice_launch_options.clone(),
..Default::default() prefix: Self::default_prefix_path(&uuid),
ovr_comp: ProfileOvrCompatibilityModule {
mod_type: self.ovr_comp.mod_type,
repo: self.ovr_comp.repo.clone(),
branch: self.ovr_comp.branch.clone(),
path: profile_dir.join(self.ovr_comp.mod_type.to_string()),
},
can_be_built: self.can_be_built,
editable: true,
uuid,
}; };
if dup.environment.contains_key("LD_LIBRARY_PATH") { if dup.environment.contains_key("LD_LIBRARY_PATH") {
dup.environment.insert( dup.environment.insert(
@ -544,21 +694,37 @@ impl Profile {
} }
/// absolute path to a given shared object in the profile prefix /// absolute path to a given shared object in the profile prefix
pub fn find_so(&self, rel_path: &str) -> Option<PathBuf> { pub fn find_so<P: AsRef<Path>>(&self, rel_path: P) -> Option<PathBuf> {
["lib", "lib64"] ["lib", "lib64"]
.into_iter() .into_iter()
.map(|lib| self.prefix.join(lib).join(rel_path)) .map(|lib| self.prefix.join(lib).join(rel_path.as_ref()))
.find(|path| path.is_file()) .find(|path| path.is_file())
} }
/// absolute path to the libmonado shared object /// absolute path to the libmonado shared object
pub fn libmonado_so(&self) -> Option<PathBuf> { pub fn libmonado_so(&self) -> Option<PathBuf> {
self.find_so(self.xrservice_type.libmonado_path()) // try by reading the openxr json file
self.openxr_config()
.and_then(|conf| conf.runtime.libmonado_path)
.and_then(|libmonado_path| self.find_so(&libmonado_path))
.or_else(||
// try with the hardcoded paths
self.find_so(self.xrservice_type.libmonado_path()))
}
fn openxr_config(&self) -> Option<ActiveRuntime> {
deserialize_file(&self.openxr_json_path())
} }
/// absolute path to the libopenxr shared object /// absolute path to the libopenxr shared object
pub fn libopenxr_so(&self) -> Option<PathBuf> { pub fn libopenxr_so(&self) -> Option<PathBuf> {
self.find_so(self.xrservice_type.libopenxr_path()) // try by reading the openxr json file
self.openxr_config()
.map(|conf| conf.runtime.library_path)
.and_then(|libmonado_path| self.find_so(&libmonado_path))
.or_else(||
// try with the hardcoded paths
self.find_so(self.xrservice_type.libopenxr_path()))
} }
pub fn missing_dependencies(&self) -> Vec<Dependency> { pub fn missing_dependencies(&self) -> Vec<Dependency> {
@ -580,12 +746,18 @@ impl Profile {
if self.features.mercury_enabled { if self.features.mercury_enabled {
missing_deps.extend(get_missing_mercury_deps()); missing_deps.extend(get_missing_mercury_deps());
} }
// no listed deps for opencomp missing_deps.extend(self.ovr_comp.mod_type.get_missing_deps());
} }
missing_deps.sort_unstable(); missing_deps.sort_unstable();
missing_deps.dedup(); // dedup only works if sorted, hence the above missing_deps.dedup(); // dedup only works if sorted, hence the above
missing_deps missing_deps
} }
/// the file that will become active_runtime.json, as installed in the
/// prefix
pub fn openxr_json_path(&self) -> PathBuf {
self.prefix.join(self.xrservice_type.openxr_json_rel_path())
}
} }
pub fn prepare_ld_library_path(prefix: &Path) -> String { pub fn prepare_ld_library_path(prefix: &Path) -> String {
@ -599,7 +771,10 @@ mod tests {
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use crate::profile::{ProfileFeature, ProfileFeatureType, ProfileFeatures, XRServiceType}; use crate::profile::{
OvrCompatibilityModuleType, ProfileFeature, ProfileFeatureType, ProfileFeatures,
ProfileOvrCompatibilityModule, XRServiceType,
};
use super::Profile; use super::Profile;
@ -609,7 +784,7 @@ mod tests {
assert_eq!(profile.name, "Demo profile"); assert_eq!(profile.name, "Demo profile");
assert_eq!(profile.xrservice_path, PathBuf::from("/home/user/monado")); assert_eq!(profile.xrservice_path, PathBuf::from("/home/user/monado"));
assert_eq!( assert_eq!(
profile.opencomposite_path, profile.ovr_comp.path,
PathBuf::from("/home/user/opencomposite") PathBuf::from("/home/user/opencomposite")
); );
assert_eq!(profile.prefix, PathBuf::from("/home/user/envisionprefix")); assert_eq!(profile.prefix, PathBuf::from("/home/user/envisionprefix"));
@ -640,7 +815,12 @@ mod tests {
name: "Demo profile".into(), name: "Demo profile".into(),
xrservice_path: PathBuf::from("/home/user/monado"), xrservice_path: PathBuf::from("/home/user/monado"),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,
opencomposite_path: PathBuf::from("/home/user/opencomposite"), ovr_comp: ProfileOvrCompatibilityModule {
path: PathBuf::from("/home/user/opencomposite"),
repo: None,
branch: None,
mod_type: OvrCompatibilityModuleType::default(),
},
features: ProfileFeatures { features: ProfileFeatures {
libsurvive: ProfileFeature { libsurvive: ProfileFeature {
feature_type: ProfileFeatureType::Libsurvive, feature_type: ProfileFeatureType::Libsurvive,

View file

@ -1,7 +1,10 @@
use crate::{ use crate::{
constants::APP_NAME, constants::APP_NAME,
paths::{data_monado_path, data_opencomposite_path, get_data_dir}, paths::{data_monado_path, data_opencomposite_path, get_data_dir},
profile::{prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeatures, XRServiceType}, profile::{
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeatures,
ProfileOvrCompatibilityModule, XRServiceType,
},
}; };
use std::collections::HashMap; use std::collections::HashMap;
@ -18,10 +21,13 @@ pub fn lighthouse_profile() -> Profile {
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix)); environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
Profile { Profile {
uuid: "lighthouse-default".into(), uuid: "lighthouse-default".into(),
name: format!("Lighthouse Driver - {name} Default", name = APP_NAME), name: format!("Lighthouse Driver - {APP_NAME} Default"),
xrservice_path: data_monado_path(), xrservice_path: data_monado_path(),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,
opencomposite_path: data_opencomposite_path(), ovr_comp: ProfileOvrCompatibilityModule {
path: data_opencomposite_path(),
..Default::default()
},
features: ProfileFeatures::default(), features: ProfileFeatures::default(),
environment, environment,
prefix, prefix,

View file

@ -3,7 +3,7 @@ use crate::{
paths::{data_monado_path, data_opencomposite_path, data_openhmd_path, get_data_dir}, paths::{data_monado_path, data_opencomposite_path, data_openhmd_path, get_data_dir},
profile::{ profile::{
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType, prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
ProfileFeatures, XRServiceType, ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType,
}, },
}; };
use std::collections::HashMap; use std::collections::HashMap;
@ -21,10 +21,13 @@ pub fn openhmd_profile() -> Profile {
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix)); environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
Profile { Profile {
uuid: "openhmd-default".into(), uuid: "openhmd-default".into(),
name: format!("OpenHMD - {name} Default", name = APP_NAME), name: format!("OpenHMD - {APP_NAME} Default"),
xrservice_path: data_monado_path(), xrservice_path: data_monado_path(),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,
opencomposite_path: data_opencomposite_path(), ovr_comp: ProfileOvrCompatibilityModule {
path: data_opencomposite_path(),
..Default::default()
},
features: ProfileFeatures { features: ProfileFeatures {
openhmd: ProfileFeature { openhmd: ProfileFeature {
feature_type: ProfileFeatureType::OpenHmd, feature_type: ProfileFeatureType::OpenHmd,

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
constants::APP_NAME, constants::APP_NAME,
paths::{data_monado_path, data_opencomposite_path, get_data_dir}, paths::{data_monado_path, data_opencomposite_path, get_data_dir},
profile::{Profile, ProfileFeatures, XRServiceType}, profile::{Profile, ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType},
}; };
use std::collections::HashMap; use std::collections::HashMap;
@ -22,10 +22,13 @@ pub fn simulated_profile() -> Profile {
); );
Profile { Profile {
uuid: "simulated-default".into(), uuid: "simulated-default".into(),
name: format!("Simulated Driver - {name} Default", name = APP_NAME), name: format!("Simulated Driver - {APP_NAME} Default"),
xrservice_path: data_monado_path(), xrservice_path: data_monado_path(),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,
opencomposite_path: data_opencomposite_path(), ovr_comp: ProfileOvrCompatibilityModule {
path: data_opencomposite_path(),
..Default::default()
},
features: ProfileFeatures::default(), features: ProfileFeatures::default(),
environment, environment,
prefix, prefix,

View file

@ -3,7 +3,7 @@ use crate::{
paths::{data_libsurvive_path, data_monado_path, data_opencomposite_path, get_data_dir}, paths::{data_libsurvive_path, data_monado_path, data_opencomposite_path, get_data_dir},
profile::{ profile::{
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType, prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
ProfileFeatures, XRServiceType, ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType,
}, },
}; };
use std::collections::HashMap; use std::collections::HashMap;
@ -23,10 +23,13 @@ pub fn survive_profile() -> Profile {
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix)); environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
Profile { Profile {
uuid: "survive-default".into(), uuid: "survive-default".into(),
name: format!("Survive - {name} Default", name = APP_NAME), name: format!("Survive - {APP_NAME} Default"),
xrservice_path: data_monado_path(), xrservice_path: data_monado_path(),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,
opencomposite_path: data_opencomposite_path(), ovr_comp: ProfileOvrCompatibilityModule {
path: data_opencomposite_path(),
..Default::default()
},
features: ProfileFeatures { features: ProfileFeatures {
libsurvive: ProfileFeature { libsurvive: ProfileFeature {
feature_type: ProfileFeatureType::Libsurvive, feature_type: ProfileFeatureType::Libsurvive,

View file

@ -1,7 +1,11 @@
use crate::{ use crate::{
constants::APP_NAME, constants::APP_NAME,
paths::{data_opencomposite_path, data_wivrn_path, get_data_dir}, paths::{data_opencomposite_path, data_wivrn_path, get_data_dir},
profile::{prepare_ld_library_path, Profile, ProfileFeatures, XRServiceType}, profile::{
prepare_ld_library_path, Profile, ProfileFeatures, ProfileOvrCompatibilityModule,
XRServiceType,
},
ui::job_worker::internal_worker::LAUNCH_OPTS_CMD_PLACEHOLDER,
}; };
use std::collections::HashMap; use std::collections::HashMap;
@ -15,13 +19,19 @@ pub fn wivrn_profile() -> Profile {
environment.insert("U_PACING_APP_USE_MIN_FRAME_PERIOD".into(), "1".into()); environment.insert("U_PACING_APP_USE_MIN_FRAME_PERIOD".into(), "1".into());
Profile { Profile {
uuid: "wivrn-default".into(), uuid: "wivrn-default".into(),
name: format!("WiVRn - {name} Default", name = APP_NAME), name: format!("WiVRn - {APP_NAME} Default"),
xrservice_path: data_wivrn_path(), xrservice_path: data_wivrn_path(),
xrservice_type: XRServiceType::Wivrn, xrservice_type: XRServiceType::Wivrn,
opencomposite_path: data_opencomposite_path(), ovr_comp: ProfileOvrCompatibilityModule {
path: data_opencomposite_path(),
..Default::default()
},
features: ProfileFeatures { features: ProfileFeatures {
..Default::default() ..Default::default()
}, },
xrservice_launch_options: format!(
"{LAUNCH_OPTS_CMD_PLACEHOLDER} --no-instructions --no-manage-active-runtime"
),
environment, environment,
prefix, prefix,
can_be_built: true, can_be_built: true,

View file

@ -3,7 +3,7 @@ use crate::{
paths::{data_basalt_path, data_monado_path, data_opencomposite_path, get_data_dir}, paths::{data_basalt_path, data_monado_path, data_opencomposite_path, get_data_dir},
profile::{ profile::{
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType, prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
ProfileFeatures, XRServiceType, ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType,
}, },
}; };
use std::collections::HashMap; use std::collections::HashMap;
@ -21,10 +21,13 @@ pub fn wmr_profile() -> Profile {
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix)); environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
Profile { Profile {
uuid: "wmr-default".into(), uuid: "wmr-default".into(),
name: format!("WMR - {name} Default", name = APP_NAME), name: format!("WMR - {APP_NAME} Default"),
xrservice_path: data_monado_path(), xrservice_path: data_monado_path(),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,
opencomposite_path: data_opencomposite_path(), ovr_comp: ProfileOvrCompatibilityModule {
path: data_opencomposite_path(),
..Default::default()
},
features: ProfileFeatures { features: ProfileFeatures {
basalt: ProfileFeature { basalt: ProfileFeature {
feature_type: ProfileFeatureType::Basalt, feature_type: ProfileFeatureType::Basalt,

View file

@ -1,101 +1,99 @@
use crate::{ use crate::{
paths::{get_backup_dir, get_home_dir}, paths::get_backup_dir,
profile::Profile, profile::Profile,
util::file_utils::{copy_file, get_writer}, util::{
file_utils::{copy_file, get_writer, mark_as_executable},
steam_library_folder::SteamLibraryFolder,
},
}; };
use anyhow::bail; use anyhow::bail;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::Deserialize;
use std::{ use std::{
collections::HashMap,
fs::read_to_string, fs::read_to_string,
io::Write, io::Write,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use tracing::error;
#[derive(Deserialize)] pub const SNIPER_RUNTIME_STEAM_APPID: u32 = 1628350;
struct LibraryFolder { pub const SOLDIER_RUNTIME_STEAM_APPID: u32 = 1391110;
pub path: String,
pub apps: HashMap<u32, usize>,
}
pub const PRESSURE_VESSEL_STEAM_APPID: u32 = 1628350; fn get_sniper_runtime_entrypoint_path() -> Option<PathBuf> {
match SteamLibraryFolder::get_folders() {
fn get_steam_main_dir_path() -> anyhow::Result<PathBuf> { Ok(libraryfolders) => libraryfolders
let steam_root: PathBuf = get_home_dir().join(".steam/root"); .iter()
.find(|(_, folder)| folder.apps.contains_key(&SNIPER_RUNTIME_STEAM_APPID))
if steam_root.is_symlink() { .map(|(_, folder)| {
Ok(steam_root.read_link()?) PathBuf::from(&folder.path)
} else if steam_root.is_dir() { .join("steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point")
Ok(steam_root) }),
} else { Err(e) => {
bail!( error!("unable to get sniper runtime entrypoint path: {e}");
"Canonical steam root '{}' is not a dir nor a symlink!", None
steam_root.to_string_lossy() }
)
} }
} }
fn parse_steam_libraryfolders_vdf(path: &Path) -> anyhow::Result<HashMap<u32, LibraryFolder>> { fn get_soldier_runtime_entrypoint_path() -> Option<PathBuf> {
Ok(keyvalues_serde::from_str(read_to_string(path)?.as_str())?) match SteamLibraryFolder::get_folders() {
} Ok(libraryfolders) => libraryfolders
.iter()
fn get_runtime_entrypoint_path() -> Option<PathBuf> { .find(|(_, folder)| folder.apps.contains_key(&SOLDIER_RUNTIME_STEAM_APPID))
match get_steam_main_dir_path() { .map(|(_, folder)| {
Ok(steam_root) => { PathBuf::from(&folder.path)
let steam_libraryfolders_path = steam_root.join("steamapps/libraryfolders.vdf"); .join("steamapps/common/SteamLinuxRuntime_soldier/_v2-entry-point")
}),
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")
})
}
Err(e) => { Err(e) => {
eprintln!("Error getting steam root path: {e}"); error!("unable to get soldier runtime entrypoint path: {e}");
None None
} }
} }
} }
lazy_static! { lazy_static! {
static ref STEAM_RUNTIME_ENTRYPOINT_PATH: Option<PathBuf> = get_runtime_entrypoint_path(); static ref STEAM_SNIPER_RUNTIME_ENTRYPOINT_PATH: Option<PathBuf> =
get_sniper_runtime_entrypoint_path();
static ref STEAM_SOLDIER_RUNTIME_ENTRYPOINT_PATH: Option<PathBuf> =
get_soldier_runtime_entrypoint_path();
} }
fn get_backup_runtime_entrypoint_location() -> PathBuf { fn get_backup_sniper_runtime_entrypoint_location() -> PathBuf {
get_backup_dir().join("_v2-entry-point.bak") get_backup_dir().join("_v2-entry-point.bak")
} }
fn backup_runtime_entrypoint(path: &Path) { fn get_backup_soldier_runtime_entrypoint_location() -> PathBuf {
copy_file(path, &get_backup_runtime_entrypoint_location()); get_backup_dir().join("_v2-entry-point.soldier.bak")
} }
pub fn restore_runtime_entrypoint() { fn backup_sniper_runtime_entrypoint(path: &Path) {
if let Some(path) = STEAM_RUNTIME_ENTRYPOINT_PATH.as_ref() { copy_file(path, &get_backup_sniper_runtime_entrypoint_location());
let backup = get_backup_runtime_entrypoint_location(); }
fn backup_soldier_runtime_entrypoint(path: &Path) {
copy_file(path, &get_backup_soldier_runtime_entrypoint_location());
}
pub fn restore_sniper_runtime_entrypoint() {
if let Some(path) = STEAM_SNIPER_RUNTIME_ENTRYPOINT_PATH.as_ref() {
let backup = get_backup_sniper_runtime_entrypoint_location();
if Path::new(&backup).is_file() { if Path::new(&backup).is_file() {
copy_file(&backup, path); copy_file(&backup, path);
let _ = mark_as_executable(path);
} }
} }
} }
pub fn restore_soldier_runtime_entrypoint() {
if let Some(path) = STEAM_SOLDIER_RUNTIME_ENTRYPOINT_PATH.as_ref() {
let backup = get_backup_soldier_runtime_entrypoint_location();
if Path::new(&backup).is_file() {
copy_file(&backup, path);
let _ = mark_as_executable(path);
}
}
}
/// this implementation is identical for both sniper and soldier runtimes
fn append_to_runtime_entrypoint(data: &str, path: &Path) -> anyhow::Result<()> { fn append_to_runtime_entrypoint(data: &str, path: &Path) -> anyhow::Result<()> {
let existing = read_to_string(path)?; let existing = read_to_string(path)?;
let new = existing.replace( let new = existing.replace(
@ -107,10 +105,12 @@ fn append_to_runtime_entrypoint(data: &str, path: &Path) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
pub fn set_runtime_entrypoint_launch_opts_from_profile(profile: &Profile) -> anyhow::Result<()> { pub fn set_sniper_runtime_entrypoint_launch_opts_from_profile(
restore_runtime_entrypoint(); profile: &Profile,
if let Some(dest) = STEAM_RUNTIME_ENTRYPOINT_PATH.as_ref() { ) -> anyhow::Result<()> {
backup_runtime_entrypoint(dest); restore_sniper_runtime_entrypoint();
if let Some(dest) = STEAM_SNIPER_RUNTIME_ENTRYPOINT_PATH.as_ref() {
backup_sniper_runtime_entrypoint(dest);
append_to_runtime_entrypoint( append_to_runtime_entrypoint(
&profile &profile
.get_env_vars() .get_env_vars()
@ -120,27 +120,31 @@ pub fn set_runtime_entrypoint_launch_opts_from_profile(profile: &Profile) -> any
.join("\n"), .join("\n"),
dest, dest,
)?; )?;
mark_as_executable(dest)?;
return Ok(()); return Ok(());
} }
bail!("Could not find valid runtime entrypoint"); bail!("Could not find valid sniper runtime entrypoint");
} }
#[cfg(test)] pub fn set_soldier_runtime_entrypoint_launch_opts_from_profile(
mod tests { profile: &Profile,
use std::path::Path; ) -> anyhow::Result<()> {
restore_soldier_runtime_entrypoint();
if let Some(dest) = STEAM_SOLDIER_RUNTIME_ENTRYPOINT_PATH.as_ref() {
backup_soldier_runtime_entrypoint(dest);
append_to_runtime_entrypoint(
&profile
.get_env_vars()
.iter()
.map(|ev| "export ".to_string() + ev)
.collect::<Vec<String>>()
.join("\n"),
dest,
)?;
mark_as_executable(dest)?;
use super::parse_steam_libraryfolders_vdf; return Ok(());
#[test]
fn deserialize_steam_libraryfolders_vdf() {
let lf = parse_steam_libraryfolders_vdf(Path::new("./test/files/steam_libraryfolders.vdf"))
.unwrap();
assert_eq!(lf.len(), 1);
let first = lf.get(&0).unwrap();
assert_eq!(first.path, "/home/gabmus/.local/share/Steam");
assert_eq!(first.apps.len(), 10);
assert_eq!(first.apps.get(&228980).unwrap(), &29212173);
assert_eq!(first.apps.get(&632360).unwrap(), &0);
} }
bail!("Could not find valid soldier runtime entrypoint");
} }

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
constants::{ constants::{
get_artists, get_developers, APP_ID, APP_NAME, BUILD_DATETIME, ISSUES_URL, REPO_URL, get_artists, APP_ID, APP_NAME, BUILD_DATETIME, ISSUES_URL, REPO_URL, SINGLE_DEVELOPER,
SINGLE_DEVELOPER, VERSION, VERSION,
}, },
device_prober::PhysicalXRDevice, device_prober::PhysicalXRDevice,
linux_distro::LinuxDistro, linux_distro::LinuxDistro,
@ -20,13 +20,20 @@ pub fn create_about_dialog() -> adw::AboutDialog {
.website(REPO_URL) .website(REPO_URL)
.issue_url(ISSUES_URL) .issue_url(ISSUES_URL)
.developer_name(SINGLE_DEVELOPER) .developer_name(SINGLE_DEVELOPER)
.developers(get_developers()) .developers(
env!("CARGO_PKG_AUTHORS")
.split(':')
.map(|s| s.to_string())
.collect::<Vec<String>>(),
)
.artists(get_artists()) .artists(get_artists())
.build() .build()
} }
const UNKNOWN: &str = "UNKNOWN";
pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo>) { pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo>) {
if dialog.debug_info().len() > 0 { if !dialog.debug_info().is_empty() {
return; return;
} }
let distro_family = LinuxDistro::get(); let distro_family = LinuxDistro::get();
@ -37,10 +44,10 @@ pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo
format!("Build time: {BUILD_DATETIME}"), format!("Build time: {BUILD_DATETIME}"),
format!( format!(
"Operating system: {d} ({f})", "Operating system: {d} ({f})",
d = distro.unwrap_or("unknown".into()), d = distro.unwrap_or(UNKNOWN.into()),
f = distro_family f = distro_family
.map(|f| f.to_string()) .map(|f| f.to_string())
.unwrap_or("unknown".into()) .unwrap_or(UNKNOWN.into())
), ),
format!( format!(
"Kernel: {}", "Kernel: {}",
@ -50,23 +57,29 @@ pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo
), ),
format!( format!(
"Session type: {}", "Session type: {}",
env::var("XDG_SESSION_TYPE").unwrap_or("unknown".into()) env::var("XDG_SESSION_TYPE").unwrap_or(UNKNOWN.into())
), ),
format!( format!(
"Desktop: {}", "Desktop: {}",
env::var("XDG_CURRENT_DESKTOP").unwrap_or("unknown".into()) env::var("XDG_CURRENT_DESKTOP").unwrap_or(UNKNOWN.into())
),
format!(
"CPU: {}",
read_to_string("/proc/cpuinfo")
.ok()
.and_then(|s| {
s.split("\n")
.find(|line| line.starts_with("model name"))
.map(|line| line.split(':').next_back().map(|s| s.trim().to_string()))
})
.flatten()
.unwrap_or(UNKNOWN.into())
), ),
format!( format!(
"GPUs: {}", "GPUs: {}",
vkinfo vkinfo
.map(|i| i.gpu_names.join(", ")) .map(|i| i.gpu_names.join(", "))
.unwrap_or("unknown".into()) .unwrap_or(UNKNOWN.into())
),
format!(
"Monado Vulkan Layers: {}",
vkinfo
.map(|i| i.has_monado_vulkan_layers.to_string())
.unwrap_or("unknown".into())
), ),
format!("Detected XR Devices: {}", { format!("Detected XR Devices: {}", {
let devs = PhysicalXRDevice::from_usb(); let devs = PhysicalXRDevice::from_usb();

View file

@ -11,6 +11,7 @@ use super::{
}, },
libsurvive_setup_window::{LibsurviveSetupMsg, LibsurviveSetupWindow}, libsurvive_setup_window::{LibsurviveSetupMsg, LibsurviveSetupWindow},
main_view::{MainView, MainViewInit, MainViewMsg, MainViewOutMsg}, main_view::{MainView, MainViewInit, MainViewMsg, MainViewOutMsg},
plugins::store::{PluginStore, PluginStoreInit, PluginStoreMsg, PluginStoreOutMsg},
util::{copiable_code_snippet, copy_text, open_with_default_handler}, util::{copiable_code_snippet, copy_text, open_with_default_handler},
wivrn_conf_editor::{WivrnConfEditor, WivrnConfEditorInit, WivrnConfEditorMsg}, wivrn_conf_editor::{WivrnConfEditor, WivrnConfEditorInit, WivrnConfEditorMsg},
}; };
@ -19,14 +20,16 @@ use crate::{
build_basalt::get_build_basalt_jobs, build_libsurvive::get_build_libsurvive_jobs, build_basalt::get_build_basalt_jobs, build_libsurvive::get_build_libsurvive_jobs,
build_mercury::get_build_mercury_jobs, build_monado::get_build_monado_jobs, build_mercury::get_build_mercury_jobs, build_monado::get_build_monado_jobs,
build_opencomposite::get_build_opencomposite_jobs, build_openhmd::get_build_openhmd_jobs, build_opencomposite::get_build_opencomposite_jobs, build_openhmd::get_build_openhmd_jobs,
build_wivrn::get_build_wivrn_jobs, build_vapor::get_build_vapor_jobs, build_wivrn::get_build_wivrn_jobs,
build_xrizer::get_build_xrizer_jobs,
}, },
config::Config, config::{Config, PluginConfig},
constants::APP_NAME, constants::APP_NAME,
depcheck::common::dep_pkexec, depcheck::common::dep_pkexec,
file_builders::{ file_builders::{
active_runtime_json::{ active_runtime_json::{
set_current_active_runtime_to_profile, set_current_active_runtime_to_steam, remove_current_active_runtime, restore_active_runtime_backup,
set_current_active_runtime_to_profile,
}, },
openvrpaths_vrpath::{ openvrpaths_vrpath::{
set_current_openvrpaths_to_profile, set_current_openvrpaths_to_steam, set_current_openvrpaths_to_profile, set_current_openvrpaths_to_steam,
@ -35,17 +38,23 @@ use crate::{
linux_distro::LinuxDistro, linux_distro::LinuxDistro,
openxr_prober::is_openxr_ready, openxr_prober::is_openxr_ready,
paths::get_data_dir, paths::get_data_dir,
profile::{Profile, XRServiceType}, profile::{OvrCompatibilityModuleType, Profile, XRServiceType},
stateless_action, stateless_action,
steam_linux_runtime_injector::{ steam_linux_runtime_injector::{
restore_runtime_entrypoint, set_runtime_entrypoint_launch_opts_from_profile, restore_sniper_runtime_entrypoint, restore_soldier_runtime_entrypoint,
set_sniper_runtime_entrypoint_launch_opts_from_profile,
set_soldier_runtime_entrypoint_launch_opts_from_profile,
},
termcolor::TermColor,
util::file_utils::{
setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd, verify_cap_sys_nice_eip,
}, },
util::file_utils::{setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd},
vulkaninfo::VulkanInfo, vulkaninfo::VulkanInfo,
wivrn_dbus, wivrn_dbus,
xr_devices::XRDevice, xr_devices::XRDevice,
}; };
use adw::{prelude::*, ResponseAppearance}; use adw::{prelude::*, ResponseAppearance};
use delicious_adwaita::{theme::Theme, ThemeEngine};
use gtk::glib::{self, clone}; use gtk::glib::{self, clone};
use notify_rust::NotificationHandle; use notify_rust::NotificationHandle;
use relm4::{ use relm4::{
@ -53,7 +62,12 @@ use relm4::{
new_action_group, new_stateful_action, new_stateless_action, new_action_group, new_stateful_action, new_stateless_action,
prelude::*, prelude::*,
}; };
use std::{collections::VecDeque, fs::remove_file, time::Duration}; use std::{
collections::{HashMap, VecDeque},
fs::remove_file,
time::Duration,
};
use tracing::error;
pub struct App { pub struct App {
application: adw::Application, application: adw::Application,
@ -70,7 +84,7 @@ pub struct App {
config: Config, config: Config,
xrservice_worker: Option<JobWorker>, xrservice_worker: Option<JobWorker>,
autostart_worker: Option<JobWorker>, plugins_worker: Option<JobWorker>,
restart_xrservice: bool, restart_xrservice: bool,
build_worker: Option<JobWorker>, build_worker: Option<JobWorker>,
profiles: Vec<Profile>, profiles: Vec<Profile>,
@ -85,13 +99,16 @@ pub struct App {
vkinfo: Option<VulkanInfo>, vkinfo: Option<VulkanInfo>,
inhibit_fail_notif: Option<NotificationHandle>, inhibit_fail_notif: Option<NotificationHandle>,
pluginstore: Option<AsyncController<PluginStore>>,
theme_engine: ThemeEngine,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum Msg { pub enum Msg {
OnServiceLog(Vec<String>), OnServiceLog(Vec<String>),
OnServiceExit(i32), OnServiceExit(i32),
OnAutostartExit(i32), OnPluginsExit(i32),
OnBuildLog(Vec<String>), OnBuildLog(Vec<String>),
OnBuildExit(i32), OnBuildExit(i32),
ClockTicking, ClockTicking,
@ -102,7 +119,8 @@ pub enum Msg {
StartWithDebug, StartWithDebug,
RestartXRService, RestartXRService,
ProfileSelected(Profile), ProfileSelected(Profile),
DeleteProfile, /// bool param: delete files
DeleteProfile(bool),
SaveProfile(Profile), SaveProfile(Profile),
RunSetCap, RunSetCap,
OpenLibsurviveSetup, OpenLibsurviveSetup,
@ -116,6 +134,10 @@ pub enum Msg {
StartProber, StartProber,
OnProberExit(bool), OnProberExit(bool),
WivrnCheckPairMode, WivrnCheckPairMode,
OpenPluginStore,
UpdateConfigPlugins(HashMap<String, PluginConfig>),
ShowThemeManager,
SaveThemeConfig,
NoOp, NoOp,
} }
@ -147,7 +169,7 @@ impl App {
} { } {
Ok(n) => Some(n), Ok(n) => Some(n),
Err(e) => { Err(e) => {
eprintln!("Failed to send desktop notification: {e:?}"); error!("failed to send desktop notification: {e:?}");
None None
} }
} }
@ -163,57 +185,7 @@ impl App {
pub fn start_xrservice(&mut self, sender: AsyncComponentSender<Self>, debug: bool) { pub fn start_xrservice(&mut self, sender: AsyncComponentSender<Self>, debug: bool) {
self.xrservice_ready = false; self.xrservice_ready = false;
let prof = self.get_selected_profile(); let prof = self.get_selected_profile();
if prof.can_start() { if !prof.can_start() {
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 {
alert( alert(
"Failed to start profile", "Failed to start profile",
Some(concat!( Some(concat!(
@ -222,32 +194,134 @@ impl App {
)), )),
Some(&self.app_win.clone().upcast::<gtk::Window>()), 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);
let set_sniper_launch_opts_res =
set_sniper_runtime_entrypoint_launch_opts_from_profile(&prof);
let set_soldier_launch_opts_res =
set_soldier_runtime_entrypoint_launch_opts_from_profile(&prof);
self.main_view
.sender()
.emit(MainViewMsg::XRServiceActiveChanged(
true,
Some(self.get_selected_profile()),
// show launch opts only if setting the runtime entrypoint fails
set_sniper_launch_opts_res.is_err() || set_soldier_launch_opts_res.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>) { pub fn run_autostart(&mut self, sender: AsyncComponentSender<Self>) {
let prof = self.get_selected_profile(); let prof = self.get_selected_profile();
if let Some(autostart_cmd) = &prof.autostart_command { let plugins_cmd = self
.config
.plugins
.values()
.filter_map(|cp| {
if cp.enabled && cp.plugin.validate() {
if let Err(e) = cp.plugin.mark_as_executable() {
error!(
"failed to mark plugin {} as executable: {e}",
cp.plugin.appid
);
None
} else if !cp.plugin.plugin_type.launches_directly() {
None
} else {
Some({
let mut cmd_parts = vec![cp
.plugin
.executable()
.unwrap()
.to_string_lossy()
.to_string()];
cmd_parts.extend(cp.plugin.args.clone().unwrap_or_default());
cmd_parts
.iter()
.map(|part| format!("'{part}'"))
.collect::<Vec<String>>()
.join(" ")
})
}
} else {
None
}
})
.collect::<Vec<String>>()
.join(" & ");
if !plugins_cmd.is_empty() {
let mut jobs = VecDeque::new(); let mut jobs = VecDeque::new();
jobs.push_back(WorkerJob::new_cmd( jobs.push_back(WorkerJob::new_cmd(
Some(prof.environment.clone()), Some(prof.environment.clone()),
"sh".into(), "sh".into(),
Some(vec!["-c".into(), autostart_cmd.clone()]), Some(vec!["-c".into(), plugins_cmd]),
)); ));
let autostart_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg { let plugins_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows), JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows),
JobWorkerOut::Exit(code) => Msg::OnAutostartExit(code), JobWorkerOut::Exit(code) => Msg::OnPluginsExit(code),
}); });
autostart_worker.start(); plugins_worker.start();
self.autostart_worker = Some(autostart_worker); self.plugins_worker = Some(plugins_worker);
} }
} }
pub fn restore_openxr_openvr_files(&self) { pub fn restore_openxr_openvr_files(&self) {
restore_runtime_entrypoint(); restore_sniper_runtime_entrypoint();
if let Err(e) = set_current_active_runtime_to_steam() { restore_soldier_runtime_entrypoint();
if let Err(e) = remove_current_active_runtime() {
alert( alert(
"Could not restore Steam active runtime", "Could not remove profile active runtime",
Some(&format!("{e}")),
Some(&self.app_win.clone().upcast::<gtk::Window>()),
);
}
if let Err(e) = restore_active_runtime_backup() {
alert(
"Could not restore previous active runtime",
Some(&format!("{e}")), Some(&format!("{e}")),
Some(&self.app_win.clone().upcast::<gtk::Window>()), Some(&self.app_win.clone().upcast::<gtk::Window>()),
); );
@ -262,27 +336,17 @@ impl App {
} }
pub fn shutdown_xrservice(&mut self) { pub fn shutdown_xrservice(&mut self) {
if let Some(worker) = self.autostart_worker.as_ref() { if let Some(w) = self.plugins_worker.as_ref() {
worker.stop(); w.stop();
} }
self.xrservice_ready = false;
if let Some(w) = self.openxr_prober_worker.as_ref() { if let Some(w) = self.openxr_prober_worker.as_ref() {
w.stop(); w.stop();
// this can cause threads to remain hanging... // this can cause threads to remain hanging...
self.openxr_prober_worker = None; self.openxr_prober_worker = None;
} }
self.set_inhibit_session(false); if let Some(w) = self.xrservice_worker.as_ref() {
if let Some(worker) = self.xrservice_worker.as_ref() { w.stop();
worker.stop();
} }
self.libmonado = None;
self.main_view
.sender()
.emit(MainViewMsg::XRServiceActiveChanged(false, None, false));
self.debug_view
.sender()
.emit(DebugViewMsg::XRServiceActiveChanged(false));
self.xr_devices = vec![];
} }
} }
@ -316,7 +380,7 @@ impl AsyncComponent for App {
set_content: Some(&adw::NavigationPage::new(model.debug_view.widget(), "Debug View")), set_content: Some(&adw::NavigationPage::new(model.debug_view.widget(), "Debug View")),
set_show_content: false, set_show_content: false,
set_collapsed: !model.config.debug_view_enabled, set_collapsed: !model.config.debug_view_enabled,
} },
}, },
connect_close_request[sender] => move |win| { connect_close_request[sender] => move |win| {
sender.input(Msg::SaveWinSize(win.width(), win.height())); sender.input(Msg::SaveWinSize(win.width(), win.height()));
@ -340,6 +404,27 @@ impl AsyncComponent for App {
) { ) {
match message { match message {
Msg::NoOp => {} Msg::NoOp => {}
Msg::ShowThemeManager => {
let dialog = self
.theme_engine
.theme_chooser_dialog(Theme::default_themes().as_ref());
dialog.set_content_height(2000);
dialog.present(Some(&self.app_win));
dialog.connect_closed(clone!(
#[strong]
sender,
move |_| {
sender.input(Msg::SaveThemeConfig);
}
));
}
Msg::SaveThemeConfig => {
let name = self.theme_engine.current_theme_name();
if self.config.theme_name != name {
self.config.theme_name = name;
self.config.save();
}
}
Msg::OnServiceLog(rows) => { Msg::OnServiceLog(rows) => {
if !rows.is_empty() { if !rows.is_empty() {
self.debug_view self.debug_view
@ -348,6 +433,8 @@ impl AsyncComponent for App {
} }
} }
Msg::OnServiceExit(code) => { Msg::OnServiceExit(code) => {
self.set_inhibit_session(false);
self.xrservice_ready = false;
self.restore_openxr_openvr_files(); self.restore_openxr_openvr_files();
self.main_view self.main_view
.sender() .sender()
@ -355,6 +442,8 @@ impl AsyncComponent for App {
self.debug_view self.debug_view
.sender() .sender()
.emit(DebugViewMsg::XRServiceActiveChanged(false)); .emit(DebugViewMsg::XRServiceActiveChanged(false));
self.libmonado = None;
self.xr_devices = vec![];
if code != 0 && code != 15 { if code != 0 && code != 15 {
// 15 is SIGTERM // 15 is SIGTERM
sender.input(Msg::OnServiceLog(vec![format!( sender.input(Msg::OnServiceLog(vec![format!(
@ -369,7 +458,7 @@ impl AsyncComponent for App {
self.start_xrservice(sender, false); self.start_xrservice(sender, false);
} }
} }
Msg::OnAutostartExit(_) => self.autostart_worker = None, Msg::OnPluginsExit(_) => self.plugins_worker = None,
Msg::ClockTicking => { Msg::ClockTicking => {
self.main_view.sender().emit(MainViewMsg::ClockTicking); self.main_view.sender().emit(MainViewMsg::ClockTicking);
let xrservice_worker_is_alive = self let xrservice_worker_is_alive = self
@ -409,7 +498,7 @@ impl AsyncComponent for App {
.emit(MainViewMsg::SetWivrnSupportsPairing(true)); .emit(MainViewMsg::SetWivrnSupportsPairing(true));
} }
Err(e) => { Err(e) => {
eprintln!("Error: failed to get wivrn pairing mode: {e:?}"); error!("failed to get wivrn pairing mode: {e:?}");
self.main_view self.main_view
.sender() .sender()
.emit(MainViewMsg::SetWivrnSupportsPairing(false)); .emit(MainViewMsg::SetWivrnSupportsPairing(false));
@ -466,14 +555,24 @@ impl AsyncComponent for App {
jobs.extend(get_build_basalt_jobs(&profile, clean_build)); jobs.extend(get_build_basalt_jobs(&profile, clean_build));
} }
if profile.features.mercury_enabled { if profile.features.mercury_enabled {
jobs.extend(get_build_mercury_jobs(&profile)); jobs.extend(get_build_mercury_jobs());
} }
jobs.extend(match profile.xrservice_type { jobs.extend(match profile.xrservice_type {
XRServiceType::Monado => get_build_monado_jobs(&profile, clean_build), XRServiceType::Monado => get_build_monado_jobs(&profile, clean_build),
XRServiceType::Wivrn => get_build_wivrn_jobs(&profile, clean_build), XRServiceType::Wivrn => get_build_wivrn_jobs(&profile, clean_build),
}); });
} }
jobs.extend(get_build_opencomposite_jobs(&profile, clean_build)); jobs.extend(match profile.ovr_comp.mod_type {
OvrCompatibilityModuleType::Opencomposite => {
get_build_opencomposite_jobs(&profile, clean_build)
}
OvrCompatibilityModuleType::Xrizer => {
get_build_xrizer_jobs(&profile, clean_build)
}
OvrCompatibilityModuleType::Vapor => {
get_build_vapor_jobs(&profile, clean_build)
}
});
let missing_deps = profile.missing_dependencies(); let missing_deps = profile.missing_dependencies();
if !(self.skip_depcheck || profile.skip_dependency_check || missing_deps.is_empty()) if !(self.skip_depcheck || profile.skip_dependency_check || missing_deps.is_empty())
{ {
@ -562,6 +661,10 @@ impl AsyncComponent for App {
if dep_pkexec().check() { if dep_pkexec().check() {
self.setcap_confirm_dialog.present(Some(&self.app_win)); self.setcap_confirm_dialog.present(Some(&self.app_win));
} else { } else {
self.build_window
.sender()
.emit(BuildWindowMsg::UpdateContent(vec![TermColor::Red
.colorize("pkexec not found, cannot set capabilities\n")]));
alert_w_widget( alert_w_widget(
"pkexec not found", "pkexec not found",
Some(&format!( Some(&format!(
@ -584,7 +687,7 @@ impl AsyncComponent for App {
self.build_window self.build_window
.sender() .sender()
.emit(BuildWindowMsg::UpdateBuildStatus(BuildStatus::Error( .emit(BuildWindowMsg::UpdateBuildStatus(BuildStatus::Error(
format!("Exit status {}", errcode), format!("Exit status {errcode}"),
))); )));
} }
}; };
@ -594,9 +697,16 @@ impl AsyncComponent for App {
w.stop(); w.stop();
} }
} }
Msg::DeleteProfile => { Msg::DeleteProfile(delete_files) => {
let todel = self.get_selected_profile(); let todel = self.get_selected_profile();
if todel.editable { if todel.editable {
if delete_files {
for res in todel.delete_files() {
if let Err(e) = res {
error!("Error deleting profile directory: {e}");
}
}
}
self.config.user_profiles.retain(|p| p.uuid != todel.uuid); self.config.user_profiles.retain(|p| p.uuid != todel.uuid);
self.config.save(); self.config.save();
self.profiles = self.config.profiles(); self.profiles = self.config.profiles();
@ -630,13 +740,51 @@ impl AsyncComponent for App {
self.debug_view self.debug_view
.sender() .sender()
.emit(DebugViewMsg::UpdateSelectedProfile(prof.clone())); .emit(DebugViewMsg::UpdateSelectedProfile(prof.clone()));
self.main_view
.sender()
.emit(MainViewMsg::QueryProfileRebuild);
} }
Msg::RunSetCap => { Msg::RunSetCap => {
if !dep_pkexec().check() { if !dep_pkexec().check() {
println!("pkexec not found, skipping setcap"); // there's a precheck ahead of this, this should likely never happen
error!("pkexec not found, skipping setcap");
} else { } else {
let profile = self.get_selected_profile(); let profile = self.get_selected_profile();
setcap_cap_sys_nice_eip(&profile).await; let setcap_failed_dialog = || {
alert_w_widget(
"Setcap failed to run",
Some("Setting the capabilities automatically failed, you can still try manually using the command below."
),
Some(&copiable_code_snippet(
&format!("sudo {}", setcap_cap_sys_nice_eip_cmd(&profile).join(" "))
)),
Some(&self.app_win.clone().upcast())
);
};
if let Err(e) = setcap_cap_sys_nice_eip(&profile).await {
setcap_failed_dialog();
error!("failed running setcap: {e}");
self.build_window
.sender()
.emit(BuildWindowMsg::UpdateContent(vec![
TermColor::Red.colorize("Setting capabilities failed\n")
]));
} else if !verify_cap_sys_nice_eip(&profile).await {
setcap_failed_dialog();
error!("setcap succeeded but capabilities were reset");
self.build_window
.sender()
.emit(BuildWindowMsg::UpdateContent(vec![TermColor::Red
.colorize(
"Setting capabilities succeeded, but capabilities have been reset\n",
)]));
} else {
self.build_window
.sender()
.emit(BuildWindowMsg::UpdateContent(vec![
TermColor::Green.colorize("Capabilities set correctly\n")
]));
}
} }
} }
Msg::ProfileSelected(prof) => { Msg::ProfileSelected(prof) => {
@ -756,6 +904,21 @@ impl AsyncComponent for App {
} }
} }
} }
Msg::OpenPluginStore => {
let pluginstore = PluginStore::builder()
.launch(PluginStoreInit {
config_plugins: self.config.plugins.clone(),
})
.forward(sender.input_sender(), move |msg| match msg {
PluginStoreOutMsg::UpdateConfigPlugins(cp) => Msg::UpdateConfigPlugins(cp),
});
pluginstore.sender().emit(PluginStoreMsg::Present);
self.pluginstore = Some(pluginstore);
}
Msg::UpdateConfigPlugins(cp) => {
self.config.plugins = cp;
self.config.save();
}
} }
} }
@ -857,6 +1020,28 @@ impl AsyncComponent for App {
} }
) )
); );
stateless_action!(
actions,
PluginStoreAction,
clone!(
#[strong]
sender,
move |_| {
sender.input(Msg::OpenPluginStore);
}
)
);
stateless_action!(
actions,
ThemeManagerAction,
clone!(
#[strong]
sender,
move |_| {
sender.input(Msg::ShowThemeManager);
}
)
);
// this bypasses the macro because I need the underlying gio action // this bypasses the macro because I need the underlying gio action
// to enable/disable it in update() // to enable/disable it in update()
let configure_wivrn_action = { let configure_wivrn_action = {
@ -878,7 +1063,7 @@ impl AsyncComponent for App {
match VulkanInfo::get() { match VulkanInfo::get() {
Ok(info) => Some(info), Ok(info) => Some(info),
Err(e) => { Err(e) => {
eprintln!("Failed to get Vulkan info: {e:#?}"); error!("failed to get Vulkan info: {e:#?}");
None None
} }
} }
@ -893,15 +1078,15 @@ impl AsyncComponent for App {
config: config.clone(), config: config.clone(),
selected_profile: selected_profile.clone(), selected_profile: selected_profile.clone(),
root_win: root.clone().into(), root_win: root.clone().into(),
vkinfo: vkinfo.clone(),
}) })
.forward(sender.input_sender(), |message| match message { .forward(sender.input_sender(), |message| match message {
MainViewOutMsg::DoStartStopXRService => Msg::DoStartStopXRService, MainViewOutMsg::DoStartStopXRService => Msg::DoStartStopXRService,
MainViewOutMsg::RestartXRService => Msg::RestartXRService, MainViewOutMsg::RestartXRService => Msg::RestartXRService,
MainViewOutMsg::ProfileSelected(uuid) => Msg::ProfileSelected(uuid), MainViewOutMsg::ProfileSelected(uuid) => Msg::ProfileSelected(uuid),
MainViewOutMsg::DeleteProfile => Msg::DeleteProfile, MainViewOutMsg::DeleteProfile(delete_files) => Msg::DeleteProfile(delete_files),
MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p), MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p),
MainViewOutMsg::OpenLibsurviveSetup => Msg::OpenLibsurviveSetup, MainViewOutMsg::OpenLibsurviveSetup => Msg::OpenLibsurviveSetup,
MainViewOutMsg::BuildProfile(clean) => Msg::BuildProfile(clean),
}), }),
vkinfo, vkinfo,
debug_view: DebugView::builder() debug_view: DebugView::builder()
@ -925,10 +1110,21 @@ impl AsyncComponent for App {
.detach(), .detach(),
split_view: None, split_view: None,
setcap_confirm_dialog, setcap_confirm_dialog,
theme_engine: ThemeEngine::new_with_theme(&{
if config.theme_name == "Follow system" {
Theme::default()
} else {
Theme::default_themes()
.into_iter()
.find(|t| t.name == config.theme_name)
.unwrap_or_default()
}
})
.unwrap(),
config, config,
profiles, profiles,
xrservice_worker: None, xrservice_worker: None,
autostart_worker: None, plugins_worker: None,
build_worker: None, build_worker: None,
xr_devices: vec![], xr_devices: vec![],
restart_xrservice: false, restart_xrservice: false,
@ -939,6 +1135,7 @@ impl AsyncComponent for App {
openxr_prober_worker: None, openxr_prober_worker: None,
xrservice_ready: false, xrservice_ready: false,
inhibit_fail_notif: None, inhibit_fail_notif: None,
pluginstore: None,
}; };
let widgets = view_output!(); let widgets = view_output!();
@ -1011,6 +1208,8 @@ new_stateless_action!(pub BuildProfileCleanAction, AppActionGroup, "buildprofile
new_stateless_action!(pub QuitAction, AppActionGroup, "quit"); new_stateless_action!(pub QuitAction, AppActionGroup, "quit");
new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool); new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool);
new_stateless_action!(pub ConfigureWivrnAction, AppActionGroup, "configurewivrn"); new_stateless_action!(pub ConfigureWivrnAction, AppActionGroup, "configurewivrn");
new_stateless_action!(pub PluginStoreAction, AppActionGroup, "store");
new_stateless_action!(pub ThemeManagerAction, AppActionGroup, "thememanager");
new_stateless_action!(pub DebugOpenDataAction, AppActionGroup, "debugopendata"); new_stateless_action!(pub DebugOpenDataAction, AppActionGroup, "debugopendata");
new_stateless_action!(pub DebugOpenPrefixAction, AppActionGroup, "debugopenprefix"); new_stateless_action!(pub DebugOpenPrefixAction, AppActionGroup, "debugopenprefix");

View file

@ -1,3 +1,5 @@
use crate::termcolor::TermColor;
use super::{term_widget::TermWidget, SENDER_IO_ERR_MSG}; use super::{term_widget::TermWidget, SENDER_IO_ERR_MSG};
use adw::prelude::*; use adw::prelude::*;
use relm4::prelude::*; use relm4::prelude::*;
@ -88,43 +90,54 @@ impl SimpleComponent for BuildWindow {
gtk::Label { gtk::Label {
#[track = "model.changed(BuildWindow::build_status())"] #[track = "model.changed(BuildWindow::build_status())"]
set_markup: match &model.build_status { set_markup: match &model.build_status {
BuildStatus::Building => "Build in progress...".to_string(), BuildStatus::Building => String::default(),
BuildStatus::Done => "Build done, you can close this window".to_string(), BuildStatus::Done => "Build done, you can close this window".into(),
BuildStatus::Error(code) => { BuildStatus::Error(code) => {
format!("Build failed: \"{c}\"", c = code) format!("Build failed: \"{code}\"")
} }
}.as_str(), }.as_str(),
#[track = "model.changed(BuildWindow::build_status())"]
set_visible: match &model.build_status {
BuildStatus::Building => false,
BuildStatus::Done | BuildStatus::Error(_) => true,
},
add_css_class: "title-2", add_css_class: "title-2",
set_wrap: true, set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word, set_wrap_mode: gtk::pango::WrapMode::Word,
set_justify: gtk::Justification::Center, set_justify: gtk::Justification::Center,
}, },
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(), model.term.container.clone(),
}, },
add_bottom_bar: bottom_bar = &gtk::Button { add_bottom_bar: bottom_bar = &gtk::Box {
add_css_class: "pill", set_orientation: gtk::Orientation::Horizontal,
set_halign: gtk::Align::Center, set_halign: gtk::Align::Center,
set_label: "Close", set_hexpand: true,
set_margin_all: 12, set_margin_bottom: 24,
#[track = "model.changed(BuildWindow::can_close())"] set_spacing: 12,
set_sensitive: model.can_close, gtk::Button {
connect_clicked[win] => move |_| { add_css_class: "pill",
set_halign: gtk::Align::Center,
win.close(); 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);
}
},
// ^^^
},
} }
} }
} }
@ -153,8 +166,18 @@ impl SimpleComponent for BuildWindow {
label.remove_css_class("success"); label.remove_css_class("success");
label.remove_css_class("error"); label.remove_css_class("error");
match status { match status {
BuildStatus::Done => label.add_css_class("success"), BuildStatus::Done => {
BuildStatus::Error(_) => label.add_css_class("error"), label.add_css_class("success");
sender.input(BuildWindowMsg::UpdateContent(vec![
TermColor::Blue.colorize("Build completed!\n")
]));
}
BuildStatus::Error(_) => {
label.add_css_class("error");
sender.input(BuildWindowMsg::UpdateContent(vec![
TermColor::Blue.colorize("Build failed!\n")
]));
}
_ => {} _ => {}
} }
if status != BuildStatus::Building { if status != BuildStatus::Building {

View file

@ -1,4 +1,7 @@
use crate::config::Config; use crate::{
config::Config,
constants::{APP_NAME, VERSION},
};
use gtk::{ use gtk::{
gio::{ gio::{
prelude::{ApplicationCommandLineExt, ApplicationExt}, prelude::{ApplicationCommandLineExt, ApplicationExt},
@ -6,9 +9,11 @@ use gtk::{
}, },
glib::{self, prelude::IsA}, glib::{self, prelude::IsA},
}; };
use tracing::error;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CmdLineOpts { pub struct CmdLineOpts {
pub version: bool,
pub start: bool, pub start: bool,
pub list_profiles: bool, pub list_profiles: bool,
pub profile_uuid: Option<String>, pub profile_uuid: Option<String>,
@ -17,6 +22,7 @@ pub struct CmdLineOpts {
} }
impl CmdLineOpts { impl CmdLineOpts {
const OPT_VERSION: (&'static str, char) = ("version", 'v');
const OPT_START: (&'static str, char) = ("start", 'S'); const OPT_START: (&'static str, char) = ("start", 'S');
const OPT_LIST_PROFILES: (&'static str, char) = ("list-profiles", 'l'); const OPT_LIST_PROFILES: (&'static str, char) = ("list-profiles", 'l');
const OPT_PROFILE: (&'static str, char) = ("profile", 'p'); const OPT_PROFILE: (&'static str, char) = ("profile", 'p');
@ -24,6 +30,14 @@ impl CmdLineOpts {
const OPT_CHECK_DEPS_FOR: (&'static str, char) = ("check-deps-for", 'c'); const OPT_CHECK_DEPS_FOR: (&'static str, char) = ("check-deps-for", 'c');
pub fn init(app: &impl IsA<Application>) { pub fn init(app: &impl IsA<Application>) {
app.add_main_option(
Self::OPT_VERSION.0,
glib::Char::try_from(Self::OPT_VERSION.1).unwrap(),
glib::OptionFlags::IN_MAIN,
glib::OptionArg::None,
"Print the version information",
None,
);
app.add_main_option( app.add_main_option(
Self::OPT_START.0, Self::OPT_START.0,
glib::Char::try_from(Self::OPT_START.1).unwrap(), glib::Char::try_from(Self::OPT_START.1).unwrap(),
@ -68,6 +82,10 @@ impl CmdLineOpts {
/// returns an exit code if the application should quit immediately /// returns an exit code if the application should quit immediately
pub fn handle_non_activating_opts(&self) -> Option<i32> { pub fn handle_non_activating_opts(&self) -> Option<i32> {
if self.version {
println!("{APP_NAME} {VERSION}");
return Some(0);
}
if self.list_profiles { if self.list_profiles {
println!("Available profiles\nUUID: \"name\""); println!("Available profiles\nUUID: \"name\"");
let profiles = Config::get_config().profiles(); let profiles = Config::get_config().profiles();
@ -88,7 +106,7 @@ impl CmdLineOpts {
} }
return Some(1); return Some(1);
} else { } else {
eprintln!("No profile found for uuid: `{prof_id}`"); error!("No profile found for uuid: `{prof_id}`");
return Some(404); return Some(404);
} }
} }
@ -98,6 +116,7 @@ impl CmdLineOpts {
pub fn from_cmdline(cmdline: &ApplicationCommandLine) -> Self { pub fn from_cmdline(cmdline: &ApplicationCommandLine) -> Self {
let opts = cmdline.options_dict(); let opts = cmdline.options_dict();
Self { Self {
version: opts.contains(Self::OPT_VERSION.0),
start: opts.contains(Self::OPT_START.0), start: opts.contains(Self::OPT_START.0),
list_profiles: opts.contains(Self::OPT_LIST_PROFILES.0), list_profiles: opts.contains(Self::OPT_LIST_PROFILES.0),
profile_uuid: opts profile_uuid: opts

View file

@ -61,9 +61,19 @@ impl SimpleComponent for DevicesBox {
} }
if !has_left && dev.roles.contains(&XRDeviceRole::Left) { if !has_left && dev.roles.contains(&XRDeviceRole::Left) {
has_left = true; has_left = true;
if ["Qwerty Left Controller"].contains(&dev.name.as_str()) {
row_model.state = Some(DeviceRowState::Warning);
row_model.subtitle =
Some(format!("No left controller detected ({})", dev.name));
}
} }
if !has_right && dev.roles.contains(&XRDeviceRole::Right) { if !has_right && dev.roles.contains(&XRDeviceRole::Right) {
has_right = true; has_right = true;
if ["Qwerty Right Controller"].contains(&dev.name.as_str()) {
row_model.state = Some(DeviceRowState::Warning);
row_model.subtitle =
Some(format!("No right controller detected ({})", dev.name));
}
} }
models.push(row_model); models.push(row_model);
} }

View file

@ -8,6 +8,7 @@ use crate::{
use gtk::prelude::*; use gtk::prelude::*;
use relm4::{new_action_group, new_stateless_action, prelude::*}; use relm4::{new_action_group, new_stateless_action, prelude::*};
use std::fs::remove_file; use std::fs::remove_file;
use tracing::error;
const WIVRN_LATEST_RELEASE_APK_URL: &str = const WIVRN_LATEST_RELEASE_APK_URL: &str =
"https://github.com/WiVRn/WiVRn/releases/latest/download/WiVRn-standard-release.apk"; "https://github.com/WiVRn/WiVRn/releases/latest/download/WiVRn-standard-release.apk";
@ -113,7 +114,7 @@ impl AsyncComponent for InstallWivrnBox {
add_css_class: "dim-label", add_css_class: "dim-label",
set_hexpand: true, set_hexpand: true,
set_label: concat!( set_label: concat!(
"Install the WiVRn APK on your standalong Android headset. ", "Install the WiVRn APK on your standalone Android headset. ",
"You will need to enable Developer Mode on your headset, ", "You will need to enable Developer Mode on your headset, ",
"then press the \"Install WiVRn\" button." "then press the \"Install WiVRn\" button."
), ),
@ -172,7 +173,7 @@ impl AsyncComponent for InstallWivrnBox {
match get_wivrn_apk_ref(&self.selected_profile) { match get_wivrn_apk_ref(&self.selected_profile) {
Err(GetWivrnApkRefErr::NotWivrn) => { Err(GetWivrnApkRefErr::NotWivrn) => {
eprintln!("This is not a WiVRn profile, how did you get here?"); error!("this is not a WiVRn profile, how did you get here?");
} }
Err(GetWivrnApkRefErr::RepoDirNotFound) => { Err(GetWivrnApkRefErr::RepoDirNotFound) => {
self.set_install_wivrn_status(InstallWivrnStatus::Done(Some( self.set_install_wivrn_status(InstallWivrnStatus::Done(Some(
@ -180,14 +181,11 @@ impl AsyncComponent for InstallWivrnBox {
))); )));
} }
Err(GetWivrnApkRefErr::RepoManipulationFailed(giterr)) => { Err(GetWivrnApkRefErr::RepoManipulationFailed(giterr)) => {
eprintln!("Error: failed to manipulate WiVRn repo: {giterr}, falling back to latest release APK"); error!("failed to manipulate WiVRn repo: {giterr}, falling back to latest release APK");
let existing = cache_file_path(WIVRN_LATEST_RELEASE_APK_URL, Some("apk")); let existing = cache_file_path(WIVRN_LATEST_RELEASE_APK_URL, Some("apk"));
if existing.is_file() { if existing.is_file() {
if let Err(e) = remove_file(&existing) { if let Err(e) = remove_file(&existing) {
eprintln!( error!("failed to remove file {}: {e}", existing.to_string_lossy());
"Failed to remove file {}: {e}",
existing.to_string_lossy()
);
} }
} }
sender.input(Self::Input::DoInstall(WIVRN_LATEST_RELEASE_APK_URL.into())); sender.input(Self::Input::DoInstall(WIVRN_LATEST_RELEASE_APK_URL.into()));
@ -208,7 +206,7 @@ impl AsyncComponent for InstallWivrnBox {
// TODO: we gonna cache or just download async every time? // TODO: we gonna cache or just download async every time?
match cache_file(&url, Some("apk")).await { match cache_file(&url, Some("apk")).await {
Err(e) => { Err(e) => {
eprintln!("Failed to download apk: {e}"); error!("failed to download apk: {e}");
self.set_install_wivrn_status(InstallWivrnStatus::Done(Some( self.set_install_wivrn_status(InstallWivrnStatus::Done(Some(
"Error downloading WiVRn client APK".into(), "Error downloading WiVRn client APK".into(),
))); )));
@ -236,14 +234,14 @@ impl AsyncComponent for InstallWivrnBox {
.into(), .into(),
)) ))
} else { } else {
eprintln!("Error: ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr); error!("ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr);
InstallWivrnStatus::Done(Some( InstallWivrnStatus::Done(Some(
format!("ADB exited with code \"{}\"", out.exit_code) format!("ADB exited with code \"{}\"", out.exit_code)
)) ))
} }
} }
Err(e) => { Err(e) => {
eprintln!("Error: failed to run ADB: {e}"); error!("failed to run ADB: {e}");
InstallWivrnStatus::Done(Some( InstallWivrnStatus::Done(Some(
"Failed to run ADB".into() "Failed to run ADB".into()
)) ))

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 { impl InternalJobWorker {
pub fn xrservice_worker_from_profile( pub fn xrservice_worker_from_profile(
@ -193,9 +193,6 @@ impl InternalJobWorker {
} else { } else {
launch_opts launch_opts
}; };
if !launch_opts.contains(" --no-instructions") {
launch_opts.push_str(" --no-instructions");
}
let (command, args) = match launch_opts.is_empty() { let (command, args) = match launch_opts.is_empty() {
false => ( false => (
"sh".into(), "sh".into(),

View file

@ -15,6 +15,7 @@ use std::{
thread::{self, sleep}, thread::{self, sleep},
time::Duration, time::Duration,
}; };
use tracing::{error, warn};
pub mod internal_worker; pub mod internal_worker;
pub mod job; pub mod job;
@ -97,7 +98,7 @@ impl JobWorker {
self.state.lock().unwrap().stop_requested = true; self.state.lock().unwrap().stop_requested = true;
if let Some(pid) = self.state.lock().unwrap().current_pid { if let Some(pid) = self.state.lock().unwrap().current_pid {
if let Err(e) = kill(pid, SIGTERM) { if let Err(e) = kill(pid, SIGTERM) {
eprintln!("Failed to send SIGTERM: {e:#?}"); error!("Failed to send SIGTERM: {e}");
} }
let state = self.state.clone(); let state = self.state.clone();
thread::spawn(move || { thread::spawn(move || {
@ -105,9 +106,9 @@ impl JobWorker {
if let Ok(s) = state.lock() { if let Ok(s) = state.lock() {
if !s.exited { if !s.exited {
// process is still alive // process is still alive
eprintln!("Process is still alive 2 seconds after SIGTERM, proceeding to send SIGKILL..."); warn!("process is still alive 2 seconds after SIGTERM, proceeding to send SIGKILL...");
if let Err(e) = kill(pid, SIGKILL) { if let Err(e) = kill(pid, SIGKILL) {
eprintln!("Failed to send SIGKILL: {e:#?}"); error!("failed to send SIGKILL: {e}");
}; };
} }
} }

View file

@ -2,7 +2,7 @@ use super::{
alert::alert, alert::alert,
app::{ app::{
AboutAction, BuildProfileAction, BuildProfileCleanAction, ConfigureWivrnAction, AboutAction, BuildProfileAction, BuildProfileCleanAction, ConfigureWivrnAction,
DebugViewToggleAction, DebugViewToggleAction, PluginStoreAction,
}, },
devices_box::{DevicesBox, DevicesBoxMsg}, devices_box::{DevicesBox, DevicesBoxMsg},
install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg}, install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg},
@ -12,6 +12,7 @@ use super::{
steamvr_calibration_box::{SteamVrCalibrationBox, SteamVrCalibrationBoxMsg}, steamvr_calibration_box::{SteamVrCalibrationBox, SteamVrCalibrationBoxMsg},
util::{limit_dropdown_width, warning_heading}, util::{limit_dropdown_width, warning_heading},
wivrn_wired_start_box::{WivrnWiredStartBox, WivrnWiredStartBoxInit, WivrnWiredStartBoxMsg}, wivrn_wired_start_box::{WivrnWiredStartBox, WivrnWiredStartBoxInit, WivrnWiredStartBoxMsg},
SENDER_IO_ERR_MSG,
}; };
use crate::{ use crate::{
config::Config, config::Config,
@ -20,11 +21,11 @@ use crate::{
paths::{get_data_dir, get_home_dir}, paths::{get_data_dir, get_home_dir},
profile::{LighthouseDriver, Profile, XRServiceType}, profile::{LighthouseDriver, Profile, XRServiceType},
stateless_action, stateless_action,
ui::app::ThemeManagerAction,
util::{ util::{
file_utils::{get_writer, mount_has_nosuid}, file_utils::{get_writer, mount_has_nosuid},
steamvr_utils::chaperone_info_exists, steamvr_utils::chaperone_info_exists,
}, },
vulkaninfo::VulkanInfo,
wivrn_dbus, wivrn_dbus,
xr_devices::XRDevice, xr_devices::XRDevice,
}; };
@ -36,6 +37,7 @@ use relm4::{
prelude::*, prelude::*,
}; };
use std::{fs::read_to_string, io::Write}; use std::{fs::read_to_string, io::Write};
use tracing::{error, warn};
#[tracker::track] #[tracker::track]
pub struct MainView { pub struct MainView {
@ -59,6 +61,8 @@ pub struct MainView {
#[tracker::do_not_track] #[tracker::do_not_track]
profile_delete_confirm_dialog: adw::AlertDialog, profile_delete_confirm_dialog: adw::AlertDialog,
#[tracker::do_not_track] #[tracker::do_not_track]
query_profile_rebuild_dialog: adw::AlertDialog,
#[tracker::do_not_track]
profile_editor: Option<Controller<ProfileEditor>>, profile_editor: Option<Controller<ProfileEditor>>,
#[tracker::do_not_track] #[tracker::do_not_track]
steamvr_calibration_box: Controller<SteamVrCalibrationBox>, steamvr_calibration_box: Controller<SteamVrCalibrationBox>,
@ -71,8 +75,6 @@ pub struct MainView {
#[tracker::do_not_track] #[tracker::do_not_track]
profile_export_action: gtk::gio::SimpleAction, profile_export_action: gtk::gio::SimpleAction,
xrservice_ready: bool, xrservice_ready: bool,
#[tracker::do_not_track]
vkinfo: Option<VulkanInfo>,
wivrn_pairing_mode: bool, wivrn_pairing_mode: bool,
wivrn_pin: Option<String>, wivrn_pin: Option<String>,
wivrn_supports_pairing: bool, wivrn_supports_pairing: bool,
@ -103,6 +105,7 @@ pub enum MainViewMsg {
SetWivrnPairingMode(bool), SetWivrnPairingMode(bool),
StopWivrnPairingMode, StopWivrnPairingMode,
StartWivrnPairingMode, StartWivrnPairingMode,
QueryProfileRebuild,
} }
#[derive(Debug)] #[derive(Debug)]
@ -110,16 +113,18 @@ pub enum MainViewOutMsg {
DoStartStopXRService, DoStartStopXRService,
RestartXRService, RestartXRService,
ProfileSelected(Profile), ProfileSelected(Profile),
DeleteProfile, /// bool param: delete files
DeleteProfile(bool),
SaveProfile(Profile), SaveProfile(Profile),
OpenLibsurviveSetup, OpenLibsurviveSetup,
/// params: clean
BuildProfile(bool),
} }
pub struct MainViewInit { pub struct MainViewInit {
pub config: Config, pub config: Config,
pub selected_profile: Profile, pub selected_profile: Profile,
pub root_win: gtk::Window, pub root_win: gtk::Window,
pub vkinfo: Option<VulkanInfo>,
} }
impl MainView { impl MainView {
@ -147,6 +152,7 @@ impl AsyncComponent for MainView {
menu! { menu! {
app_menu: { app_menu: {
section! { section! {
"Plugin_s" => PluginStoreAction,
// value inside action is ignored // value inside action is ignored
"_Debug View" => DebugViewToggleAction, "_Debug View" => DebugViewToggleAction,
"_Build Profile" => BuildProfileAction, "_Build Profile" => BuildProfileAction,
@ -154,6 +160,7 @@ impl AsyncComponent for MainView {
"Configure _WiVRn" => ConfigureWivrnAction, "Configure _WiVRn" => ConfigureWivrnAction,
}, },
section! { section! {
"Change _Theme" => ThemeManagerAction,
"_About" => AboutAction, "_About" => AboutAction,
}, },
}, },
@ -389,8 +396,8 @@ impl AsyncComponent for MainView {
set_visible: match mount_has_nosuid(&model.selected_profile.prefix) { set_visible: match mount_has_nosuid(&model.selected_profile.prefix) {
Ok(b) => b, Ok(b) => b,
Err(_) => { Err(_) => {
eprintln!( warn!(
"Warning (nosuid detection): could not get stat on path {}", "nosuid detection: could not get stat on path {}",
model.selected_profile.prefix.to_string_lossy()); model.selected_profile.prefix.to_string_lossy());
false false
}, },
@ -445,35 +452,7 @@ impl AsyncComponent for MainView {
set_label: concat!( set_label: concat!(
"SteamVR room configuration not found.\n", "SteamVR room configuration not found.\n",
"To use the SteamVR lighthouse driver, you ", "To use the SteamVR lighthouse driver, you ",
"will need to run SteamVR and perform the room setup.", "will need to run SteamVR Quick Calibration.",
),
add_css_class: "warning",
set_xalign: 0.0,
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
}
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_vexpand: false,
set_spacing: 12,
add_css_class: "card",
add_css_class: "padded",
set_visible: model
.vkinfo
.as_ref()
.is_some_and(
|i| i.has_nvidia_gpu && !i.has_monado_vulkan_layers
),
warning_heading(),
gtk::Label {
set_label: concat!(
"An Nvidia GPU has been detected, but it ",
"seems you don't have the Monado Vulkan Layers ",
"installed on your system.\n\nInstall the ",
"Monado Vulkan Layers or your XR session will ",
"crash."
), ),
add_css_class: "warning", add_css_class: "warning",
set_xalign: 0.0, set_xalign: 0.0,
@ -627,7 +606,7 @@ impl AsyncComponent for MainView {
self.set_wivrn_pin(Some(pin)); self.set_wivrn_pin(Some(pin));
} }
Err(e) => { Err(e) => {
eprintln!("Error: failed to get wivrn pairing pin: {e:?}"); error!("failed to get wivrn pairing pin: {e}");
} }
}; };
} else { } else {
@ -637,12 +616,12 @@ impl AsyncComponent for MainView {
} }
Self::Input::StopWivrnPairingMode => { Self::Input::StopWivrnPairingMode => {
if let Err(e) = wivrn_dbus::disable_pairing().await { if let Err(e) = wivrn_dbus::disable_pairing().await {
eprintln!("Error: failed to stop wivrn pairing mode: {e:?}"); error!("failed to stop wivrn pairing mode: {e}");
} }
} }
Self::Input::StartWivrnPairingMode => { Self::Input::StartWivrnPairingMode => {
if let Err(e) = wivrn_dbus::enable_pairing().await { if let Err(e) = wivrn_dbus::enable_pairing().await {
eprintln!("Error: failed to start wivrn pairing mode: {e:?}"); error!("failed to start wivrn pairing mode: {e}");
} }
} }
Self::Input::StartStopClicked => { Self::Input::StartStopClicked => {
@ -720,6 +699,10 @@ impl AsyncComponent for MainView {
} }
})); }));
} }
Self::Input::QueryProfileRebuild => {
self.query_profile_rebuild_dialog
.present(Some(&self.root_win));
}
Self::Input::SetSelectedProfile(index) => { Self::Input::SetSelectedProfile(index) => {
self.profiles_dropdown self.profiles_dropdown
.as_ref() .as_ref()
@ -757,7 +740,7 @@ impl AsyncComponent for MainView {
Self::Input::SaveProfile(prof) => { Self::Input::SaveProfile(prof) => {
sender sender
.output(Self::Output::SaveProfile(prof)) .output(Self::Output::SaveProfile(prof))
.expect("Sender output failed"); .expect(SENDER_IO_ERR_MSG);
} }
Self::Input::DuplicateProfile => { Self::Input::DuplicateProfile => {
if self.selected_profile.can_be_built { if self.selected_profile.can_be_built {
@ -926,8 +909,38 @@ impl AsyncComponent for MainView {
), ),
); );
let query_profile_rebuild_dialog = adw::AlertDialog::builder()
.heading("Do you want to build this profile now?")
.body("This will trigger a clean build")
.build();
query_profile_rebuild_dialog.add_response("no", "_No");
query_profile_rebuild_dialog.add_response("yes", "_Yes");
query_profile_rebuild_dialog.set_response_appearance("yes", ResponseAppearance::Suggested);
query_profile_rebuild_dialog.connect_response(
None,
clone!(
#[strong]
sender,
move |_, res| {
if res == "yes" {
sender
.output(Self::Output::BuildProfile(true))
.expect(SENDER_IO_ERR_MSG);
}
}
),
);
let profile_delete_confirm_dialog = adw::AlertDialog::builder() let profile_delete_confirm_dialog = adw::AlertDialog::builder()
.heading("Are you sure you want to delete this profile?") .heading("Are you sure you want to delete this profile?")
.extra_child(
&gtk::CheckButton::builder()
.label("Delete all files and folders associated with profile")
.halign(gtk::Align::Center)
.hexpand(true)
.build(),
)
.build(); .build();
profile_delete_confirm_dialog.add_response("no", "_No"); profile_delete_confirm_dialog.add_response("no", "_No");
profile_delete_confirm_dialog.add_response("yes", "_Yes"); profile_delete_confirm_dialog.add_response("yes", "_Yes");
@ -939,10 +952,19 @@ impl AsyncComponent for MainView {
clone!( clone!(
#[strong] #[strong]
sender, sender,
move |_, res| { move |dialog, res| {
let delete_files_checkbox = dialog
.extra_child()
.and_then(|child| child.downcast::<gtk::CheckButton>().ok());
let delete_files = delete_files_checkbox
.as_ref()
.is_some_and(|c| c.is_active());
if let Some(check) = delete_files_checkbox {
check.set_active(false);
}
if res == "yes" { if res == "yes" {
sender sender
.output(Self::Output::DeleteProfile) .output(Self::Output::DeleteProfile(delete_files))
.expect("Sender output failed"); .expect("Sender output failed");
} }
} }
@ -1060,6 +1082,7 @@ impl AsyncComponent for MainView {
selected_profile: init.selected_profile.clone(), selected_profile: init.selected_profile.clone(),
profile_not_editable_dialog, profile_not_editable_dialog,
profile_delete_confirm_dialog, profile_delete_confirm_dialog,
query_profile_rebuild_dialog,
root_win: init.root_win.clone(), root_win: init.root_win.clone(),
steamvr_calibration_box, steamvr_calibration_box,
openhmd_calibration_box, openhmd_calibration_box,
@ -1067,7 +1090,6 @@ impl AsyncComponent for MainView {
xrservice_ready: false, xrservice_ready: false,
profile_delete_action, profile_delete_action,
profile_export_action, profile_export_action,
vkinfo: init.vkinfo,
wivrn_pairing_mode: false, wivrn_pairing_mode: false,
wivrn_supports_pairing: false, wivrn_supports_pairing: false,
wivrn_pin: None, wivrn_pin: None,

View file

@ -13,6 +13,7 @@ mod libsurvive_setup_window;
mod macros; mod macros;
mod main_view; mod main_view;
mod openhmd_calibration_box; mod openhmd_calibration_box;
pub mod plugins;
mod preference_rows; mod preference_rows;
mod profile_editor; mod profile_editor;
mod steam_launch_options_box; mod steam_launch_options_box;

View file

@ -3,6 +3,7 @@ use relm4::{
gtk::{self, prelude::*}, gtk::{self, prelude::*},
ComponentParts, ComponentSender, SimpleComponent, ComponentParts, ComponentSender, SimpleComponent,
}; };
use tracing::{debug, error};
#[tracker::track] #[tracker::track]
pub struct OpenHmdCalibrationBox { pub struct OpenHmdCalibrationBox {
@ -59,10 +60,10 @@ impl SimpleComponent for OpenHmdCalibrationBox {
let target = XDG.get_config_home().join("openhmd/rift-room-config.json"); let target = XDG.get_config_home().join("openhmd/rift-room-config.json");
if target.is_file() { if target.is_file() {
if let Err(e) = std::fs::remove_file(target) { if let Err(e) = std::fs::remove_file(target) {
eprintln!("Failed to remove openhmd config: {e}"); error!("Failed to remove openhmd config: {e}");
} }
} else { } else {
println!("info: trying to delete openhmd calibration config, but file is missing") debug!("trying to delete openhmd calibration config, but file is missing")
} }
} }
}, },

View file

@ -0,0 +1,199 @@
use std::path::PathBuf;
use crate::{
constants::APP_ID,
ui::{
preference_rows::{entry_row, file_row},
SENDER_IO_ERR_MSG,
},
};
use super::Plugin;
use adw::prelude::*;
use gtk::glib::clone;
use relm4::prelude::*;
#[tracker::track]
pub struct AddCustomPluginWin {
#[tracker::do_not_track]
parent: gtk::Window,
#[tracker::do_not_track]
win: Option<adw::Dialog>,
/// this is true when enough fields are populated, allowing the creation
/// of the plugin object to add
can_add: bool,
#[tracker::do_not_track]
plugin: Plugin,
}
#[derive(Debug)]
pub enum AddCustomPluginWinMsg {
Present,
Close,
OnNameChange(String),
OnArgsChange(String),
OnExecPathChange(Option<String>),
Add,
}
#[derive(Debug)]
pub enum AddCustomPluginWinOutMsg {
Add(Plugin),
}
#[derive(Debug)]
pub struct AddCustomPluginWinInit {
pub parent: gtk::Window,
}
#[relm4::component(pub)]
impl SimpleComponent for AddCustomPluginWin {
type Init = AddCustomPluginWinInit;
type Input = AddCustomPluginWinMsg;
type Output = AddCustomPluginWinOutMsg;
view! {
#[name(win)]
adw::Dialog {
set_can_close: true,
#[wrap(Some)]
set_child: inner = &adw::ToolbarView {
set_top_bar_style: adw::ToolbarStyle::Flat,
set_bottom_bar_style: adw::ToolbarStyle::Flat,
set_vexpand: true,
set_hexpand: true,
add_top_bar: top_bar = &adw::HeaderBar {
set_show_end_title_buttons: false,
set_show_start_title_buttons: false,
pack_start: cancel_btn = &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)
)
)
),
add: &entry_row(
"Plugin Arguments",
"",
clone!(
#[strong] sender,
move |row| sender.input(
Self::Input::OnArgsChange(
row.text().to_string()
)
)
)
),
},
},
},
}
}
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
self.reset();
match message {
Self::Input::Present => self.win.as_ref().unwrap().present(Some(&self.parent)),
Self::Input::Close => {
self.win.as_ref().unwrap().close();
}
Self::Input::Add => {
if self.plugin.validate() {
sender
.output(Self::Output::Add(self.plugin.clone()))
.expect(SENDER_IO_ERR_MSG);
self.win.as_ref().unwrap().close();
}
}
Self::Input::OnNameChange(name) => {
self.plugin.appid = if !name.is_empty() {
format!("{APP_ID}.customPlugin.{name}")
} else {
String::default()
};
self.plugin.name = name;
self.set_can_add(self.plugin.validate());
}
Self::Input::OnArgsChange(args) => {
let args = args.trim().to_string();
self.plugin.args = if args.is_empty() {
None
} else {
// it's fine to have them joined
// since they will ultimately be
// passed as a joined string
Some(vec![args])
}
}
Self::Input::OnExecPathChange(ep) => {
self.plugin.exec_path = ep.map(PathBuf::from);
self.set_can_add(self.plugin.validate());
}
}
}
fn init(
init: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let mut model = Self {
tracker: 0,
win: None,
parent: init.parent,
can_add: false,
plugin: Plugin {
short_description: Some("Custom Plugin".into()),
..Default::default()
},
};
let widgets = view_output!();
model.win = Some(widgets.win.clone());
ComponentParts { model, widgets }
}
}

189
src/ui/plugins/mod.rs Normal file
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;3] = [
"https://github.com/galister/wlx-overlay-s/raw/refs/heads/meta/com.github.galister.wlx-overlay-s.json",
"https://github.com/StardustXR/telescope/raw/refs/heads/main/envision/org.stardustxr.telescope.json",
"https://github.com/olekolek1000/wayvr-dashboard/raw/refs/heads/meta/dev.oo8.wayvr_dashboard.json",
];
pub async fn refresh_plugins() -> anyhow::Result<Vec<anyhow::Result<Plugin>>> {
let mut results = Vec::new();
for jh in MANIFESTS
.iter()
.map(|url| -> tokio::task::JoinHandle<anyhow::Result<Plugin>> {
tokio::spawn(async move {
let path = cache_file_path(url, Some("json"));
download_file_async(url, &path).await?;
Ok(serde_json::from_str::<Plugin>(
&tokio::fs::read_to_string(path).await?,
)?)
})
})
{
results.push(jh.await?);
}
Ok(results)
}

552
src/ui/plugins/store.rs Normal file
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 row
} }
pub fn path_row<F: Fn(Option<String>) + 'static + Clone>( fn filedialog_row_base<F: Fn(Option<String>) + 'static + Clone>(
title: &str, title: &str,
description: Option<&str>, description: Option<&str>,
value: Option<String>, value: Option<String>,
root_win: Option<gtk::Window>,
cb: F, cb: F,
) -> adw::ActionRow { ) -> (adw::ActionRow, gtk::Label) {
let row = adw::ActionRow::builder() let row = adw::ActionRow::builder()
.title(title) .title(title)
.subtitle_lines(0) .subtitle_lines(0)
@ -174,14 +173,14 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
row.set_subtitle(d); row.set_subtitle(d);
} }
let path_label = &gtk::Label::builder() let path_label = gtk::Label::builder()
.label(match value.as_ref() { .label(match value.as_ref() {
None => "(None)", None => "(None)",
Some(p) => p.as_str(), Some(p) => p.as_str(),
}) })
.wrap(true) .wrap(true)
.build(); .build();
row.add_suffix(path_label); row.add_suffix(&path_label);
let clear_btn = gtk::Button::builder() let clear_btn = gtk::Button::builder()
.icon_name("edit-clear-symbolic") .icon_name("edit-clear-symbolic")
@ -200,9 +199,63 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
cb(None) cb(None)
} }
)); ));
(row, path_label)
}
pub fn file_row<F: Fn(Option<String>) + 'static + Clone>(
title: &str,
description: Option<&str>,
value: Option<String>,
root_win: Option<gtk::Window>,
cb: F,
) -> adw::ActionRow {
let (row, path_label) = filedialog_row_base(title, description, value, cb.clone());
let filedialog = gtk::FileDialog::builder() let filedialog = gtk::FileDialog::builder()
.modal(true) .modal(true)
.title(format!("Select Path for {}", title)) .title(format!("Select {title}"))
.build();
row.connect_activated(clone!(
#[weak]
path_label,
move |_| {
filedialog.open(
root_win.as_ref(),
gio::Cancellable::NONE,
clone!(
#[weak]
path_label,
#[strong]
cb,
move |res| {
if let Ok(file) = res {
if let Some(path) = file.path() {
let path_s = path.to_string_lossy().to_string();
path_label.set_text(&path_s);
cb(Some(path_s))
}
}
}
),
)
}
));
row
}
pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
title: &str,
description: Option<&str>,
value: Option<String>,
root_win: Option<gtk::Window>,
cb: F,
) -> adw::ActionRow {
let (row, path_label) = filedialog_row_base(title, description, value, cb.clone());
let filedialog = gtk::FileDialog::builder()
.modal(true)
.title(format!("Select Path for {title}"))
.build(); .build();
row.connect_activated(clone!( row.connect_activated(clone!(
@ -220,8 +273,8 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
move |res| { move |res| {
if let Ok(file) = res { if let Ok(file) = res {
if let Some(path) = file.path() { if let Some(path) = file.path() {
let path_s = path.to_str().unwrap().to_string(); let path_s = path.to_string_lossy().to_string();
path_label.set_text(path_s.as_str()); path_label.set_text(&path_s);
cb(Some(path_s)) cb(Some(path_s))
} }
} }

View file

@ -6,12 +6,13 @@ use super::{
}; };
use crate::{ use crate::{
env_var_descriptions::ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH, env_var_descriptions::ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH,
profile::{LighthouseDriver, Profile, XRServiceType}, profile::{LighthouseDriver, OvrCompatibilityModuleType, Profile, XRServiceType},
}; };
use adw::prelude::*; use adw::prelude::*;
use gtk::glib::{self, clone}; use gtk::glib::{self, clone};
use relm4::{factory::AsyncFactoryVecDeque, prelude::*}; use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
use std::{cell::RefCell, path::PathBuf, rc::Rc}; use std::{cell::RefCell, path::PathBuf, rc::Rc};
use tracing::warn;
#[tracker::track] #[tracker::track]
pub struct ProfileEditor { pub struct ProfileEditor {
@ -129,14 +130,6 @@ impl SimpleComponent for ProfileEditor {
prof.borrow_mut().prefix = n_path.unwrap_or_default().into(); prof.borrow_mut().prefix = n_path.unwrap_or_default().into();
}), }),
), ),
add: &entry_row("Autostart Command",
model.profile.borrow().autostart_command.as_ref().unwrap_or(&String::default()),
clone!(#[strong] prof, move |row| {
let txt = row.text().trim().to_string();
prof.borrow_mut().autostart_command =
if txt.is_empty() {None} else {Some(txt)};
})
),
add: &switch_row("Dependency Check", add: &switch_row("Dependency Check",
Some("Warning: disabling dependency checks may result in build failures"), Some("Warning: disabling dependency checks may result in build failures"),
!model.profile.borrow().skip_dependency_check, !model.profile.borrow().skip_dependency_check,
@ -216,31 +209,43 @@ impl SimpleComponent for ProfileEditor {
), ),
}, },
add: model.xrservice_cmake_flags_rows.widget(), add: model.xrservice_cmake_flags_rows.widget(),
add: opencompgrp = &adw::PreferencesGroup { add: ovr_comp_grp = &adw::PreferencesGroup {
set_title: "OpenComposite", set_title: "OpenVR Compatibility",
set_description: Some("OpenVR driver built on top of OpenXR"), set_description: Some("OpenVR compatibility module, translates between OpenXR and OpenVR to run legacy OpenVR apps"),
add: &combo_row(
"OpenVR Module Type",
None,
model.profile.borrow().ovr_comp.mod_type.to_string().as_str(),
OvrCompatibilityModuleType::iter()
.map(OvrCompatibilityModuleType::to_string)
.collect::<Vec<String>>(),
clone!(#[strong] prof, move |row| {
prof.borrow_mut().ovr_comp.mod_type =
OvrCompatibilityModuleType::from(row.selected());
}),
),
add: &path_row( add: &path_row(
"OpenComposite Path", None, "OpenVR Module Path", None,
Some(model.profile.borrow().opencomposite_path.clone().to_string_lossy().to_string()), Some(model.profile.borrow().ovr_comp.path.clone().to_string_lossy().to_string()),
Some(init.root_win.clone()), Some(init.root_win.clone()),
clone!(#[strong] prof, move |n_path| { clone!(#[strong] prof, move |n_path| {
prof.borrow_mut().opencomposite_path = n_path.unwrap_or_default().into(); prof.borrow_mut().ovr_comp.path = n_path.unwrap_or_default().into();
}) })
), ),
add: &entry_row( add: &entry_row(
"OpenComposite Repo", "OpenVR Compatibility Repo",
model.profile.borrow().opencomposite_repo.clone().unwrap_or_default().as_str(), model.profile.borrow().ovr_comp.repo.clone().unwrap_or_default().as_str(),
clone!(#[strong] prof, move |row| { clone!(#[strong] prof, move |row| {
let n_val = row.text().to_string(); let n_val = row.text().to_string();
prof.borrow_mut().opencomposite_repo = (!n_val.is_empty()).then_some(n_val); prof.borrow_mut().ovr_comp.repo = (!n_val.is_empty()).then_some(n_val);
}) })
), ),
add: &entry_row( add: &entry_row(
"OpenComposite Branch", "OpenVR Compatibility Branch",
model.profile.borrow().opencomposite_branch.clone().unwrap_or_default().as_str(), model.profile.borrow().ovr_comp.branch.clone().unwrap_or_default().as_str(),
clone!(#[strong] prof, move |row| { clone!(#[strong] prof, move |row| {
let n_val = row.text().to_string(); let n_val = row.text().to_string();
prof.borrow_mut().opencomposite_branch = (!n_val.is_empty()).then_some(n_val); prof.borrow_mut().ovr_comp.branch = (!n_val.is_empty()).then_some(n_val);
}) })
), ),
}, },
@ -499,14 +504,14 @@ impl SimpleComponent for ProfileEditor {
.halign(gtk::Align::End) .halign(gtk::Align::End)
.build(); .build();
add_btn.connect_clicked(clone!( let on_add = clone!(
#[strong] #[strong]
sender, sender,
#[weak] #[weak]
name_entry, name_entry,
#[weak] #[weak]
popover, popover,
move |_| { move || {
let key_gstr = name_entry.text(); let key_gstr = name_entry.text();
let key = key_gstr.trim(); let key = key_gstr.trim();
if !key.is_empty() { if !key.is_empty() {
@ -515,7 +520,13 @@ impl SimpleComponent for ProfileEditor {
sender.input($event(key.to_string())); sender.input($event(key.to_string()));
} }
} }
);
name_entry.connect_activate(clone!(
#[strong]
on_add,
move |_| on_add()
)); ));
add_btn.connect_clicked(move |_| on_add());
btn btn
}}; }};
} }
@ -528,17 +539,30 @@ impl SimpleComponent for ProfileEditor {
let profile = Rc::new(RefCell::new(init.profile)); let profile = Rc::new(RefCell::new(init.profile));
let prof = profile.clone(); let prof = profile.clone();
let env_var_prefs_group = {
let pg = adw::PreferencesGroup::builder()
.title("Environment Variables")
.description(ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH.as_str())
.header_suffix(&add_env_var_btn)
.build();
if let Some(desc) = pg
.first_child()
.and_then(|c| c.first_child())
.and_then(|c| c.first_child())
.and_then(|c| c.last_child())
.and_downcast::<gtk::Label>()
{
desc.set_selectable(true);
} else {
warn!("failed to make env var preference group description selectable, please open a bug report");
}
pg
};
let mut model = Self { let mut model = Self {
profile, profile,
win: None, win: None,
env_rows: AsyncFactoryVecDeque::builder() env_rows: AsyncFactoryVecDeque::builder()
.launch( .launch(env_var_prefs_group)
adw::PreferencesGroup::builder()
.title("Environment Variables")
.description(ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH.as_str())
.header_suffix(&add_env_var_btn)
.build(),
)
.forward(sender.input_sender(), |msg| match msg { .forward(sender.input_sender(), |msg| match msg {
EnvVarModelOutMsg::Changed(name, value) => { EnvVarModelOutMsg::Changed(name, value) => {
ProfileEditorMsg::EnvVarChanged(name, value) ProfileEditorMsg::EnvVarChanged(name, value)

View file

@ -45,8 +45,7 @@ impl SimpleComponent for SteamLaunchOptionsBox {
add_css_class: "dim-label", add_css_class: "dim-label",
set_hexpand: true, set_hexpand: true,
set_label: format!( set_label: format!(
"Set this string in the launch options of Steam games, so that they can pick up the {app} runtime correctly", "Set this string in the launch options of Steam games, so that they can pick up the {APP_NAME} runtime correctly")
app = APP_NAME)
.as_str(), .as_str(),
set_xalign: 0.0, set_xalign: 0.0,
set_wrap: true, set_wrap: true,

View file

@ -10,10 +10,10 @@ use relm4::{
}; };
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
path::Path,
thread::sleep, thread::sleep,
time::Duration, time::Duration,
}; };
use tracing::error;
#[tracker::track] #[tracker::track]
pub struct SteamVrCalibrationBox { pub struct SteamVrCalibrationBox {
@ -144,55 +144,59 @@ impl SimpleComponent for SteamVrCalibrationBox {
} }
Self::Input::RunCalibration => { Self::Input::RunCalibration => {
self.set_calibration_result(None); self.set_calibration_result(None);
let steamvr_bin_dir = get_steamvr_bin_dir_path().to_string_lossy().to_string(); match get_steamvr_bin_dir_path() {
if !Path::new(&steamvr_bin_dir).is_dir() { Err(e) => {
self.set_calibration_success(false); error!("could not get SteamVR bin dir: {e}");
self.set_calibration_result(Some("SteamVR not found".into())); self.set_calibration_success(false);
return; self.set_calibration_result(Some("SteamVR not found".into()));
} }
let mut env: HashMap<String, String> = HashMap::new(); Ok(bin_dir_p) => {
env.insert("LD_LIBRARY_PATH".into(), steamvr_bin_dir.clone()); let steamvr_bin_dir = bin_dir_p.to_string_lossy().to_string();
let vrcmd = format!("{steamvr_bin_dir}/vrcmd"); let mut env: HashMap<String, String> = HashMap::new();
let server_worker = { env.insert("LD_LIBRARY_PATH".into(), steamvr_bin_dir.clone());
let mut jobs: VecDeque<WorkerJob> = VecDeque::new(); let vrcmd = format!("{steamvr_bin_dir}/vrcmd");
jobs.push_back(WorkerJob::new_cmd( let server_worker = {
Some(env.clone()), let mut jobs: VecDeque<WorkerJob> = VecDeque::new();
vrcmd.clone(), jobs.push_back(WorkerJob::new_cmd(
Some(vec!["--pollposes".into()]), Some(env.clone()),
)); vrcmd.clone(),
JobWorker::new(jobs, sender.input_sender(), |msg| match msg { Some(vec!["--pollposes".into()]),
JobWorkerOut::Log(_) => Self::Input::NoOp, ));
JobWorkerOut::Exit(code) => Self::Input::OnServerWorkerExit(code), 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 || { let cal_worker = {
sleep(Duration::from_secs(2)); let mut jobs: VecDeque<WorkerJob> = VecDeque::new();
FuncWorkerOut { jobs.push_back(WorkerJob::new_func(Box::new(move || {
success: true, sleep(Duration::from_secs(2));
out: vec![], FuncWorkerOut {
} success: true,
}))); out: vec![],
jobs.push_back(WorkerJob::new_cmd( }
Some(env), })));
vrcmd, jobs.push_back(WorkerJob::new_cmd(
Some(vec!["--resetroomsetup".into()]), Some(env),
)); vrcmd,
JobWorker::new(jobs, sender.input_sender(), |msg| match msg { Some(vec!["--resetroomsetup".into()]),
JobWorkerOut::Log(_) => Self::Input::NoOp, ));
JobWorkerOut::Exit(code) => Self::Input::OnCalWorkerExit(code), JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
}) JobWorkerOut::Log(_) => Self::Input::NoOp,
}; JobWorkerOut::Exit(code) => Self::Input::OnCalWorkerExit(code),
})
};
server_worker.start(); server_worker.start();
cal_worker.start(); cal_worker.start();
self.server_worker = Some(server_worker); self.server_worker = Some(server_worker);
self.calibration_worker = Some(cal_worker); self.calibration_worker = Some(cal_worker);
}
};
} }
Self::Input::OnServerWorkerExit(code) => { Self::Input::OnServerWorkerExit(code) => {
if code != 0 { if code != 0 {
eprintln!("Calibration exited with code {code}"); error!("calibration exited with code {code}");
} }
self.calibration_running = false; self.calibration_running = false;
} }

View file

@ -1,4 +1,5 @@
use gtk::{gdk, gio, glib::clone, prelude::*}; use gtk::{gdk, gio, glib::clone, prelude::*};
use tracing::{error, warn};
pub fn limit_dropdown_width(dd: &gtk::DropDown) { pub fn limit_dropdown_width(dd: &gtk::DropDown) {
let mut dd_child = dd let mut dd_child = dd
@ -46,14 +47,14 @@ pub fn warning_heading() -> gtk::Box {
pub fn open_with_default_handler(uri: &str) { pub fn open_with_default_handler(uri: &str) {
if let Err(e) = gio::AppInfo::launch_default_for_uri(uri, gio::AppLaunchContext::NONE) { if let Err(e) = gio::AppInfo::launch_default_for_uri(uri, gio::AppLaunchContext::NONE) {
eprintln!("Error opening uri {}: {}", uri, e) error!("opening uri {uri}: {e}")
}; };
} }
pub fn copy_text(txt: &str) { pub fn copy_text(txt: &str) {
match gdk::Display::default() { match gdk::Display::default() {
None => { None => {
eprintln!("Warning: could not get default gdk display") warn!("could not get default gdk display")
} }
Some(d) => { Some(d) => {
d.clipboard().set_text(txt); d.clipboard().set_text(txt);

View file

@ -21,6 +21,7 @@ use crate::{
use adw::prelude::*; use adw::prelude::*;
use gtk::glib::clone; use gtk::glib::clone;
use relm4::{factory::AsyncFactoryVecDeque, prelude::*}; use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
use tracing::error;
#[tracker::track] #[tracker::track]
pub struct WivrnConfEditor { pub struct WivrnConfEditor {
@ -255,7 +256,7 @@ impl SimpleComponent for WivrnConfEditor {
if let Some(idx) = idx_opt { if let Some(idx) = idx_opt {
self.encoder_models.as_mut().unwrap().guard().remove(idx); self.encoder_models.as_mut().unwrap().guard().remove(idx);
} else { } else {
eprintln!("Couldn't find encoder model with id {id}"); error!("couldn't find encoder model with id {id}");
} }
} }
} }

View file

@ -7,6 +7,7 @@ use crate::{
}; };
use gtk::prelude::*; use gtk::prelude::*;
use relm4::prelude::*; use relm4::prelude::*;
use tracing::error;
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(PartialEq, Eq, Debug, Clone)]
pub enum StartClientStatus { pub enum StartClientStatus {
@ -111,7 +112,7 @@ impl AsyncComponent for WivrnWiredStartBox {
Self::Input::UpdateSelectedProfile(p) => self.set_selected_profile(p), Self::Input::UpdateSelectedProfile(p) => self.set_selected_profile(p),
Self::Input::StartWivrnClient => { Self::Input::StartWivrnClient => {
if !dep_adb().check() { if !dep_adb().check() {
alert("ADB is not installed", Some(&format!("Please install ADB on your computer to start the WiVRn client from {}.", APP_NAME)), Some(&self.root_win)); alert("ADB is not installed", Some(&format!("Please install ADB on your computer to start the WiVRn client from {APP_NAME}.")), Some(&self.root_win));
return; return;
} }
self.set_start_client_status(StartClientStatus::InProgress); self.set_start_client_status(StartClientStatus::InProgress);
@ -153,14 +154,14 @@ impl AsyncComponent for WivrnWiredStartBox {
.into(), .into(),
)) ))
} else { } else {
eprintln!("Error: ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr); error!("ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr);
StartClientStatus::Done(Some( StartClientStatus::Done(Some(
format!("ADB exited with code \"{}\"", out.exit_code) format!("ADB exited with code \"{}\"", out.exit_code)
)) ))
} }
}, },
Err(e) => { Err(e) => {
eprintln!("Error: failed to run ADB: {e}"); error!("failed to run ADB: {e}");
StartClientStatus::Done(Some( StartClientStatus::Done(Some(
"Failed to run ADB".into() "Failed to run ADB".into()
)) ))

View file

@ -1,4 +1,4 @@
use crate::{async_process::async_process, profile::Profile}; use crate::{async_process::async_process, depcheck::common::dep_getcap_setcap, profile::Profile};
use anyhow::bail; use anyhow::bail;
use nix::{ use nix::{
errno::Errno, errno::Errno,
@ -7,8 +7,10 @@ use nix::{
use std::{ use std::{
fs::{self, copy, create_dir_all, remove_dir_all, File, OpenOptions}, fs::{self, copy, create_dir_all, remove_dir_all, File, OpenOptions},
io::{BufReader, BufWriter}, io::{BufReader, BufWriter},
os::unix::fs::PermissionsExt,
path::Path, path::Path,
}; };
use tracing::{debug, error};
pub fn get_writer(path: &Path) -> anyhow::Result<BufWriter<std::fs::File>> { pub fn get_writer(path: &Path) -> anyhow::Result<BufWriter<std::fs::File>> {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
@ -36,7 +38,7 @@ pub fn get_reader(path: &Path) -> Option<BufReader<File>> {
} }
match File::open(path) { match File::open(path) {
Err(e) => { Err(e) => {
eprintln!("Error opening {}: {}", path.to_string_lossy(), e); error!("Error opening {}: {}", path.to_string_lossy(), e);
None None
} }
Ok(fd) => Some(BufReader::new(fd)), Ok(fd) => Some(BufReader::new(fd)),
@ -48,7 +50,7 @@ pub fn deserialize_file<T: serde::de::DeserializeOwned>(path: &Path) -> Option<T
None => None, None => None,
Some(reader) => match serde_json::from_reader(reader) { Some(reader) => match serde_json::from_reader(reader) {
Err(e) => { Err(e) => {
eprintln!("Failed to deserialize {}: {}", path.to_string_lossy(), e); error!("Failed to deserialize {}: {}", path.to_string_lossy(), e);
None None
} }
Ok(res) => Some(res), Ok(res) => Some(res),
@ -56,21 +58,50 @@ pub fn deserialize_file<T: serde::de::DeserializeOwned>(path: &Path) -> Option<T
} }
} }
pub fn set_file_readonly(path: &Path, readonly: bool) -> Result<(), std::io::Error> { pub fn set_file_readonly(path: &Path, readonly: bool) -> anyhow::Result<()> {
if path.is_symlink() {
bail!(
"path {} is a symlink, trying to change its write permission will only change the original file",
path.to_string_lossy()
);
}
if !path.is_file() { if !path.is_file() {
eprintln!("WARN: trying to set readonly on a file that does not exist"); debug!(
"trying to set readonly on a file that does not exist: {}",
path.to_string_lossy()
);
return Ok(()); return Ok(());
} }
let mut perms = fs::metadata(path) let mut perms = fs::metadata(path)
.expect("Could not get metadata for file") .expect("Could not get metadata for file")
.permissions(); .permissions();
perms.set_readonly(readonly); perms.set_readonly(readonly);
fs::set_permissions(path, perms) Ok(fs::set_permissions(path, perms)?)
}
pub fn setcap_executable() -> Option<String> {
if dep_getcap_setcap().check() {
Some("setcap".into())
} else if Path::new("/sbin/setcap").try_exists().unwrap_or_default() {
Some("/sbin/setcap".into())
} else {
None
}
}
pub fn getcap_executable() -> Option<String> {
if dep_getcap_setcap().check() {
Some("getcap".into())
} else if Path::new("/sbin/getcap").try_exists().unwrap_or_default() {
Some("/sbin/getcap".into())
} else {
None
}
} }
pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> { pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
vec![ vec![
"setcap".into(), setcap_executable().unwrap_or("setcap".into()),
"CAP_SYS_NICE=eip".into(), "CAP_SYS_NICE=eip".into(),
profile profile
.prefix .prefix
@ -80,16 +111,42 @@ pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
] ]
} }
pub async fn setcap_cap_sys_nice_eip(profile: &Profile) { pub async fn verify_cap_sys_nice_eip(profile: &Profile) -> bool {
if let Err(e) = async_process("pkexec", Some(&setcap_cap_sys_nice_eip_cmd(profile)), None).await let xrservice_binary = profile.xrservice_binary().to_string_lossy().to_string();
{ if let Some(getcap_exec) = getcap_executable() {
eprintln!("Error: failed running setcap: {e}"); match async_process(&getcap_exec, Some(&[&xrservice_binary]), None).await {
Err(e) => {
error!("failed to run `getcap {xrservice_binary}`: {e:?}");
false
}
Ok(out) => {
debug!("getcap {xrservice_binary} stdout: {}", out.stdout);
debug!("getcap {xrservice_binary} stderr: {}", out.stderr);
if out.exit_code != 0 {
error!(
"command `getcap {xrservice_binary}` failed with status code {}",
out.exit_code
);
false
} else {
out.stdout.to_lowercase().contains("cap_sys_nice=eip")
}
}
}
} else {
error!("getcap executable does not exist");
false
} }
} }
pub async fn setcap_cap_sys_nice_eip(profile: &Profile) -> anyhow::Result<()> {
async_process("pkexec", Some(&setcap_cap_sys_nice_eip_cmd(profile)), None).await?;
Ok(())
}
pub fn rm_rf(path: &Path) { pub fn rm_rf(path: &Path) {
if remove_dir_all(path).is_err() { if remove_dir_all(path).is_err() {
eprintln!("Failed to remove path {}", path.to_string_lossy()); error!("failed to remove path {}", path.to_string_lossy());
} }
} }
@ -100,11 +157,13 @@ pub fn copy_file(source: &Path, dest: &Path) {
.unwrap_or_else(|_| panic!("Failed to create dir {}", parent.to_str().unwrap())); .unwrap_or_else(|_| panic!("Failed to create dir {}", parent.to_str().unwrap()));
} }
} }
set_file_readonly(dest, false) if !dest.is_symlink() {
.unwrap_or_else(|_| panic!("Failed to set file {} as rw", dest.to_string_lossy())); set_file_readonly(dest, false)
copy(source, dest).unwrap_or_else(|_| { .unwrap_or_else(|_| panic!("Failed to set file {} as rw", dest.to_string_lossy()));
}
copy(source, dest).unwrap_or_else(|e| {
panic!( panic!(
"Failed to copy {} to {}", "Failed to copy {} to {}: {e}",
source.to_string_lossy(), source.to_string_lossy(),
dest.to_string_lossy() dest.to_string_lossy()
) )
@ -118,6 +177,17 @@ pub fn mount_has_nosuid(path: &Path) -> Result<bool, Errno> {
} }
} }
pub fn mark_as_executable(path: &Path) -> anyhow::Result<()> {
if !path.is_file() {
bail!("Path '{}' is not a file", path.to_string_lossy())
} else {
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(perms.mode() | 0o111);
fs::set_permissions(path, perms)?;
Ok(())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::mount_has_nosuid; use super::mount_has_nosuid;

View file

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

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)] #[derive(Debug, Clone)]
pub struct VulkanInfo { pub struct VulkanInfo {
pub has_nvidia_gpu: bool,
pub has_monado_vulkan_layers: bool,
pub gpu_names: Vec<String>, pub gpu_names: Vec<String>,
} }
const NVIDIA_VENDOR_ID: u32 = 0x10de; // const NVIDIA_VENDOR_ID: u32 = 0x10de;
impl VulkanInfo { impl VulkanInfo {
/// # Safety /// # Safety
@ -25,40 +23,19 @@ impl VulkanInfo {
None, None,
) )
}?; }?;
let mut has_nvidia_gpu = false;
let mut has_monado_vulkan_layers = false;
let gpu_names = unsafe { instance.enumerate_physical_devices() }? let gpu_names = unsafe { instance.enumerate_physical_devices() }?
.into_iter() .into_iter()
.filter_map(|d| { .filter_map(|d| {
let props = unsafe { instance.get_physical_device_properties(d) }; Some(
if props.vendor_id == NVIDIA_VENDOR_ID { unsafe { instance.get_physical_device_properties(d) }
has_nvidia_gpu = true; .device_name_as_c_str()
} .ok()?
if !has_monado_vulkan_layers { .to_string_lossy()
has_monado_vulkan_layers = .to_string(),
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())
}) })
.collect(); .collect();
unsafe { instance.destroy_instance(None) }; unsafe { instance.destroy_instance(None) };
Ok(Self { Ok(Self { gpu_names })
gpu_names,
has_nvidia_gpu,
has_monado_vulkan_layers,
})
} }
} }

View file

@ -11,9 +11,6 @@
#[allow(non_snake_case)] #[allow(non_snake_case)]
mod internal; mod internal;
/// timeout for dbus methods in seconds
const TIMEOUT: i32 = 10;
async fn proxy<'a>() -> zbus::Result<internal::ServerProxy<'a>> { async fn proxy<'a>() -> zbus::Result<internal::ServerProxy<'a>> {
let connection = zbus::Connection::session().await?; let connection = zbus::Connection::session().await?;
let proxy = internal::ServerProxy::new(&connection).await?; let proxy = internal::ServerProxy::new(&connection).await?;
@ -25,7 +22,7 @@ pub async fn is_pairing_mode() -> zbus::Result<bool> {
} }
pub async fn enable_pairing() -> zbus::Result<String> { pub async fn enable_pairing() -> zbus::Result<String> {
proxy().await?.enable_pairing(TIMEOUT).await proxy().await?.enable_pairing(0).await
} }
pub async fn disable_pairing() -> zbus::Result<()> { pub async fn disable_pairing() -> zbus::Result<()> {

View file

@ -1,5 +1,6 @@
use libmonado::{self, BatteryStatus, DeviceRole}; use libmonado::{self, BatteryStatus, DeviceRole};
use std::{collections::HashMap, fmt::Display, slice::Iter}; use std::{collections::HashMap, fmt::Display, slice::Iter};
use tracing::error;
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum XRDeviceRole { pub enum XRDeviceRole {
@ -280,8 +281,8 @@ impl XRDevice {
if let Some(target) = devs.get_mut(&index) { if let Some(target) = devs.get_mut(&index) {
target.roles.push(role.into()); target.roles.push(role.into());
} else { } else {
eprintln!( error!(
"Could not find device index {index} for role {}", "could not find device index {index} for role {}",
XRDeviceRole::from(role) XRDeviceRole::from(role)
) )
} }

View file

@ -5,9 +5,7 @@
"xrservice_path": "/home/user/monado", "xrservice_path": "/home/user/monado",
"xrservice_repo": null, "xrservice_repo": null,
"xrservice_branch": null, "xrservice_branch": null,
"opencomposite_path": "/home/user/opencomposite", "ovr_comp": { "mod_type": "Opencomposite", "path": "/home/user/opencomposite", "repo": null, "branch": null },
"opencomposite_repo": null,
"opencomposite_branch": null,
"features": { "features": {
"libsurvive": { "libsurvive": {
"feature_type": "Libsurvive", "feature_type": "Libsurvive",
@ -34,4 +32,4 @@
"can_be_built": true, "can_be_built": true,
"editable": true, "editable": true,
"pull_on_build": true "pull_on_build": true
} }

View file

@ -12,5 +12,5 @@
} }
], ],
"application": ["foobar", "baz"], "application": ["foobar", "baz"],
"tcp_only": true "tcp-only": true
} }