mirror of
https://gitlab.com/gabmus/envision.git
synced 2025-04-20 03:24:52 +00:00
Compare commits
101 commits
Author | SHA1 | Date | |
---|---|---|---|
|
139f72e294 | ||
|
7a02fcc5d1 | ||
|
2f5ec57a0a | ||
|
8742a27b7c | ||
|
25c90d175f | ||
|
2fc33b10b0 | ||
|
f38199601e | ||
|
db45103d1b | ||
|
e117986715 | ||
|
1ac253ecbf | ||
|
92d17512a4 | ||
|
40503d0895 | ||
|
96717d193f | ||
|
9c6bfe110a | ||
|
39ace1d8db | ||
|
33db18bd62 | ||
|
1ed031a2bf | ||
|
338e711455 | ||
|
3680e305a9 | ||
|
9bdda7d63d | ||
|
67e2ade501 | ||
|
35d268e01b | ||
|
2bec37ee24 | ||
|
160d733054 | ||
|
eda2105566 | ||
|
18e5670d90 | ||
|
879637115c | ||
|
b24c8e4c0b | ||
|
5187a00971 | ||
|
1a71c82d1a | ||
|
96e1a20eda | ||
|
869927bb5c | ||
|
e62d0ced36 | ||
|
1c3b4decb5 | ||
|
8ffb44aa11 | ||
|
a651b87cc3 | ||
|
cfb874fa35 | ||
|
69eba0153b | ||
|
aa9bd09372 | ||
|
6fa7d1e2a3 | ||
|
e5a59ebf62 | ||
|
eef963793d | ||
|
4767a4eb13 | ||
|
0adf894b45 | ||
|
31b22b59f3 | ||
|
db5c295435 | ||
|
d38acf0a7e | ||
|
e5435d0aa3 | ||
|
696c541598 | ||
|
e69a7a9bd6 | ||
|
ca813d6168 | ||
|
0020dcf3d4 | ||
|
36322b3b2c | ||
|
bc5c4a4a40 | ||
|
e781736ffa | ||
|
f04723c1c4 | ||
|
67172df567 | ||
|
b61f2d963f | ||
|
9711c257a6 | ||
|
380f800fa8 | ||
|
46df6d36e5 | ||
|
68d7757aa4 | ||
|
ce5f486596 | ||
|
4f80aed3c2 | ||
|
7f05d696c4 | ||
|
9a4ef01ed9 | ||
|
92cd8f6a94 | ||
|
4905c8fed1 | ||
|
e685cf757d | ||
|
4ea0ce53b0 | ||
|
a9fa4f8cf4 | ||
|
61f13dbd8f | ||
|
c78b844b60 | ||
|
592709ab56 | ||
|
448b97469e | ||
|
3f846b26e0 | ||
|
2217f84ff4 | ||
|
f1e8a010c8 | ||
|
d8ca8cf961 | ||
|
356b42b056 | ||
|
88dea2aa43 | ||
|
1bd34b9ad0 | ||
|
802337a8f1 | ||
|
e514e55008 | ||
|
afbebfc2ec | ||
|
00322492bd | ||
|
355ad050f0 | ||
|
4638ac1bf4 | ||
|
3e9c4bed80 | ||
|
c5460758c5 | ||
|
e65c4a8d9d | ||
|
af5c57f0f8 | ||
|
7e27614fb8 | ||
|
09172d6f6c | ||
|
e19df22cce | ||
|
a813015885 | ||
|
0b54808d20 | ||
|
3318ad4cc6 | ||
|
2c642b489c | ||
|
fa79ec3749 | ||
|
bdd61dfbab |
83 changed files with 5440 additions and 1362 deletions
|
@ -1,54 +1,17 @@
|
|||
image: "debian:unstable"
|
||||
image: "ubuntu:24.04"
|
||||
|
||||
stages:
|
||||
- check
|
||||
- deploy
|
||||
|
||||
cargo:fmtcheck:
|
||||
image: "rust:slim"
|
||||
stage: check
|
||||
script:
|
||||
- rustup component add rustfmt
|
||||
# Create blank versions of our configured files
|
||||
# so rustfmt does not yell about non-existent files or completely empty files
|
||||
- echo -e "" >> src/constants.rs
|
||||
- rustc -Vv && cargo -Vv
|
||||
- cargo fmt --version
|
||||
- cargo fmt --all -- --check
|
||||
|
||||
cargo:clippy:
|
||||
commitcheck:
|
||||
image: "python"
|
||||
stage: check
|
||||
variables:
|
||||
RUSTFLAGS: "-Dwarnings"
|
||||
GIT_STRATEGY: clone
|
||||
script:
|
||||
- apt-get update
|
||||
- apt-get install libgtk-4-dev libadwaita-1-dev libssl-dev libjxl-dev libvte-2.91-gtk4-dev meson ninja-build git desktop-file-utils gettext file libusb-dev libusb-1.0-0-dev libopenxr-dev curl -y
|
||||
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup.sh
|
||||
- chmod +x /tmp/rustup.sh
|
||||
- /tmp/rustup.sh -y
|
||||
- source "$HOME/.cargo/env"
|
||||
- rustup component add clippy
|
||||
- rustc -Vv && cargo -Vv
|
||||
- cp src/constants.rs.in src/constants.rs
|
||||
- cargo clippy --version
|
||||
- cargo clippy --all-targets --all-features
|
||||
|
||||
cargo:test:
|
||||
stage: check
|
||||
script:
|
||||
- apt-get update
|
||||
- apt-get install libgtk-4-dev libadwaita-1-dev libssl-dev libjxl-dev libvte-2.91-gtk4-dev meson ninja-build git desktop-file-utils gettext file libusb-dev libusb-1.0-0-dev libopenxr-dev curl -y
|
||||
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup.sh
|
||||
- chmod +x /tmp/rustup.sh
|
||||
- /tmp/rustup.sh -y
|
||||
- source "$HOME/.cargo/env"
|
||||
- rustc --version && cargo --version # Print version info for debugging
|
||||
- meson setup build -Dprefix="$PWD/build/localprefix" -Dprofile=development
|
||||
- ninja -C build
|
||||
- cargo test --workspace --verbose
|
||||
cache:
|
||||
paths:
|
||||
- /var/cache/apt
|
||||
# only run for merge requests
|
||||
- if [ -z "$CI_MERGE_REQUEST_TITLTE" ]; then true; else python ./dist/tagging/check_conventional_commit.py "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"; fi
|
||||
|
||||
appimage:
|
||||
stage: deploy
|
||||
|
@ -59,6 +22,7 @@ appimage:
|
|||
- chmod +x /tmp/rustup.sh
|
||||
- /tmp/rustup.sh -y
|
||||
- source "$HOME/.cargo/env"
|
||||
- rustup component add clippy
|
||||
- bash ./dist/appimage/build_appimage.sh
|
||||
artifacts:
|
||||
paths:
|
||||
|
|
2125
Cargo.lock
generated
2125
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
17
Cargo.toml
17
Cargo.toml
|
@ -1,7 +1,16 @@
|
|||
[package]
|
||||
name = "envision"
|
||||
version = "0.0.1"
|
||||
version = "3.1.0"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"Gabriele Musco <gabmus@disroot.org>",
|
||||
]
|
||||
description = "Orchestrator for the free XR stack"
|
||||
repository = "https://gitlab.com/gabmus/envision"
|
||||
documentation = "https://gitlab.com/gabmus/envision"
|
||||
license = "AGPL-3.0-or-later"
|
||||
keywords = ["desktop", "linux", "vr", "xr", "gtk"]
|
||||
readme = "README.md"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
@ -29,3 +38,9 @@ openxr = { version = "0.19.0", features = ["linked"] }
|
|||
ash = "0.38.0"
|
||||
sha2 = "0.10.8"
|
||||
tokio = { version = "1.39.3", features = ["process"] }
|
||||
notify-rust = "4.11.3"
|
||||
zbus = { version = "5.1.1", features = ["tokio"] }
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
|
||||
tracing = "0.1.41"
|
||||
tracing-appender = "0.2.3"
|
||||
serde_yaml = "0.9.34"
|
||||
|
|
|
@ -60,6 +60,10 @@ cd envision
|
|||
|
||||
</details>
|
||||
|
||||
# Debugging
|
||||
|
||||
To view all the logs you need to run envision with the env var `RUST_LOG=trace`.
|
||||
|
||||
# Common issues
|
||||
|
||||
## NOSUID with systemd-homed
|
||||
|
|
|
@ -11,3 +11,4 @@ Keywords=vr;virtual;reality;monado;
|
|||
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
|
||||
Icon=@APP_ID@
|
||||
StartupNotify=true
|
||||
X-GNOME-UsesNotifications=true
|
||||
|
|
|
@ -2,22 +2,228 @@
|
|||
<component type="desktop-application">
|
||||
<id>@APP_ID@</id>
|
||||
<metadata_license>CC0</metadata_license>
|
||||
<project_license>AGPL-3.0</project_license>
|
||||
<project_license>AGPL-3.0-or-later</project_license>
|
||||
<name translatable="no">@PRETTY_NAME@</name>
|
||||
<summary>GUI for Monado</summary>
|
||||
<summary>Orchestrator for the free XR stack</summary>
|
||||
<description>
|
||||
<p>GUI for Monado</p> <!-- temporary -->
|
||||
<p>Orchestrator for the free XR stack</p>
|
||||
</description>
|
||||
<!--screenshots>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://gitlab.com/gabmus/envision/raw/main/misc/screenshots/screenshot1.png</image>
|
||||
<image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/01.png</image>
|
||||
<caption>Main window</caption>
|
||||
</screenshot>
|
||||
</screenshots-->
|
||||
<screenshot type="default">
|
||||
<image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/02.png</image>
|
||||
<caption>Profile editor</caption>
|
||||
</screenshot>
|
||||
<screenshot type="default">
|
||||
<image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/03.png</image>
|
||||
<caption>Profile running</caption>
|
||||
</screenshot>
|
||||
<screenshot type="default">
|
||||
<image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/04.png</image>
|
||||
<caption>Profile running with debug view open</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<url type="homepage">@REPO_URL@</url>
|
||||
<url type="bugtracker">@REPO_URL@/issues</url>
|
||||
<content_rating type="oars-1.0" />
|
||||
<releases>
|
||||
<release version="3.1.0" date="2025-04-08">
|
||||
<description>
|
||||
<p>What's new</p>
|
||||
<ul>
|
||||
<li>don't set openvrpaths as read only during profile startup</li>
|
||||
<li>small design changes to build window ui</li>
|
||||
<li>add support for vapor openvr compatibility module</li>
|
||||
<li>remove monado vulkan layers check for nvidia</li>
|
||||
</ul>
|
||||
<p>Fixes</p>
|
||||
<ul>
|
||||
<li>disable and blacklist wayvr dashboard plugin</li>
|
||||
<li>monado dependencies: use wayland-protocols-devel on Fedora</li>
|
||||
</ul>
|
||||
<p>Other changes</p>
|
||||
<ul>
|
||||
<li>clippy</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.0.1" date="2025-03-02">
|
||||
<description>
|
||||
<p>Fixes</p>
|
||||
<ul>
|
||||
<li>libnotify headers path in wivrn depcheck</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.0.0" date="2025-03-01">
|
||||
<description>
|
||||
<p>Breaking changes</p>
|
||||
<ul>
|
||||
<li>plugin store</li>
|
||||
</ul>
|
||||
<p>What's new</p>
|
||||
<ul>
|
||||
<li>Add WayVR Dashboard to the plugin list</li>
|
||||
<li>wivrn: replace pulse dependency with pipewire</li>
|
||||
<li>set wivrn launch options in the default profile</li>
|
||||
<li>support for plugin dependencies and wayvr dashboards (using unreleased api)</li>
|
||||
<li>launch options for plugins</li>
|
||||
<li>homepage and author in plugin details</li>
|
||||
<li>write rolling logs to file</li>
|
||||
<li>add xrizer as an option for openvr compatibility module</li>
|
||||
<li>switch wlx manifest</li>
|
||||
<li>fetch plugins manifests online</li>
|
||||
<li>add telescope to plugin store</li>
|
||||
<li>ask to build profile after editing it</li>
|
||||
<li>add cpu to debug info</li>
|
||||
<li>make env var description selectable</li>
|
||||
<li>press enter on env var entry to add</li>
|
||||
<li>clearer messaging around setcap failures; getcap after setcap</li>
|
||||
<li>version command line option</li>
|
||||
<li>single stage ci with tests, clippy and fmt check all in one</li>
|
||||
<li>use ubuntu for the ci</li>
|
||||
</ul>
|
||||
<p>Fixes</p>
|
||||
<ul>
|
||||
<li>onnxruntime build error when latest release has no artifacts</li>
|
||||
<li>typo in install wivrn box</li>
|
||||
<li>typo in XRT_COMPOSITOR_SCALE_PERCENTAGE</li>
|
||||
<li>add plugin to config via function instead of signal</li>
|
||||
<li>refresh all rows on plugin install (fixes dependencies showing up as not installed)</li>
|
||||
<li>always mark plugin executable as executable</li>
|
||||
<li>wrap single plugin cmd parts in single quotes</li>
|
||||
<li>use correct wayland-protocols package name for open suse</li>
|
||||
<li>typo in plugin schema</li>
|
||||
<li>remove canonicalize from get steamvr bin dir path function</li>
|
||||
<li>actually return steamvr dir in get_steamvr_base_dir</li>
|
||||
<li>canonicalize some steamvr related paths to hopefully resolve symlinks</li>
|
||||
<li>get ovr compatibility module runtime dir from profile ovr compatibility module struct</li>
|
||||
<li>use exists() to verify existance of socket file</li>
|
||||
<li>correct wording of lighthouse calibration</li>
|
||||
<li>get steamvr bin dir by parsing libraryfolders.vdf</li>
|
||||
<li>switch to searching for the xml for deb based distros</li>
|
||||
<li>Include not shared object wayland-protocols</li>
|
||||
<li>add libbsd deps for monado</li>
|
||||
<li>add wayland drm-lease protocols dep for monado</li>
|
||||
<li>use boost dev packages</li>
|
||||
<li>debian package name for gstreamer plugins base</li>
|
||||
<li>print active runtime related informative logs as debug</li>
|
||||
</ul>
|
||||
<p>Other changes</p>
|
||||
<ul>
|
||||
<li>plugins: point to stardust hosted manifest</li>
|
||||
<li>cargo: revert back to serde_yaml over unsound advisory</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.0.1" date="2024-12-11">
|
||||
<description>
|
||||
<p>Fixes</p>
|
||||
<ul>
|
||||
<li>add screenshots to appdata</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.0.0" date="2024-12-09">
|
||||
<description>
|
||||
<p>Breaking changes</p>
|
||||
<ul>
|
||||
<li>enable support for different openvr compatibility modules other than opencomposite</li>
|
||||
</ul>
|
||||
<p>What's new</p>
|
||||
<ul>
|
||||
<li>add metadata to Cargo.toml; get developers from Cargo.toml authors; rectify SPDX id for license as AGPL-3.0-or-later</li>
|
||||
<li>refactor builders cmake vars and env to use inner blocks</li>
|
||||
<li>disable wivrnctl; refactor cmake vars in wivrn builder</li>
|
||||
<li>make left and right qwerty controllers appear as no controller detected</li>
|
||||
<li>try to find libmonado and openxr shared objects by reading openxr config</li>
|
||||
<li>prefer symlinks over generating files for openxr active runtime json file</li>
|
||||
<li>move steam library folders parser to own module; function to find steam openxr json; format</li>
|
||||
<li>proper logging framework</li>
|
||||
</ul>
|
||||
<p>Fixes</p>
|
||||
<ul>
|
||||
<li>build profile can be specified manually</li>
|
||||
<li>update wivrn libmonado path to wirvn/libmonado_wivrn.so</li>
|
||||
<li>create openxr config dir when starting profile</li>
|
||||
<li>add libnotify-dev dependency for wivrn</li>
|
||||
<li>openssl dep is an include</li>
|
||||
<li>add openssl-devel dep for wivrn</li>
|
||||
<li>negative logic and early return in start xrservice func</li>
|
||||
<li>use let err instead of match in restore xr files func</li>
|
||||
</ul>
|
||||
<p>Other changes</p>
|
||||
<ul>
|
||||
<li>format</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.1.1" date="2024-11-29">
|
||||
<description>
|
||||
<p>Fixes</p>
|
||||
<ul>
|
||||
<li>remove wivrn pairing mode timer</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.1.0" date="2024-11-28">
|
||||
<description>
|
||||
<p>What's new</p>
|
||||
<ul>
|
||||
<li>check missing deps command line option; handle non activating opts is a proper method and all opts are part of the struct</li>
|
||||
<li>move dependency collection to profile method</li>
|
||||
</ul>
|
||||
<p>Fixes</p>
|
||||
<ul>
|
||||
<li>profile context menu binding</li>
|
||||
<li>profile context menu should prefer opening towards the top</li>
|
||||
<li>exit on listing profiles</li>
|
||||
</ul>
|
||||
<p>Other changes</p>
|
||||
<ul>
|
||||
<li>in tagging script add cargo lock as well</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.0" date="2024-11-27">
|
||||
<description>
|
||||
<p>Breaking changes</p>
|
||||
<ul>
|
||||
<li>wivrn pairing support</li>
|
||||
</ul>
|
||||
<p>What's new</p>
|
||||
<ul>
|
||||
<li>send desktop notification instead of showing alert when failing to inhibit screen lock</li>
|
||||
<li>split cmake var into name and value when it contains =</li>
|
||||
</ul>
|
||||
<p>Fixes</p>
|
||||
<ul>
|
||||
<li>disable wivrn launch options instructions in debug view</li>
|
||||
<li>lower wivrn default foveation to 0.5</li>
|
||||
<li>separate debugbuild option to build in debug mode</li>
|
||||
<li>openhmd: add meson as a dependency</li>
|
||||
<li>dependencies: correct packages for GL/gl.h</li>
|
||||
<li>appimage: build for devel releases</li>
|
||||
<li>monado dependencies: add libxrandr</li>
|
||||
</ul>
|
||||
<p>Other changes</p>
|
||||
<ul>
|
||||
<li>format</li>
|
||||
<li>ci: conventional commit check</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.0.2" date="2024-11-02">
|
||||
<description>
|
||||
<p>Fixes</p>
|
||||
<ul>
|
||||
<li>appimage: build for non-devel releases</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.0.1" date="2024-11-02">
|
||||
<description>
|
||||
<p>Initial release</p>
|
||||
|
|
BIN
data/screenshots/01.png
Normal file
BIN
data/screenshots/01.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
BIN
data/screenshots/02.png
Normal file
BIN
data/screenshots/02.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
BIN
data/screenshots/03.png
Normal file
BIN
data/screenshots/03.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
BIN
data/screenshots/04.png
Normal file
BIN
data/screenshots/04.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 95 KiB |
11
dist/appimage/build_appimage.sh
vendored
11
dist/appimage/build_appimage.sh
vendored
|
@ -8,17 +8,24 @@ if [[ ! -f Cargo.toml ]]; then
|
|||
fi
|
||||
|
||||
meson setup appimage_build -Dprefix=/usr -Dprofile=default
|
||||
meson test -C appimage_build --print-errorlogs
|
||||
DESTDIR="$PWD/AppDir" ninja -C appimage_build install
|
||||
curl -SsLO https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||
chmod +x linuxdeploy-x86_64.AppImage
|
||||
cp dist/appimage/linuxdeploy-plugin-gtk.sh ./
|
||||
|
||||
if [ -f "AppDir/usr/share/icons/hicolor/scalable/apps/org.gabmus.envision.Devel.svg" ]; then
|
||||
APPID="org.gabmus.envision.Devel"
|
||||
else
|
||||
APPID="org.gabmus.envision"
|
||||
fi
|
||||
|
||||
./linuxdeploy-x86_64.AppImage \
|
||||
--appimage-extract-and-run \
|
||||
--appdir AppDir \
|
||||
--plugin gtk \
|
||||
--output appimage \
|
||||
--icon-file AppDir/usr/share/icons/hicolor/scalable/apps/org.gabmus.envision.Devel.svg \
|
||||
--desktop-file AppDir/usr/share/applications/org.gabmus.envision.Devel.desktop
|
||||
--icon-file AppDir/usr/share/icons/hicolor/scalable/apps/$APPID.svg \
|
||||
--desktop-file AppDir/usr/share/applications/$APPID.desktop
|
||||
|
||||
rm ./linuxdeploy-plugin-gtk.sh
|
||||
|
|
1
dist/arch/PKGBUILD
vendored
1
dist/arch/PKGBUILD
vendored
|
@ -33,7 +33,6 @@ makedepends=(
|
|||
)
|
||||
optdepends=(
|
||||
'libudev0-shim: steamvr_lh lighthouse driver support'
|
||||
'monado-vulkan-layers-git: Vulkan layers for NVIDIA users'
|
||||
)
|
||||
provides=(envision)
|
||||
conflicts=(envision)
|
||||
|
|
37
dist/tagging/check_conventional_commit.py
vendored
Executable file
37
dist/tagging/check_conventional_commit.py
vendored
Executable file
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from typing import List
|
||||
from sys import argv, stderr
|
||||
import re
|
||||
|
||||
|
||||
CONVENTIONAL_COMMIT_RE = re.compile(r"(feat|fix|chore)(\([\w_\-\/ ]+\))?: .+")
|
||||
|
||||
|
||||
def eprint(*args, **kwargs):
|
||||
print(*args, file=stderr, **kwargs)
|
||||
|
||||
|
||||
def cmd(args: List[str]) -> List[str]:
|
||||
proc = Popen(args, stdout=PIPE)
|
||||
(stdout, _) = proc.communicate()
|
||||
retcode = proc.returncode
|
||||
if retcode != 0:
|
||||
raise ValueError(f"Command {" ".join(args)} failed with code {retcode}")
|
||||
return stdout.decode().splitlines()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
target_branch = argv[1]
|
||||
if target_branch is None:
|
||||
eprint(f"Usage: {argv[0]} CURRENT_COMMIT TARGET_BRANCH")
|
||||
exit(1)
|
||||
cmd(["git", "fetch", "origin", f"{target_branch}:{target_branch}"])
|
||||
cmsgs = cmd(["git", "show", "-s", "--format=%s", f"{target_branch}..HEAD"])
|
||||
success = True
|
||||
for cmsg in cmsgs:
|
||||
if CONVENTIONAL_COMMIT_RE.match(cmsg) is None:
|
||||
eprint(f"Error: commit message '{cmsg}' does not follow the conventional commit standard")
|
||||
if not success:
|
||||
exit(1)
|
2
dist/tagging/release.py
vendored
2
dist/tagging/release.py
vendored
|
@ -109,7 +109,7 @@ if __name__ == "__main__":
|
|||
print(f"Will commit with the following message: '{commitmsg}'")
|
||||
if not yes_no():
|
||||
sys.exit(0)
|
||||
cmd(["git", "add", "meson.build", "Cargo.toml", METAINFO_PATH])
|
||||
cmd(["git", "add", "meson.build", "Cargo.toml", "Cargo.lock", METAINFO_PATH])
|
||||
cmd(["git", "commit", "-m", commitmsg])
|
||||
print(f"Will add tag '{tag}'")
|
||||
if not yes_no():
|
||||
|
|
33
meson.build
33
meson.build
|
@ -1,9 +1,9 @@
|
|||
project(
|
||||
'envision',
|
||||
'rust',
|
||||
version: '0.0.1', # version number row
|
||||
version: '3.1.0', # version number row
|
||||
meson_version: '>= 0.59',
|
||||
license: 'AGPL-3.0',
|
||||
license: 'AGPL-3.0-or-later',
|
||||
)
|
||||
|
||||
i18n = import('i18n')
|
||||
|
@ -38,17 +38,30 @@ iconsdir = datadir / 'icons'
|
|||
podir = meson.project_source_root() / 'po'
|
||||
gettext_package = meson.project_name()
|
||||
|
||||
# are we building a tagged version?
|
||||
if run_command('git', 'describe', '--tags', '--exact-match').returncode() != 0
|
||||
profile = 'Devel'
|
||||
vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip()
|
||||
if vcs_tag == ''
|
||||
version_suffix = '-devel'
|
||||
opt_profile = get_option('profile')
|
||||
|
||||
# if a profile isn't specified infer from git
|
||||
if opt_profile == 'default'
|
||||
# are we building a tagged version?
|
||||
if run_command('git', 'describe', '--tags', '--exact-match').returncode() != 0
|
||||
profile = 'Devel'
|
||||
vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip()
|
||||
if vcs_tag == ''
|
||||
version_suffix = '-devel'
|
||||
else
|
||||
version_suffix = '-@0@'.format(vcs_tag)
|
||||
endif
|
||||
application_id = '@0@.@1@'.format(base_id, profile)
|
||||
else
|
||||
version_suffix = '-@0@'.format(vcs_tag)
|
||||
profile = ''
|
||||
version_suffix = ''
|
||||
application_id = base_id
|
||||
endif
|
||||
elif opt_profile == 'development'
|
||||
profile = 'Devel'
|
||||
version_suffix = '-devel'
|
||||
application_id = '@0@.@1@'.format(base_id, profile)
|
||||
else
|
||||
elif opt_profile == 'release'
|
||||
profile = ''
|
||||
version_suffix = ''
|
||||
application_id = base_id
|
||||
|
|
|
@ -3,8 +3,16 @@ option(
|
|||
type: 'combo',
|
||||
choices: [
|
||||
'default',
|
||||
'release',
|
||||
'development'
|
||||
],
|
||||
value: 'default',
|
||||
description: 'The build profile. One of "default" or "development".'
|
||||
)
|
||||
|
||||
option(
|
||||
'debugbuild',
|
||||
type: 'boolean',
|
||||
value: false,
|
||||
description: 'Build in debug mode, false by default.'
|
||||
)
|
||||
|
|
|
@ -11,7 +11,22 @@ if [[ -z $PREFIX ]] || [[ -z $CACHE_DIR ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
ONNX_VER=$(curl -sSL "https://api.github.com/repos/microsoft/onnxruntime/releases/latest" | jq -r .tag_name | tr -d v)
|
||||
ONNX_RELEASES=$(curl -sSL "https://api.github.com/repos/microsoft/onnxruntime/releases")
|
||||
NUM_RELEASES=$(echo "$ONNX_RELEASES" | jq -r '[ select (.[]!=null) ] | length')
|
||||
|
||||
for (( IDX=0; IDX<NUM_RELEASES; IDX++ )); do
|
||||
ASSETS_LEN=$(echo "$ONNX_RELEASES" | jq -r ".[$IDX].assets_url" | xargs -n 1 curl -sSL | jq -r '[ select (.[]!=null) ] | length')
|
||||
if [[ $ASSETS_LEN -gt 0 ]]; then
|
||||
ONNX_VER=$(echo "$ONNX_RELEASES" | jq -r ".[$IDX].tag_name" | tr -d v)
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z $ONNX_VER ]]; then
|
||||
echo "Failed to find a suitable ONNX Runtime release."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SYS_ARCH=$(uname -m)
|
||||
|
||||
if [[ $SYS_ARCH == x*64 ]]; then
|
||||
|
|
|
@ -35,28 +35,39 @@ pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<W
|
|||
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
||||
|
||||
let build_dir = profile.features.basalt.path.as_ref().unwrap().join("build");
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
cmake_vars.insert("CMAKE_EXPORT_COMPILE_COMMANDS".into(), "ON".into());
|
||||
cmake_vars.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert("BUILD_TESTS".into(), "OFF".into());
|
||||
cmake_vars.insert("BASALT_INSTANTIATIONS_DOUBLE".into(), "OFF".into());
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_LIBDIR".into(),
|
||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||
);
|
||||
|
||||
let mut cmake_env: HashMap<String, String> = HashMap::new();
|
||||
cmake_env.insert("CMAKE_BUILD_PARALLEL_LEVEL".into(), "2".into());
|
||||
cmake_env.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
|
||||
cmake_env.insert("BUILD_TESTS".into(), "off".into());
|
||||
|
||||
let cmake = Cmake {
|
||||
env: Some(cmake_env),
|
||||
vars: Some(cmake_vars),
|
||||
env: Some({
|
||||
let mut cmake_env: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("CMAKE_BUILD_PARALLEL_LEVEL", "2"),
|
||||
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||
("BUILD_TESTS", "off"),
|
||||
] {
|
||||
cmake_env.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
cmake_env
|
||||
}),
|
||||
vars: Some({
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||
("BUILD_TESTS", "OFF"),
|
||||
("BASALT_INSTANTIATIONS_DOUBLE", "OFF"),
|
||||
] {
|
||||
cmake_vars.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_LIBDIR".into(),
|
||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars
|
||||
}),
|
||||
source_dir: profile.features.basalt.path.as_ref().unwrap().clone(),
|
||||
build_dir: build_dir.clone(),
|
||||
};
|
||||
|
|
|
@ -44,24 +44,30 @@ pub fn get_build_libsurvive_jobs(profile: &Profile, clean_build: bool) -> VecDeq
|
|||
.as_ref()
|
||||
.unwrap()
|
||||
.join("build");
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
cmake_vars.insert("CMAKE_EXPORT_COMPILE_COMMANDS".into(), "ON".into());
|
||||
cmake_vars.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
|
||||
cmake_vars.insert("ENABLE_api_example".into(), "OFF".into());
|
||||
cmake_vars.insert("USE_HIDAPI".into(), "ON".into());
|
||||
cmake_vars.insert("CMAKE_SKIP_INSTALL_RPATH".into(), "YES".into());
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_LIBDIR".into(),
|
||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||
);
|
||||
|
||||
let cmake = Cmake {
|
||||
env: None,
|
||||
vars: Some(cmake_vars),
|
||||
vars: Some({
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||
("ENABLE_api_example", "OFF"),
|
||||
("USE_HIDAPI", "ON"),
|
||||
("CMAKE_SKIP_INSTALL_RPATH", "YES"),
|
||||
] {
|
||||
cmake_vars.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_LIBDIR".into(),
|
||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars
|
||||
}),
|
||||
source_dir: profile.features.libsurvive.path.as_ref().unwrap().clone(),
|
||||
build_dir: build_dir.clone(),
|
||||
};
|
||||
|
|
|
@ -43,37 +43,43 @@ pub fn get_build_monado_jobs(profile: &Profile, clean_build: bool) -> VecDeque<W
|
|||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
cmake_vars.insert("CMAKE_EXPORT_COMPILE_COMMANDS".into(), "ON".into());
|
||||
cmake_vars.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
|
||||
cmake_vars.insert("XRT_HAVE_SYSTEM_CJSON".into(), "NO".into());
|
||||
cmake_vars.insert(
|
||||
"CMAKE_LIBDIR".into(),
|
||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_C_FLAGS".into(),
|
||||
format!("-Wl,-rpath='{}/lib'", profile.prefix.to_string_lossy(),),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_CXX_FLAGS".into(),
|
||||
format!("-Wl,-rpath='{}/lib'", profile.prefix.to_string_lossy(),),
|
||||
);
|
||||
profile.xrservice_cmake_flags.iter().for_each(|(k, v)| {
|
||||
if k == "CMAKE_C_FLAGS" || k == "CMAKE_CXX_FLAGS" {
|
||||
cmake_vars.insert(k.clone(), format!("{} {}", cmake_vars.get(k).unwrap(), v));
|
||||
} else {
|
||||
cmake_vars.insert(k.clone(), v.clone());
|
||||
}
|
||||
});
|
||||
|
||||
let cmake = Cmake {
|
||||
env: Some(env),
|
||||
vars: Some(cmake_vars),
|
||||
vars: Some({
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||
("XRT_HAVE_SYSTEM_CJSON", "NO"),
|
||||
] {
|
||||
cmake_vars.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
cmake_vars.insert(
|
||||
"CMAKE_LIBDIR".into(),
|
||||
profile.prefix.join("lib").to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_C_FLAGS".into(),
|
||||
format!("-Wl,-rpath='{}/lib'", profile.prefix.to_string_lossy(),),
|
||||
);
|
||||
cmake_vars.insert(
|
||||
"CMAKE_CXX_FLAGS".into(),
|
||||
format!("-Wl,-rpath='{}/lib'", profile.prefix.to_string_lossy(),),
|
||||
);
|
||||
profile.xrservice_cmake_flags.iter().for_each(|(k, v)| {
|
||||
if k == "CMAKE_C_FLAGS" || k == "CMAKE_CXX_FLAGS" {
|
||||
cmake_vars.insert(k.clone(), format!("{} {}", cmake_vars.get(k).unwrap(), v));
|
||||
} else {
|
||||
cmake_vars.insert(k.clone(), v.clone());
|
||||
}
|
||||
});
|
||||
cmake_vars
|
||||
}),
|
||||
source_dir: profile.xrservice_path.clone(),
|
||||
build_dir: build_dir.clone(),
|
||||
};
|
||||
|
|
|
@ -19,13 +19,15 @@ pub fn get_build_opencomposite_jobs(profile: &Profile, clean_build: bool) -> Vec
|
|||
|
||||
let git = Git {
|
||||
repo: profile
|
||||
.opencomposite_repo
|
||||
.ovr_comp
|
||||
.repo
|
||||
.as_ref()
|
||||
.unwrap_or(&"https://gitlab.com/znixian/OpenOVR.git".into())
|
||||
.clone(),
|
||||
dir: profile.opencomposite_path.clone(),
|
||||
dir: profile.ovr_comp.path.clone(),
|
||||
branch: profile
|
||||
.opencomposite_branch
|
||||
.ovr_comp
|
||||
.branch
|
||||
.as_ref()
|
||||
.unwrap_or(&"openxr".into())
|
||||
.clone(),
|
||||
|
@ -33,14 +35,20 @@ pub fn get_build_opencomposite_jobs(profile: &Profile, clean_build: bool) -> Vec
|
|||
|
||||
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
||||
|
||||
let build_dir = profile.opencomposite_path.join("build");
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
cmake_vars.insert("CMAKE_EXPORT_COMPILE_COMMANDS".into(), "ON".into());
|
||||
cmake_vars.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
|
||||
let build_dir = profile.ovr_comp.path.join("build");
|
||||
let cmake = Cmake {
|
||||
env: None,
|
||||
vars: Some(cmake_vars),
|
||||
source_dir: profile.opencomposite_path.clone(),
|
||||
vars: Some({
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||
] {
|
||||
cmake_vars.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
cmake_vars
|
||||
}),
|
||||
source_dir: profile.ovr_comp.path.clone(),
|
||||
build_dir: build_dir.clone(),
|
||||
};
|
||||
if !Path::new(&build_dir).is_dir() || clean_build {
|
||||
|
|
85
src/builders/build_vapor.rs
Normal file
85
src/builders/build_vapor.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use crate::{
|
||||
build_tools::{cmake::Cmake, git::Git},
|
||||
profile::Profile,
|
||||
termcolor::TermColor,
|
||||
ui::job_worker::job::{FuncWorkerData, FuncWorkerOut, WorkerJob},
|
||||
util::file_utils::{copy_file, rm_rf},
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
fs::create_dir_all,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
pub fn get_build_vapor_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> {
|
||||
let mut jobs = VecDeque::<WorkerJob>::new();
|
||||
jobs.push_back(WorkerJob::new_printer(
|
||||
"Building VapoR...",
|
||||
Some(TermColor::Blue),
|
||||
));
|
||||
|
||||
let git = Git {
|
||||
repo: profile
|
||||
.ovr_comp
|
||||
.repo
|
||||
.as_ref()
|
||||
.unwrap_or(&"https://github.com/micheal65536/VapoR.git".into())
|
||||
.clone(),
|
||||
dir: profile.ovr_comp.path.clone(),
|
||||
branch: profile
|
||||
.ovr_comp
|
||||
.branch
|
||||
.as_ref()
|
||||
.unwrap_or(&"master".into())
|
||||
.clone(),
|
||||
};
|
||||
|
||||
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
||||
|
||||
let build_dir = profile.ovr_comp.path.join("build");
|
||||
let cmake = Cmake {
|
||||
env: None,
|
||||
vars: Some({
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("VAPOR_LOG_SILENT=ON", "ON"),
|
||||
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||
] {
|
||||
cmake_vars.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
cmake_vars
|
||||
}),
|
||||
source_dir: profile.ovr_comp.path.clone(),
|
||||
build_dir: build_dir.clone(),
|
||||
};
|
||||
if !Path::new(&build_dir).is_dir() || clean_build {
|
||||
rm_rf(&build_dir);
|
||||
jobs.push_back(cmake.get_prepare_job());
|
||||
}
|
||||
jobs.push_back(cmake.get_build_job());
|
||||
jobs.push_back(WorkerJob::Func(FuncWorkerData {
|
||||
func: Box::new(move || {
|
||||
let dest_dir = build_dir.join("bin/linux64");
|
||||
if let Err(e) = create_dir_all(&dest_dir) {
|
||||
return FuncWorkerOut {
|
||||
success: false,
|
||||
out: vec![format!(
|
||||
"failed to create dir {}: {e}",
|
||||
dest_dir.to_string_lossy()
|
||||
)],
|
||||
};
|
||||
}
|
||||
copy_file(
|
||||
&build_dir.join("src/vrclient.so"),
|
||||
&dest_dir.join("vrclient.so"),
|
||||
);
|
||||
|
||||
FuncWorkerOut {
|
||||
success: true,
|
||||
out: Vec::default(),
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
jobs
|
||||
}
|
|
@ -34,23 +34,29 @@ pub fn get_build_wivrn_jobs(profile: &Profile, clean_build: bool) -> VecDeque<Wo
|
|||
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
||||
|
||||
let build_dir = profile.xrservice_path.join("build");
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
cmake_vars.insert("CMAKE_EXPORT_COMPILE_COMMANDS".into(), "ON".into());
|
||||
cmake_vars.insert("CMAKE_BUILD_TYPE".into(), "RelWithDebInfo".into());
|
||||
cmake_vars.insert("XRT_HAVE_SYSTEM_CJSON".into(), "NO".into());
|
||||
cmake_vars.insert("WIVRN_BUILD_CLIENT".into(), "OFF".into());
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
|
||||
profile.xrservice_cmake_flags.iter().for_each(|(k, v)| {
|
||||
cmake_vars.insert(k.clone(), v.clone());
|
||||
});
|
||||
|
||||
let cmake = Cmake {
|
||||
env: None,
|
||||
vars: Some(cmake_vars),
|
||||
vars: Some({
|
||||
let mut cmake_vars: HashMap<String, String> = HashMap::new();
|
||||
for (k, v) in [
|
||||
("CMAKE_EXPORT_COMPILE_COMMANDS", "ON"),
|
||||
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
|
||||
("XRT_HAVE_SYSTEM_CJSON", "NO"),
|
||||
("WIVRN_BUILD_CLIENT", "OFF"),
|
||||
("WIVRN_BUILD_WIVRNCTL", "OFF"),
|
||||
] {
|
||||
cmake_vars.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
cmake_vars.insert(
|
||||
"CMAKE_INSTALL_PREFIX".into(),
|
||||
profile.prefix.to_string_lossy().to_string(),
|
||||
);
|
||||
profile.xrservice_cmake_flags.iter().for_each(|(k, v)| {
|
||||
cmake_vars.insert(k.clone(), v.clone());
|
||||
});
|
||||
cmake_vars
|
||||
}),
|
||||
source_dir: profile.xrservice_path.clone(),
|
||||
build_dir: build_dir.clone(),
|
||||
};
|
||||
|
|
50
src/builders/build_xrizer.rs
Normal file
50
src/builders/build_xrizer.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use crate::{
|
||||
build_tools::git::Git, profile::Profile, termcolor::TermColor, ui::job_worker::job::WorkerJob,
|
||||
util::file_utils::rm_rf,
|
||||
};
|
||||
use std::{collections::VecDeque, path::Path};
|
||||
|
||||
pub fn get_build_xrizer_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> {
|
||||
let mut jobs = VecDeque::<WorkerJob>::new();
|
||||
jobs.push_back(WorkerJob::new_printer(
|
||||
"Building xrizer...",
|
||||
Some(TermColor::Blue),
|
||||
));
|
||||
|
||||
let git = Git {
|
||||
repo: profile
|
||||
.ovr_comp
|
||||
.repo
|
||||
.as_ref()
|
||||
.unwrap_or(&"https://github.com/Supreeeme/xrizer".into())
|
||||
.clone(),
|
||||
dir: profile.ovr_comp.path.clone(),
|
||||
branch: profile
|
||||
.ovr_comp
|
||||
.branch
|
||||
.as_ref()
|
||||
.unwrap_or(&"main".into())
|
||||
.clone(),
|
||||
};
|
||||
|
||||
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
|
||||
|
||||
let build_dir = profile.ovr_comp.path.join("target");
|
||||
if !Path::new(&build_dir).is_dir() || clean_build {
|
||||
rm_rf(&build_dir);
|
||||
}
|
||||
|
||||
jobs.push_back(WorkerJob::new_cmd(
|
||||
None,
|
||||
"sh".into(),
|
||||
Some(vec![
|
||||
"-c".into(),
|
||||
format!(
|
||||
"cd '{}' && cargo xbuild --release",
|
||||
profile.ovr_comp.path.to_string_lossy()
|
||||
),
|
||||
]),
|
||||
));
|
||||
|
||||
jobs
|
||||
}
|
|
@ -4,4 +4,6 @@ pub mod build_mercury;
|
|||
pub mod build_monado;
|
||||
pub mod build_opencomposite;
|
||||
pub mod build_openhmd;
|
||||
pub mod build_vapor;
|
||||
pub mod build_wivrn;
|
||||
pub mod build_xrizer;
|
||||
|
|
|
@ -7,15 +7,38 @@ use crate::{
|
|||
lighthouse::lighthouse_profile, openhmd::openhmd_profile, simulated::simulated_profile,
|
||||
survive::survive_profile, wivrn::wivrn_profile, wmr::wmr_profile,
|
||||
},
|
||||
ui::plugins::Plugin,
|
||||
util::file_utils::get_writer,
|
||||
};
|
||||
use serde::{de::Error, Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::BufReader,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PluginConfig {
|
||||
pub plugin: Plugin,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl From<&Plugin> for PluginConfig {
|
||||
fn from(p: &Plugin) -> Self {
|
||||
Self {
|
||||
plugin: p.clone(),
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PluginConfig> for Plugin {
|
||||
fn from(cp: &PluginConfig) -> Self {
|
||||
cp.plugin.clone()
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_WIN_SIZE: [i32; 2] = [360, 400];
|
||||
|
||||
const fn default_win_size() -> [i32; 2] {
|
||||
|
@ -29,6 +52,8 @@ pub struct Config {
|
|||
pub user_profiles: Vec<Profile>,
|
||||
#[serde(default = "default_win_size")]
|
||||
pub win_size: [i32; 2],
|
||||
#[serde(default)]
|
||||
pub plugins: HashMap<String, PluginConfig>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
@ -37,8 +62,9 @@ impl Default for Config {
|
|||
// TODO: using an empty string here is ugly
|
||||
selected_profile_uuid: "".to_string(),
|
||||
debug_view_enabled: false,
|
||||
user_profiles: vec![],
|
||||
user_profiles: Vec::default(),
|
||||
win_size: DEFAULT_WIN_SIZE,
|
||||
plugins: HashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,10 +91,42 @@ impl Config {
|
|||
}
|
||||
|
||||
fn from_path(path: &Path) -> Self {
|
||||
File::open(path)
|
||||
let mut this: Self = File::open(path)
|
||||
.ok()
|
||||
.and_then(|file| serde_json::from_reader(BufReader::new(file)).ok())
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut needs_save = false;
|
||||
|
||||
// remap legacy opencomposite data to new ovr_comp
|
||||
#[allow(deprecated)]
|
||||
for prof in this.user_profiles.iter_mut() {
|
||||
if prof
|
||||
.ovr_comp
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
== "__envision__fallbackovrcomp"
|
||||
{
|
||||
prof.ovr_comp.path = prof.opencomposite_path.clone();
|
||||
needs_save = true;
|
||||
}
|
||||
if prof.opencomposite_repo.is_some() && prof.ovr_comp.repo.is_none() {
|
||||
prof.ovr_comp.repo = prof.opencomposite_repo.take();
|
||||
needs_save = true;
|
||||
}
|
||||
if prof.opencomposite_branch.is_some() && prof.ovr_comp.branch.is_none() {
|
||||
prof.ovr_comp.branch = prof.opencomposite_branch.take();
|
||||
needs_save = true;
|
||||
}
|
||||
}
|
||||
|
||||
if needs_save {
|
||||
this.save_to_path(path).expect("Failed to save config");
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn save_to_path(&self, path: &Path) -> Result<(), serde_json::Error> {
|
||||
|
|
|
@ -16,10 +16,6 @@ pub const LOCALE_DIR: &str = "@LOCALEDIR@";
|
|||
pub const BUILD_PROFILE: &str = "@PROFILE@";
|
||||
pub const BUILD_DATETIME: &str = "@BUILD_DATETIME@";
|
||||
|
||||
pub fn get_developers() -> Vec<String> {
|
||||
vec!["Gabriele Musco <gabmus@disroot.org>".into()]
|
||||
}
|
||||
|
||||
pub fn get_artists() -> Vec<String> {
|
||||
vec!["App Icon: Yannick (@Yandr)".into()]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::{
|
||||
boost_deps::boost_deps,
|
||||
common::{dep_cmake, dep_eigen, dep_gpp, dep_libglvnd, dep_ninja, dep_opencv},
|
||||
common::{dep_cmake, dep_eigen, dep_gpp, dep_libgl, dep_ninja, dep_opencv},
|
||||
DepType, Dependency, DependencyCheckResult,
|
||||
};
|
||||
use crate::linux_distro::LinuxDistro;
|
||||
|
@ -11,7 +11,7 @@ fn basalt_deps() -> Vec<Dependency> {
|
|||
dep_gpp(),
|
||||
dep_cmake(),
|
||||
dep_ninja(),
|
||||
dep_libglvnd(),
|
||||
dep_libgl(),
|
||||
Dependency {
|
||||
name: "lz4-dev".into(),
|
||||
dep_type: DepType::Include,
|
||||
|
|
|
@ -52,8 +52,8 @@ pub fn boost_deps() -> Vec<Dependency> {
|
|||
packages: HashMap::from([
|
||||
(LinuxDistro::Arch, "boost".into()),
|
||||
(LinuxDistro::Debian, "libboost-all-dev".into()),
|
||||
(LinuxDistro::Fedora, "boost".into()),
|
||||
(LinuxDistro::Alpine, "boost".into()),
|
||||
(LinuxDistro::Fedora, "boost-devel".into()),
|
||||
(LinuxDistro::Alpine, "boost-dev".into()),
|
||||
(LinuxDistro::Gentoo, "dev-libs/boost".into()),
|
||||
(LinuxDistro::Suse, package.into()),
|
||||
]),
|
||||
|
|
|
@ -223,19 +223,20 @@ pub fn dep_libudev() -> Dependency {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn dep_libglvnd() -> Dependency {
|
||||
pub fn dep_libgl() -> Dependency {
|
||||
Dependency {
|
||||
name: "libglvnd-dev".into(),
|
||||
dep_type: DepType::Include,
|
||||
filename: "GL/gl.h".into(),
|
||||
packages: HashMap::from([
|
||||
(LinuxDistro::Arch, "libglvnd".into()),
|
||||
(LinuxDistro::Debian, "libglvnd-dev".into()),
|
||||
(LinuxDistro::Fedora, "libglvnd-devel".into()),
|
||||
// WARN: can't find anything exact for alpine, mesa-dev offers
|
||||
// GL/gl.h hopefully that's the only one needed
|
||||
// the right debian package would be libgl-dev but the mesa one
|
||||
// has it as a dependency
|
||||
(LinuxDistro::Debian, "libgl1-mesa-dev".into()),
|
||||
// as above, the right package would be libglvnd-devel
|
||||
(LinuxDistro::Fedora, "mesa-libGL-devel".into()),
|
||||
(LinuxDistro::Alpine, "mesa-dev".into()),
|
||||
(LinuxDistro::Suse, "libglvnd-devel".into()),
|
||||
(LinuxDistro::Suse, "Mesa-libGL-devel".into()),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ pub enum DepType {
|
|||
Executable,
|
||||
Include,
|
||||
UdevRule,
|
||||
Share,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
@ -49,6 +50,7 @@ impl Dependency {
|
|||
.collect(),
|
||||
DepType::Include => include_paths(),
|
||||
DepType::UdevRule => udev_rules_paths(),
|
||||
DepType::Share => share_paths(),
|
||||
} {
|
||||
let path_s = &format!("{dir}/{fname}", dir = dir, fname = self.filename);
|
||||
let path = Path::new(&path_s);
|
||||
|
@ -145,6 +147,10 @@ fn udev_rules_paths() -> Vec<String> {
|
|||
vec!["/usr/lib/udev/rules.d".into()]
|
||||
}
|
||||
|
||||
fn share_paths() -> Vec<String> {
|
||||
vec!["/usr/share".into()]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DepType, Dependency};
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
use super::{
|
||||
common::{
|
||||
dep_cmake, dep_eigen, dep_gcc, dep_git, dep_glslang_validator, dep_gpp, dep_libdrm,
|
||||
dep_libglvnd, dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr,
|
||||
dep_vulkan_headers, dep_vulkan_icd_loader,
|
||||
dep_libgl, dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr, dep_vulkan_headers,
|
||||
dep_vulkan_icd_loader,
|
||||
},
|
||||
DepType, Dependency, DependencyCheckResult,
|
||||
};
|
||||
use crate::linux_distro::LinuxDistro;
|
||||
use crate::{depcheck::common::dep_libxrandr, linux_distro::LinuxDistro};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn monado_deps() -> Vec<Dependency> {
|
||||
|
@ -17,6 +17,7 @@ fn monado_deps() -> Vec<Dependency> {
|
|||
dep_vulkan_headers(),
|
||||
dep_libxcb(),
|
||||
dep_libx11(),
|
||||
dep_libxrandr(),
|
||||
Dependency {
|
||||
name: "wayland".into(),
|
||||
dep_type: DepType::SharedObject,
|
||||
|
@ -29,6 +30,30 @@ fn monado_deps() -> Vec<Dependency> {
|
|||
(LinuxDistro::Suse, "wayland-devel".into()),
|
||||
]),
|
||||
},
|
||||
Dependency {
|
||||
name: "wayland-protocols".into(),
|
||||
dep_type: DepType::Share,
|
||||
filename: "wayland-protocols/staging/drm-lease/drm-lease-v1.xml".into(),
|
||||
packages: HashMap::from([
|
||||
(LinuxDistro::Arch, "wayland-protocols".into()),
|
||||
(LinuxDistro::Debian, "wayland-protocols".into()),
|
||||
(LinuxDistro::Fedora, "wayland-protocols-devel".into()),
|
||||
(LinuxDistro::Gentoo, "dev-libs/wayland-protocols".into()),
|
||||
(LinuxDistro::Suse, "wayland-protocols-devel".into()),
|
||||
]),
|
||||
},
|
||||
Dependency {
|
||||
name: "libbsd".into(),
|
||||
dep_type: DepType::SharedObject,
|
||||
filename: "libbsd.so".into(),
|
||||
packages: HashMap::from([
|
||||
(LinuxDistro::Arch, "libbsd".into()),
|
||||
(LinuxDistro::Debian, "libbsd-dev".into()),
|
||||
(LinuxDistro::Fedora, "libbsd-devel".into()),
|
||||
(LinuxDistro::Gentoo, "dev-libs/libbsd".into()),
|
||||
(LinuxDistro::Suse, "libbsd-devel".into()),
|
||||
]),
|
||||
},
|
||||
dep_cmake(),
|
||||
dep_eigen(),
|
||||
dep_git(),
|
||||
|
@ -73,7 +98,7 @@ fn monado_deps() -> Vec<Dependency> {
|
|||
(LinuxDistro::Suse, "Mesa-dri-devel".into()),
|
||||
]),
|
||||
},
|
||||
dep_libglvnd(),
|
||||
dep_libgl(),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,29 @@
|
|||
use super::{
|
||||
common::{dep_cmake, dep_gcc, dep_git, dep_gpp, dep_ninja},
|
||||
common::{dep_gcc, dep_git, dep_gpp, dep_ninja},
|
||||
Dependency, DependencyCheckResult,
|
||||
};
|
||||
use crate::linux_distro::LinuxDistro;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn openhmd_deps() -> Vec<Dependency> {
|
||||
vec![dep_gcc(), dep_gpp(), dep_cmake(), dep_ninja(), dep_git()]
|
||||
vec![
|
||||
dep_gcc(),
|
||||
dep_gpp(),
|
||||
dep_ninja(),
|
||||
dep_git(),
|
||||
Dependency {
|
||||
name: "meson".into(),
|
||||
filename: "meson".into(),
|
||||
dep_type: crate::depcheck::DepType::Executable,
|
||||
packages: HashMap::from([
|
||||
(LinuxDistro::Arch, "meson".into()),
|
||||
(LinuxDistro::Debian, "meson".into()),
|
||||
(LinuxDistro::Fedora, "meson".into()),
|
||||
(LinuxDistro::Alpine, "meson".into()),
|
||||
(LinuxDistro::Suse, "meson".into()),
|
||||
]),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub fn check_openhmd_deps() -> Vec<DependencyCheckResult> {
|
||||
|
|
|
@ -6,7 +6,10 @@ use super::{
|
|||
},
|
||||
DepType, Dependency, DependencyCheckResult,
|
||||
};
|
||||
use crate::{depcheck::common::dep_libxrandr, linux_distro::LinuxDistro};
|
||||
use crate::{
|
||||
depcheck::common::{dep_libgl, dep_libxrandr},
|
||||
linux_distro::LinuxDistro,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn wivrn_deps() -> Vec<Dependency> {
|
||||
|
@ -23,6 +26,7 @@ fn wivrn_deps() -> Vec<Dependency> {
|
|||
dep_libxcb(),
|
||||
dep_libx11(),
|
||||
dep_libxrandr(),
|
||||
dep_libgl(),
|
||||
Dependency {
|
||||
name: "patch".into(),
|
||||
dep_type: DepType::Executable,
|
||||
|
@ -74,15 +78,15 @@ fn wivrn_deps() -> Vec<Dependency> {
|
|||
]),
|
||||
},
|
||||
Dependency {
|
||||
name: "libpulse-dev".into(),
|
||||
name: "libpipewire-dev".into(),
|
||||
dep_type: DepType::Include,
|
||||
filename: "pulse/context.h".into(),
|
||||
filename: "pipewire-0.3/pipewire/pipewire.h".into(),
|
||||
packages: HashMap::from([
|
||||
(LinuxDistro::Arch, "libpulse".into()),
|
||||
(LinuxDistro::Debian, "libpulse-dev".into()),
|
||||
(LinuxDistro::Fedora, "pulseaudio-libs-devel".into()),
|
||||
(LinuxDistro::Gentoo, "media-libs/libpulse".into()),
|
||||
(LinuxDistro::Suse, "libpulse-devel".into()),
|
||||
(LinuxDistro::Arch, "libpipewire".into()),
|
||||
(LinuxDistro::Debian, "libpipewire-0.3-dev".into()),
|
||||
(LinuxDistro::Fedora, "pipewire-devel".into()),
|
||||
(LinuxDistro::Gentoo, "media-video/pipewire".into()),
|
||||
(LinuxDistro::Suse, "pipewire-devel".into()),
|
||||
]),
|
||||
},
|
||||
dep_eigen(),
|
||||
|
@ -165,7 +169,10 @@ fn wivrn_deps() -> Vec<Dependency> {
|
|||
filename: "pkgconfig/gstreamer-app-1.0.pc".into(),
|
||||
packages: HashMap::from([
|
||||
(LinuxDistro::Arch, "gst-plugins-base-libs".into()),
|
||||
(LinuxDistro::Debian, "libgstreamer1.0-dev".into()),
|
||||
(
|
||||
LinuxDistro::Debian,
|
||||
"libgstreamer-plugins-base1.0-dev".into(),
|
||||
),
|
||||
(LinuxDistro::Fedora, "gstreamer1-plugins-base-devel".into()),
|
||||
(LinuxDistro::Gentoo, "media-libs/gst-plugins-base".into()),
|
||||
(LinuxDistro::Suse, "gstreamer-plugins-base-devel".into()),
|
||||
|
@ -231,6 +238,30 @@ fn wivrn_deps() -> Vec<Dependency> {
|
|||
(LinuxDistro::Suse, "glib2-devel".into()),
|
||||
]),
|
||||
},
|
||||
Dependency {
|
||||
name: "openssl-dev".into(),
|
||||
dep_type: DepType::Include,
|
||||
filename: "openssl/ssl3.h".into(),
|
||||
packages: HashMap::from([
|
||||
(LinuxDistro::Arch, "openssl".into()),
|
||||
(LinuxDistro::Alpine, "openssl-dev".into()),
|
||||
(LinuxDistro::Debian, "libssl-dev".into()),
|
||||
(LinuxDistro::Fedora, "openssl-devel".into()),
|
||||
(LinuxDistro::Suse, "openssl-devel".into()),
|
||||
]),
|
||||
},
|
||||
Dependency {
|
||||
name: "libnotify-dev".into(),
|
||||
dep_type: DepType::Include,
|
||||
filename: "libnotify/notify.h".into(),
|
||||
packages: HashMap::from([
|
||||
(LinuxDistro::Arch, "libnotify".into()),
|
||||
(LinuxDistro::Alpine, "libnotify-dev".into()),
|
||||
(LinuxDistro::Debian, "libnotify-dev".into()),
|
||||
(LinuxDistro::Fedora, "libnotify-devel".into()),
|
||||
(LinuxDistro::Suse, "libnotify-devel".into()),
|
||||
]),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ use lazy_static::lazy_static;
|
|||
fn env_var_descriptions() -> Vec<(&'static str, &'static str)> {
|
||||
vec![
|
||||
(
|
||||
"XRT_COMPOSITOR_SCALE_PECENTAGE",
|
||||
"XRT_COMPOSITOR_SCALE_PERCENTAGE",
|
||||
"Render resolution percentage. A percentage higher than the native resolution (>100) will help with antialiasing and image clarity."
|
||||
),
|
||||
(
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
use crate::{
|
||||
paths::{get_backup_dir, SYSTEM_PREFIX},
|
||||
paths::SYSTEM_PREFIX,
|
||||
profile::Profile,
|
||||
util::file_utils::{copy_file, deserialize_file, get_writer, set_file_readonly},
|
||||
util::file_utils::{deserialize_file, get_writer, set_file_readonly},
|
||||
xdg::XDG,
|
||||
};
|
||||
use anyhow::bail;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs::remove_file,
|
||||
fs::{create_dir_all, remove_file, rename},
|
||||
os::unix::fs::symlink,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ActiveRuntimeInnerRuntime {
|
||||
|
@ -34,29 +37,6 @@ fn get_active_runtime_json_path() -> PathBuf {
|
|||
get_openxr_conf_dir().join("1/active_runtime.json")
|
||||
}
|
||||
|
||||
pub fn is_steam(active_runtime: &ActiveRuntime) -> bool {
|
||||
matches!(active_runtime.runtime.valve_runtime_is_steamvr, Some(true))
|
||||
}
|
||||
|
||||
fn get_backup_steam_active_runtime_path() -> PathBuf {
|
||||
get_backup_dir().join("active_runtime.json.steam.bak")
|
||||
}
|
||||
|
||||
fn get_backed_up_steam_active_runtime() -> Option<ActiveRuntime> {
|
||||
get_active_runtime_from_path(&get_backup_steam_active_runtime_path())
|
||||
}
|
||||
|
||||
fn backup_steam_active_runtime() {
|
||||
if let Some(ar) = get_current_active_runtime() {
|
||||
if is_steam(&ar) {
|
||||
copy_file(
|
||||
&get_active_runtime_json_path(),
|
||||
&get_backup_steam_active_runtime_path(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_active_runtime_from_path(path: &Path) -> Option<ActiveRuntime> {
|
||||
deserialize_file(path)
|
||||
}
|
||||
|
@ -78,29 +58,6 @@ pub fn dump_current_active_runtime(active_runtime: &ActiveRuntime) -> anyhow::Re
|
|||
dump_active_runtime_to_path(active_runtime, &get_active_runtime_json_path())
|
||||
}
|
||||
|
||||
fn build_steam_active_runtime() -> ActiveRuntime {
|
||||
if let Some(backup) = get_backed_up_steam_active_runtime() {
|
||||
return backup;
|
||||
}
|
||||
ActiveRuntime {
|
||||
file_format_version: "1.0.0".into(),
|
||||
runtime: ActiveRuntimeInnerRuntime {
|
||||
valve_runtime_is_steamvr: Some(true),
|
||||
libmonado_path: None,
|
||||
library_path: XDG
|
||||
.get_data_home()
|
||||
.join("Steam/steamapps/common/SteamVR/bin/linux64/vrclient.so"),
|
||||
name: Some("SteamVR".into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current_active_runtime_to_steam() -> anyhow::Result<()> {
|
||||
set_file_readonly(&get_active_runtime_json_path(), false)?;
|
||||
dump_current_active_runtime(&build_steam_active_runtime())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_profile_active_runtime(profile: &Profile) -> anyhow::Result<ActiveRuntime> {
|
||||
let Some(libopenxr_path) = profile.libopenxr_so() else {
|
||||
anyhow::bail!(
|
||||
|
@ -137,18 +94,67 @@ fn relativize_active_runtime_lib_path(ar: &ActiveRuntime, path: &Path) -> Active
|
|||
res
|
||||
}
|
||||
|
||||
const ACTIVE_RUNTIME_BAK: &str = "active_runtime.json.envision.bak";
|
||||
|
||||
pub fn set_current_active_runtime_to_profile(profile: &Profile) -> anyhow::Result<()> {
|
||||
let dest = get_active_runtime_json_path();
|
||||
set_file_readonly(&dest, false)?;
|
||||
backup_steam_active_runtime();
|
||||
let pfx = profile.clone().prefix;
|
||||
let mut ar = build_profile_active_runtime(profile)?;
|
||||
// hack: relativize libopenxr_monado.so path for system installs
|
||||
if pfx == PathBuf::from(SYSTEM_PREFIX) {
|
||||
ar = relativize_active_runtime_lib_path(&ar, &dest);
|
||||
if dest.is_dir() {
|
||||
bail!("{} is a directory", dest.to_string_lossy());
|
||||
}
|
||||
dump_current_active_runtime(&ar)?;
|
||||
set_file_readonly(&dest, true)?;
|
||||
if !dest.is_symlink() {
|
||||
set_file_readonly(&dest, false)?;
|
||||
}
|
||||
if dest.is_file() || dest.is_symlink() {
|
||||
rename(&dest, dest.parent().unwrap().join(ACTIVE_RUNTIME_BAK))?;
|
||||
} else {
|
||||
debug!("no active_runtime.json file to backup")
|
||||
}
|
||||
|
||||
let profile_openxr_json = profile.openxr_json_path();
|
||||
if profile_openxr_json.is_file() {
|
||||
create_dir_all(dest.parent().unwrap())?;
|
||||
symlink(profile_openxr_json, &dest)?;
|
||||
} else {
|
||||
warn!("profile openxr json file doesn't exist");
|
||||
// fallback: build the file from scratch
|
||||
let pfx = profile.clone().prefix;
|
||||
let mut ar = build_profile_active_runtime(profile)?;
|
||||
// hack: relativize libopenxr_monado.so path for system installs
|
||||
if pfx == PathBuf::from(SYSTEM_PREFIX) {
|
||||
ar = relativize_active_runtime_lib_path(&ar, &dest);
|
||||
}
|
||||
dump_current_active_runtime(&ar)?;
|
||||
set_file_readonly(&dest, true)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_current_active_runtime() -> anyhow::Result<()> {
|
||||
let dest = get_active_runtime_json_path();
|
||||
if dest.is_dir() {
|
||||
bail!("{} is a directory", dest.to_string_lossy());
|
||||
}
|
||||
if !dest.exists() {
|
||||
debug!("no current active_runtime.json to remove")
|
||||
}
|
||||
Ok(remove_file(dest)?)
|
||||
}
|
||||
|
||||
pub fn restore_active_runtime_backup() -> anyhow::Result<()> {
|
||||
let dest = get_active_runtime_json_path();
|
||||
let bak = dest.parent().unwrap().join(ACTIVE_RUNTIME_BAK);
|
||||
if bak.is_file() || bak.is_symlink() {
|
||||
if dest.is_dir() {
|
||||
bail!("{} is a directory", dest.to_string_lossy());
|
||||
}
|
||||
if !dest.is_symlink() {
|
||||
set_file_readonly(&dest, false)?;
|
||||
}
|
||||
rename(&bak, &dest)?;
|
||||
} else {
|
||||
debug!("{ACTIVE_RUNTIME_BAK} does not exist, nothing to restore");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
pub mod active_runtime_json;
|
||||
pub mod monado_autorun;
|
||||
pub mod openvrpaths_vrpath;
|
||||
pub mod wayvr_dashboard_config;
|
||||
pub mod wivrn_config;
|
||||
pub mod wivrn_encoder_presets;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{
|
||||
paths::get_backup_dir,
|
||||
profile::Profile,
|
||||
|
@ -7,6 +5,7 @@ use crate::{
|
|||
xdg::XDG,
|
||||
};
|
||||
use serde::{ser::Error, Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OpenVrPaths {
|
||||
|
@ -86,6 +85,7 @@ fn build_steam_openvrpaths() -> OpenVrPaths {
|
|||
}
|
||||
|
||||
pub fn set_current_openvrpaths_to_steam() -> anyhow::Result<()> {
|
||||
// removing readonly flag just in case, remove this line in the future
|
||||
set_file_readonly(&get_openvrpaths_vrpath_path(), false)?;
|
||||
dump_current_openvrpaths(&build_steam_openvrpaths())?;
|
||||
Ok(())
|
||||
|
@ -98,25 +98,24 @@ pub fn build_profile_openvrpaths(profile: &Profile) -> OpenVrPaths {
|
|||
external_drivers: None,
|
||||
jsonid: "vrpathreg".into(),
|
||||
log: vec![datadir.join("Steam/logs")],
|
||||
runtime: vec![profile.opencomposite_path.join("build")],
|
||||
runtime: vec![profile.ovr_comp.runtime_dir()],
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current_openvrpaths_to_profile(profile: &Profile) -> anyhow::Result<()> {
|
||||
let dest = get_openvrpaths_vrpath_path();
|
||||
// removing readonly flag just in case, remove this line in the future
|
||||
set_file_readonly(&dest, false)?;
|
||||
backup_steam_openvrpaths();
|
||||
dump_current_openvrpaths(&build_profile_openvrpaths(profile))?;
|
||||
set_file_readonly(&dest, true)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::{dump_openvrpaths_to_path, get_openvrpaths_from_path, OpenVrPaths};
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn can_read_openvrpaths_vrpath_steamvr() {
|
||||
|
|
13
src/file_builders/wayvr_dashboard_config.rs
Normal file
13
src/file_builders/wayvr_dashboard_config.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct WayVrDashboardConfigFragmentInner {
|
||||
pub exec: String,
|
||||
pub args: Option<String>,
|
||||
pub env: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct WayVrDashboardConfigFragment {
|
||||
pub dashboard: WayVrDashboardConfigFragmentInner,
|
||||
}
|
|
@ -153,7 +153,7 @@ pub struct WivrnConfig {
|
|||
impl Default for WivrnConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scale: Some([0.8, 0.8]),
|
||||
scale: Some([0.5, 0.5]),
|
||||
bitrate: Some(50000000),
|
||||
encoders: vec![],
|
||||
application: None,
|
||||
|
|
|
@ -49,13 +49,13 @@ impl LinuxDistro {
|
|||
Ok(_) if buf.starts_with("PRETTY_NAME=\"") => {
|
||||
return buf
|
||||
.split('=')
|
||||
.last()
|
||||
.next_back()
|
||||
.map(|b| b.trim().trim_matches('"').trim().to_string());
|
||||
}
|
||||
Ok(_) if buf.starts_with("NAME=\"") => {
|
||||
name = buf
|
||||
.split('=')
|
||||
.last()
|
||||
.next_back()
|
||||
.map(|b| b.trim().trim_matches('"').trim().to_string());
|
||||
}
|
||||
_ => {}
|
||||
|
@ -79,7 +79,7 @@ impl LinuxDistro {
|
|||
{
|
||||
let name = buf
|
||||
.split('=')
|
||||
.last()
|
||||
.next_back()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
|
|
52
src/main.rs
52
src/main.rs
|
@ -1,10 +1,11 @@
|
|||
use anyhow::Result;
|
||||
use constants::{resources, APP_ID, APP_NAME, GETTEXT_PACKAGE, LOCALE_DIR, RESOURCES_BASE_PATH};
|
||||
use file_builders::{
|
||||
active_runtime_json::{get_current_active_runtime, set_current_active_runtime_to_steam},
|
||||
active_runtime_json::restore_active_runtime_backup,
|
||||
openvrpaths_vrpath::{get_current_openvrpaths, set_current_openvrpaths_to_steam},
|
||||
};
|
||||
use gettextrs::LocaleCategory;
|
||||
use paths::get_logs_dir;
|
||||
use relm4::{
|
||||
adw,
|
||||
gtk::{self, gdk, gio, glib, prelude::*},
|
||||
|
@ -12,6 +13,10 @@ use relm4::{
|
|||
};
|
||||
use std::env;
|
||||
use steam_linux_runtime_injector::restore_runtime_entrypoint;
|
||||
use tracing::warn;
|
||||
use tracing_subscriber::{
|
||||
filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
|
||||
};
|
||||
use ui::{
|
||||
app::{App, AppInit, Msg},
|
||||
cmdline_opts::CmdLineOpts,
|
||||
|
@ -22,6 +27,7 @@ pub mod build_tools;
|
|||
pub mod builders;
|
||||
pub mod cmd_runner;
|
||||
pub mod config;
|
||||
#[rustfmt::skip]
|
||||
pub mod constants;
|
||||
pub mod depcheck;
|
||||
pub mod device_prober;
|
||||
|
@ -41,26 +47,20 @@ pub mod termcolor;
|
|||
pub mod ui;
|
||||
pub mod util;
|
||||
pub mod vulkaninfo;
|
||||
pub mod wivrn_dbus;
|
||||
pub mod xdg;
|
||||
pub mod xr_devices;
|
||||
|
||||
fn restore_steam_xr_files() {
|
||||
let active_runtime = get_current_active_runtime();
|
||||
let openvrpaths = get_current_openvrpaths();
|
||||
if let Some(ar) = active_runtime {
|
||||
if !file_builders::active_runtime_json::is_steam(&ar) {
|
||||
match set_current_active_runtime_to_steam() {
|
||||
Ok(_) => {}
|
||||
Err(e) => eprintln!("Warning: failed to restore active runtime to steam: {e}"),
|
||||
};
|
||||
}
|
||||
if let Err(e) = restore_active_runtime_backup() {
|
||||
warn!("failed to restore active runtime to steam: {e}");
|
||||
}
|
||||
if let Some(ovrp) = openvrpaths {
|
||||
if !file_builders::openvrpaths_vrpath::is_steam(&ovrp) {
|
||||
match set_current_openvrpaths_to_steam() {
|
||||
Ok(_) => {}
|
||||
Err(e) => eprintln!("Warning: failed to restore openvrpaths to steam: {e}"),
|
||||
};
|
||||
if let Err(e) = set_current_openvrpaths_to_steam() {
|
||||
warn!("failed to restore openvrpaths to steam: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
restore_runtime_entrypoint();
|
||||
|
@ -72,6 +72,24 @@ fn main() -> Result<()> {
|
|||
}
|
||||
restore_steam_xr_files();
|
||||
|
||||
let rolling_log_writer = tracing_appender::rolling::daily(get_logs_dir(), "log");
|
||||
let (non_blocking_appender, _appender_guard) =
|
||||
tracing_appender::non_blocking(rolling_log_writer);
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer().pretty().with_filter(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy(),
|
||||
),
|
||||
)
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.json()
|
||||
.with_writer(non_blocking_appender),
|
||||
)
|
||||
.init();
|
||||
|
||||
// Prepare i18n
|
||||
gettextrs::setlocale(LocaleCategory::LcAll, "");
|
||||
gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALE_DIR).expect("Unable to bind the text domain");
|
||||
|
@ -109,11 +127,13 @@ fn main() -> Result<()> {
|
|||
CmdLineOpts::init(&main_app);
|
||||
let sender = BROKER.sender();
|
||||
main_app.connect_command_line(move |this, cmdline| {
|
||||
if CmdLineOpts::handle_non_activating_opts(cmdline) {
|
||||
return 0;
|
||||
let opts = CmdLineOpts::from_cmdline(cmdline);
|
||||
if let Some(exit_code) = opts.handle_non_activating_opts() {
|
||||
this.quit();
|
||||
return exit_code;
|
||||
}
|
||||
this.activate();
|
||||
sender.emit(Msg::HandleCommandLine(CmdLineOpts::from_cmdline(cmdline)));
|
||||
sender.emit(Msg::HandleCommandLine(opts));
|
||||
0
|
||||
});
|
||||
let app = RelmApp::from_app(main_app.clone()).with_broker(&BROKER);
|
||||
|
|
|
@ -3,7 +3,7 @@ config = configure_file(
|
|||
output: 'constants.rs',
|
||||
configuration: global_conf
|
||||
)
|
||||
# Copy the config.rs output to the source directory.
|
||||
# Copy the constants.rs output to the source directory.
|
||||
run_command(
|
||||
'cp',
|
||||
meson.project_build_root() / 'src' / 'constants.rs',
|
||||
|
@ -14,13 +14,13 @@ run_command(
|
|||
cargo_options = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ]
|
||||
cargo_options += [ '--target-dir', meson.project_build_root() / 'src' ]
|
||||
|
||||
if get_option('profile') == 'default'
|
||||
if get_option('debugbuild')
|
||||
rust_target = 'debug'
|
||||
message('Building in debug mode')
|
||||
else
|
||||
cargo_options += [ '--release' ]
|
||||
rust_target = 'release'
|
||||
message('Building in release mode')
|
||||
else
|
||||
rust_target = 'debug'
|
||||
message('Building in debug mode')
|
||||
endif
|
||||
|
||||
cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ]
|
||||
|
@ -43,3 +43,24 @@ cargo_build = custom_target(
|
|||
'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@',
|
||||
]
|
||||
)
|
||||
|
||||
test(
|
||||
'cargo-fmt-check',
|
||||
cargo,
|
||||
args: ['fmt', '--all', '--check']
|
||||
)
|
||||
|
||||
test(
|
||||
'cargo-clippy',
|
||||
cargo,
|
||||
env: ['RUSTFLAGS=-Dwarnings'],
|
||||
args: ['clippy', '--all-targets', '--all-features'],
|
||||
timeout: 0,
|
||||
)
|
||||
|
||||
test(
|
||||
'cargo-test',
|
||||
cargo,
|
||||
args: ['test'],
|
||||
timeout: 0,
|
||||
)
|
||||
|
|
33
src/paths.rs
33
src/paths.rs
|
@ -1,4 +1,6 @@
|
|||
use crate::{constants::CMD_NAME, xdg::XDG};
|
||||
use anyhow::bail;
|
||||
|
||||
use crate::{constants::CMD_NAME, util::steam_library_folder::SteamLibraryFolder, xdg::XDG};
|
||||
use std::{
|
||||
env,
|
||||
fs::create_dir_all,
|
||||
|
@ -54,6 +56,10 @@ pub fn get_cache_dir() -> PathBuf {
|
|||
XDG.get_cache_home().join(CMD_NAME)
|
||||
}
|
||||
|
||||
pub fn get_logs_dir() -> PathBuf {
|
||||
get_cache_dir().join("logs")
|
||||
}
|
||||
|
||||
pub fn get_backup_dir() -> PathBuf {
|
||||
let p = get_data_dir().join("backups");
|
||||
if !p.is_dir() {
|
||||
|
@ -83,7 +89,26 @@ pub fn get_exec_prefix() -> PathBuf {
|
|||
.into()
|
||||
}
|
||||
|
||||
pub fn get_steamvr_bin_dir_path() -> PathBuf {
|
||||
XDG.get_data_home()
|
||||
.join("Steam/steamapps/common/SteamVR/bin/linux64")
|
||||
const STEAMVR_STEAM_APPID: u32 = 250820;
|
||||
|
||||
fn get_steamvr_base_dir() -> anyhow::Result<PathBuf> {
|
||||
SteamLibraryFolder::get_folders()?
|
||||
.into_iter()
|
||||
.find(|(_, lf)| lf.apps.contains_key(&STEAMVR_STEAM_APPID))
|
||||
.map(|(_, lf)| PathBuf::from(lf.path).join("steamapps/common/SteamVR"))
|
||||
.ok_or(anyhow::Error::msg(
|
||||
"Could not find SteamVR in Steam libraryfolders.vdf",
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_steamvr_bin_dir_path() -> anyhow::Result<PathBuf> {
|
||||
let res = get_steamvr_base_dir()?.join("bin/linux64");
|
||||
if !res.is_dir() {
|
||||
bail!("SteamVR bin dir `{}` does not exist", res.to_string_lossy());
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn get_plugins_dir() -> PathBuf {
|
||||
get_data_dir().join("plugins")
|
||||
}
|
||||
|
|
213
src/profile.rs
213
src/profile.rs
|
@ -1,6 +1,12 @@
|
|||
use crate::{
|
||||
depcheck::{
|
||||
basalt_deps::get_missing_basalt_deps, libsurvive_deps::get_missing_libsurvive_deps,
|
||||
mercury_deps::get_missing_mercury_deps, monado_deps::get_missing_monado_deps,
|
||||
openhmd_deps::get_missing_openhmd_deps, wivrn_deps::get_missing_wivrn_deps, Dependency,
|
||||
},
|
||||
file_builders::active_runtime_json::ActiveRuntime,
|
||||
paths::{get_data_dir, BWRAP_SYSTEM_PREFIX, SYSTEM_PREFIX},
|
||||
util::file_utils::get_writer,
|
||||
util::file_utils::{deserialize_file, get_writer},
|
||||
xdg::XDG,
|
||||
};
|
||||
use nix::NixPath;
|
||||
|
@ -12,6 +18,7 @@ use std::{
|
|||
io::BufReader,
|
||||
path::{Path, PathBuf},
|
||||
slice::Iter,
|
||||
str::FromStr,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -38,7 +45,14 @@ impl XRServiceType {
|
|||
pub fn libmonado_path(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Monado => "libmonado.so",
|
||||
Self::Wivrn => "wivrn/libmonado.so",
|
||||
Self::Wivrn => "wivrn/libmonado_wivrn.so",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn openxr_json_rel_path(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Monado => "share/openxr/1/openxr_monado.json",
|
||||
Self::Wivrn => "share/openxr/1/openxr_wivrn.json",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,6 +260,97 @@ impl Display for LighthouseDriver {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum OvrCompatibilityModuleType {
|
||||
#[default]
|
||||
Opencomposite,
|
||||
Xrizer,
|
||||
Vapor,
|
||||
}
|
||||
|
||||
impl Display for OvrCompatibilityModuleType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Opencomposite => "OpenComposite",
|
||||
Self::Xrizer => "xrizer",
|
||||
Self::Vapor => "VapoR",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OvrCompatibilityModuleType {
|
||||
pub fn iter() -> Iter<'static, Self> {
|
||||
[Self::Opencomposite, Self::Xrizer, Self::Vapor].iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for OvrCompatibilityModuleType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().trim() {
|
||||
"opencomposite" => Ok(Self::Opencomposite),
|
||||
"xrizer" => Ok(Self::Xrizer),
|
||||
"vapor" => Ok(Self::Vapor),
|
||||
_ => Err(format!("no match for ovr compatibility module `{s}`")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for OvrCompatibilityModuleType {
|
||||
fn from(value: u32) -> Self {
|
||||
match value {
|
||||
0 => Self::Opencomposite,
|
||||
1 => Self::Xrizer,
|
||||
2 => Self::Vapor,
|
||||
_ => panic!("OvrCompatibilityModuleType index out of bounds"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ProfileOvrCompatibilityModule {
|
||||
pub mod_type: OvrCompatibilityModuleType,
|
||||
pub repo: Option<String>,
|
||||
pub branch: Option<String>,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl ProfileOvrCompatibilityModule {
|
||||
pub fn default_for_uuid(uuid: &str) -> Self {
|
||||
let mod_type = OvrCompatibilityModuleType::default();
|
||||
Self {
|
||||
mod_type,
|
||||
repo: None,
|
||||
branch: None,
|
||||
path: get_data_dir().join(uuid).join(mod_type.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// get the directory corresponding to the openvr runtime.
|
||||
/// this should correspond to the build output directory
|
||||
pub fn runtime_dir(&self) -> PathBuf {
|
||||
match self.mod_type {
|
||||
OvrCompatibilityModuleType::Opencomposite | OvrCompatibilityModuleType::Vapor => {
|
||||
self.path.join("build")
|
||||
}
|
||||
OvrCompatibilityModuleType::Xrizer => self.path.join("target/release"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProfileOvrCompatibilityModule {
|
||||
fn default() -> Self {
|
||||
let mod_type = OvrCompatibilityModuleType::default();
|
||||
Self {
|
||||
mod_type,
|
||||
repo: None,
|
||||
branch: None,
|
||||
path: get_data_dir().join("__envision__fallbackovrcomp"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Profile {
|
||||
pub uuid: String,
|
||||
|
@ -256,9 +361,15 @@ pub struct Profile {
|
|||
pub xrservice_branch: Option<String>,
|
||||
#[serde(default = "HashMap::<String, String>::default")]
|
||||
pub xrservice_cmake_flags: HashMap<String, String>,
|
||||
#[deprecated]
|
||||
#[serde(default)]
|
||||
pub opencomposite_path: PathBuf,
|
||||
#[deprecated]
|
||||
pub opencomposite_repo: Option<String>,
|
||||
#[deprecated]
|
||||
pub opencomposite_branch: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ovr_comp: ProfileOvrCompatibilityModule,
|
||||
pub features: ProfileFeatures,
|
||||
pub environment: HashMap<String, String>,
|
||||
/// Install prefix
|
||||
|
@ -271,7 +382,6 @@ pub struct Profile {
|
|||
pub lighthouse_driver: LighthouseDriver,
|
||||
#[serde(default = "String::default")]
|
||||
pub xrservice_launch_options: String,
|
||||
pub autostart_command: Option<String>,
|
||||
#[serde(default)]
|
||||
pub skip_dependency_check: bool,
|
||||
}
|
||||
|
@ -283,6 +393,7 @@ impl Display for Profile {
|
|||
}
|
||||
|
||||
impl Default for Profile {
|
||||
#[allow(deprecated)]
|
||||
fn default() -> Self {
|
||||
let uuid = Self::new_uuid();
|
||||
let profile_dir = get_data_dir().join(&uuid);
|
||||
|
@ -318,23 +429,27 @@ impl Default for Profile {
|
|||
mercury_enabled: false,
|
||||
},
|
||||
environment: HashMap::new(),
|
||||
prefix: get_data_dir().join("prefixes").join(&uuid),
|
||||
prefix: Self::default_prefix_path(&uuid),
|
||||
can_be_built: true,
|
||||
pull_on_build: true,
|
||||
opencomposite_path: profile_dir.join("opencomposite"),
|
||||
opencomposite_repo: None,
|
||||
opencomposite_branch: None,
|
||||
ovr_comp: ProfileOvrCompatibilityModule::default_for_uuid(&uuid),
|
||||
editable: true,
|
||||
lighthouse_driver: LighthouseDriver::default(),
|
||||
xrservice_launch_options: String::default(),
|
||||
uuid,
|
||||
autostart_command: None,
|
||||
skip_dependency_check: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
fn default_prefix_path(uuid: &str) -> PathBuf {
|
||||
get_data_dir().join("prefixes").join(uuid)
|
||||
}
|
||||
|
||||
pub fn xr_runtime_json_env_var(&self) -> String {
|
||||
format!(
|
||||
"XR_RUNTIME_JSON=\"{prefix}/share/openxr/1/openxr_{runtime}.json\"",
|
||||
|
@ -353,8 +468,8 @@ impl Profile {
|
|||
pub fn env_vars_full(&self) -> Vec<String> {
|
||||
vec![
|
||||
// format!(
|
||||
// "VR_OVERRIDE={opencomp}/build",
|
||||
// opencomp = self.opencomposite_path,
|
||||
// "VR_OVERRIDE={}",
|
||||
// self.ovr_comp.runtime_dir(),
|
||||
// ),
|
||||
self.xr_runtime_json_env_var(),
|
||||
format!(
|
||||
|
@ -412,8 +527,8 @@ impl Profile {
|
|||
}
|
||||
let uuid = Self::new_uuid();
|
||||
let profile_dir = get_data_dir().join(&uuid);
|
||||
#[allow(deprecated)]
|
||||
let mut dup = Self {
|
||||
uuid,
|
||||
name: format!("Duplicate of {}", self.name),
|
||||
xrservice_type: self.xrservice_type.clone(),
|
||||
xrservice_repo: self.xrservice_repo.clone(),
|
||||
|
@ -445,7 +560,6 @@ impl Profile {
|
|||
mercury_enabled: self.features.mercury_enabled,
|
||||
},
|
||||
environment: self.environment.clone(),
|
||||
autostart_command: self.autostart_command.clone(),
|
||||
pull_on_build: self.pull_on_build,
|
||||
lighthouse_driver: self.lighthouse_driver,
|
||||
opencomposite_repo: self.opencomposite_repo.clone(),
|
||||
|
@ -453,7 +567,16 @@ impl Profile {
|
|||
opencomposite_path: profile_dir.join("opencomposite"),
|
||||
skip_dependency_check: self.skip_dependency_check,
|
||||
xrservice_launch_options: self.xrservice_launch_options.clone(),
|
||||
..Default::default()
|
||||
prefix: Self::default_prefix_path(&uuid),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
mod_type: self.ovr_comp.mod_type,
|
||||
repo: self.ovr_comp.repo.clone(),
|
||||
branch: self.ovr_comp.branch.clone(),
|
||||
path: profile_dir.join(self.ovr_comp.mod_type.to_string()),
|
||||
},
|
||||
can_be_built: self.can_be_built,
|
||||
editable: true,
|
||||
uuid,
|
||||
};
|
||||
if dup.environment.contains_key("LD_LIBRARY_PATH") {
|
||||
dup.environment.insert(
|
||||
|
@ -539,21 +662,69 @@ impl Profile {
|
|||
}
|
||||
|
||||
/// absolute path to a given shared object in the profile prefix
|
||||
pub fn find_so(&self, rel_path: &str) -> Option<PathBuf> {
|
||||
pub fn find_so<P: AsRef<Path>>(&self, rel_path: P) -> Option<PathBuf> {
|
||||
["lib", "lib64"]
|
||||
.into_iter()
|
||||
.map(|lib| self.prefix.join(lib).join(rel_path))
|
||||
.map(|lib| self.prefix.join(lib).join(rel_path.as_ref()))
|
||||
.find(|path| path.is_file())
|
||||
}
|
||||
|
||||
/// absolute path to the libmonado shared object
|
||||
pub fn libmonado_so(&self) -> Option<PathBuf> {
|
||||
self.find_so(self.xrservice_type.libmonado_path())
|
||||
// try by reading the openxr json file
|
||||
self.openxr_config()
|
||||
.and_then(|conf| conf.runtime.libmonado_path)
|
||||
.and_then(|libmonado_path| self.find_so(&libmonado_path))
|
||||
.or_else(||
|
||||
// try with the hardcoded paths
|
||||
self.find_so(self.xrservice_type.libmonado_path()))
|
||||
}
|
||||
|
||||
fn openxr_config(&self) -> Option<ActiveRuntime> {
|
||||
deserialize_file(&self.openxr_json_path())
|
||||
}
|
||||
|
||||
/// absolute path to the libopenxr shared object
|
||||
pub fn libopenxr_so(&self) -> Option<PathBuf> {
|
||||
self.find_so(self.xrservice_type.libopenxr_path())
|
||||
// try by reading the openxr json file
|
||||
self.openxr_config()
|
||||
.map(|conf| conf.runtime.library_path)
|
||||
.and_then(|libmonado_path| self.find_so(&libmonado_path))
|
||||
.or_else(||
|
||||
// try with the hardcoded paths
|
||||
self.find_so(self.xrservice_type.libopenxr_path()))
|
||||
}
|
||||
|
||||
pub fn missing_dependencies(&self) -> Vec<Dependency> {
|
||||
let mut missing_deps = Vec::new();
|
||||
if self.can_be_built {
|
||||
missing_deps.extend(match self.xrservice_type {
|
||||
XRServiceType::Monado => get_missing_monado_deps(),
|
||||
XRServiceType::Wivrn => get_missing_wivrn_deps(),
|
||||
});
|
||||
if self.features.libsurvive.enabled {
|
||||
missing_deps.extend(get_missing_libsurvive_deps());
|
||||
}
|
||||
if self.features.openhmd.enabled {
|
||||
missing_deps.extend(get_missing_openhmd_deps());
|
||||
}
|
||||
if self.features.basalt.enabled {
|
||||
missing_deps.extend(get_missing_basalt_deps());
|
||||
}
|
||||
if self.features.mercury_enabled {
|
||||
missing_deps.extend(get_missing_mercury_deps());
|
||||
}
|
||||
// no listed deps for opencomp
|
||||
}
|
||||
missing_deps.sort_unstable();
|
||||
missing_deps.dedup(); // dedup only works if sorted, hence the above
|
||||
missing_deps
|
||||
}
|
||||
|
||||
/// the file that will become active_runtime.json, as installed in the
|
||||
/// prefix
|
||||
pub fn openxr_json_path(&self) -> PathBuf {
|
||||
self.prefix.join(self.xrservice_type.openxr_json_rel_path())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -568,7 +739,10 @@ mod tests {
|
|||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::profile::{ProfileFeature, ProfileFeatureType, ProfileFeatures, XRServiceType};
|
||||
use crate::profile::{
|
||||
OvrCompatibilityModuleType, ProfileFeature, ProfileFeatureType, ProfileFeatures,
|
||||
ProfileOvrCompatibilityModule, XRServiceType,
|
||||
};
|
||||
|
||||
use super::Profile;
|
||||
|
||||
|
@ -578,7 +752,7 @@ mod tests {
|
|||
assert_eq!(profile.name, "Demo profile");
|
||||
assert_eq!(profile.xrservice_path, PathBuf::from("/home/user/monado"));
|
||||
assert_eq!(
|
||||
profile.opencomposite_path,
|
||||
profile.ovr_comp.path,
|
||||
PathBuf::from("/home/user/opencomposite")
|
||||
);
|
||||
assert_eq!(profile.prefix, PathBuf::from("/home/user/envisionprefix"));
|
||||
|
@ -609,7 +783,12 @@ mod tests {
|
|||
name: "Demo profile".into(),
|
||||
xrservice_path: PathBuf::from("/home/user/monado"),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
opencomposite_path: PathBuf::from("/home/user/opencomposite"),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: PathBuf::from("/home/user/opencomposite"),
|
||||
repo: None,
|
||||
branch: None,
|
||||
mod_type: OvrCompatibilityModuleType::default(),
|
||||
},
|
||||
features: ProfileFeatures {
|
||||
libsurvive: ProfileFeature {
|
||||
feature_type: ProfileFeatureType::Libsurvive,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use crate::{
|
||||
constants::APP_NAME,
|
||||
paths::{data_monado_path, data_opencomposite_path, get_data_dir},
|
||||
profile::{prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeatures, XRServiceType},
|
||||
profile::{
|
||||
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeatures,
|
||||
ProfileOvrCompatibilityModule, XRServiceType,
|
||||
},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -21,7 +24,10 @@ pub fn lighthouse_profile() -> Profile {
|
|||
name: format!("Lighthouse Driver - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_monado_path(),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: data_opencomposite_path(),
|
||||
..Default::default()
|
||||
},
|
||||
features: ProfileFeatures::default(),
|
||||
environment,
|
||||
prefix,
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
paths::{data_monado_path, data_opencomposite_path, data_openhmd_path, get_data_dir},
|
||||
profile::{
|
||||
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
|
||||
ProfileFeatures, XRServiceType,
|
||||
ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType,
|
||||
},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
@ -24,7 +24,10 @@ pub fn openhmd_profile() -> Profile {
|
|||
name: format!("OpenHMD - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_monado_path(),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: data_opencomposite_path(),
|
||||
..Default::default()
|
||||
},
|
||||
features: ProfileFeatures {
|
||||
openhmd: ProfileFeature {
|
||||
feature_type: ProfileFeatureType::OpenHmd,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
constants::APP_NAME,
|
||||
paths::{data_monado_path, data_opencomposite_path, get_data_dir},
|
||||
profile::{Profile, ProfileFeatures, XRServiceType},
|
||||
profile::{Profile, ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -25,7 +25,10 @@ pub fn simulated_profile() -> Profile {
|
|||
name: format!("Simulated Driver - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_monado_path(),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: data_opencomposite_path(),
|
||||
..Default::default()
|
||||
},
|
||||
features: ProfileFeatures::default(),
|
||||
environment,
|
||||
prefix,
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
paths::{data_libsurvive_path, data_monado_path, data_opencomposite_path, get_data_dir},
|
||||
profile::{
|
||||
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
|
||||
ProfileFeatures, XRServiceType,
|
||||
ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType,
|
||||
},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
@ -26,7 +26,10 @@ pub fn survive_profile() -> Profile {
|
|||
name: format!("Survive - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_monado_path(),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: data_opencomposite_path(),
|
||||
..Default::default()
|
||||
},
|
||||
features: ProfileFeatures {
|
||||
libsurvive: ProfileFeature {
|
||||
feature_type: ProfileFeatureType::Libsurvive,
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
use crate::{
|
||||
constants::APP_NAME,
|
||||
paths::{data_opencomposite_path, data_wivrn_path, get_data_dir},
|
||||
profile::{prepare_ld_library_path, Profile, ProfileFeatures, XRServiceType},
|
||||
profile::{
|
||||
prepare_ld_library_path, Profile, ProfileFeatures, ProfileOvrCompatibilityModule,
|
||||
XRServiceType,
|
||||
},
|
||||
ui::job_worker::internal_worker::LAUNCH_OPTS_CMD_PLACEHOLDER,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -18,10 +22,16 @@ pub fn wivrn_profile() -> Profile {
|
|||
name: format!("WiVRn - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_wivrn_path(),
|
||||
xrservice_type: XRServiceType::Wivrn,
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: data_opencomposite_path(),
|
||||
..Default::default()
|
||||
},
|
||||
features: ProfileFeatures {
|
||||
..Default::default()
|
||||
},
|
||||
xrservice_launch_options: format!(
|
||||
"{LAUNCH_OPTS_CMD_PLACEHOLDER} --no-instructions --no-manage-active-runtime"
|
||||
),
|
||||
environment,
|
||||
prefix,
|
||||
can_be_built: true,
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
paths::{data_basalt_path, data_monado_path, data_opencomposite_path, get_data_dir},
|
||||
profile::{
|
||||
prepare_ld_library_path, LighthouseDriver, Profile, ProfileFeature, ProfileFeatureType,
|
||||
ProfileFeatures, XRServiceType,
|
||||
ProfileFeatures, ProfileOvrCompatibilityModule, XRServiceType,
|
||||
},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
@ -24,7 +24,10 @@ pub fn wmr_profile() -> Profile {
|
|||
name: format!("WMR - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_monado_path(),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
ovr_comp: ProfileOvrCompatibilityModule {
|
||||
path: data_opencomposite_path(),
|
||||
..Default::default()
|
||||
},
|
||||
features: ProfileFeatures {
|
||||
basalt: ProfileFeature {
|
||||
feature_type: ProfileFeatureType::Basalt,
|
||||
|
|
|
@ -1,75 +1,33 @@
|
|||
use crate::{
|
||||
paths::{get_backup_dir, get_home_dir},
|
||||
paths::get_backup_dir,
|
||||
profile::Profile,
|
||||
util::file_utils::{copy_file, get_writer},
|
||||
util::{
|
||||
file_utils::{copy_file, get_writer},
|
||||
steam_library_folder::SteamLibraryFolder,
|
||||
},
|
||||
};
|
||||
use anyhow::bail;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::read_to_string,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LibraryFolder {
|
||||
pub path: String,
|
||||
pub apps: HashMap<u32, usize>,
|
||||
}
|
||||
use tracing::error;
|
||||
|
||||
pub const PRESSURE_VESSEL_STEAM_APPID: u32 = 1628350;
|
||||
|
||||
fn get_steam_main_dir_path() -> anyhow::Result<PathBuf> {
|
||||
let steam_root: PathBuf = get_home_dir().join(".steam/root");
|
||||
|
||||
if steam_root.is_symlink() {
|
||||
Ok(steam_root.read_link()?)
|
||||
} else if steam_root.is_dir() {
|
||||
Ok(steam_root)
|
||||
} else {
|
||||
bail!(
|
||||
"Canonical steam root '{}' is not a dir nor a symlink!",
|
||||
steam_root.to_string_lossy()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_steam_libraryfolders_vdf(path: &Path) -> anyhow::Result<HashMap<u32, LibraryFolder>> {
|
||||
Ok(keyvalues_serde::from_str(read_to_string(path)?.as_str())?)
|
||||
}
|
||||
|
||||
fn get_runtime_entrypoint_path() -> Option<PathBuf> {
|
||||
match get_steam_main_dir_path() {
|
||||
Ok(steam_root) => {
|
||||
let steam_libraryfolders_path = steam_root.join("steamapps/libraryfolders.vdf");
|
||||
|
||||
if !steam_libraryfolders_path.is_file() {
|
||||
eprintln!(
|
||||
"Steam libraryfolders.vdf does not exist in its canonical location {}",
|
||||
steam_libraryfolders_path.to_string_lossy()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let libraryfolders: HashMap<u32, LibraryFolder> =
|
||||
parse_steam_libraryfolders_vdf(&steam_libraryfolders_path).ok()?;
|
||||
|
||||
libraryfolders
|
||||
.iter()
|
||||
.find(|(_, libraryfolder)| {
|
||||
libraryfolder
|
||||
.apps
|
||||
.contains_key(&PRESSURE_VESSEL_STEAM_APPID)
|
||||
})
|
||||
.map(|(_, libraryfolder)| {
|
||||
PathBuf::from(&libraryfolder.path)
|
||||
.join("steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point")
|
||||
})
|
||||
}
|
||||
match SteamLibraryFolder::get_folders() {
|
||||
Ok(libraryfolders) => libraryfolders
|
||||
.iter()
|
||||
.find(|(_, folder)| folder.apps.contains_key(&PRESSURE_VESSEL_STEAM_APPID))
|
||||
.map(|(_, folder)| {
|
||||
PathBuf::from(&folder.path)
|
||||
.join("steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point")
|
||||
}),
|
||||
Err(e) => {
|
||||
eprintln!("Error getting steam root path: {e}");
|
||||
error!("unable to get runtime entrypoint path: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -125,22 +83,3 @@ pub fn set_runtime_entrypoint_launch_opts_from_profile(profile: &Profile) -> any
|
|||
}
|
||||
bail!("Could not find valid runtime entrypoint");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::parse_steam_libraryfolders_vdf;
|
||||
|
||||
#[test]
|
||||
fn deserialize_steam_libraryfolders_vdf() {
|
||||
let lf = parse_steam_libraryfolders_vdf(Path::new("./test/files/steam_libraryfolders.vdf"))
|
||||
.unwrap();
|
||||
assert_eq!(lf.len(), 1);
|
||||
let first = lf.get(&0).unwrap();
|
||||
assert_eq!(first.path, "/home/gabmus/.local/share/Steam");
|
||||
assert_eq!(first.apps.len(), 10);
|
||||
assert_eq!(first.apps.get(&228980).unwrap(), &29212173);
|
||||
assert_eq!(first.apps.get(&632360).unwrap(), &0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
constants::{
|
||||
get_artists, get_developers, APP_ID, APP_NAME, BUILD_DATETIME, ISSUES_URL, REPO_URL,
|
||||
SINGLE_DEVELOPER, VERSION,
|
||||
get_artists, APP_ID, APP_NAME, BUILD_DATETIME, ISSUES_URL, REPO_URL, SINGLE_DEVELOPER,
|
||||
VERSION,
|
||||
},
|
||||
device_prober::PhysicalXRDevice,
|
||||
linux_distro::LinuxDistro,
|
||||
|
@ -20,13 +20,20 @@ pub fn create_about_dialog() -> adw::AboutDialog {
|
|||
.website(REPO_URL)
|
||||
.issue_url(ISSUES_URL)
|
||||
.developer_name(SINGLE_DEVELOPER)
|
||||
.developers(get_developers())
|
||||
.developers(
|
||||
env!("CARGO_PKG_AUTHORS")
|
||||
.split(':')
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.artists(get_artists())
|
||||
.build()
|
||||
}
|
||||
|
||||
const UNKNOWN: &str = "UNKNOWN";
|
||||
|
||||
pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo>) {
|
||||
if dialog.debug_info().len() > 0 {
|
||||
if !dialog.debug_info().is_empty() {
|
||||
return;
|
||||
}
|
||||
let distro_family = LinuxDistro::get();
|
||||
|
@ -37,10 +44,10 @@ pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo
|
|||
format!("Build time: {BUILD_DATETIME}"),
|
||||
format!(
|
||||
"Operating system: {d} ({f})",
|
||||
d = distro.unwrap_or("unknown".into()),
|
||||
d = distro.unwrap_or(UNKNOWN.into()),
|
||||
f = distro_family
|
||||
.map(|f| f.to_string())
|
||||
.unwrap_or("unknown".into())
|
||||
.unwrap_or(UNKNOWN.into())
|
||||
),
|
||||
format!(
|
||||
"Kernel: {}",
|
||||
|
@ -50,23 +57,29 @@ pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo
|
|||
),
|
||||
format!(
|
||||
"Session type: {}",
|
||||
env::var("XDG_SESSION_TYPE").unwrap_or("unknown".into())
|
||||
env::var("XDG_SESSION_TYPE").unwrap_or(UNKNOWN.into())
|
||||
),
|
||||
format!(
|
||||
"Desktop: {}",
|
||||
env::var("XDG_CURRENT_DESKTOP").unwrap_or("unknown".into())
|
||||
env::var("XDG_CURRENT_DESKTOP").unwrap_or(UNKNOWN.into())
|
||||
),
|
||||
format!(
|
||||
"CPU: {}",
|
||||
read_to_string("/proc/cpuinfo")
|
||||
.ok()
|
||||
.and_then(|s| {
|
||||
s.split("\n")
|
||||
.find(|line| line.starts_with("model name"))
|
||||
.map(|line| line.split(':').next_back().map(|s| s.trim().to_string()))
|
||||
})
|
||||
.flatten()
|
||||
.unwrap_or(UNKNOWN.into())
|
||||
),
|
||||
format!(
|
||||
"GPUs: {}",
|
||||
vkinfo
|
||||
.map(|i| i.gpu_names.join(", "))
|
||||
.unwrap_or("unknown".into())
|
||||
),
|
||||
format!(
|
||||
"Monado Vulkan Layers: {}",
|
||||
vkinfo
|
||||
.map(|i| i.has_monado_vulkan_layers.to_string())
|
||||
.unwrap_or("unknown".into())
|
||||
.unwrap_or(UNKNOWN.into())
|
||||
),
|
||||
format!("Detected XR Devices: {}", {
|
||||
let devs = PhysicalXRDevice::from_usb();
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use crate::constants::APP_NAME;
|
||||
use gtk::prelude::GtkApplicationExt;
|
||||
use notify_rust::Notification;
|
||||
use relm4::{adw::prelude::*, prelude::*};
|
||||
|
||||
fn alert_base(title: &str, msg: Option<&str>) -> adw::AlertDialog {
|
||||
|
@ -36,3 +38,12 @@ pub fn alert_w_widget(
|
|||
}
|
||||
present_alert(d, parent);
|
||||
}
|
||||
|
||||
pub fn notification(title: &str, msg: &str) -> Notification {
|
||||
Notification::new()
|
||||
.summary(title)
|
||||
.body(msg)
|
||||
.icon("org.gabmus.envision-symbolic")
|
||||
.appname(APP_NAME)
|
||||
.finalize()
|
||||
}
|
||||
|
|
378
src/ui/app.rs
378
src/ui/app.rs
|
@ -1,6 +1,6 @@
|
|||
use super::{
|
||||
about_dialog::{create_about_dialog, populate_debug_info},
|
||||
alert::{alert, alert_w_widget},
|
||||
alert::{alert, alert_w_widget, notification},
|
||||
build_window::{BuildStatus, BuildWindow, BuildWindowInit, BuildWindowMsg, BuildWindowOutMsg},
|
||||
cmdline_opts::CmdLineOpts,
|
||||
debug_view::{DebugView, DebugViewInit, DebugViewMsg, DebugViewOutMsg},
|
||||
|
@ -11,6 +11,7 @@ use super::{
|
|||
},
|
||||
libsurvive_setup_window::{LibsurviveSetupMsg, LibsurviveSetupWindow},
|
||||
main_view::{MainView, MainViewInit, MainViewMsg, MainViewOutMsg},
|
||||
plugins::store::{PluginStore, PluginStoreInit, PluginStoreMsg, PluginStoreOutMsg},
|
||||
util::{copiable_code_snippet, copy_text, open_with_default_handler},
|
||||
wivrn_conf_editor::{WivrnConfEditor, WivrnConfEditorInit, WivrnConfEditorMsg},
|
||||
};
|
||||
|
@ -19,19 +20,16 @@ use crate::{
|
|||
build_basalt::get_build_basalt_jobs, build_libsurvive::get_build_libsurvive_jobs,
|
||||
build_mercury::get_build_mercury_jobs, build_monado::get_build_monado_jobs,
|
||||
build_opencomposite::get_build_opencomposite_jobs, build_openhmd::get_build_openhmd_jobs,
|
||||
build_wivrn::get_build_wivrn_jobs,
|
||||
build_vapor::get_build_vapor_jobs, build_wivrn::get_build_wivrn_jobs,
|
||||
build_xrizer::get_build_xrizer_jobs,
|
||||
},
|
||||
config::Config,
|
||||
config::{Config, PluginConfig},
|
||||
constants::APP_NAME,
|
||||
depcheck::{
|
||||
basalt_deps::get_missing_basalt_deps, common::dep_pkexec,
|
||||
libsurvive_deps::get_missing_libsurvive_deps, mercury_deps::get_missing_mercury_deps,
|
||||
monado_deps::get_missing_monado_deps, openhmd_deps::get_missing_openhmd_deps,
|
||||
wivrn_deps::get_missing_wivrn_deps,
|
||||
},
|
||||
depcheck::common::dep_pkexec,
|
||||
file_builders::{
|
||||
active_runtime_json::{
|
||||
set_current_active_runtime_to_profile, set_current_active_runtime_to_steam,
|
||||
remove_current_active_runtime, restore_active_runtime_backup,
|
||||
set_current_active_runtime_to_profile,
|
||||
},
|
||||
openvrpaths_vrpath::{
|
||||
set_current_openvrpaths_to_profile, set_current_openvrpaths_to_steam,
|
||||
|
@ -40,30 +38,39 @@ use crate::{
|
|||
linux_distro::LinuxDistro,
|
||||
openxr_prober::is_openxr_ready,
|
||||
paths::get_data_dir,
|
||||
profile::{Profile, XRServiceType},
|
||||
profile::{OvrCompatibilityModuleType, Profile, XRServiceType},
|
||||
stateless_action,
|
||||
steam_linux_runtime_injector::{
|
||||
restore_runtime_entrypoint, set_runtime_entrypoint_launch_opts_from_profile,
|
||||
},
|
||||
util::file_utils::{setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd},
|
||||
util::file_utils::{
|
||||
setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd, verify_cap_sys_nice_eip,
|
||||
},
|
||||
vulkaninfo::VulkanInfo,
|
||||
wivrn_dbus,
|
||||
xr_devices::XRDevice,
|
||||
};
|
||||
use adw::{prelude::*, ResponseAppearance};
|
||||
use gtk::glib::{self, clone};
|
||||
use notify_rust::NotificationHandle;
|
||||
use relm4::{
|
||||
actions::{AccelsPlus, ActionGroupName, RelmAction, RelmActionGroup},
|
||||
new_action_group, new_stateful_action, new_stateless_action,
|
||||
prelude::*,
|
||||
};
|
||||
use std::{collections::VecDeque, fs::remove_file, time::Duration};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
fs::remove_file,
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
pub struct App {
|
||||
application: adw::Application,
|
||||
app_win: adw::ApplicationWindow,
|
||||
inhibit_id: Option<u32>,
|
||||
|
||||
main_view: Controller<MainView>,
|
||||
main_view: AsyncController<MainView>,
|
||||
debug_view: Controller<DebugView>,
|
||||
split_view: Option<adw::NavigationSplitView>,
|
||||
about_dialog: adw::AboutDialog,
|
||||
|
@ -73,7 +80,7 @@ pub struct App {
|
|||
|
||||
config: Config,
|
||||
xrservice_worker: Option<JobWorker>,
|
||||
autostart_worker: Option<JobWorker>,
|
||||
plugins_worker: Option<JobWorker>,
|
||||
restart_xrservice: bool,
|
||||
build_worker: Option<JobWorker>,
|
||||
profiles: Vec<Profile>,
|
||||
|
@ -86,13 +93,16 @@ pub struct App {
|
|||
openxr_prober_worker: Option<JobWorker>,
|
||||
xrservice_ready: bool,
|
||||
vkinfo: Option<VulkanInfo>,
|
||||
|
||||
inhibit_fail_notif: Option<NotificationHandle>,
|
||||
pluginstore: Option<AsyncController<PluginStore>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
OnServiceLog(Vec<String>),
|
||||
OnServiceExit(i32),
|
||||
OnAutostartExit(i32),
|
||||
OnPluginsExit(i32),
|
||||
OnBuildLog(Vec<String>),
|
||||
OnBuildExit(i32),
|
||||
ClockTicking,
|
||||
|
@ -116,6 +126,9 @@ pub enum Msg {
|
|||
HandleCommandLine(CmdLineOpts),
|
||||
StartProber,
|
||||
OnProberExit(bool),
|
||||
WivrnCheckPairMode,
|
||||
OpenPluginStore,
|
||||
UpdateConfigPlugins(HashMap<String, PluginConfig>),
|
||||
NoOp,
|
||||
}
|
||||
|
||||
|
@ -135,11 +148,22 @@ impl App {
|
|||
Some("XR session running"),
|
||||
);
|
||||
if inhibit_id == 0 {
|
||||
alert(
|
||||
"Failed to inhibit desktop locking",
|
||||
Some(&format!("{APP_NAME} tries to inhibit desktop locking to avoid automatic suspension or screen locking kicking in while the XR session is active, but this process failed.\n\nThe session is still running but you might want to manually disable automatic suspension and screen locking.")),
|
||||
Some(&self.app_win.clone().upcast())
|
||||
);
|
||||
self.inhibit_fail_notif = match if let Some(notif) =
|
||||
self.inhibit_fail_notif.as_ref()
|
||||
{
|
||||
notif.show()
|
||||
} else {
|
||||
notification(
|
||||
"Failed to inhibit desktop locking",
|
||||
&format!("{APP_NAME} tries to inhibit desktop locking to avoid automatic suspension or screen locking kicking in while the XR session is active, but this process failed.\n\nThe session is still running but you might want to manually disable automatic suspension and screen locking."),
|
||||
).show()
|
||||
} {
|
||||
Ok(n) => Some(n),
|
||||
Err(e) => {
|
||||
error!("failed to send desktop notification: {e:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.inhibit_id = Some(inhibit_id);
|
||||
}
|
||||
|
@ -152,57 +176,7 @@ impl App {
|
|||
pub fn start_xrservice(&mut self, sender: AsyncComponentSender<Self>, debug: bool) {
|
||||
self.xrservice_ready = false;
|
||||
let prof = self.get_selected_profile();
|
||||
if prof.can_start() {
|
||||
if let Err(e) = set_current_active_runtime_to_profile(&prof) {
|
||||
alert(
|
||||
"Failed to start XR Service",
|
||||
Some(&format!(
|
||||
"Error setting current active runtime to profile: {e}"
|
||||
)),
|
||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if let Err(e) = set_current_openvrpaths_to_profile(&prof) {
|
||||
alert(
|
||||
"Failed to start XR Service",
|
||||
Some(&format!(
|
||||
"Error setting current openvrpaths file to profile: {e}"
|
||||
)),
|
||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
};
|
||||
self.debug_view.sender().emit(DebugViewMsg::ClearLog);
|
||||
self.xr_devices = vec![];
|
||||
remove_file(prof.xrservice_type.ipc_file_path())
|
||||
.is_err()
|
||||
.then(|| println!("Failed to remove xrservice IPC file"));
|
||||
let worker = JobWorker::xrservice_worker_wrap_from_profile(
|
||||
&prof,
|
||||
sender.input_sender(),
|
||||
|msg| match msg {
|
||||
JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows),
|
||||
JobWorkerOut::Exit(code) => Msg::OnServiceExit(code),
|
||||
},
|
||||
debug,
|
||||
);
|
||||
worker.start();
|
||||
self.xrservice_worker = Some(worker);
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::XRServiceActiveChanged(
|
||||
true,
|
||||
Some(self.get_selected_profile()),
|
||||
// show launch opts only if setting the runtime entrypoint fails
|
||||
set_runtime_entrypoint_launch_opts_from_profile(&prof).is_err(),
|
||||
));
|
||||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::XRServiceActiveChanged(true));
|
||||
self.set_inhibit_session(true);
|
||||
sender.input(Msg::StartProber);
|
||||
} else {
|
||||
if !prof.can_start() {
|
||||
alert(
|
||||
"Failed to start profile",
|
||||
Some(concat!(
|
||||
|
@ -211,32 +185,133 @@ impl App {
|
|||
)),
|
||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if let Err(e) = set_current_active_runtime_to_profile(&prof) {
|
||||
alert(
|
||||
"Failed to start XR Service",
|
||||
Some(&format!(
|
||||
"Error setting current active runtime to profile: {e}"
|
||||
)),
|
||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if let Err(e) = set_current_openvrpaths_to_profile(&prof) {
|
||||
alert(
|
||||
"Failed to start XR Service",
|
||||
Some(&format!(
|
||||
"Error setting current openvrpaths file to profile: {e}"
|
||||
)),
|
||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
};
|
||||
self.debug_view.sender().emit(DebugViewMsg::ClearLog);
|
||||
self.xr_devices = vec![];
|
||||
{
|
||||
let ipc_file = prof.xrservice_type.ipc_file_path();
|
||||
if ipc_file.exists() {
|
||||
remove_file(ipc_file)
|
||||
.unwrap_or_else(|e| error!("failed to remove xrservice IPC file: {e}"));
|
||||
};
|
||||
}
|
||||
let worker = JobWorker::xrservice_worker_wrap_from_profile(
|
||||
&prof,
|
||||
sender.input_sender(),
|
||||
|msg| match msg {
|
||||
JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows),
|
||||
JobWorkerOut::Exit(code) => Msg::OnServiceExit(code),
|
||||
},
|
||||
debug,
|
||||
);
|
||||
worker.start();
|
||||
self.xrservice_worker = Some(worker);
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::XRServiceActiveChanged(
|
||||
true,
|
||||
Some(self.get_selected_profile()),
|
||||
// show launch opts only if setting the runtime entrypoint fails
|
||||
set_runtime_entrypoint_launch_opts_from_profile(&prof).is_err(),
|
||||
));
|
||||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::XRServiceActiveChanged(true));
|
||||
self.set_inhibit_session(true);
|
||||
sender.input(Msg::StartProber);
|
||||
}
|
||||
|
||||
pub fn run_autostart(&mut self, sender: AsyncComponentSender<Self>) {
|
||||
let prof = self.get_selected_profile();
|
||||
if let Some(autostart_cmd) = &prof.autostart_command {
|
||||
let plugins_cmd = self
|
||||
.config
|
||||
.plugins
|
||||
.values()
|
||||
.filter_map(|cp| {
|
||||
// disable potentially unsafe wayvr_dashboard
|
||||
if cp.plugin.appid.contains("wayvr_dashboard") {
|
||||
return None;
|
||||
}
|
||||
if cp.enabled && cp.plugin.validate() {
|
||||
if let Err(e) = cp.plugin.mark_as_executable() {
|
||||
error!(
|
||||
"failed to mark plugin {} as executable: {e}",
|
||||
cp.plugin.appid
|
||||
);
|
||||
None
|
||||
} else if !cp.plugin.plugin_type.launches_directly() {
|
||||
None
|
||||
} else {
|
||||
Some({
|
||||
let mut cmd_parts = vec![cp
|
||||
.plugin
|
||||
.executable()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string()];
|
||||
cmd_parts.extend(cp.plugin.args.clone().unwrap_or_default());
|
||||
cmd_parts
|
||||
.iter()
|
||||
.map(|part| format!("'{part}'"))
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" & ");
|
||||
if !plugins_cmd.is_empty() {
|
||||
let mut jobs = VecDeque::new();
|
||||
jobs.push_back(WorkerJob::new_cmd(
|
||||
Some(prof.environment.clone()),
|
||||
"sh".into(),
|
||||
Some(vec!["-c".into(), autostart_cmd.clone()]),
|
||||
Some(vec!["-c".into(), plugins_cmd]),
|
||||
));
|
||||
let autostart_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
|
||||
let plugins_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
|
||||
JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows),
|
||||
JobWorkerOut::Exit(code) => Msg::OnAutostartExit(code),
|
||||
JobWorkerOut::Exit(code) => Msg::OnPluginsExit(code),
|
||||
});
|
||||
autostart_worker.start();
|
||||
self.autostart_worker = Some(autostart_worker);
|
||||
plugins_worker.start();
|
||||
self.plugins_worker = Some(plugins_worker);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restore_openxr_openvr_files(&self) {
|
||||
restore_runtime_entrypoint();
|
||||
if let Err(e) = set_current_active_runtime_to_steam() {
|
||||
if let Err(e) = remove_current_active_runtime() {
|
||||
alert(
|
||||
"Could not restore Steam active runtime",
|
||||
"Could not remove profile active runtime",
|
||||
Some(&format!("{e}")),
|
||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
}
|
||||
if let Err(e) = restore_active_runtime_backup() {
|
||||
alert(
|
||||
"Could not restore previous active runtime",
|
||||
Some(&format!("{e}")),
|
||||
Some(&self.app_win.clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
|
@ -251,27 +326,17 @@ impl App {
|
|||
}
|
||||
|
||||
pub fn shutdown_xrservice(&mut self) {
|
||||
if let Some(worker) = self.autostart_worker.as_ref() {
|
||||
worker.stop();
|
||||
if let Some(w) = self.plugins_worker.as_ref() {
|
||||
w.stop();
|
||||
}
|
||||
self.xrservice_ready = false;
|
||||
if let Some(w) = self.openxr_prober_worker.as_ref() {
|
||||
w.stop();
|
||||
// this can cause threads to remain hanging...
|
||||
self.openxr_prober_worker = None;
|
||||
}
|
||||
self.set_inhibit_session(false);
|
||||
if let Some(worker) = self.xrservice_worker.as_ref() {
|
||||
worker.stop();
|
||||
if let Some(w) = self.xrservice_worker.as_ref() {
|
||||
w.stop();
|
||||
}
|
||||
self.libmonado = None;
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::XRServiceActiveChanged(false, None, false));
|
||||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::XRServiceActiveChanged(false));
|
||||
self.xr_devices = vec![];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -337,6 +402,8 @@ impl AsyncComponent for App {
|
|||
}
|
||||
}
|
||||
Msg::OnServiceExit(code) => {
|
||||
self.set_inhibit_session(false);
|
||||
self.xrservice_ready = false;
|
||||
self.restore_openxr_openvr_files();
|
||||
self.main_view
|
||||
.sender()
|
||||
|
@ -344,6 +411,8 @@ impl AsyncComponent for App {
|
|||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::XRServiceActiveChanged(false));
|
||||
self.libmonado = None;
|
||||
self.xr_devices = vec![];
|
||||
if code != 0 && code != 15 {
|
||||
// 15 is SIGTERM
|
||||
sender.input(Msg::OnServiceLog(vec![format!(
|
||||
|
@ -358,14 +427,14 @@ impl AsyncComponent for App {
|
|||
self.start_xrservice(sender, false);
|
||||
}
|
||||
}
|
||||
Msg::OnAutostartExit(_) => self.autostart_worker = None,
|
||||
Msg::OnPluginsExit(_) => self.plugins_worker = None,
|
||||
Msg::ClockTicking => {
|
||||
self.main_view.sender().emit(MainViewMsg::ClockTicking);
|
||||
let should_poll_for_devices = self.xrservice_ready
|
||||
&& self
|
||||
.xrservice_worker
|
||||
.as_ref()
|
||||
.is_some_and(JobWorker::is_alive);
|
||||
let xrservice_worker_is_alive = self
|
||||
.xrservice_worker
|
||||
.as_ref()
|
||||
.is_some_and(JobWorker::is_alive);
|
||||
let should_poll_for_devices = self.xrservice_ready && xrservice_worker_is_alive;
|
||||
if should_poll_for_devices {
|
||||
if let Some(monado) = self.libmonado.as_ref() {
|
||||
self.xr_devices = XRDevice::from_libmonado(monado);
|
||||
|
@ -379,6 +448,32 @@ impl AsyncComponent for App {
|
|||
}
|
||||
}
|
||||
}
|
||||
if xrservice_worker_is_alive
|
||||
&& self.get_selected_profile().xrservice_type == XRServiceType::Wivrn
|
||||
{
|
||||
// is in pairing mode?
|
||||
sender.input(Msg::WivrnCheckPairMode);
|
||||
}
|
||||
}
|
||||
Msg::WivrnCheckPairMode => {
|
||||
if self.get_selected_profile().xrservice_type == XRServiceType::Wivrn {
|
||||
match wivrn_dbus::is_pairing_mode().await {
|
||||
Ok(state) => {
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::SetWivrnPairingMode(state));
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::SetWivrnSupportsPairing(true));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("failed to get wivrn pairing mode: {e:?}");
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::SetWivrnSupportsPairing(false));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Msg::EnableDebugViewChanged(val) => {
|
||||
self.config.debug_view_enabled = val;
|
||||
|
@ -416,41 +511,40 @@ impl AsyncComponent for App {
|
|||
},
|
||||
Msg::BuildProfile(clean_build) => {
|
||||
let profile = self.get_selected_profile();
|
||||
let mut missing_deps = vec![];
|
||||
let mut jobs = VecDeque::<WorkerJob>::new();
|
||||
// profile per se can't be built, but we still need opencomp
|
||||
if profile.can_be_built {
|
||||
missing_deps.extend(match profile.xrservice_type {
|
||||
XRServiceType::Monado => get_missing_monado_deps(),
|
||||
XRServiceType::Wivrn => get_missing_wivrn_deps(),
|
||||
});
|
||||
if profile.features.libsurvive.enabled {
|
||||
missing_deps.extend(get_missing_libsurvive_deps());
|
||||
jobs.extend(get_build_libsurvive_jobs(&profile, clean_build));
|
||||
}
|
||||
if profile.features.openhmd.enabled {
|
||||
missing_deps.extend(get_missing_openhmd_deps());
|
||||
jobs.extend(get_build_openhmd_jobs(&profile, clean_build));
|
||||
}
|
||||
if profile.features.basalt.enabled {
|
||||
missing_deps.extend(get_missing_basalt_deps());
|
||||
jobs.extend(get_build_basalt_jobs(&profile, clean_build));
|
||||
}
|
||||
if profile.features.mercury_enabled {
|
||||
missing_deps.extend(get_missing_mercury_deps());
|
||||
jobs.extend(get_build_mercury_jobs(&profile));
|
||||
}
|
||||
jobs.extend(match profile.xrservice_type {
|
||||
XRServiceType::Monado => get_build_monado_jobs(&profile, clean_build),
|
||||
XRServiceType::Wivrn => get_build_wivrn_jobs(&profile, clean_build),
|
||||
});
|
||||
// no listed deps for opencomp
|
||||
}
|
||||
jobs.extend(get_build_opencomposite_jobs(&profile, clean_build));
|
||||
jobs.extend(match profile.ovr_comp.mod_type {
|
||||
OvrCompatibilityModuleType::Opencomposite => {
|
||||
get_build_opencomposite_jobs(&profile, clean_build)
|
||||
}
|
||||
OvrCompatibilityModuleType::Xrizer => {
|
||||
get_build_xrizer_jobs(&profile, clean_build)
|
||||
}
|
||||
OvrCompatibilityModuleType::Vapor => {
|
||||
get_build_vapor_jobs(&profile, clean_build)
|
||||
}
|
||||
});
|
||||
let missing_deps = profile.missing_dependencies();
|
||||
if !(self.skip_depcheck || profile.skip_dependency_check || missing_deps.is_empty())
|
||||
{
|
||||
missing_deps.sort_unstable();
|
||||
missing_deps.dedup(); // dedup only works if sorted, hence the above
|
||||
let distro = LinuxDistro::get();
|
||||
let (missing_package_list, install_missing_widget): (
|
||||
String,
|
||||
|
@ -604,13 +698,32 @@ impl AsyncComponent for App {
|
|||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::UpdateSelectedProfile(prof.clone()));
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::QueryProfileRebuild);
|
||||
}
|
||||
Msg::RunSetCap => {
|
||||
if !dep_pkexec().check() {
|
||||
println!("pkexec not found, skipping setcap");
|
||||
error!("pkexec not found, skipping setcap");
|
||||
} else {
|
||||
let profile = self.get_selected_profile();
|
||||
setcap_cap_sys_nice_eip(&profile).await;
|
||||
let setcap_failed_dialog = || {
|
||||
alert_w_widget(
|
||||
"Setcap failed to run",
|
||||
Some("Setting the capabilities automatically failed, you can still try manually using the command below."
|
||||
),
|
||||
Some(&copiable_code_snippet(
|
||||
&format!("sudo {}", setcap_cap_sys_nice_eip_cmd(&profile).join(" "))
|
||||
)),
|
||||
Some(&self.app_win.clone().upcast())
|
||||
);
|
||||
};
|
||||
if let Err(e) = setcap_cap_sys_nice_eip(&profile).await {
|
||||
setcap_failed_dialog();
|
||||
error!("failed running setcap: {e}");
|
||||
} else if !verify_cap_sys_nice_eip(&profile).await {
|
||||
setcap_failed_dialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::ProfileSelected(prof) => {
|
||||
|
@ -730,6 +843,21 @@ impl AsyncComponent for App {
|
|||
}
|
||||
}
|
||||
}
|
||||
Msg::OpenPluginStore => {
|
||||
let pluginstore = PluginStore::builder()
|
||||
.launch(PluginStoreInit {
|
||||
config_plugins: self.config.plugins.clone(),
|
||||
})
|
||||
.forward(sender.input_sender(), move |msg| match msg {
|
||||
PluginStoreOutMsg::UpdateConfigPlugins(cp) => Msg::UpdateConfigPlugins(cp),
|
||||
});
|
||||
pluginstore.sender().emit(PluginStoreMsg::Present);
|
||||
self.pluginstore = Some(pluginstore);
|
||||
}
|
||||
Msg::UpdateConfigPlugins(cp) => {
|
||||
self.config.plugins = cp;
|
||||
self.config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -831,6 +959,17 @@ impl AsyncComponent for App {
|
|||
}
|
||||
)
|
||||
);
|
||||
stateless_action!(
|
||||
actions,
|
||||
PluginStoreAction,
|
||||
clone!(
|
||||
#[strong]
|
||||
sender,
|
||||
move |_| {
|
||||
sender.input(Msg::OpenPluginStore);
|
||||
}
|
||||
)
|
||||
);
|
||||
// this bypasses the macro because I need the underlying gio action
|
||||
// to enable/disable it in update()
|
||||
let configure_wivrn_action = {
|
||||
|
@ -852,7 +991,7 @@ impl AsyncComponent for App {
|
|||
match VulkanInfo::get() {
|
||||
Ok(info) => Some(info),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get Vulkan info: {e:#?}");
|
||||
error!("failed to get Vulkan info: {e:#?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -867,7 +1006,6 @@ impl AsyncComponent for App {
|
|||
config: config.clone(),
|
||||
selected_profile: selected_profile.clone(),
|
||||
root_win: root.clone().into(),
|
||||
vkinfo: vkinfo.clone(),
|
||||
})
|
||||
.forward(sender.input_sender(), |message| match message {
|
||||
MainViewOutMsg::DoStartStopXRService => Msg::DoStartStopXRService,
|
||||
|
@ -876,6 +1014,7 @@ impl AsyncComponent for App {
|
|||
MainViewOutMsg::DeleteProfile => Msg::DeleteProfile,
|
||||
MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p),
|
||||
MainViewOutMsg::OpenLibsurviveSetup => Msg::OpenLibsurviveSetup,
|
||||
MainViewOutMsg::BuildProfile(clean) => Msg::BuildProfile(clean),
|
||||
}),
|
||||
vkinfo,
|
||||
debug_view: DebugView::builder()
|
||||
|
@ -902,7 +1041,7 @@ impl AsyncComponent for App {
|
|||
config,
|
||||
profiles,
|
||||
xrservice_worker: None,
|
||||
autostart_worker: None,
|
||||
plugins_worker: None,
|
||||
build_worker: None,
|
||||
xr_devices: vec![],
|
||||
restart_xrservice: false,
|
||||
|
@ -912,6 +1051,8 @@ impl AsyncComponent for App {
|
|||
configure_wivrn_action,
|
||||
openxr_prober_worker: None,
|
||||
xrservice_ready: false,
|
||||
inhibit_fail_notif: None,
|
||||
pluginstore: None,
|
||||
};
|
||||
|
||||
let widgets = view_output!();
|
||||
|
@ -984,6 +1125,7 @@ new_stateless_action!(pub BuildProfileCleanAction, AppActionGroup, "buildprofile
|
|||
new_stateless_action!(pub QuitAction, AppActionGroup, "quit");
|
||||
new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool);
|
||||
new_stateless_action!(pub ConfigureWivrnAction, AppActionGroup, "configurewivrn");
|
||||
new_stateless_action!(pub PluginStoreAction, AppActionGroup, "store");
|
||||
|
||||
new_stateless_action!(pub DebugOpenDataAction, AppActionGroup, "debugopendata");
|
||||
new_stateless_action!(pub DebugOpenPrefixAction, AppActionGroup, "debugopenprefix");
|
||||
|
|
|
@ -88,43 +88,54 @@ impl SimpleComponent for BuildWindow {
|
|||
gtk::Label {
|
||||
#[track = "model.changed(BuildWindow::build_status())"]
|
||||
set_markup: match &model.build_status {
|
||||
BuildStatus::Building => "Build in progress...".to_string(),
|
||||
BuildStatus::Done => "Build done, you can close this window".to_string(),
|
||||
BuildStatus::Building => String::default(),
|
||||
BuildStatus::Done => "Build done, you can close this window".into(),
|
||||
BuildStatus::Error(code) => {
|
||||
format!("Build failed: \"{c}\"", c = code)
|
||||
}
|
||||
}.as_str(),
|
||||
#[track = "model.changed(BuildWindow::build_status())"]
|
||||
set_visible: match &model.build_status {
|
||||
BuildStatus::Building => false,
|
||||
BuildStatus::Done | BuildStatus::Error(_) => true,
|
||||
},
|
||||
add_css_class: "title-2",
|
||||
set_wrap: true,
|
||||
set_wrap_mode: gtk::pango::WrapMode::Word,
|
||||
set_justify: gtk::Justification::Center,
|
||||
},
|
||||
gtk::Button {
|
||||
#[track = "model.changed(BuildWindow::build_status())"]
|
||||
set_visible: matches!(&model.build_status, BuildStatus::Building),
|
||||
add_css_class: "destructive-action",
|
||||
add_css_class: "circular",
|
||||
set_icon_name: "window-close-symbolic",
|
||||
set_tooltip_text: Some("Cancel build"),
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.output(Self::Output::CancelBuild).expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
},
|
||||
},
|
||||
model.term.container.clone(),
|
||||
},
|
||||
add_bottom_bar: bottom_bar = >k::Button {
|
||||
add_css_class: "pill",
|
||||
add_bottom_bar: bottom_bar = >k::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_halign: gtk::Align::Center,
|
||||
set_label: "Close",
|
||||
set_margin_all: 12,
|
||||
#[track = "model.changed(BuildWindow::can_close())"]
|
||||
set_sensitive: model.can_close,
|
||||
connect_clicked[win] => move |_| {
|
||||
|
||||
win.close();
|
||||
set_hexpand: true,
|
||||
set_margin_bottom: 24,
|
||||
set_spacing: 12,
|
||||
gtk::Button {
|
||||
add_css_class: "pill",
|
||||
set_halign: gtk::Align::Center,
|
||||
set_label: "Close",
|
||||
#[track = "model.changed(BuildWindow::can_close())"]
|
||||
set_visible: model.can_close,
|
||||
connect_clicked[win] => move |_| {
|
||||
win.close();
|
||||
},
|
||||
},
|
||||
}
|
||||
// this
|
||||
gtk::Button {
|
||||
#[track = "model.changed(BuildWindow::build_status())"]
|
||||
set_visible: matches!(&model.build_status, BuildStatus::Building),
|
||||
add_css_class: "destructive-action",
|
||||
add_css_class: "pill",
|
||||
set_label: "Cancel build",
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.output(Self::Output::CancelBuild).expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
},
|
||||
// ^^^
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use crate::config::Config;
|
||||
use crate::{
|
||||
config::Config,
|
||||
constants::{APP_NAME, VERSION},
|
||||
};
|
||||
use gtk::{
|
||||
gio::{
|
||||
prelude::{ApplicationCommandLineExt, ApplicationExt},
|
||||
|
@ -6,21 +9,35 @@ use gtk::{
|
|||
},
|
||||
glib::{self, prelude::IsA},
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CmdLineOpts {
|
||||
pub version: bool,
|
||||
pub start: bool,
|
||||
pub list_profiles: bool,
|
||||
pub profile_uuid: Option<String>,
|
||||
pub skip_depcheck: bool,
|
||||
pub check_dependencies_for: Option<String>,
|
||||
}
|
||||
|
||||
impl CmdLineOpts {
|
||||
const OPT_VERSION: (&'static str, char) = ("version", 'v');
|
||||
const OPT_START: (&'static str, char) = ("start", 'S');
|
||||
const OPT_LIST_PROFILES: (&'static str, char) = ("list-profiles", 'l');
|
||||
const OPT_PROFILE: (&'static str, char) = ("profile", 'p');
|
||||
const OPT_SKIP_DEPCHECK: (&'static str, char) = ("skip-dependency-check", 'd');
|
||||
const OPT_CHECK_DEPS_FOR: (&'static str, char) = ("check-deps-for", 'c');
|
||||
|
||||
pub fn init(app: &impl IsA<Application>) {
|
||||
app.add_main_option(
|
||||
Self::OPT_VERSION.0,
|
||||
glib::Char::try_from(Self::OPT_VERSION.1).unwrap(),
|
||||
glib::OptionFlags::IN_MAIN,
|
||||
glib::OptionArg::None,
|
||||
"Print the version information",
|
||||
None,
|
||||
);
|
||||
app.add_main_option(
|
||||
Self::OPT_START.0,
|
||||
glib::Char::try_from(Self::OPT_START.1).unwrap(),
|
||||
|
@ -53,31 +70,62 @@ impl CmdLineOpts {
|
|||
"Skip dependency checks when building profiles",
|
||||
None,
|
||||
);
|
||||
app.add_main_option(
|
||||
Self::OPT_CHECK_DEPS_FOR.0,
|
||||
glib::Char::try_from(Self::OPT_CHECK_DEPS_FOR.1).unwrap(),
|
||||
glib::OptionFlags::IN_MAIN,
|
||||
glib::OptionArg::String,
|
||||
"Prints missing dependencies for given profile id; returns nothing if all dependencies are satisfied",
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
/// returns true if the application should quit
|
||||
pub fn handle_non_activating_opts(cmdline: &ApplicationCommandLine) -> bool {
|
||||
if cmdline.options_dict().contains(Self::OPT_LIST_PROFILES.0) {
|
||||
/// returns an exit code if the application should quit immediately
|
||||
pub fn handle_non_activating_opts(&self) -> Option<i32> {
|
||||
if self.version {
|
||||
println!("{APP_NAME} {VERSION}");
|
||||
return Some(0);
|
||||
}
|
||||
if self.list_profiles {
|
||||
println!("Available profiles\nUUID: \"name\"");
|
||||
let profiles = Config::get_config().profiles();
|
||||
profiles.iter().for_each(|p| {
|
||||
println!("{}: \"{}\"", p.uuid, p.name);
|
||||
});
|
||||
return true;
|
||||
return Some(0);
|
||||
}
|
||||
false
|
||||
if let Some(prof_id) = self.check_dependencies_for.as_ref() {
|
||||
let profiles = Config::get_config().profiles();
|
||||
if let Some(prof) = profiles.iter().find(|p| &p.uuid == prof_id) {
|
||||
let deps = prof.missing_dependencies();
|
||||
if deps.is_empty() {
|
||||
return Some(0);
|
||||
}
|
||||
for dep in deps {
|
||||
println!("{}", dep.name);
|
||||
}
|
||||
return Some(1);
|
||||
} else {
|
||||
error!("No profile found for uuid: `{prof_id}`");
|
||||
return Some(404);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn from_cmdline(cmdline: &ApplicationCommandLine) -> Self {
|
||||
let opts = cmdline.options_dict();
|
||||
Self {
|
||||
version: opts.contains(Self::OPT_VERSION.0),
|
||||
start: opts.contains(Self::OPT_START.0),
|
||||
profile_uuid: match opts.lookup::<String>(Self::OPT_PROFILE.0) {
|
||||
Err(_) => None,
|
||||
Ok(None) => None,
|
||||
Ok(Some(variant)) => Some(variant),
|
||||
},
|
||||
list_profiles: opts.contains(Self::OPT_LIST_PROFILES.0),
|
||||
profile_uuid: opts
|
||||
.lookup::<String>(Self::OPT_PROFILE.0)
|
||||
.unwrap_or_default(),
|
||||
skip_depcheck: opts.contains(Self::OPT_SKIP_DEPCHECK.0),
|
||||
check_dependencies_for: opts
|
||||
.lookup::<String>(Self::OPT_CHECK_DEPS_FOR.0)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,9 +61,19 @@ impl SimpleComponent for DevicesBox {
|
|||
}
|
||||
if !has_left && dev.roles.contains(&XRDeviceRole::Left) {
|
||||
has_left = true;
|
||||
if ["Qwerty Left Controller"].contains(&dev.name.as_str()) {
|
||||
row_model.state = Some(DeviceRowState::Warning);
|
||||
row_model.subtitle =
|
||||
Some(format!("No left controller detected ({})", dev.name));
|
||||
}
|
||||
}
|
||||
if !has_right && dev.roles.contains(&XRDeviceRole::Right) {
|
||||
has_right = true;
|
||||
if ["Qwerty Right Controller"].contains(&dev.name.as_str()) {
|
||||
row_model.state = Some(DeviceRowState::Warning);
|
||||
row_model.subtitle =
|
||||
Some(format!("No right controller detected ({})", dev.name));
|
||||
}
|
||||
}
|
||||
models.push(row_model);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use crate::{
|
|||
use gtk::prelude::*;
|
||||
use relm4::{new_action_group, new_stateless_action, prelude::*};
|
||||
use std::fs::remove_file;
|
||||
use tracing::error;
|
||||
|
||||
const WIVRN_LATEST_RELEASE_APK_URL: &str =
|
||||
"https://github.com/WiVRn/WiVRn/releases/latest/download/WiVRn-standard-release.apk";
|
||||
|
@ -113,7 +114,7 @@ impl AsyncComponent for InstallWivrnBox {
|
|||
add_css_class: "dim-label",
|
||||
set_hexpand: true,
|
||||
set_label: concat!(
|
||||
"Install the WiVRn APK on your standalong Android headset. ",
|
||||
"Install the WiVRn APK on your standalone Android headset. ",
|
||||
"You will need to enable Developer Mode on your headset, ",
|
||||
"then press the \"Install WiVRn\" button."
|
||||
),
|
||||
|
@ -172,7 +173,7 @@ impl AsyncComponent for InstallWivrnBox {
|
|||
|
||||
match get_wivrn_apk_ref(&self.selected_profile) {
|
||||
Err(GetWivrnApkRefErr::NotWivrn) => {
|
||||
eprintln!("This is not a WiVRn profile, how did you get here?");
|
||||
error!("this is not a WiVRn profile, how did you get here?");
|
||||
}
|
||||
Err(GetWivrnApkRefErr::RepoDirNotFound) => {
|
||||
self.set_install_wivrn_status(InstallWivrnStatus::Done(Some(
|
||||
|
@ -180,14 +181,11 @@ impl AsyncComponent for InstallWivrnBox {
|
|||
)));
|
||||
}
|
||||
Err(GetWivrnApkRefErr::RepoManipulationFailed(giterr)) => {
|
||||
eprintln!("Error: failed to manipulate WiVRn repo: {giterr}, falling back to latest release APK");
|
||||
error!("failed to manipulate WiVRn repo: {giterr}, falling back to latest release APK");
|
||||
let existing = cache_file_path(WIVRN_LATEST_RELEASE_APK_URL, Some("apk"));
|
||||
if existing.is_file() {
|
||||
if let Err(e) = remove_file(&existing) {
|
||||
eprintln!(
|
||||
"Failed to remove file {}: {e}",
|
||||
existing.to_string_lossy()
|
||||
);
|
||||
error!("failed to remove file {}: {e}", existing.to_string_lossy());
|
||||
}
|
||||
}
|
||||
sender.input(Self::Input::DoInstall(WIVRN_LATEST_RELEASE_APK_URL.into()));
|
||||
|
@ -208,7 +206,7 @@ impl AsyncComponent for InstallWivrnBox {
|
|||
// TODO: we gonna cache or just download async every time?
|
||||
match cache_file(&url, Some("apk")).await {
|
||||
Err(e) => {
|
||||
eprintln!("Failed to download apk: {e}");
|
||||
error!("failed to download apk: {e}");
|
||||
self.set_install_wivrn_status(InstallWivrnStatus::Done(Some(
|
||||
"Error downloading WiVRn client APK".into(),
|
||||
)));
|
||||
|
@ -236,14 +234,14 @@ impl AsyncComponent for InstallWivrnBox {
|
|||
.into(),
|
||||
))
|
||||
} else {
|
||||
eprintln!("Error: ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr);
|
||||
error!("ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr);
|
||||
InstallWivrnStatus::Done(Some(
|
||||
format!("ADB exited with code \"{}\"", out.exit_code)
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: failed to run ADB: {e}");
|
||||
error!("failed to run ADB: {e}");
|
||||
InstallWivrnStatus::Done(Some(
|
||||
"Failed to run ADB".into()
|
||||
))
|
||||
|
|
|
@ -155,7 +155,7 @@ impl Worker for InternalJobWorker {
|
|||
}
|
||||
}
|
||||
|
||||
const LAUNCH_OPTS_CMD_PLACEHOLDER: &str = "%command%";
|
||||
pub const LAUNCH_OPTS_CMD_PLACEHOLDER: &str = "%command%";
|
||||
|
||||
impl InternalJobWorker {
|
||||
pub fn xrservice_worker_from_profile(
|
||||
|
@ -175,7 +175,7 @@ impl InternalJobWorker {
|
|||
}
|
||||
}
|
||||
}
|
||||
let mut launch_opts = prof.xrservice_launch_options.trim();
|
||||
let mut launch_opts = prof.xrservice_launch_options.trim().to_string();
|
||||
let debug_launch_opts = if debug {
|
||||
if launch_opts.contains(LAUNCH_OPTS_CMD_PLACEHOLDER) {
|
||||
format!("{} {}", "gdbserver localhost:9000", launch_opts)
|
||||
|
@ -189,7 +189,7 @@ impl InternalJobWorker {
|
|||
String::default()
|
||||
};
|
||||
launch_opts = if debug {
|
||||
debug_launch_opts.as_str()
|
||||
debug_launch_opts
|
||||
} else {
|
||||
launch_opts
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ use std::{
|
|||
thread::{self, sleep},
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::{error, warn};
|
||||
|
||||
pub mod internal_worker;
|
||||
pub mod job;
|
||||
|
@ -97,7 +98,7 @@ impl JobWorker {
|
|||
self.state.lock().unwrap().stop_requested = true;
|
||||
if let Some(pid) = self.state.lock().unwrap().current_pid {
|
||||
if let Err(e) = kill(pid, SIGTERM) {
|
||||
eprintln!("Failed to send SIGTERM: {e:#?}");
|
||||
error!("Failed to send SIGTERM: {e}");
|
||||
}
|
||||
let state = self.state.clone();
|
||||
thread::spawn(move || {
|
||||
|
@ -105,9 +106,9 @@ impl JobWorker {
|
|||
if let Ok(s) = state.lock() {
|
||||
if !s.exited {
|
||||
// process is still alive
|
||||
eprintln!("Process is still alive 2 seconds after SIGTERM, proceeding to send SIGKILL...");
|
||||
warn!("process is still alive 2 seconds after SIGTERM, proceeding to send SIGKILL...");
|
||||
if let Err(e) = kill(pid, SIGKILL) {
|
||||
eprintln!("Failed to send SIGKILL: {e:#?}");
|
||||
error!("failed to send SIGKILL: {e}");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::{
|
|||
alert::alert,
|
||||
app::{
|
||||
AboutAction, BuildProfileAction, BuildProfileCleanAction, ConfigureWivrnAction,
|
||||
DebugViewToggleAction,
|
||||
DebugViewToggleAction, PluginStoreAction,
|
||||
},
|
||||
devices_box::{DevicesBox, DevicesBoxMsg},
|
||||
install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg},
|
||||
|
@ -12,6 +12,7 @@ use super::{
|
|||
steamvr_calibration_box::{SteamVrCalibrationBox, SteamVrCalibrationBoxMsg},
|
||||
util::{limit_dropdown_width, warning_heading},
|
||||
wivrn_wired_start_box::{WivrnWiredStartBox, WivrnWiredStartBoxInit, WivrnWiredStartBoxMsg},
|
||||
SENDER_IO_ERR_MSG,
|
||||
};
|
||||
use crate::{
|
||||
config::Config,
|
||||
|
@ -24,7 +25,7 @@ use crate::{
|
|||
file_utils::{get_writer, mount_has_nosuid},
|
||||
steamvr_utils::chaperone_info_exists,
|
||||
},
|
||||
vulkaninfo::VulkanInfo,
|
||||
wivrn_dbus,
|
||||
xr_devices::XRDevice,
|
||||
};
|
||||
use adw::{prelude::*, ResponseAppearance};
|
||||
|
@ -33,9 +34,9 @@ use relm4::{
|
|||
actions::{ActionGroupName, RelmAction, RelmActionGroup},
|
||||
new_action_group, new_stateless_action,
|
||||
prelude::*,
|
||||
ComponentParts, ComponentSender, SimpleComponent,
|
||||
};
|
||||
use std::{fs::read_to_string, io::Write};
|
||||
use tracing::{error, warn};
|
||||
|
||||
#[tracker::track]
|
||||
pub struct MainView {
|
||||
|
@ -59,6 +60,8 @@ pub struct MainView {
|
|||
#[tracker::do_not_track]
|
||||
profile_delete_confirm_dialog: adw::AlertDialog,
|
||||
#[tracker::do_not_track]
|
||||
query_profile_rebuild_dialog: adw::AlertDialog,
|
||||
#[tracker::do_not_track]
|
||||
profile_editor: Option<Controller<ProfileEditor>>,
|
||||
#[tracker::do_not_track]
|
||||
steamvr_calibration_box: Controller<SteamVrCalibrationBox>,
|
||||
|
@ -71,8 +74,9 @@ pub struct MainView {
|
|||
#[tracker::do_not_track]
|
||||
profile_export_action: gtk::gio::SimpleAction,
|
||||
xrservice_ready: bool,
|
||||
#[tracker::do_not_track]
|
||||
vkinfo: Option<VulkanInfo>,
|
||||
wivrn_pairing_mode: bool,
|
||||
wivrn_pin: Option<String>,
|
||||
wivrn_supports_pairing: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -96,6 +100,11 @@ pub enum MainViewMsg {
|
|||
ExportProfile,
|
||||
ImportProfile,
|
||||
OpenProfileEditor(Profile),
|
||||
SetWivrnSupportsPairing(bool),
|
||||
SetWivrnPairingMode(bool),
|
||||
StopWivrnPairingMode,
|
||||
StartWivrnPairingMode,
|
||||
QueryProfileRebuild,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -106,17 +115,18 @@ pub enum MainViewOutMsg {
|
|||
DeleteProfile,
|
||||
SaveProfile(Profile),
|
||||
OpenLibsurviveSetup,
|
||||
/// params: clean
|
||||
BuildProfile(bool),
|
||||
}
|
||||
|
||||
pub struct MainViewInit {
|
||||
pub config: Config,
|
||||
pub selected_profile: Profile,
|
||||
pub root_win: gtk::Window,
|
||||
pub vkinfo: Option<VulkanInfo>,
|
||||
}
|
||||
|
||||
impl MainView {
|
||||
fn create_profile_editor(&mut self, sender: ComponentSender<MainView>, prof: Profile) {
|
||||
fn create_profile_editor(&mut self, sender: AsyncComponentSender<Self>, prof: Profile) {
|
||||
self.profile_editor = Some(
|
||||
ProfileEditor::builder()
|
||||
.launch(ProfileEditorInit {
|
||||
|
@ -130,15 +140,17 @@ impl MainView {
|
|||
}
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for MainView {
|
||||
#[relm4::component(pub async)]
|
||||
impl AsyncComponent for MainView {
|
||||
type Init = MainViewInit;
|
||||
type Input = MainViewMsg;
|
||||
type Output = MainViewOutMsg;
|
||||
type CommandOutput = ();
|
||||
|
||||
menu! {
|
||||
app_menu: {
|
||||
section! {
|
||||
"Plugin_s" => PluginStoreAction,
|
||||
// value inside action is ignored
|
||||
"_Debug View" => DebugViewToggleAction,
|
||||
"_Build Profile" => BuildProfileAction,
|
||||
|
@ -240,28 +252,136 @@ impl SimpleComponent for MainView {
|
|||
set_visible: model.xrservice_active,
|
||||
add_css_class: "card",
|
||||
gtk::Label {
|
||||
#[track = "model.changed(Self::xrservice_active()) || model.changed(Self::xrservice_ready())"]
|
||||
set_label: if model.xrservice_ready {
|
||||
"Service ready, you can launch XR apps"
|
||||
} else {
|
||||
#[track = "model.changed(Self::xrservice_active()) || model.changed(Self::xrservice_ready()) || model.changed(Self::wivrn_pairing_mode())"]
|
||||
set_label: {
|
||||
match model.selected_profile.xrservice_type {
|
||||
XRServiceType::Monado =>
|
||||
"Starting…",
|
||||
if model.xrservice_ready {
|
||||
"Service ready, you can launch XR apps"
|
||||
} else {
|
||||
"Starting…"
|
||||
}
|
||||
XRServiceType::Wivrn =>
|
||||
"Starting, please connect your client device…",
|
||||
if model.wivrn_pairing_mode {
|
||||
"Pairing mode"
|
||||
} else {
|
||||
"Starting, connect your client device…"
|
||||
}
|
||||
}
|
||||
},
|
||||
set_margin_all: 18,
|
||||
add_css_class: "heading",
|
||||
add_css_class: "success",
|
||||
add_css_class: "warning",
|
||||
#[track = "model.changed(Self::xrservice_active()) || model.changed(Self::xrservice_ready())"]
|
||||
set_class_active: ("warning", !model.xrservice_ready),
|
||||
#[track = "model.changed(Self::xrservice_active()) || model.changed(Self::xrservice_ready()) || model.changed(Self::wivrn_pairing_mode())"]
|
||||
set_class_active: (
|
||||
"success",
|
||||
model.xrservice_ready
|
||||
&& (
|
||||
model.selected_profile.xrservice_type != XRServiceType::Wivrn
|
||||
|| !model.wivrn_pairing_mode
|
||||
)
|
||||
),
|
||||
set_wrap: true,
|
||||
set_justify: gtk::Justification::Center,
|
||||
},
|
||||
},
|
||||
model.devices_box.widget(),
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_hexpand: true,
|
||||
set_vexpand: false,
|
||||
set_spacing: 12,
|
||||
add_css_class: "card",
|
||||
add_css_class: "padded",
|
||||
#[track = "model.changed(Self::wivrn_supports_pairing()) || model.changed(Self::xrservice_active()) || model.changed(Self::selected_profile()) || model.changed(Self::wivrn_pairing_mode()) || model.changed(Self::wivrn_pin())"]
|
||||
set_visible: model.wivrn_supports_pairing
|
||||
&& model.xrservice_active
|
||||
&& model.selected_profile.xrservice_type == XRServiceType::Wivrn
|
||||
&& !model.wivrn_pairing_mode,
|
||||
gtk::Label {
|
||||
add_css_class: "heading",
|
||||
set_hexpand: true,
|
||||
set_xalign: 0.0,
|
||||
set_label: "Pairing mode",
|
||||
set_wrap: true,
|
||||
set_wrap_mode: gtk::pango::WrapMode::Word,
|
||||
},
|
||||
gtk::Label {
|
||||
add_css_class: "dim-label",
|
||||
set_hexpand: true,
|
||||
set_label: concat!(
|
||||
"To connect a new device to WiVRn, you ",
|
||||
"will need to pair it first.\n\n",
|
||||
"You can do so by starting the pairing mode ",
|
||||
"with the button below."
|
||||
),
|
||||
set_xalign: 0.0,
|
||||
set_wrap: true,
|
||||
set_wrap_mode: gtk::pango::WrapMode::Word,
|
||||
},
|
||||
gtk::Button {
|
||||
add_css_class: "suggested-action",
|
||||
set_label: "Start pairing mode",
|
||||
set_halign: gtk::Align::Start,
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::StartWivrnPairingMode);
|
||||
}
|
||||
},
|
||||
},
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_hexpand: true,
|
||||
set_vexpand: false,
|
||||
set_spacing: 12,
|
||||
add_css_class: "card",
|
||||
add_css_class: "padded",
|
||||
#[track = "model.changed(Self::wivrn_supports_pairing()) || model.changed(Self::xrservice_active()) || model.changed(Self::selected_profile()) || model.changed(Self::wivrn_pairing_mode()) || model.changed(Self::wivrn_pin())"]
|
||||
set_visible: model.wivrn_supports_pairing
|
||||
&& model.xrservice_active
|
||||
&& model.selected_profile.xrservice_type == XRServiceType::Wivrn
|
||||
&& model.wivrn_pairing_mode && model.wivrn_pin.is_some(),
|
||||
gtk::Label {
|
||||
add_css_class: "heading",
|
||||
set_hexpand: true,
|
||||
set_xalign: 0.0,
|
||||
set_label: "Pairing mode",
|
||||
set_wrap: true,
|
||||
set_wrap_mode: gtk::pango::WrapMode::Word,
|
||||
},
|
||||
gtk::Label {
|
||||
add_css_class: "dim-label",
|
||||
set_hexpand: true,
|
||||
set_label: concat!(
|
||||
"WiVRn is in pairing mode. Pair your client ",
|
||||
"device with the following PIN:"
|
||||
),
|
||||
set_xalign: 0.0,
|
||||
set_wrap: true,
|
||||
set_wrap_mode: gtk::pango::WrapMode::Word,
|
||||
},
|
||||
gtk::Label {
|
||||
add_css_class: "title-2",
|
||||
add_css_class: "monospace",
|
||||
set_hexpand: true,
|
||||
set_selectable: true,
|
||||
#[track = "model.changed(Self::wivrn_pin())"]
|
||||
set_label: model.wivrn_pin
|
||||
.as_deref().unwrap_or(""),
|
||||
set_xalign: 0.5,
|
||||
set_justify: gtk::Justification::Center,
|
||||
set_wrap: true,
|
||||
set_wrap_mode: gtk::pango::WrapMode::Word,
|
||||
},
|
||||
gtk::Button {
|
||||
add_css_class: "destructive-action",
|
||||
set_label: "Stop pairing mode",
|
||||
set_halign: gtk::Align::Start,
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::StopWivrnPairingMode);
|
||||
}
|
||||
},
|
||||
},
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_hexpand: true,
|
||||
|
@ -273,8 +393,8 @@ impl SimpleComponent for MainView {
|
|||
set_visible: match mount_has_nosuid(&model.selected_profile.prefix) {
|
||||
Ok(b) => b,
|
||||
Err(_) => {
|
||||
eprintln!(
|
||||
"Warning (nosuid detection): could not get stat on path {}",
|
||||
warn!(
|
||||
"nosuid detection: could not get stat on path {}",
|
||||
model.selected_profile.prefix.to_string_lossy());
|
||||
false
|
||||
},
|
||||
|
@ -329,35 +449,7 @@ impl SimpleComponent for MainView {
|
|||
set_label: concat!(
|
||||
"SteamVR room configuration not found.\n",
|
||||
"To use the SteamVR lighthouse driver, you ",
|
||||
"will need to run SteamVR and perform the room setup.",
|
||||
),
|
||||
add_css_class: "warning",
|
||||
set_xalign: 0.0,
|
||||
set_wrap: true,
|
||||
set_wrap_mode: gtk::pango::WrapMode::Word,
|
||||
}
|
||||
},
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_hexpand: true,
|
||||
set_vexpand: false,
|
||||
set_spacing: 12,
|
||||
add_css_class: "card",
|
||||
add_css_class: "padded",
|
||||
set_visible: model
|
||||
.vkinfo
|
||||
.as_ref()
|
||||
.is_some_and(
|
||||
|i| i.has_nvidia_gpu && !i.has_monado_vulkan_layers
|
||||
),
|
||||
warning_heading(),
|
||||
gtk::Label {
|
||||
set_label: concat!(
|
||||
"An Nvidia GPU has been detected, but it ",
|
||||
"seems you don't have the Monado Vulkan Layers ",
|
||||
"installed on your system.\n\nInstall the ",
|
||||
"Monado Vulkan Layers or your XR session will ",
|
||||
"crash."
|
||||
"will need to run SteamVR Quick Calibration.",
|
||||
),
|
||||
add_css_class: "warning",
|
||||
set_xalign: 0.0,
|
||||
|
@ -480,17 +572,55 @@ impl SimpleComponent for MainView {
|
|||
set_icon_name: "view-more-symbolic",
|
||||
set_tooltip_text: Some("Menu"),
|
||||
set_menu_model: Some(&profile_actions_menu),
|
||||
set_direction: gtk::ArrowType::Up,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
|
||||
async fn update(
|
||||
&mut self,
|
||||
message: Self::Input,
|
||||
sender: AsyncComponentSender<Self>,
|
||||
_root: &Self::Root,
|
||||
) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
Self::Input::ClockTicking => {}
|
||||
Self::Input::SetWivrnSupportsPairing(supported) => {
|
||||
if self.wivrn_supports_pairing != supported {
|
||||
self.set_wivrn_supports_pairing(supported)
|
||||
}
|
||||
}
|
||||
Self::Input::SetWivrnPairingMode(enabled) => {
|
||||
if self.wivrn_pairing_mode != enabled {
|
||||
self.set_wivrn_pairing_mode(enabled);
|
||||
if enabled {
|
||||
match wivrn_dbus::pairing_pin().await {
|
||||
Ok(pin) => {
|
||||
self.set_wivrn_pin(Some(pin));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("failed to get wivrn pairing pin: {e}");
|
||||
}
|
||||
};
|
||||
} else {
|
||||
self.set_wivrn_pin(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Input::StopWivrnPairingMode => {
|
||||
if let Err(e) = wivrn_dbus::disable_pairing().await {
|
||||
error!("failed to stop wivrn pairing mode: {e}");
|
||||
}
|
||||
}
|
||||
Self::Input::StartWivrnPairingMode => {
|
||||
if let Err(e) = wivrn_dbus::enable_pairing().await {
|
||||
error!("failed to start wivrn pairing mode: {e}");
|
||||
}
|
||||
}
|
||||
Self::Input::StartStopClicked => {
|
||||
sender
|
||||
.output(Self::Output::DoStartStopXRService)
|
||||
|
@ -504,6 +634,7 @@ impl SimpleComponent for MainView {
|
|||
Self::Input::XRServiceActiveChanged(active, profile, show_launch_opts) => {
|
||||
if !active {
|
||||
self.set_xrservice_ready(false);
|
||||
sender.input(Self::Input::SetWivrnPairingMode(false));
|
||||
}
|
||||
self.set_xrservice_active(active);
|
||||
self.steamvr_calibration_box
|
||||
|
@ -565,6 +696,10 @@ impl SimpleComponent for MainView {
|
|||
}
|
||||
}));
|
||||
}
|
||||
Self::Input::QueryProfileRebuild => {
|
||||
self.query_profile_rebuild_dialog
|
||||
.present(Some(&self.root_win));
|
||||
}
|
||||
Self::Input::SetSelectedProfile(index) => {
|
||||
self.profiles_dropdown
|
||||
.as_ref()
|
||||
|
@ -602,7 +737,7 @@ impl SimpleComponent for MainView {
|
|||
Self::Input::SaveProfile(prof) => {
|
||||
sender
|
||||
.output(Self::Output::SaveProfile(prof))
|
||||
.expect("Sender output failed");
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
Self::Input::DuplicateProfile => {
|
||||
if self.selected_profile.can_be_built {
|
||||
|
@ -742,11 +877,11 @@ impl SimpleComponent for MainView {
|
|||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
async fn init(
|
||||
init: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
sender: AsyncComponentSender<Self>,
|
||||
) -> AsyncComponentParts<Self> {
|
||||
let profile_not_editable_dialog = adw::AlertDialog::builder()
|
||||
.heading("This profile is not editable")
|
||||
.body(concat!(
|
||||
|
@ -771,6 +906,29 @@ impl SimpleComponent for MainView {
|
|||
),
|
||||
);
|
||||
|
||||
let query_profile_rebuild_dialog = adw::AlertDialog::builder()
|
||||
.heading("Do you want to build this profile now?")
|
||||
.body("This will trigger a clean build")
|
||||
.build();
|
||||
query_profile_rebuild_dialog.add_response("no", "_No");
|
||||
query_profile_rebuild_dialog.add_response("yes", "_Yes");
|
||||
query_profile_rebuild_dialog.set_response_appearance("yes", ResponseAppearance::Suggested);
|
||||
|
||||
query_profile_rebuild_dialog.connect_response(
|
||||
None,
|
||||
clone!(
|
||||
#[strong]
|
||||
sender,
|
||||
move |_, res| {
|
||||
if res == "yes" {
|
||||
sender
|
||||
.output(Self::Output::BuildProfile(true))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
let profile_delete_confirm_dialog = adw::AlertDialog::builder()
|
||||
.heading("Are you sure you want to delete this profile?")
|
||||
.build();
|
||||
|
@ -836,42 +994,6 @@ impl SimpleComponent for MainView {
|
|||
ret
|
||||
};
|
||||
|
||||
let mut model = Self {
|
||||
xrservice_active: false,
|
||||
enable_debug_view: init.config.debug_view_enabled,
|
||||
profiles_dropdown: None,
|
||||
profiles: vec![],
|
||||
steam_launch_options_box: SteamLaunchOptionsBox::builder().launch(()).detach(),
|
||||
install_wivrn_box: InstallWivrnBox::builder()
|
||||
.launch(InstallWivrnBoxInit {
|
||||
selected_profile: init.selected_profile.clone(),
|
||||
root_win: init.root_win.clone(),
|
||||
})
|
||||
.detach(),
|
||||
wivrn_wired_start_box: WivrnWiredStartBox::builder()
|
||||
.launch(WivrnWiredStartBoxInit {
|
||||
selected_profile: init.selected_profile.clone(),
|
||||
root_win: init.root_win.clone(),
|
||||
})
|
||||
.detach(),
|
||||
devices_box: DevicesBox::builder().launch(()).detach(),
|
||||
selected_profile: init.selected_profile.clone(),
|
||||
profile_not_editable_dialog,
|
||||
profile_delete_confirm_dialog,
|
||||
root_win: init.root_win.clone(),
|
||||
steamvr_calibration_box,
|
||||
openhmd_calibration_box,
|
||||
profile_editor: None,
|
||||
xrservice_ready: false,
|
||||
profile_delete_action,
|
||||
profile_export_action,
|
||||
vkinfo: init.vkinfo,
|
||||
tracker: 0,
|
||||
};
|
||||
let widgets = view_output!();
|
||||
|
||||
model.profiles_dropdown = Some(widgets.profiles_dropdown.clone());
|
||||
|
||||
stateless_action!(
|
||||
actions,
|
||||
ProfileMenuNewAction,
|
||||
|
@ -919,7 +1041,46 @@ impl SimpleComponent for MainView {
|
|||
|
||||
root.insert_action_group(ProfileActionGroup::NAME, Some(&actions.into_action_group()));
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
let mut model = Self {
|
||||
xrservice_active: false,
|
||||
enable_debug_view: init.config.debug_view_enabled,
|
||||
profiles_dropdown: None,
|
||||
profiles: vec![],
|
||||
steam_launch_options_box: SteamLaunchOptionsBox::builder().launch(()).detach(),
|
||||
install_wivrn_box: InstallWivrnBox::builder()
|
||||
.launch(InstallWivrnBoxInit {
|
||||
selected_profile: init.selected_profile.clone(),
|
||||
root_win: init.root_win.clone(),
|
||||
})
|
||||
.detach(),
|
||||
wivrn_wired_start_box: WivrnWiredStartBox::builder()
|
||||
.launch(WivrnWiredStartBoxInit {
|
||||
selected_profile: init.selected_profile.clone(),
|
||||
root_win: init.root_win.clone(),
|
||||
})
|
||||
.detach(),
|
||||
devices_box: DevicesBox::builder().launch(()).detach(),
|
||||
selected_profile: init.selected_profile.clone(),
|
||||
profile_not_editable_dialog,
|
||||
profile_delete_confirm_dialog,
|
||||
query_profile_rebuild_dialog,
|
||||
root_win: init.root_win.clone(),
|
||||
steamvr_calibration_box,
|
||||
openhmd_calibration_box,
|
||||
profile_editor: None,
|
||||
xrservice_ready: false,
|
||||
profile_delete_action,
|
||||
profile_export_action,
|
||||
wivrn_pairing_mode: false,
|
||||
wivrn_supports_pairing: false,
|
||||
wivrn_pin: None,
|
||||
tracker: 0,
|
||||
};
|
||||
let widgets = view_output!();
|
||||
|
||||
model.profiles_dropdown = Some(widgets.profiles_dropdown.clone());
|
||||
|
||||
AsyncComponentParts { model, widgets }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ mod libsurvive_setup_window;
|
|||
mod macros;
|
||||
mod main_view;
|
||||
mod openhmd_calibration_box;
|
||||
pub mod plugins;
|
||||
mod preference_rows;
|
||||
mod profile_editor;
|
||||
mod steam_launch_options_box;
|
||||
|
|
|
@ -3,6 +3,7 @@ use relm4::{
|
|||
gtk::{self, prelude::*},
|
||||
ComponentParts, ComponentSender, SimpleComponent,
|
||||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[tracker::track]
|
||||
pub struct OpenHmdCalibrationBox {
|
||||
|
@ -59,10 +60,10 @@ impl SimpleComponent for OpenHmdCalibrationBox {
|
|||
let target = XDG.get_config_home().join("openhmd/rift-room-config.json");
|
||||
if target.is_file() {
|
||||
if let Err(e) = std::fs::remove_file(target) {
|
||||
eprintln!("Failed to remove openhmd config: {e}");
|
||||
error!("Failed to remove openhmd config: {e}");
|
||||
}
|
||||
} else {
|
||||
println!("info: trying to delete openhmd calibration config, but file is missing")
|
||||
debug!("trying to delete openhmd calibration config, but file is missing")
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
169
src/ui/plugins/add_custom_plugin_win.rs
Normal file
169
src/ui/plugins/add_custom_plugin_win.rs
Normal file
|
@ -0,0 +1,169 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
constants::APP_ID,
|
||||
ui::{
|
||||
preference_rows::{entry_row, file_row},
|
||||
SENDER_IO_ERR_MSG,
|
||||
},
|
||||
};
|
||||
|
||||
use super::Plugin;
|
||||
use adw::prelude::*;
|
||||
use gtk::glib::clone;
|
||||
use relm4::prelude::*;
|
||||
|
||||
#[tracker::track]
|
||||
pub struct AddCustomPluginWin {
|
||||
#[tracker::do_not_track]
|
||||
parent: gtk::Window,
|
||||
#[tracker::do_not_track]
|
||||
win: Option<adw::Dialog>,
|
||||
/// this is true when enough fields are populated, allowing the creation
|
||||
/// of the plugin object to add
|
||||
can_add: bool,
|
||||
#[tracker::do_not_track]
|
||||
plugin: Plugin,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AddCustomPluginWinMsg {
|
||||
Present,
|
||||
Close,
|
||||
OnNameChange(String),
|
||||
OnExecPathChange(Option<String>),
|
||||
Add,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AddCustomPluginWinOutMsg {
|
||||
Add(Plugin),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AddCustomPluginWinInit {
|
||||
pub parent: gtk::Window,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for AddCustomPluginWin {
|
||||
type Init = AddCustomPluginWinInit;
|
||||
type Input = AddCustomPluginWinMsg;
|
||||
type Output = AddCustomPluginWinOutMsg;
|
||||
|
||||
view! {
|
||||
#[name(win)]
|
||||
adw::Dialog {
|
||||
set_can_close: true,
|
||||
#[wrap(Some)]
|
||||
set_child: inner = &adw::ToolbarView {
|
||||
set_top_bar_style: adw::ToolbarStyle::Flat,
|
||||
set_bottom_bar_style: adw::ToolbarStyle::Flat,
|
||||
set_vexpand: true,
|
||||
set_hexpand: true,
|
||||
add_top_bar: top_bar = &adw::HeaderBar {
|
||||
set_show_end_title_buttons: false,
|
||||
set_show_start_title_buttons: false,
|
||||
pack_start: cancel_btn = >k::Button {
|
||||
set_label: "Cancel",
|
||||
add_css_class: "destructive-action",
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::Close)
|
||||
},
|
||||
},
|
||||
pack_end: add_btn = >k::Button {
|
||||
set_label: "Add",
|
||||
add_css_class: "suggested-action",
|
||||
#[track = "model.changed(AddCustomPluginWin::can_add())"]
|
||||
set_sensitive: model.can_add,
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::Add)
|
||||
},
|
||||
},
|
||||
#[wrap(Some)]
|
||||
set_title_widget: title_label = &adw::WindowTitle {
|
||||
set_title: "Add Custom Plugin",
|
||||
},
|
||||
},
|
||||
#[wrap(Some)]
|
||||
set_content: content = &adw::PreferencesPage {
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
add: grp = &adw::PreferencesGroup {
|
||||
add: &entry_row(
|
||||
"Plugin Name",
|
||||
"",
|
||||
clone!(
|
||||
#[strong] sender,
|
||||
move |row| sender.input(Self::Input::OnNameChange(row.text().to_string()))
|
||||
)
|
||||
),
|
||||
add: &file_row(
|
||||
"Plugin Executable",
|
||||
None,
|
||||
None,
|
||||
Some(model.parent.clone()),
|
||||
clone!(
|
||||
#[strong] sender,
|
||||
move |path_s| sender.input(Self::Input::OnExecPathChange(path_s))
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
Self::Input::Present => self.win.as_ref().unwrap().present(Some(&self.parent)),
|
||||
Self::Input::Close => {
|
||||
self.win.as_ref().unwrap().close();
|
||||
}
|
||||
Self::Input::Add => {
|
||||
if self.plugin.validate() {
|
||||
sender
|
||||
.output(Self::Output::Add(self.plugin.clone()))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
self.win.as_ref().unwrap().close();
|
||||
}
|
||||
}
|
||||
Self::Input::OnNameChange(name) => {
|
||||
self.plugin.appid = if !name.is_empty() {
|
||||
format!("{APP_ID}.customPlugin.{name}")
|
||||
} else {
|
||||
String::default()
|
||||
};
|
||||
self.plugin.name = name;
|
||||
self.set_can_add(self.plugin.validate());
|
||||
}
|
||||
Self::Input::OnExecPathChange(ep) => {
|
||||
self.plugin.exec_path = ep.map(PathBuf::from);
|
||||
self.set_can_add(self.plugin.validate());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let mut model = Self {
|
||||
tracker: 0,
|
||||
win: None,
|
||||
parent: init.parent,
|
||||
can_add: false,
|
||||
plugin: Plugin {
|
||||
short_description: Some("Custom Plugin".into()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
let widgets = view_output!();
|
||||
model.win = Some(widgets.win.clone());
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
}
|
189
src/ui/plugins/mod.rs
Normal file
189
src/ui/plugins/mod.rs
Normal file
|
@ -0,0 +1,189 @@
|
|||
pub mod add_custom_plugin_win;
|
||||
pub mod store;
|
||||
mod store_detail;
|
||||
mod store_row_factory;
|
||||
|
||||
use crate::{
|
||||
constants::APP_ID,
|
||||
downloader::{cache_file_path, download_file_async},
|
||||
file_builders::wayvr_dashboard_config::{
|
||||
WayVrDashboardConfigFragment, WayVrDashboardConfigFragmentInner,
|
||||
},
|
||||
paths::get_plugins_dir,
|
||||
util::file_utils::{get_writer, mark_as_executable},
|
||||
xdg::XDG,
|
||||
};
|
||||
use anyhow::bail;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{create_dir_all, remove_file},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)]
|
||||
pub enum PluginType {
|
||||
#[default]
|
||||
Executable,
|
||||
WayVrApp,
|
||||
WayVrDashboard,
|
||||
}
|
||||
|
||||
impl PluginType {
|
||||
pub fn launches_directly(&self) -> bool {
|
||||
self == &Self::Executable
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)]
|
||||
pub struct Plugin {
|
||||
pub appid: String,
|
||||
pub name: String,
|
||||
pub author: Option<String>,
|
||||
pub icon_url: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub short_description: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub homepage_url: Option<String>,
|
||||
pub screenshots: Vec<String>,
|
||||
/// either one of exec_url or exec_path must be provided
|
||||
pub exec_url: Option<String>,
|
||||
/// either one of exec_url or exec_path must be provided
|
||||
pub exec_path: Option<PathBuf>,
|
||||
/// options and arguments that should be passed to the plugin executable
|
||||
pub args: Option<Vec<String>>,
|
||||
pub env_vars: Option<HashMap<String, String>>,
|
||||
/// defined as a list of appids of other plugins
|
||||
pub dependencies: Option<Vec<String>>,
|
||||
/// defined as a list of appids of other plugins
|
||||
/// all plugins of type WayVrDashboard should conflict with each other by default
|
||||
pub conflicts: Option<Vec<String>>,
|
||||
#[serde(default = "PluginType::default")]
|
||||
pub plugin_type: PluginType,
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
fn wayvr_config_fragment_filename(&self) -> String {
|
||||
format!("99-{APP_ID}-plugin.{}.yaml", self.appid)
|
||||
}
|
||||
|
||||
fn wayvr_conf_dir() -> PathBuf {
|
||||
XDG.get_config_home().join("wlxoverlay/wayvr.conf.d")
|
||||
}
|
||||
|
||||
pub fn enable(&self) -> anyhow::Result<()> {
|
||||
match self.plugin_type {
|
||||
PluginType::Executable => {}
|
||||
PluginType::WayVrApp => todo!(),
|
||||
PluginType::WayVrDashboard => {
|
||||
let wayvr_conf_dir = Self::wayvr_conf_dir();
|
||||
if !wayvr_conf_dir.exists() {
|
||||
create_dir_all(&wayvr_conf_dir)?;
|
||||
} else if wayvr_conf_dir.is_file() {
|
||||
bail!("wayvr.conf.d is a file and not a directory")
|
||||
}
|
||||
let config_fragment = WayVrDashboardConfigFragment {
|
||||
dashboard: WayVrDashboardConfigFragmentInner {
|
||||
exec: self
|
||||
.executable()
|
||||
.ok_or(anyhow::Error::msg("executable missing"))?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
args: self.args.as_ref().map(|args| args.join(" ")),
|
||||
env: self
|
||||
.env_vars
|
||||
.as_ref()
|
||||
.map(|vars| vars.iter().map(|(k, v)| format!("{k}={v}")).collect()),
|
||||
},
|
||||
};
|
||||
let writer =
|
||||
get_writer(&wayvr_conf_dir.join(self.wayvr_config_fragment_filename()))?;
|
||||
serde_yaml::to_writer(writer, &config_fragment)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable(&self) -> anyhow::Result<()> {
|
||||
match self.plugin_type {
|
||||
PluginType::Executable => {}
|
||||
PluginType::WayVrApp => todo!(),
|
||||
PluginType::WayVrDashboard => {
|
||||
let wayvr_conf_dir = Self::wayvr_conf_dir();
|
||||
remove_file(wayvr_conf_dir.join(self.wayvr_config_fragment_filename()))?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_enabled(&self, enabled: bool) -> anyhow::Result<()> {
|
||||
if enabled {
|
||||
self.enable()
|
||||
} else {
|
||||
self.disable()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn executable(&self) -> Option<PathBuf> {
|
||||
if self.exec_path.is_some() {
|
||||
self.exec_path.clone()
|
||||
} else {
|
||||
let canonical = self.canonical_exec_path();
|
||||
if canonical.is_file() {
|
||||
Some(canonical)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn canonical_exec_path(&self) -> PathBuf {
|
||||
get_plugins_dir().join(&self.appid)
|
||||
}
|
||||
|
||||
pub fn is_installed(&self) -> bool {
|
||||
self.executable().as_ref().is_some_and(|p| p.is_file())
|
||||
}
|
||||
|
||||
pub fn mark_as_executable(&self) -> anyhow::Result<()> {
|
||||
if let Some(p) = self.executable().as_ref() {
|
||||
mark_as_executable(p)
|
||||
} else {
|
||||
bail!("no executable found for plugin")
|
||||
}
|
||||
}
|
||||
|
||||
/// validate if the plugin can be displayed correctly and run
|
||||
pub fn validate(&self) -> bool {
|
||||
!self.appid.is_empty()
|
||||
&& !self.name.is_empty()
|
||||
&& self.executable().as_ref().is_some_and(|p| p.is_file())
|
||||
}
|
||||
}
|
||||
|
||||
/// urls to manifest json files representing plugins.
|
||||
/// each manifest should be json and the link should always point to the latest version
|
||||
const MANIFESTS: [&str;2] = [
|
||||
"https://github.com/galister/wlx-overlay-s/raw/refs/heads/meta/com.github.galister.wlx-overlay-s.json",
|
||||
"https://github.com/StardustXR/telescope/raw/refs/heads/main/envision/org.stardustxr.telescope.json",
|
||||
// wayvr dashboard potentially unsafe
|
||||
];
|
||||
|
||||
pub async fn refresh_plugins() -> anyhow::Result<Vec<anyhow::Result<Plugin>>> {
|
||||
let mut results = Vec::new();
|
||||
for jh in MANIFESTS
|
||||
.iter()
|
||||
.map(|url| -> tokio::task::JoinHandle<anyhow::Result<Plugin>> {
|
||||
tokio::spawn(async move {
|
||||
let path = cache_file_path(url, Some("json"));
|
||||
download_file_async(url, &path).await?;
|
||||
Ok(serde_json::from_str::<Plugin>(
|
||||
&tokio::fs::read_to_string(path).await?,
|
||||
)?)
|
||||
})
|
||||
})
|
||||
{
|
||||
results.push(jh.await?);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
552
src/ui/plugins/store.rs
Normal file
552
src/ui/plugins/store.rs
Normal file
|
@ -0,0 +1,552 @@
|
|||
use super::{
|
||||
add_custom_plugin_win::{
|
||||
AddCustomPluginWin, AddCustomPluginWinInit, AddCustomPluginWinMsg, AddCustomPluginWinOutMsg,
|
||||
},
|
||||
refresh_plugins,
|
||||
store_detail::{StoreDetail, StoreDetailMsg, StoreDetailOutMsg},
|
||||
store_row_factory::{StoreRowModel, StoreRowModelInit, StoreRowModelMsg, StoreRowModelOutMsg},
|
||||
Plugin,
|
||||
};
|
||||
use crate::{
|
||||
config::PluginConfig,
|
||||
downloader::download_file_async,
|
||||
ui::{alert::alert, SENDER_IO_ERR_MSG},
|
||||
};
|
||||
use adw::prelude::*;
|
||||
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
||||
use std::{collections::HashMap, fs::remove_file};
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[tracker::track]
|
||||
pub struct PluginStore {
|
||||
#[tracker::do_not_track]
|
||||
win: Option<adw::Window>,
|
||||
#[tracker::do_not_track]
|
||||
plugin_rows: Option<AsyncFactoryVecDeque<StoreRowModel>>,
|
||||
#[tracker::do_not_track]
|
||||
details: AsyncController<StoreDetail>,
|
||||
#[tracker::do_not_track]
|
||||
main_stack: Option<gtk::Stack>,
|
||||
#[tracker::do_not_track]
|
||||
config_plugins: HashMap<String, PluginConfig>,
|
||||
refreshing: bool,
|
||||
locked: bool,
|
||||
plugins: Vec<Plugin>,
|
||||
#[tracker::do_not_track]
|
||||
add_custom_plugin_win: Option<Controller<AddCustomPluginWin>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum PluginStoreSignalSource {
|
||||
Row,
|
||||
Detail,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PluginStoreMsg {
|
||||
Present,
|
||||
/// sets state and calls DoRefresh
|
||||
Refresh,
|
||||
/// called by Refresh
|
||||
DoRefresh,
|
||||
Install(Plugin),
|
||||
InstallDownload(Vec<Plugin>),
|
||||
Remove(Plugin),
|
||||
SetEnabled(PluginStoreSignalSource, Plugin, bool),
|
||||
ShowDetails(usize),
|
||||
ShowPluginList,
|
||||
PresentAddCustomPluginWin,
|
||||
AddCustomPlugin(Plugin),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PluginStoreInit {
|
||||
pub config_plugins: HashMap<String, PluginConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PluginStoreOutMsg {
|
||||
UpdateConfigPlugins(HashMap<String, PluginConfig>),
|
||||
}
|
||||
|
||||
impl PluginStore {
|
||||
fn refresh_plugin_rows(&mut self) {
|
||||
let mut guard = self.plugin_rows.as_mut().unwrap().guard();
|
||||
guard.clear();
|
||||
self.plugins.iter().for_each(|plugin| {
|
||||
guard.push_back(StoreRowModelInit {
|
||||
plugin: plugin.clone(),
|
||||
enabled: self
|
||||
.config_plugins
|
||||
.get(&plugin.appid)
|
||||
.is_some_and(|cp| cp.enabled),
|
||||
needs_update: self
|
||||
.config_plugins
|
||||
.get(&plugin.appid)
|
||||
.is_some_and(|cp| cp.plugin.version != plugin.version),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn add_plugin_to_config(&mut self, sender: &relm4::AsyncComponentSender<Self>, plugin: Plugin) {
|
||||
self.config_plugins
|
||||
.insert(plugin.appid.clone(), PluginConfig::from(&plugin));
|
||||
sender
|
||||
.output(PluginStoreOutMsg::UpdateConfigPlugins(
|
||||
self.config_plugins.clone(),
|
||||
))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
}
|
||||
|
||||
#[relm4::component(pub async)]
|
||||
impl AsyncComponent for PluginStore {
|
||||
type Init = PluginStoreInit;
|
||||
type Input = PluginStoreMsg;
|
||||
type Output = PluginStoreOutMsg;
|
||||
type CommandOutput = ();
|
||||
|
||||
view! {
|
||||
#[name(win)]
|
||||
adw::Window {
|
||||
set_title: Some("Plugins"),
|
||||
#[name(main_stack)]
|
||||
gtk::Stack {
|
||||
add_child = &adw::ToolbarView {
|
||||
set_top_bar_style: adw::ToolbarStyle::Flat,
|
||||
add_top_bar: headerbar = &adw::HeaderBar {
|
||||
pack_start: add_custom_plugin_btn = >k::Button {
|
||||
set_icon_name: "list-add-symbolic",
|
||||
set_tooltip_text: Some("Add custom plugin"),
|
||||
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
|
||||
set_sensitive: !(model.refreshing || model.locked),
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::PresentAddCustomPluginWin)
|
||||
},
|
||||
},
|
||||
pack_end: refreshbtn = >k::Button {
|
||||
set_icon_name: "view-refresh-symbolic",
|
||||
set_tooltip_text: Some("Refresh"),
|
||||
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
|
||||
set_sensitive: !(model.refreshing || model.locked),
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::Refresh);
|
||||
},
|
||||
},
|
||||
},
|
||||
#[wrap(Some)]
|
||||
set_content: inner = >k::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
gtk::Stack {
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
add_child = >k::ScrolledWindow {
|
||||
set_hscrollbar_policy: gtk::PolicyType::Never,
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
adw::Clamp {
|
||||
#[name(listbox)]
|
||||
gtk::ListBox {
|
||||
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
|
||||
set_sensitive: !(model.refreshing || model.locked),
|
||||
add_css_class: "boxed-list",
|
||||
set_valign: gtk::Align::Start,
|
||||
set_margin_all: 12,
|
||||
set_selection_mode: gtk::SelectionMode::None,
|
||||
connect_row_activated[sender] => move |_, row| {
|
||||
sender.input(
|
||||
Self::Input::ShowDetails(
|
||||
row.index() as usize
|
||||
)
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
} -> {
|
||||
set_name: "pluginlist"
|
||||
},
|
||||
add_child = >k::Spinner {
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
set_width_request: 32,
|
||||
set_height_request: 32,
|
||||
set_valign: gtk::Align::Center,
|
||||
set_halign: gtk::Align::Center,
|
||||
#[track = "model.changed(PluginStore::refreshing())"]
|
||||
set_spinning: model.refreshing,
|
||||
} -> {
|
||||
set_name: "spinner"
|
||||
},
|
||||
add_child = &adw::StatusPage {
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
set_title: "No Plugins Found",
|
||||
set_description: Some("Make sure you're connected to the internet and refresh"),
|
||||
set_icon_name: Some("application-x-addon-symbolic"),
|
||||
} -> {
|
||||
set_name: "emptystate"
|
||||
},
|
||||
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::plugins())"]
|
||||
set_visible_child_name: if model.refreshing {
|
||||
"spinner"
|
||||
} else if model.plugins.is_empty() {
|
||||
"emptystate"
|
||||
} else {
|
||||
"pluginlist"
|
||||
},
|
||||
},
|
||||
}
|
||||
} -> {
|
||||
set_name: "listview"
|
||||
},
|
||||
add_child = &adw::Bin {
|
||||
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
|
||||
set_sensitive: !(model.refreshing || model.locked),
|
||||
set_child: Some(details_view),
|
||||
} -> {
|
||||
set_name: "detailsview",
|
||||
},
|
||||
set_visible_child_name: "listview",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&mut self,
|
||||
message: Self::Input,
|
||||
sender: AsyncComponentSender<Self>,
|
||||
_root: &Self::Root,
|
||||
) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
Self::Input::Present => {
|
||||
self.win.as_ref().unwrap().present();
|
||||
sender.input(Self::Input::Refresh);
|
||||
}
|
||||
Self::Input::AddCustomPlugin(plugin) => {
|
||||
if self.config_plugins.contains_key(&plugin.appid) {
|
||||
alert(
|
||||
"Failed to add custom plugin",
|
||||
Some("A plugin with the same name already exists"),
|
||||
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.add_plugin_to_config(&sender, plugin);
|
||||
sender.input(Self::Input::Refresh);
|
||||
}
|
||||
Self::Input::PresentAddCustomPluginWin => {
|
||||
let add_win = AddCustomPluginWin::builder()
|
||||
.launch(AddCustomPluginWinInit {
|
||||
parent: self.win.as_ref().unwrap().clone().upcast(),
|
||||
})
|
||||
.forward(sender.input_sender(), |msg| match msg {
|
||||
AddCustomPluginWinOutMsg::Add(plugin) => {
|
||||
Self::Input::AddCustomPlugin(plugin)
|
||||
}
|
||||
});
|
||||
add_win.sender().emit(AddCustomPluginWinMsg::Present);
|
||||
self.add_custom_plugin_win = Some(add_win);
|
||||
}
|
||||
Self::Input::Refresh => {
|
||||
self.set_refreshing(true);
|
||||
sender.input(Self::Input::DoRefresh);
|
||||
}
|
||||
Self::Input::DoRefresh => {
|
||||
let mut plugins = match refresh_plugins().await {
|
||||
Err(e) => {
|
||||
error!("failed to refresh plugins: {e}");
|
||||
Vec::new()
|
||||
}
|
||||
Ok(results) => results
|
||||
.into_iter()
|
||||
.filter_map(|res| match res {
|
||||
Ok(plugin) => Some(plugin),
|
||||
Err(e) => {
|
||||
error!("failed to refresh single plugin manifest: {e}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
{
|
||||
let appids_from_web = plugins
|
||||
.iter()
|
||||
.map(|p| p.appid.clone())
|
||||
.collect::<Vec<String>>();
|
||||
// add all plugins that are in config but not retrieved
|
||||
plugins.extend(self.config_plugins.values().filter_map(|cp| {
|
||||
if appids_from_web.contains(&cp.plugin.appid) {
|
||||
None
|
||||
} else {
|
||||
Some(Plugin::from(cp))
|
||||
}
|
||||
}));
|
||||
}
|
||||
self.set_plugins(plugins);
|
||||
self.refresh_plugin_rows();
|
||||
self.set_refreshing(false);
|
||||
}
|
||||
Self::Input::Install(plugin) => {
|
||||
self.set_locked(true);
|
||||
let mut plugins = vec![plugin];
|
||||
for dep in plugins[0].dependencies.clone().unwrap_or_default() {
|
||||
if let Some(dep_plugin) = self
|
||||
.plugins
|
||||
.iter()
|
||||
.find(|plugin| plugin.appid == dep)
|
||||
.cloned()
|
||||
{
|
||||
plugins.push(dep_plugin);
|
||||
} else {
|
||||
error!(
|
||||
"unable to find dependency for {}: {}",
|
||||
plugins[0].appid, dep
|
||||
);
|
||||
alert(
|
||||
"Missing dependencies",
|
||||
Some(&format!(
|
||||
"{} depends on unknown plugin {}",
|
||||
plugins[0].name, dep
|
||||
)),
|
||||
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
sender.input(Self::Input::InstallDownload(plugins))
|
||||
}
|
||||
Self::Input::InstallDownload(plugins) => {
|
||||
for plugin in plugins {
|
||||
let mut plugin = plugin.clone();
|
||||
match plugin.exec_url.as_ref() {
|
||||
Some(url) => {
|
||||
let exec_path = plugin.canonical_exec_path();
|
||||
if let Err(e) = download_file_async(url, &exec_path).await {
|
||||
alert(
|
||||
"Download failed",
|
||||
Some(&format!(
|
||||
"Downloading {} {} failed:\n\n{e}",
|
||||
plugin.name,
|
||||
plugin
|
||||
.version
|
||||
.as_ref()
|
||||
.unwrap_or(&"(no version)".to_string())
|
||||
)),
|
||||
Some(
|
||||
&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
plugin.exec_path = Some(exec_path);
|
||||
if let Err(e) = plugin.enable() {
|
||||
error!("failed to enable plugin {}: {e}", plugin.appid);
|
||||
alert(
|
||||
"Failed to enable plugin",
|
||||
Some(&e.to_string()),
|
||||
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.add_plugin_to_config(&sender, plugin.clone());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
alert(
|
||||
"Download failed",
|
||||
Some(&format!(
|
||||
"Downloading {} {} failed:\n\nNo executable url provided for this plugin, this is likely a bug!",
|
||||
plugin.name,
|
||||
plugin.version.as_ref().unwrap_or(&"(no version)".to_string()))
|
||||
),
|
||||
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>())
|
||||
);
|
||||
}
|
||||
};
|
||||
self.refresh_plugin_rows();
|
||||
self.details
|
||||
.emit(StoreDetailMsg::Refresh(plugin.appid, true, false));
|
||||
}
|
||||
self.set_locked(false);
|
||||
}
|
||||
Self::Input::Remove(plugin) => {
|
||||
self.set_locked(true);
|
||||
if let Err(e) = plugin.disable() {
|
||||
error!("failed to disable plugin {}: {e}", plugin.appid);
|
||||
alert(
|
||||
"Failed to disable plugin",
|
||||
Some(&e.to_string()),
|
||||
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if let Some(exec) = plugin.executable() {
|
||||
// delete executable only if it's not a custom plugin
|
||||
if exec.is_file() && plugin.exec_url.is_some() {
|
||||
if let Err(e) = remove_file(&exec) {
|
||||
alert(
|
||||
"Failed removing plugin",
|
||||
Some(&format!(
|
||||
"Could not remove plugin executable {}:\n\n{e}",
|
||||
exec.to_string_lossy()
|
||||
)),
|
||||
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.config_plugins.remove(&plugin.appid);
|
||||
self.set_plugins(
|
||||
self.plugins
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|p| !(p.appid == plugin.appid && p.exec_url.is_none()))
|
||||
.collect(),
|
||||
);
|
||||
sender
|
||||
.output(Self::Output::UpdateConfigPlugins(
|
||||
self.config_plugins.clone(),
|
||||
))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
self.refresh_plugin_rows();
|
||||
self.details
|
||||
.emit(StoreDetailMsg::Refresh(plugin.appid, false, false));
|
||||
self.set_locked(false);
|
||||
}
|
||||
Self::Input::SetEnabled(signal_sender, plugin, enabled) => {
|
||||
if let Some(cp) = self.config_plugins.get_mut(&plugin.appid) {
|
||||
if let Err(e) = plugin.set_enabled(enabled) {
|
||||
error!(
|
||||
"failed to {} plugin {}: {e}",
|
||||
if enabled { "enable" } else { "disable" },
|
||||
plugin.appid
|
||||
);
|
||||
alert(
|
||||
&format!(
|
||||
"Failed to {} plugin",
|
||||
if enabled { "enable" } else { "disable" }
|
||||
),
|
||||
Some(&e.to_string()),
|
||||
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
cp.enabled = enabled;
|
||||
if signal_sender == PluginStoreSignalSource::Detail {
|
||||
if let Some(row) = self
|
||||
.plugin_rows
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.guard()
|
||||
.iter()
|
||||
.find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid))
|
||||
.flatten()
|
||||
{
|
||||
row.input_sender.emit(StoreRowModelMsg::Refresh(
|
||||
enabled,
|
||||
cp.plugin.version != plugin.version,
|
||||
));
|
||||
} else {
|
||||
error!("could not find corresponding listbox row")
|
||||
}
|
||||
}
|
||||
if signal_sender == PluginStoreSignalSource::Row {
|
||||
self.details.emit(StoreDetailMsg::Refresh(
|
||||
plugin.appid,
|
||||
enabled,
|
||||
cp.plugin.version != plugin.version,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
debug!(
|
||||
"failed to set plugin {} enabled: could not find in hashmap",
|
||||
plugin.appid
|
||||
)
|
||||
}
|
||||
sender
|
||||
.output(Self::Output::UpdateConfigPlugins(
|
||||
self.config_plugins.clone(),
|
||||
))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
// we use index here because it's the listbox not the row that can
|
||||
// send this signal, so I don't directly have the plugin object
|
||||
Self::Input::ShowDetails(index) => {
|
||||
if let Some(plugin) = self.plugins.get(index) {
|
||||
self.details.sender().emit(StoreDetailMsg::SetPlugin(
|
||||
plugin.clone(),
|
||||
self.config_plugins
|
||||
.get(&plugin.appid)
|
||||
.is_some_and(|cp| cp.enabled),
|
||||
self.config_plugins
|
||||
.get(&plugin.appid)
|
||||
.is_some_and(|cp| cp.plugin.version != plugin.version),
|
||||
));
|
||||
self.main_stack
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.set_visible_child_name("detailsview");
|
||||
} else {
|
||||
error!("plugins list index out of range!")
|
||||
}
|
||||
}
|
||||
Self::Input::ShowPluginList => {
|
||||
self.main_stack
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.set_visible_child_name("listview");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn init(
|
||||
init: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: AsyncComponentSender<Self>,
|
||||
) -> AsyncComponentParts<Self> {
|
||||
let mut model = Self {
|
||||
tracker: 0,
|
||||
refreshing: false,
|
||||
locked: false,
|
||||
win: None,
|
||||
plugins: Vec::default(),
|
||||
plugin_rows: None,
|
||||
details: StoreDetail::builder()
|
||||
.launch(())
|
||||
.forward(sender.input_sender(), move |msg| match msg {
|
||||
StoreDetailOutMsg::GoBack => Self::Input::ShowPluginList,
|
||||
StoreDetailOutMsg::Install(plugin) => Self::Input::Install(plugin),
|
||||
StoreDetailOutMsg::Remove(plugin) => Self::Input::Remove(plugin),
|
||||
StoreDetailOutMsg::SetEnabled(plugin, enabled) => {
|
||||
Self::Input::SetEnabled(PluginStoreSignalSource::Detail, plugin, enabled)
|
||||
}
|
||||
}),
|
||||
config_plugins: init.config_plugins,
|
||||
main_stack: None,
|
||||
add_custom_plugin_win: None,
|
||||
};
|
||||
|
||||
let details_view = model.details.widget();
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
model.win = Some(widgets.win.clone());
|
||||
model.plugin_rows = Some(
|
||||
AsyncFactoryVecDeque::builder()
|
||||
.launch(widgets.listbox.clone())
|
||||
.forward(sender.input_sender(), move |msg| match msg {
|
||||
StoreRowModelOutMsg::Install(appid) => Self::Input::Install(appid),
|
||||
StoreRowModelOutMsg::Remove(appid) => Self::Input::Remove(appid),
|
||||
StoreRowModelOutMsg::SetEnabled(plugin, enabled) => {
|
||||
Self::Input::SetEnabled(PluginStoreSignalSource::Row, plugin, enabled)
|
||||
}
|
||||
}),
|
||||
);
|
||||
model.main_stack = Some(widgets.main_stack.clone());
|
||||
|
||||
AsyncComponentParts { model, widgets }
|
||||
}
|
||||
}
|
373
src/ui/plugins/store_detail.rs
Normal file
373
src/ui/plugins/store_detail.rs
Normal file
|
@ -0,0 +1,373 @@
|
|||
use super::Plugin;
|
||||
use crate::{downloader::cache_file, ui::SENDER_IO_ERR_MSG};
|
||||
use adw::prelude::*;
|
||||
use relm4::prelude::*;
|
||||
use tracing::{error, warn};
|
||||
|
||||
#[tracker::track]
|
||||
pub struct StoreDetail {
|
||||
plugin: Option<Plugin>,
|
||||
enabled: bool,
|
||||
#[tracker::do_not_track]
|
||||
carousel: Option<adw::Carousel>,
|
||||
#[tracker::do_not_track]
|
||||
icon: Option<gtk::Image>,
|
||||
needs_update: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum StoreDetailMsg {
|
||||
SetPlugin(Plugin, bool, bool),
|
||||
SetIcon,
|
||||
SetScreenshots,
|
||||
Refresh(String, bool, bool),
|
||||
Install,
|
||||
Remove,
|
||||
SetEnabled(bool),
|
||||
OpenHomepage,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StoreDetailOutMsg {
|
||||
Install(Plugin),
|
||||
Remove(Plugin),
|
||||
GoBack,
|
||||
SetEnabled(Plugin, bool),
|
||||
}
|
||||
|
||||
#[relm4::component(pub async)]
|
||||
impl AsyncComponent for StoreDetail {
|
||||
type Init = ();
|
||||
type Input = StoreDetailMsg;
|
||||
type Output = StoreDetailOutMsg;
|
||||
type CommandOutput = ();
|
||||
|
||||
view! {
|
||||
adw::ToolbarView {
|
||||
set_top_bar_style: adw::ToolbarStyle::Flat,
|
||||
add_top_bar: headerbar = &adw::HeaderBar {
|
||||
#[wrap(Some)]
|
||||
set_title_widget: title = &adw::WindowTitle {
|
||||
#[track = "model.changed(Self::plugin())"]
|
||||
set_title: model
|
||||
.plugin
|
||||
.as_ref()
|
||||
.map(|p| p.name.as_str())
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
pack_start: backbtn = >k::Button {
|
||||
set_icon_name: "go-previous-symbolic",
|
||||
set_tooltip_text: Some("Back"),
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.output(Self::Output::GoBack).expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
},
|
||||
},
|
||||
#[wrap(Some)]
|
||||
set_content: inner = >k::ScrolledWindow {
|
||||
set_hscrollbar_policy: gtk::PolicyType::Never,
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
set_margin_top: 12,
|
||||
set_margin_bottom: 48,
|
||||
set_margin_start: 12,
|
||||
set_margin_end: 12,
|
||||
set_spacing: 24,
|
||||
adw::Clamp { // icon, name, buttons
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_hexpand: true,
|
||||
set_vexpand: false,
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_hexpand: true,
|
||||
#[name(icon)]
|
||||
gtk::Image {
|
||||
set_icon_name: Some("application-x-addon-symbolic"),
|
||||
set_margin_end: 12,
|
||||
set_pixel_size: 96,
|
||||
},
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_hexpand: true,
|
||||
set_valign: gtk::Align::Center,
|
||||
set_spacing: 6,
|
||||
gtk::Label {
|
||||
add_css_class: "title-2",
|
||||
set_xalign: 0.0,
|
||||
#[track = "model.changed(Self::plugin())"]
|
||||
set_text: model
|
||||
.plugin
|
||||
.as_ref()
|
||||
.map(|p| p.name.as_str())
|
||||
.unwrap_or_default(),
|
||||
set_ellipsize: gtk::pango::EllipsizeMode::None,
|
||||
set_wrap: true,
|
||||
},
|
||||
gtk::Label {
|
||||
add_css_class: "dim-label",
|
||||
set_xalign: 0.0,
|
||||
#[track = "model.changed(Self::plugin())"]
|
||||
set_visible: model
|
||||
.plugin
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.author.is_some()),
|
||||
#[track = "model.changed(Self::plugin())"]
|
||||
set_text: model
|
||||
.plugin
|
||||
.as_ref()
|
||||
.and_then(
|
||||
|p| p.author.as_deref()
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
set_ellipsize: gtk::pango::EllipsizeMode::None,
|
||||
set_wrap: true,
|
||||
},
|
||||
},
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_halign: gtk::Align::Center,
|
||||
set_valign: gtk::Align::Center,
|
||||
set_spacing: 6,
|
||||
gtk::Button {
|
||||
#[track = "model.changed(Self::plugin())"]
|
||||
set_visible: !model
|
||||
.plugin
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.is_installed()),
|
||||
set_label: "Install",
|
||||
add_css_class: "suggested-action",
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::Install);
|
||||
}
|
||||
},
|
||||
gtk::Button {
|
||||
#[track = "model.changed(Self::plugin())"]
|
||||
set_visible: model
|
||||
.plugin
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.is_installed()),
|
||||
set_label: "Remove",
|
||||
add_css_class: "destructive-action",
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::Remove);
|
||||
}
|
||||
},
|
||||
gtk::Button {
|
||||
#[track = "model.changed(Self::plugin()) || model.changed(Self::needs_update())"]
|
||||
set_visible: model
|
||||
.plugin
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.is_installed()) && model.needs_update,
|
||||
add_css_class: "suggested-action",
|
||||
set_label: "Update",
|
||||
set_valign: gtk::Align::Center,
|
||||
set_halign: gtk::Align::Center,
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::Install);
|
||||
}
|
||||
},
|
||||
gtk::Switch {
|
||||
#[track = "model.changed(Self::plugin())"]
|
||||
set_visible: model.plugin.as_ref()
|
||||
.is_some_and(|p| p.is_installed()),
|
||||
#[track = "model.changed(Self::enabled())"]
|
||||
set_active: model.enabled,
|
||||
set_tooltip_text: Some("Plugin enabled"),
|
||||
set_valign: gtk::Align::Center,
|
||||
set_halign: gtk::Align::Center,
|
||||
connect_state_set[sender] => move |_, state| {
|
||||
sender.input(Self::Input::SetEnabled(state));
|
||||
gtk::glib::Propagation::Proceed
|
||||
}
|
||||
},
|
||||
gtk::Button {
|
||||
#[track = "model.changed(Self::plugin())"]
|
||||
set_visible: model.plugin.as_ref()
|
||||
.is_some_and(|p| p.homepage_url.is_some()),
|
||||
set_label: "Homepage",
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::OpenHomepage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
gtk::Box { // screenshots
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 12,
|
||||
#[name(carousel)]
|
||||
adw::Carousel {
|
||||
set_allow_mouse_drag: true,
|
||||
set_allow_scroll_wheel: false,
|
||||
set_spacing: 24,
|
||||
},
|
||||
adw::CarouselIndicatorDots {
|
||||
set_carousel: Some(&carousel),
|
||||
},
|
||||
},
|
||||
adw::Clamp { // description
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
gtk::Label {
|
||||
set_xalign: 0.0,
|
||||
#[track = "model.changed(Self::plugin())"]
|
||||
set_text: model
|
||||
.plugin
|
||||
.as_ref()
|
||||
.and_then(|p| p
|
||||
.description
|
||||
.as_deref()
|
||||
).unwrap_or(""),
|
||||
set_ellipsize: gtk::pango::EllipsizeMode::None,
|
||||
set_wrap: true,
|
||||
set_justify: gtk::Justification::Fill,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&mut self,
|
||||
message: Self::Input,
|
||||
sender: AsyncComponentSender<Self>,
|
||||
_root: &Self::Root,
|
||||
) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
Self::Input::OpenHomepage => {
|
||||
if let Some(plugin) = self.plugin.as_ref() {
|
||||
if let Some(homepage) = plugin.homepage_url.as_ref() {
|
||||
if let Err(e) = gtk::gio::AppInfo::launch_default_for_uri(
|
||||
homepage,
|
||||
gtk::gio::AppLaunchContext::NONE,
|
||||
) {
|
||||
error!("opening uri {homepage}: {e}");
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Input::SetPlugin(p, enabled, needs_update) => {
|
||||
self.set_plugin(Some(p));
|
||||
self.set_enabled(enabled);
|
||||
self.set_needs_update(needs_update);
|
||||
sender.input(Self::Input::SetIcon);
|
||||
sender.input(Self::Input::SetScreenshots);
|
||||
}
|
||||
Self::Input::SetIcon => {
|
||||
if let Some(plugin) = self.plugin.as_ref() {
|
||||
if let Some(url) = plugin.icon_url.as_ref() {
|
||||
match cache_file(url, None).await {
|
||||
Ok(dest) => {
|
||||
self.icon.as_ref().unwrap().set_from_file(Some(dest));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed downloading icon '{url}': {e}");
|
||||
}
|
||||
};
|
||||
} else {
|
||||
self.icon
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.set_icon_name(Some("application-x-addon-symbolic"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Input::SetScreenshots => {
|
||||
let carousel = self.carousel.as_ref().unwrap().clone();
|
||||
while let Some(child) = carousel.first_child() {
|
||||
carousel.remove(&child);
|
||||
}
|
||||
if let Some(plugin) = self.plugin.as_ref() {
|
||||
carousel
|
||||
.parent()
|
||||
.unwrap()
|
||||
.set_visible(!plugin.screenshots.is_empty());
|
||||
for url in plugin.screenshots.iter() {
|
||||
match cache_file(url, None).await {
|
||||
Ok(dest) => {
|
||||
let pic = gtk::Picture::builder()
|
||||
.height_request(300)
|
||||
.css_classes(["card"])
|
||||
.overflow(gtk::Overflow::Hidden)
|
||||
.valign(gtk::Align::Center)
|
||||
.build();
|
||||
pic.set_filename(Some(dest));
|
||||
let clamp = adw::Clamp::builder().child(&pic).build();
|
||||
carousel.append(&clamp);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("failed downloading screenshot '{url}': {e}");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Input::Refresh(appid, enabled, needs_update) => {
|
||||
if self.plugin.as_ref().is_some_and(|p| p.appid == appid) {
|
||||
self.mark_all_changed();
|
||||
self.set_enabled(enabled);
|
||||
self.set_needs_update(needs_update);
|
||||
}
|
||||
}
|
||||
Self::Input::Install => {
|
||||
if let Some(plugin) = self.plugin.as_ref() {
|
||||
sender
|
||||
.output(Self::Output::Install(plugin.clone()))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
}
|
||||
Self::Input::Remove => {
|
||||
if let Some(plugin) = self.plugin.as_ref() {
|
||||
sender
|
||||
.output(Self::Output::Remove(plugin.clone()))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
if plugin.exec_url.is_none() {
|
||||
sender
|
||||
.output(Self::Output::GoBack)
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Input::SetEnabled(enabled) => {
|
||||
self.set_enabled(enabled);
|
||||
if let Some(plugin) = self.plugin.as_ref() {
|
||||
sender
|
||||
.output(Self::Output::SetEnabled(plugin.clone(), enabled))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn init(
|
||||
_init: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: AsyncComponentSender<Self>,
|
||||
) -> AsyncComponentParts<Self> {
|
||||
let mut model = Self {
|
||||
tracker: 0,
|
||||
plugin: None,
|
||||
enabled: false,
|
||||
carousel: None,
|
||||
icon: None,
|
||||
needs_update: false,
|
||||
};
|
||||
let widgets = view_output!();
|
||||
|
||||
model.carousel = Some(widgets.carousel.clone());
|
||||
model.icon = Some(widgets.icon.clone());
|
||||
|
||||
AsyncComponentParts { model, widgets }
|
||||
}
|
||||
}
|
225
src/ui/plugins/store_row_factory.rs
Normal file
225
src/ui/plugins/store_row_factory.rs
Normal file
|
@ -0,0 +1,225 @@
|
|||
use super::Plugin;
|
||||
use crate::{downloader::cache_file, ui::SENDER_IO_ERR_MSG};
|
||||
use gtk::prelude::*;
|
||||
use relm4::{
|
||||
factory::AsyncFactoryComponent, prelude::DynamicIndex, AsyncFactorySender, RelmWidgetExt,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[tracker::track]
|
||||
pub struct StoreRowModel {
|
||||
#[no_eq]
|
||||
pub plugin: Plugin,
|
||||
#[tracker::do_not_track]
|
||||
icon: Option<gtk::Image>,
|
||||
#[tracker::do_not_track]
|
||||
pub input_sender: relm4::Sender<StoreRowModelMsg>,
|
||||
pub enabled: bool,
|
||||
pub needs_update: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StoreRowModelInit {
|
||||
pub plugin: Plugin,
|
||||
pub enabled: bool,
|
||||
pub needs_update: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StoreRowModelMsg {
|
||||
LoadIcon,
|
||||
/// params: enabled, needs_update
|
||||
Refresh(bool, bool),
|
||||
SetEnabled(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StoreRowModelOutMsg {
|
||||
Install(Plugin),
|
||||
Remove(Plugin),
|
||||
SetEnabled(Plugin, bool),
|
||||
}
|
||||
|
||||
#[relm4::factory(async pub)]
|
||||
impl AsyncFactoryComponent for StoreRowModel {
|
||||
type Init = StoreRowModelInit;
|
||||
type Input = StoreRowModelMsg;
|
||||
type Output = StoreRowModelOutMsg;
|
||||
type CommandOutput = ();
|
||||
type ParentWidget = gtk::ListBox;
|
||||
|
||||
view! {
|
||||
root = gtk::ListBoxRow {
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_hexpand: true,
|
||||
set_vexpand: false,
|
||||
set_spacing: 12,
|
||||
set_margin_all: 12,
|
||||
#[name(icon)]
|
||||
gtk::Image {
|
||||
set_icon_name: Some("application-x-addon-symbolic"),
|
||||
set_icon_size: gtk::IconSize::Large,
|
||||
},
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 6,
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
gtk::Label {
|
||||
add_css_class: "title-3",
|
||||
set_hexpand: true,
|
||||
set_xalign: 0.0,
|
||||
set_text: &self.plugin.name,
|
||||
set_ellipsize: gtk::pango::EllipsizeMode::None,
|
||||
set_wrap: true,
|
||||
},
|
||||
gtk::Label {
|
||||
add_css_class: "dim-label",
|
||||
set_hexpand: true,
|
||||
set_xalign: 0.0,
|
||||
set_text: self.plugin.short_description
|
||||
.as_deref()
|
||||
.unwrap_or(""),
|
||||
set_ellipsize: gtk::pango::EllipsizeMode::None,
|
||||
set_wrap: true,
|
||||
},
|
||||
},
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 6,
|
||||
set_vexpand: true,
|
||||
set_valign: gtk::Align::Center,
|
||||
set_halign: gtk::Align::Center,
|
||||
gtk::Button {
|
||||
#[track = "self.changed(StoreRowModel::plugin())"]
|
||||
set_visible: !self.plugin.is_installed(),
|
||||
set_icon_name: "folder-download-symbolic",
|
||||
add_css_class: "suggested-action",
|
||||
set_tooltip_text: Some("Install"),
|
||||
set_valign: gtk::Align::Center,
|
||||
set_halign: gtk::Align::Center,
|
||||
connect_clicked[sender, plugin] => move |_| {
|
||||
sender
|
||||
.output(Self::Output::Install(
|
||||
plugin.clone(),
|
||||
))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
},
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_spacing: 6,
|
||||
set_valign: gtk::Align::Center,
|
||||
set_halign: gtk::Align::Center,
|
||||
gtk::Button {
|
||||
#[track = "self.changed(StoreRowModel::plugin())"]
|
||||
set_visible: self.plugin.is_installed(),
|
||||
set_icon_name: "user-trash-symbolic",
|
||||
add_css_class: "destructive-action",
|
||||
set_tooltip_text: Some("Remove"),
|
||||
set_valign: gtk::Align::Center,
|
||||
set_halign: gtk::Align::Center,
|
||||
connect_clicked[sender, plugin] => move |_| {
|
||||
sender
|
||||
.output(Self::Output::Remove(
|
||||
plugin.clone(),
|
||||
))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
},
|
||||
gtk::Button {
|
||||
#[track = "self.changed(StoreRowModel::plugin()) || self.changed(StoreRowModel::needs_update())"]
|
||||
set_visible: self.plugin.is_installed() && self.needs_update,
|
||||
set_icon_name: "view-refresh-symbolic",
|
||||
add_css_class: "suggested-action",
|
||||
set_tooltip_text: Some("Update"),
|
||||
set_valign: gtk::Align::Center,
|
||||
set_halign: gtk::Align::Center,
|
||||
connect_clicked[sender, plugin] => move |_| {
|
||||
sender
|
||||
.output(Self::Output::Install(
|
||||
plugin.clone(),
|
||||
))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
},
|
||||
},
|
||||
gtk::Switch {
|
||||
#[track = "self.changed(StoreRowModel::plugin())"]
|
||||
set_visible: self.plugin.is_installed(),
|
||||
#[track = "self.changed(StoreRowModel::enabled())"]
|
||||
set_active: self.enabled,
|
||||
set_valign: gtk::Align::Center,
|
||||
set_halign: gtk::Align::Center,
|
||||
set_tooltip_text: Some("Plugin enabled"),
|
||||
connect_state_set[sender] => move |_, state| {
|
||||
sender.input(Self::Input::SetEnabled(state));
|
||||
gtk::glib::Propagation::Proceed
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn update(&mut self, message: Self::Input, sender: AsyncFactorySender<Self>) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
Self::Input::LoadIcon => {
|
||||
if let Some(url) = self.plugin.icon_url.as_ref() {
|
||||
match cache_file(url, None).await {
|
||||
Ok(dest) => {
|
||||
self.icon.as_ref().unwrap().set_from_file(Some(dest));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("failed downloading icon '{url}': {e}");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Self::Input::SetEnabled(state) => {
|
||||
self.set_enabled(state);
|
||||
sender
|
||||
.output(Self::Output::SetEnabled(self.plugin.clone(), state))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
Self::Input::Refresh(enabled, needs_update) => {
|
||||
self.mark_all_changed();
|
||||
self.set_enabled(enabled);
|
||||
self.set_needs_update(needs_update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn init_model(
|
||||
init: Self::Init,
|
||||
_index: &DynamicIndex,
|
||||
sender: AsyncFactorySender<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tracker: 0,
|
||||
plugin: init.plugin,
|
||||
enabled: init.enabled,
|
||||
icon: None,
|
||||
input_sender: sender.input_sender().clone(),
|
||||
needs_update: init.needs_update,
|
||||
}
|
||||
}
|
||||
|
||||
fn init_widgets(
|
||||
&mut self,
|
||||
_index: &DynamicIndex,
|
||||
root: Self::Root,
|
||||
_returned_widget: &<Self::ParentWidget as relm4::factory::FactoryView>::ReturnedWidget,
|
||||
sender: AsyncFactorySender<Self>,
|
||||
) -> Self::Widgets {
|
||||
let plugin = self.plugin.clone(); // for use in a signal handler
|
||||
let widgets = view_output!();
|
||||
self.icon = Some(widgets.icon.clone());
|
||||
sender.input(Self::Input::LoadIcon);
|
||||
widgets
|
||||
}
|
||||
}
|
|
@ -156,13 +156,12 @@ pub fn spin_row<F: Fn(>k::Adjustment) + 'static>(
|
|||
row
|
||||
}
|
||||
|
||||
pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||
fn filedialog_row_base<F: Fn(Option<String>) + 'static + Clone>(
|
||||
title: &str,
|
||||
description: Option<&str>,
|
||||
value: Option<String>,
|
||||
root_win: Option<gtk::Window>,
|
||||
cb: F,
|
||||
) -> adw::ActionRow {
|
||||
) -> (adw::ActionRow, gtk::Label) {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(title)
|
||||
.subtitle_lines(0)
|
||||
|
@ -174,14 +173,14 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
|||
row.set_subtitle(d);
|
||||
}
|
||||
|
||||
let path_label = >k::Label::builder()
|
||||
let path_label = gtk::Label::builder()
|
||||
.label(match value.as_ref() {
|
||||
None => "(None)",
|
||||
Some(p) => p.as_str(),
|
||||
})
|
||||
.wrap(true)
|
||||
.build();
|
||||
row.add_suffix(path_label);
|
||||
row.add_suffix(&path_label);
|
||||
|
||||
let clear_btn = gtk::Button::builder()
|
||||
.icon_name("edit-clear-symbolic")
|
||||
|
@ -200,6 +199,60 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
|||
cb(None)
|
||||
}
|
||||
));
|
||||
(row, path_label)
|
||||
}
|
||||
|
||||
pub fn file_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||
title: &str,
|
||||
description: Option<&str>,
|
||||
value: Option<String>,
|
||||
root_win: Option<gtk::Window>,
|
||||
cb: F,
|
||||
) -> adw::ActionRow {
|
||||
let (row, path_label) = filedialog_row_base(title, description, value, cb.clone());
|
||||
|
||||
let filedialog = gtk::FileDialog::builder()
|
||||
.modal(true)
|
||||
.title(format!("Select {}", title))
|
||||
.build();
|
||||
|
||||
row.connect_activated(clone!(
|
||||
#[weak]
|
||||
path_label,
|
||||
move |_| {
|
||||
filedialog.open(
|
||||
root_win.as_ref(),
|
||||
gio::Cancellable::NONE,
|
||||
clone!(
|
||||
#[weak]
|
||||
path_label,
|
||||
#[strong]
|
||||
cb,
|
||||
move |res| {
|
||||
if let Ok(file) = res {
|
||||
if let Some(path) = file.path() {
|
||||
let path_s = path.to_string_lossy().to_string();
|
||||
path_label.set_text(&path_s);
|
||||
cb(Some(path_s))
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
));
|
||||
|
||||
row
|
||||
}
|
||||
|
||||
pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||
title: &str,
|
||||
description: Option<&str>,
|
||||
value: Option<String>,
|
||||
root_win: Option<gtk::Window>,
|
||||
cb: F,
|
||||
) -> adw::ActionRow {
|
||||
let (row, path_label) = filedialog_row_base(title, description, value, cb.clone());
|
||||
let filedialog = gtk::FileDialog::builder()
|
||||
.modal(true)
|
||||
.title(format!("Select Path for {}", title))
|
||||
|
@ -220,8 +273,8 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
|||
move |res| {
|
||||
if let Ok(file) = res {
|
||||
if let Some(path) = file.path() {
|
||||
let path_s = path.to_str().unwrap().to_string();
|
||||
path_label.set_text(path_s.as_str());
|
||||
let path_s = path.to_string_lossy().to_string();
|
||||
path_label.set_text(&path_s);
|
||||
cb(Some(path_s))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,13 @@ use super::{
|
|||
};
|
||||
use crate::{
|
||||
env_var_descriptions::ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH,
|
||||
profile::{LighthouseDriver, Profile, XRServiceType},
|
||||
profile::{LighthouseDriver, OvrCompatibilityModuleType, Profile, XRServiceType},
|
||||
};
|
||||
use adw::prelude::*;
|
||||
use gtk::glib::{self, clone};
|
||||
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
||||
use std::{cell::RefCell, path::PathBuf, rc::Rc};
|
||||
use tracing::warn;
|
||||
|
||||
#[tracker::track]
|
||||
pub struct ProfileEditor {
|
||||
|
@ -48,6 +49,21 @@ pub struct ProfileEditorInit {
|
|||
pub profile: Profile,
|
||||
}
|
||||
|
||||
/// This parses a var (either env var or cmake var) and if it contains
|
||||
/// a '=' char, it assumes it's key and value, otherwise it's just going to
|
||||
/// be the key and the value is gonna be an empty string
|
||||
fn parse_var(input: &str) -> (String, String) {
|
||||
if input.contains('=') {
|
||||
let mut sp = input.split('=');
|
||||
(
|
||||
sp.next().unwrap().to_string(),
|
||||
sp.next().unwrap().to_string(),
|
||||
)
|
||||
} else {
|
||||
(input.to_string(), "".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for ProfileEditor {
|
||||
type Init = ProfileEditorInit;
|
||||
|
@ -114,14 +130,6 @@ impl SimpleComponent for ProfileEditor {
|
|||
prof.borrow_mut().prefix = n_path.unwrap_or_default().into();
|
||||
}),
|
||||
),
|
||||
add: &entry_row("Autostart Command",
|
||||
model.profile.borrow().autostart_command.as_ref().unwrap_or(&String::default()),
|
||||
clone!(#[strong] prof, move |row| {
|
||||
let txt = row.text().trim().to_string();
|
||||
prof.borrow_mut().autostart_command =
|
||||
if txt.is_empty() {None} else {Some(txt)};
|
||||
})
|
||||
),
|
||||
add: &switch_row("Dependency Check",
|
||||
Some("Warning: disabling dependency checks may result in build failures"),
|
||||
!model.profile.borrow().skip_dependency_check,
|
||||
|
@ -201,31 +209,43 @@ impl SimpleComponent for ProfileEditor {
|
|||
),
|
||||
},
|
||||
add: model.xrservice_cmake_flags_rows.widget(),
|
||||
add: opencompgrp = &adw::PreferencesGroup {
|
||||
set_title: "OpenComposite",
|
||||
set_description: Some("OpenVR driver built on top of OpenXR"),
|
||||
add: ovr_comp_grp = &adw::PreferencesGroup {
|
||||
set_title: "OpenVR Compatibility",
|
||||
set_description: Some("OpenVR compatibility module, translates between OpenXR and OpenVR to run legacy OpenVR apps"),
|
||||
add: &combo_row(
|
||||
"OpenVR Module Type",
|
||||
None,
|
||||
model.profile.borrow().ovr_comp.mod_type.to_string().as_str(),
|
||||
OvrCompatibilityModuleType::iter()
|
||||
.map(OvrCompatibilityModuleType::to_string)
|
||||
.collect::<Vec<String>>(),
|
||||
clone!(#[strong] prof, move |row| {
|
||||
prof.borrow_mut().ovr_comp.mod_type =
|
||||
OvrCompatibilityModuleType::from(row.selected());
|
||||
}),
|
||||
),
|
||||
add: &path_row(
|
||||
"OpenComposite Path", None,
|
||||
Some(model.profile.borrow().opencomposite_path.clone().to_string_lossy().to_string()),
|
||||
"OpenVR Module Path", None,
|
||||
Some(model.profile.borrow().ovr_comp.path.clone().to_string_lossy().to_string()),
|
||||
Some(init.root_win.clone()),
|
||||
clone!(#[strong] prof, move |n_path| {
|
||||
prof.borrow_mut().opencomposite_path = n_path.unwrap_or_default().into();
|
||||
prof.borrow_mut().ovr_comp.path = n_path.unwrap_or_default().into();
|
||||
})
|
||||
),
|
||||
add: &entry_row(
|
||||
"OpenComposite Repo",
|
||||
model.profile.borrow().opencomposite_repo.clone().unwrap_or_default().as_str(),
|
||||
"OpenVR Compatibility Repo",
|
||||
model.profile.borrow().ovr_comp.repo.clone().unwrap_or_default().as_str(),
|
||||
clone!(#[strong] prof, move |row| {
|
||||
let n_val = row.text().to_string();
|
||||
prof.borrow_mut().opencomposite_repo = (!n_val.is_empty()).then_some(n_val);
|
||||
prof.borrow_mut().ovr_comp.repo = (!n_val.is_empty()).then_some(n_val);
|
||||
})
|
||||
),
|
||||
add: &entry_row(
|
||||
"OpenComposite Branch",
|
||||
model.profile.borrow().opencomposite_branch.clone().unwrap_or_default().as_str(),
|
||||
"OpenVR Compatibility Branch",
|
||||
model.profile.borrow().ovr_comp.branch.clone().unwrap_or_default().as_str(),
|
||||
clone!(#[strong] prof, move |row| {
|
||||
let n_val = row.text().to_string();
|
||||
prof.borrow_mut().opencomposite_branch = (!n_val.is_empty()).then_some(n_val);
|
||||
prof.borrow_mut().ovr_comp.branch = (!n_val.is_empty()).then_some(n_val);
|
||||
})
|
||||
),
|
||||
},
|
||||
|
@ -428,33 +448,23 @@ impl SimpleComponent for ProfileEditor {
|
|||
}
|
||||
Self::Input::AddEnvVar(var) => {
|
||||
let mut prof = self.profile.borrow_mut();
|
||||
if !prof.environment.contains_key(&var) {
|
||||
let (name, value) = if var.contains('=') {
|
||||
let mut sp = var.split('=');
|
||||
(
|
||||
sp.next().unwrap().to_string(),
|
||||
sp.next().unwrap().to_string(),
|
||||
)
|
||||
} else {
|
||||
(var, "".to_string())
|
||||
};
|
||||
let (name, value) = parse_var(&var);
|
||||
if !prof.environment.contains_key(&name) {
|
||||
prof.environment.insert(name.clone(), value.clone());
|
||||
self.env_rows
|
||||
.guard()
|
||||
.push_back(EnvVarModelInit { name, value });
|
||||
}
|
||||
}
|
||||
Self::Input::AddXrServiceCmakeFlag(name) => {
|
||||
Self::Input::AddXrServiceCmakeFlag(var) => {
|
||||
let mut prof = self.profile.borrow_mut();
|
||||
let (name, value) = parse_var(&var);
|
||||
if !prof.xrservice_cmake_flags.contains_key(&name) {
|
||||
prof.xrservice_cmake_flags
|
||||
.insert(name.clone(), "".to_string());
|
||||
.insert(name.clone(), value.clone());
|
||||
self.xrservice_cmake_flags_rows
|
||||
.guard()
|
||||
.push_back(EnvVarModelInit {
|
||||
name,
|
||||
value: "".to_string(),
|
||||
});
|
||||
.push_back(EnvVarModelInit { name, value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -494,14 +504,14 @@ impl SimpleComponent for ProfileEditor {
|
|||
.halign(gtk::Align::End)
|
||||
.build();
|
||||
|
||||
add_btn.connect_clicked(clone!(
|
||||
let on_add = clone!(
|
||||
#[strong]
|
||||
sender,
|
||||
#[weak]
|
||||
name_entry,
|
||||
#[weak]
|
||||
popover,
|
||||
move |_| {
|
||||
move || {
|
||||
let key_gstr = name_entry.text();
|
||||
let key = key_gstr.trim();
|
||||
if !key.is_empty() {
|
||||
|
@ -510,7 +520,13 @@ impl SimpleComponent for ProfileEditor {
|
|||
sender.input($event(key.to_string()));
|
||||
}
|
||||
}
|
||||
);
|
||||
name_entry.connect_activate(clone!(
|
||||
#[strong]
|
||||
on_add,
|
||||
move |_| on_add()
|
||||
));
|
||||
add_btn.connect_clicked(move |_| on_add());
|
||||
btn
|
||||
}};
|
||||
}
|
||||
|
@ -523,17 +539,30 @@ impl SimpleComponent for ProfileEditor {
|
|||
let profile = Rc::new(RefCell::new(init.profile));
|
||||
let prof = profile.clone();
|
||||
|
||||
let env_var_prefs_group = {
|
||||
let pg = adw::PreferencesGroup::builder()
|
||||
.title("Environment Variables")
|
||||
.description(ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH.as_str())
|
||||
.header_suffix(&add_env_var_btn)
|
||||
.build();
|
||||
if let Some(desc) = pg
|
||||
.first_child()
|
||||
.and_then(|c| c.first_child())
|
||||
.and_then(|c| c.first_child())
|
||||
.and_then(|c| c.last_child())
|
||||
.and_downcast::<gtk::Label>()
|
||||
{
|
||||
desc.set_selectable(true);
|
||||
} else {
|
||||
warn!("failed to make env var preference group description selectable, please open a bug report");
|
||||
}
|
||||
pg
|
||||
};
|
||||
let mut model = Self {
|
||||
profile,
|
||||
win: None,
|
||||
env_rows: AsyncFactoryVecDeque::builder()
|
||||
.launch(
|
||||
adw::PreferencesGroup::builder()
|
||||
.title("Environment Variables")
|
||||
.description(ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH.as_str())
|
||||
.header_suffix(&add_env_var_btn)
|
||||
.build(),
|
||||
)
|
||||
.launch(env_var_prefs_group)
|
||||
.forward(sender.input_sender(), |msg| match msg {
|
||||
EnvVarModelOutMsg::Changed(name, value) => {
|
||||
ProfileEditorMsg::EnvVarChanged(name, value)
|
||||
|
|
|
@ -10,10 +10,10 @@ use relm4::{
|
|||
};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
path::Path,
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
#[tracker::track]
|
||||
pub struct SteamVrCalibrationBox {
|
||||
|
@ -144,55 +144,59 @@ impl SimpleComponent for SteamVrCalibrationBox {
|
|||
}
|
||||
Self::Input::RunCalibration => {
|
||||
self.set_calibration_result(None);
|
||||
let steamvr_bin_dir = get_steamvr_bin_dir_path().to_string_lossy().to_string();
|
||||
if !Path::new(&steamvr_bin_dir).is_dir() {
|
||||
self.set_calibration_success(false);
|
||||
self.set_calibration_result(Some("SteamVR not found".into()));
|
||||
return;
|
||||
}
|
||||
let mut env: HashMap<String, String> = HashMap::new();
|
||||
env.insert("LD_LIBRARY_PATH".into(), steamvr_bin_dir.clone());
|
||||
let vrcmd = format!("{steamvr_bin_dir}/vrcmd");
|
||||
let server_worker = {
|
||||
let mut jobs: VecDeque<WorkerJob> = VecDeque::new();
|
||||
jobs.push_back(WorkerJob::new_cmd(
|
||||
Some(env.clone()),
|
||||
vrcmd.clone(),
|
||||
Some(vec!["--pollposes".into()]),
|
||||
));
|
||||
JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
|
||||
JobWorkerOut::Log(_) => Self::Input::NoOp,
|
||||
JobWorkerOut::Exit(code) => Self::Input::OnServerWorkerExit(code),
|
||||
})
|
||||
};
|
||||
let cal_worker = {
|
||||
let mut jobs: VecDeque<WorkerJob> = VecDeque::new();
|
||||
jobs.push_back(WorkerJob::new_func(Box::new(move || {
|
||||
sleep(Duration::from_secs(2));
|
||||
FuncWorkerOut {
|
||||
success: true,
|
||||
out: vec![],
|
||||
}
|
||||
})));
|
||||
jobs.push_back(WorkerJob::new_cmd(
|
||||
Some(env),
|
||||
vrcmd,
|
||||
Some(vec!["--resetroomsetup".into()]),
|
||||
));
|
||||
JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
|
||||
JobWorkerOut::Log(_) => Self::Input::NoOp,
|
||||
JobWorkerOut::Exit(code) => Self::Input::OnCalWorkerExit(code),
|
||||
})
|
||||
};
|
||||
match get_steamvr_bin_dir_path() {
|
||||
Err(e) => {
|
||||
error!("could not get SteamVR bin dir: {e}");
|
||||
self.set_calibration_success(false);
|
||||
self.set_calibration_result(Some("SteamVR not found".into()));
|
||||
}
|
||||
Ok(bin_dir_p) => {
|
||||
let steamvr_bin_dir = bin_dir_p.to_string_lossy().to_string();
|
||||
let mut env: HashMap<String, String> = HashMap::new();
|
||||
env.insert("LD_LIBRARY_PATH".into(), steamvr_bin_dir.clone());
|
||||
let vrcmd = format!("{steamvr_bin_dir}/vrcmd");
|
||||
let server_worker = {
|
||||
let mut jobs: VecDeque<WorkerJob> = VecDeque::new();
|
||||
jobs.push_back(WorkerJob::new_cmd(
|
||||
Some(env.clone()),
|
||||
vrcmd.clone(),
|
||||
Some(vec!["--pollposes".into()]),
|
||||
));
|
||||
JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
|
||||
JobWorkerOut::Log(_) => Self::Input::NoOp,
|
||||
JobWorkerOut::Exit(code) => Self::Input::OnServerWorkerExit(code),
|
||||
})
|
||||
};
|
||||
let cal_worker = {
|
||||
let mut jobs: VecDeque<WorkerJob> = VecDeque::new();
|
||||
jobs.push_back(WorkerJob::new_func(Box::new(move || {
|
||||
sleep(Duration::from_secs(2));
|
||||
FuncWorkerOut {
|
||||
success: true,
|
||||
out: vec![],
|
||||
}
|
||||
})));
|
||||
jobs.push_back(WorkerJob::new_cmd(
|
||||
Some(env),
|
||||
vrcmd,
|
||||
Some(vec!["--resetroomsetup".into()]),
|
||||
));
|
||||
JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
|
||||
JobWorkerOut::Log(_) => Self::Input::NoOp,
|
||||
JobWorkerOut::Exit(code) => Self::Input::OnCalWorkerExit(code),
|
||||
})
|
||||
};
|
||||
|
||||
server_worker.start();
|
||||
cal_worker.start();
|
||||
self.server_worker = Some(server_worker);
|
||||
self.calibration_worker = Some(cal_worker);
|
||||
server_worker.start();
|
||||
cal_worker.start();
|
||||
self.server_worker = Some(server_worker);
|
||||
self.calibration_worker = Some(cal_worker);
|
||||
}
|
||||
};
|
||||
}
|
||||
Self::Input::OnServerWorkerExit(code) => {
|
||||
if code != 0 {
|
||||
eprintln!("Calibration exited with code {code}");
|
||||
error!("calibration exited with code {code}");
|
||||
}
|
||||
self.calibration_running = false;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use gtk::{gdk, gio, glib::clone, prelude::*};
|
||||
use tracing::{error, warn};
|
||||
|
||||
pub fn limit_dropdown_width(dd: >k::DropDown) {
|
||||
let mut dd_child = dd
|
||||
|
@ -46,14 +47,14 @@ pub fn warning_heading() -> gtk::Box {
|
|||
|
||||
pub fn open_with_default_handler(uri: &str) {
|
||||
if let Err(e) = gio::AppInfo::launch_default_for_uri(uri, gio::AppLaunchContext::NONE) {
|
||||
eprintln!("Error opening uri {}: {}", uri, e)
|
||||
error!("opening uri {uri}: {e}")
|
||||
};
|
||||
}
|
||||
|
||||
pub fn copy_text(txt: &str) {
|
||||
match gdk::Display::default() {
|
||||
None => {
|
||||
eprintln!("Warning: could not get default gdk display")
|
||||
warn!("could not get default gdk display")
|
||||
}
|
||||
Some(d) => {
|
||||
d.clipboard().set_text(txt);
|
||||
|
|
|
@ -21,6 +21,7 @@ use crate::{
|
|||
use adw::prelude::*;
|
||||
use gtk::glib::clone;
|
||||
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
||||
use tracing::error;
|
||||
|
||||
#[tracker::track]
|
||||
pub struct WivrnConfEditor {
|
||||
|
@ -255,7 +256,7 @@ impl SimpleComponent for WivrnConfEditor {
|
|||
if let Some(idx) = idx_opt {
|
||||
self.encoder_models.as_mut().unwrap().guard().remove(idx);
|
||||
} else {
|
||||
eprintln!("Couldn't find encoder model with id {id}");
|
||||
error!("couldn't find encoder model with id {id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use crate::{
|
|||
};
|
||||
use gtk::prelude::*;
|
||||
use relm4::prelude::*;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum StartClientStatus {
|
||||
|
@ -153,14 +154,14 @@ impl AsyncComponent for WivrnWiredStartBox {
|
|||
.into(),
|
||||
))
|
||||
} else {
|
||||
eprintln!("Error: ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr);
|
||||
error!("ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr);
|
||||
StartClientStatus::Done(Some(
|
||||
format!("ADB exited with code \"{}\"", out.exit_code)
|
||||
))
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Error: failed to run ADB: {e}");
|
||||
error!("failed to run ADB: {e}");
|
||||
StartClientStatus::Done(Some(
|
||||
"Failed to run ADB".into()
|
||||
))
|
||||
|
|
|
@ -7,8 +7,10 @@ use nix::{
|
|||
use std::{
|
||||
fs::{self, copy, create_dir_all, remove_dir_all, File, OpenOptions},
|
||||
io::{BufReader, BufWriter},
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::Path,
|
||||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
pub fn get_writer(path: &Path) -> anyhow::Result<BufWriter<std::fs::File>> {
|
||||
if let Some(parent) = path.parent() {
|
||||
|
@ -36,7 +38,7 @@ pub fn get_reader(path: &Path) -> Option<BufReader<File>> {
|
|||
}
|
||||
match File::open(path) {
|
||||
Err(e) => {
|
||||
eprintln!("Error opening {}: {}", path.to_string_lossy(), e);
|
||||
error!("Error opening {}: {}", path.to_string_lossy(), e);
|
||||
None
|
||||
}
|
||||
Ok(fd) => Some(BufReader::new(fd)),
|
||||
|
@ -48,7 +50,7 @@ pub fn deserialize_file<T: serde::de::DeserializeOwned>(path: &Path) -> Option<T
|
|||
None => None,
|
||||
Some(reader) => match serde_json::from_reader(reader) {
|
||||
Err(e) => {
|
||||
eprintln!("Failed to deserialize {}: {}", path.to_string_lossy(), e);
|
||||
error!("Failed to deserialize {}: {}", path.to_string_lossy(), e);
|
||||
None
|
||||
}
|
||||
Ok(res) => Some(res),
|
||||
|
@ -56,16 +58,25 @@ pub fn deserialize_file<T: serde::de::DeserializeOwned>(path: &Path) -> Option<T
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_file_readonly(path: &Path, readonly: bool) -> Result<(), std::io::Error> {
|
||||
pub fn set_file_readonly(path: &Path, readonly: bool) -> anyhow::Result<()> {
|
||||
if path.is_symlink() {
|
||||
bail!(
|
||||
"path {} is a symlink, trying to change its write permission will only change the original file",
|
||||
path.to_string_lossy()
|
||||
);
|
||||
}
|
||||
if !path.is_file() {
|
||||
eprintln!("WARN: trying to set readonly on a file that does not exist");
|
||||
debug!(
|
||||
"trying to set readonly on a file that does not exist: {}",
|
||||
path.to_string_lossy()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let mut perms = fs::metadata(path)
|
||||
.expect("Could not get metadata for file")
|
||||
.permissions();
|
||||
perms.set_readonly(readonly);
|
||||
fs::set_permissions(path, perms)
|
||||
Ok(fs::set_permissions(path, perms)?)
|
||||
}
|
||||
|
||||
pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
|
||||
|
@ -80,16 +91,37 @@ pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
|
|||
]
|
||||
}
|
||||
|
||||
pub async fn setcap_cap_sys_nice_eip(profile: &Profile) {
|
||||
if let Err(e) = async_process("pkexec", Some(&setcap_cap_sys_nice_eip_cmd(profile)), None).await
|
||||
{
|
||||
eprintln!("Error: failed running setcap: {e}");
|
||||
pub async fn verify_cap_sys_nice_eip(profile: &Profile) -> bool {
|
||||
let xrservice_binary = profile.xrservice_binary().to_string_lossy().to_string();
|
||||
match async_process("getcap", Some(&[&xrservice_binary]), None).await {
|
||||
Err(e) => {
|
||||
error!("failed to run `getcap {xrservice_binary}`: {e:?}");
|
||||
false
|
||||
}
|
||||
Ok(out) => {
|
||||
debug!("getcap {xrservice_binary} stdout: {}", out.stdout);
|
||||
debug!("getcap {xrservice_binary} stderr: {}", out.stderr);
|
||||
if out.exit_code != 0 {
|
||||
error!(
|
||||
"command `getcap {xrservice_binary}` failed with status code {}",
|
||||
out.exit_code
|
||||
);
|
||||
false
|
||||
} else {
|
||||
out.stdout.to_lowercase().contains("cap_sys_nice=eip")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn setcap_cap_sys_nice_eip(profile: &Profile) -> anyhow::Result<()> {
|
||||
async_process("pkexec", Some(&setcap_cap_sys_nice_eip_cmd(profile)), None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rm_rf(path: &Path) {
|
||||
if remove_dir_all(path).is_err() {
|
||||
eprintln!("Failed to remove path {}", path.to_string_lossy());
|
||||
error!("failed to remove path {}", path.to_string_lossy());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,11 +132,13 @@ pub fn copy_file(source: &Path, dest: &Path) {
|
|||
.unwrap_or_else(|_| panic!("Failed to create dir {}", parent.to_str().unwrap()));
|
||||
}
|
||||
}
|
||||
set_file_readonly(dest, false)
|
||||
.unwrap_or_else(|_| panic!("Failed to set file {} as rw", dest.to_string_lossy()));
|
||||
copy(source, dest).unwrap_or_else(|_| {
|
||||
if !dest.is_symlink() {
|
||||
set_file_readonly(dest, false)
|
||||
.unwrap_or_else(|_| panic!("Failed to set file {} as rw", dest.to_string_lossy()));
|
||||
}
|
||||
copy(source, dest).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"Failed to copy {} to {}",
|
||||
"Failed to copy {} to {}: {e}",
|
||||
source.to_string_lossy(),
|
||||
dest.to_string_lossy()
|
||||
)
|
||||
|
@ -118,6 +152,17 @@ pub fn mount_has_nosuid(path: &Path) -> Result<bool, Errno> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn mark_as_executable(path: &Path) -> anyhow::Result<()> {
|
||||
if !path.is_file() {
|
||||
bail!("Path '{}' is not a file", path.to_string_lossy())
|
||||
} else {
|
||||
let mut perms = fs::metadata(path)?.permissions();
|
||||
perms.set_mode(perms.mode() | 0o111);
|
||||
fs::set_permissions(path, perms)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::mount_has_nosuid;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod file_utils;
|
||||
pub mod hash;
|
||||
pub mod steam_library_folder;
|
||||
pub mod steamvr_utils;
|
||||
|
|
69
src/util/steam_library_folder.rs
Normal file
69
src/util/steam_library_folder.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use crate::paths::get_home_dir;
|
||||
use anyhow::bail;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::read_to_string,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SteamLibraryFolder {
|
||||
pub path: String,
|
||||
pub apps: HashMap<u32, usize>,
|
||||
}
|
||||
|
||||
fn get_steam_main_dir_path() -> anyhow::Result<PathBuf> {
|
||||
let steam_root: PathBuf = get_home_dir().join(".steam/root").canonicalize()?;
|
||||
|
||||
if steam_root.is_dir() {
|
||||
Ok(steam_root)
|
||||
} else {
|
||||
bail!(
|
||||
"Canonical steam root '{}' is not a dir nor a symlink!",
|
||||
steam_root.to_string_lossy()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SteamLibraryFolder {
|
||||
pub fn get_folders() -> anyhow::Result<HashMap<u32, Self>> {
|
||||
let libraryfolders_path = get_steam_main_dir_path()?
|
||||
.join("steamapps/libraryfolders.vdf")
|
||||
.canonicalize()?;
|
||||
if !libraryfolders_path.is_file() {
|
||||
bail!(
|
||||
"Steam libraryfolders.vdf does not exist in its canonical location {}",
|
||||
libraryfolders_path.to_string_lossy()
|
||||
);
|
||||
}
|
||||
Self::get_folders_from_path(&libraryfolders_path)
|
||||
}
|
||||
|
||||
/// Do not use this: use get_folders() instead as it always uses Steam's
|
||||
/// canonical root path. This is intended to be directly used only for
|
||||
/// unit tests
|
||||
pub fn get_folders_from_path(p: &Path) -> anyhow::Result<HashMap<u32, Self>> {
|
||||
Ok(keyvalues_serde::from_str(read_to_string(p)?.as_str())?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SteamLibraryFolder;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn deserialize_steam_libraryfolders_vdf() {
|
||||
let lf = SteamLibraryFolder::get_folders_from_path(Path::new(
|
||||
"./test/files/steam_libraryfolders.vdf",
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(lf.len(), 1);
|
||||
let first = lf.get(&0).unwrap();
|
||||
assert_eq!(first.path, "/home/gabmus/.local/share/Steam");
|
||||
assert_eq!(first.apps.len(), 10);
|
||||
assert_eq!(first.apps.get(&228980).unwrap(), &29212173);
|
||||
assert_eq!(first.apps.get(&632360).unwrap(), &0);
|
||||
}
|
||||
}
|
|
@ -5,12 +5,10 @@ use ash::{
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VulkanInfo {
|
||||
pub has_nvidia_gpu: bool,
|
||||
pub has_monado_vulkan_layers: bool,
|
||||
pub gpu_names: Vec<String>,
|
||||
}
|
||||
|
||||
const NVIDIA_VENDOR_ID: u32 = 0x10de;
|
||||
// const NVIDIA_VENDOR_ID: u32 = 0x10de;
|
||||
|
||||
impl VulkanInfo {
|
||||
/// # Safety
|
||||
|
@ -25,40 +23,19 @@ impl VulkanInfo {
|
|||
None,
|
||||
)
|
||||
}?;
|
||||
let mut has_nvidia_gpu = false;
|
||||
let mut has_monado_vulkan_layers = false;
|
||||
let gpu_names = unsafe { instance.enumerate_physical_devices() }?
|
||||
.into_iter()
|
||||
.filter_map(|d| {
|
||||
let props = unsafe { instance.get_physical_device_properties(d) };
|
||||
if props.vendor_id == NVIDIA_VENDOR_ID {
|
||||
has_nvidia_gpu = true;
|
||||
}
|
||||
if !has_monado_vulkan_layers {
|
||||
has_monado_vulkan_layers =
|
||||
unsafe { instance.enumerate_device_layer_properties(d) }
|
||||
.ok()
|
||||
.map(|layerprops| {
|
||||
layerprops.iter().any(|lp| {
|
||||
lp.layer_name_as_c_str().is_ok_and(|name| {
|
||||
name.to_string_lossy()
|
||||
== "VK_LAYER_MND_enable_timeline_semaphore"
|
||||
})
|
||||
})
|
||||
})
|
||||
== Some(true);
|
||||
}
|
||||
props
|
||||
.device_name_as_c_str()
|
||||
.ok()
|
||||
.map(|cs| cs.to_string_lossy().to_string())
|
||||
Some(
|
||||
unsafe { instance.get_physical_device_properties(d) }
|
||||
.device_name_as_c_str()
|
||||
.ok()?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
unsafe { instance.destroy_instance(None) };
|
||||
Ok(Self {
|
||||
gpu_names,
|
||||
has_nvidia_gpu,
|
||||
has_monado_vulkan_layers,
|
||||
})
|
||||
Ok(Self { gpu_names })
|
||||
}
|
||||
}
|
||||
|
|
122
src/wivrn_dbus/internal.rs
Normal file
122
src/wivrn_dbus/internal.rs
Normal file
|
@ -0,0 +1,122 @@
|
|||
//! # D-Bus interface proxy for: `io.github.wivrn.Server`
|
||||
//!
|
||||
//! This code was generated by `zbus-xmlgen` `5.0.1` from D-Bus introspection data.
|
||||
//! Source: `io.github.wivrn.Server.xml`.
|
||||
//!
|
||||
//! You may prefer to adapt it, instead of using it verbatim.
|
||||
//!
|
||||
//! More information can be found in the [Writing a client proxy] section of the zbus
|
||||
//! documentation.
|
||||
//!
|
||||
//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
|
||||
//! following zbus API can be used:
|
||||
//!
|
||||
//! * [`zbus::fdo::PropertiesProxy`]
|
||||
//!
|
||||
//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
|
||||
//!
|
||||
//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
|
||||
//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
|
||||
use zbus::proxy;
|
||||
#[proxy(
|
||||
interface = "io.github.wivrn.Server",
|
||||
default_path = "/io/github/wivrn/Server",
|
||||
default_service = "io.github.wivrn.Server"
|
||||
)]
|
||||
pub trait Server {
|
||||
/// DisablePairing method
|
||||
fn disable_pairing(&self) -> zbus::Result<()>;
|
||||
|
||||
/// Disconnect method
|
||||
fn disconnect(&self) -> zbus::Result<()>;
|
||||
|
||||
/// EnablePairing method
|
||||
fn enable_pairing(&self, TimeoutSecs: i32) -> zbus::Result<String>;
|
||||
|
||||
/// Quit method
|
||||
fn quit(&self) -> zbus::Result<()>;
|
||||
|
||||
/// RenameKey method
|
||||
fn rename_key(&self, PublicKey: &str, Name: &str) -> zbus::Result<()>;
|
||||
|
||||
/// RevokeKey method
|
||||
fn revoke_key(&self, PublicKey: &str) -> zbus::Result<()>;
|
||||
|
||||
/// AvailableRefreshRates property
|
||||
#[zbus(property)]
|
||||
fn available_refresh_rates(&self) -> zbus::Result<Vec<f64>>;
|
||||
|
||||
/// EncryptionEnabled property
|
||||
#[zbus(property)]
|
||||
fn encryption_enabled(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// EyeGaze property
|
||||
#[zbus(property)]
|
||||
fn eye_gaze(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// FaceTracking property
|
||||
#[zbus(property)]
|
||||
fn face_tracking(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// FieldOfView property
|
||||
#[zbus(property)]
|
||||
fn field_of_view(&self) -> zbus::Result<Vec<(f64, f64, f64, f64)>>;
|
||||
|
||||
/// HandTracking property
|
||||
#[zbus(property)]
|
||||
fn hand_tracking(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// HeadsetConnected property
|
||||
#[zbus(property)]
|
||||
fn headset_connected(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// JsonConfiguration property
|
||||
#[zbus(property)]
|
||||
fn json_configuration(&self) -> zbus::Result<String>;
|
||||
#[zbus(property)]
|
||||
fn set_json_configuration(&self, value: &str) -> zbus::Result<()>;
|
||||
|
||||
/// KnownKeys property
|
||||
#[zbus(property)]
|
||||
fn known_keys(&self) -> zbus::Result<Vec<(String, String)>>;
|
||||
|
||||
/// MicChannels property
|
||||
#[zbus(property)]
|
||||
fn mic_channels(&self) -> zbus::Result<u32>;
|
||||
|
||||
/// MicSampleRate property
|
||||
#[zbus(property)]
|
||||
fn mic_sample_rate(&self) -> zbus::Result<u32>;
|
||||
|
||||
/// PairingEnabled property
|
||||
#[zbus(property)]
|
||||
fn pairing_enabled(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// Pin property
|
||||
#[zbus(property)]
|
||||
fn pin(&self) -> zbus::Result<String>;
|
||||
|
||||
/// PreferredRefreshRate property
|
||||
#[zbus(property)]
|
||||
fn preferred_refresh_rate(&self) -> zbus::Result<f64>;
|
||||
|
||||
/// RecommendedEyeSize property
|
||||
#[zbus(property)]
|
||||
fn recommended_eye_size(&self) -> zbus::Result<(u32, u32)>;
|
||||
|
||||
/// SpeakerChannels property
|
||||
#[zbus(property)]
|
||||
fn speaker_channels(&self) -> zbus::Result<u32>;
|
||||
|
||||
/// SpeakerSampleRate property
|
||||
#[zbus(property)]
|
||||
fn speaker_sample_rate(&self) -> zbus::Result<u32>;
|
||||
|
||||
/// SteamCommand property
|
||||
#[zbus(property)]
|
||||
fn steam_command(&self) -> zbus::Result<String>;
|
||||
|
||||
/// SupportedCodecs property
|
||||
#[zbus(property)]
|
||||
fn supported_codecs(&self) -> zbus::Result<Vec<String>>;
|
||||
}
|
34
src/wivrn_dbus/mod.rs
Normal file
34
src/wivrn_dbus/mod.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
// how to regenerate this one:
|
||||
//
|
||||
// ```bash
|
||||
// cargo install zbus_xmlgen
|
||||
// curl -sSLO https://github.com/WiVRn/WiVRn/blob/master/dbus/io.github.wivrn.Server.xml
|
||||
// zbus-xmlgen file io.github.wivrn.Server.xml
|
||||
// ```
|
||||
//
|
||||
// it should output a file called server.rs, move it accordingly
|
||||
#[rustfmt::skip]
|
||||
#[allow(non_snake_case)]
|
||||
mod internal;
|
||||
|
||||
async fn proxy<'a>() -> zbus::Result<internal::ServerProxy<'a>> {
|
||||
let connection = zbus::Connection::session().await?;
|
||||
let proxy = internal::ServerProxy::new(&connection).await?;
|
||||
Ok(proxy)
|
||||
}
|
||||
|
||||
pub async fn is_pairing_mode() -> zbus::Result<bool> {
|
||||
proxy().await?.pairing_enabled().await
|
||||
}
|
||||
|
||||
pub async fn enable_pairing() -> zbus::Result<String> {
|
||||
proxy().await?.enable_pairing(0).await
|
||||
}
|
||||
|
||||
pub async fn disable_pairing() -> zbus::Result<()> {
|
||||
proxy().await?.disable_pairing().await
|
||||
}
|
||||
|
||||
pub async fn pairing_pin() -> zbus::Result<String> {
|
||||
proxy().await?.pin().await
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
use libmonado::{self, BatteryStatus, DeviceRole};
|
||||
use std::{collections::HashMap, fmt::Display, slice::Iter};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum XRDeviceRole {
|
||||
|
@ -280,8 +281,8 @@ impl XRDevice {
|
|||
if let Some(target) = devs.get_mut(&index) {
|
||||
target.roles.push(role.into());
|
||||
} else {
|
||||
eprintln!(
|
||||
"Could not find device index {index} for role {}",
|
||||
error!(
|
||||
"could not find device index {index} for role {}",
|
||||
XRDeviceRole::from(role)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
"xrservice_path": "/home/user/monado",
|
||||
"xrservice_repo": null,
|
||||
"xrservice_branch": null,
|
||||
"opencomposite_path": "/home/user/opencomposite",
|
||||
"opencomposite_repo": null,
|
||||
"opencomposite_branch": null,
|
||||
"ovr_comp": { "mod_type": "Opencomposite", "path": "/home/user/opencomposite", "repo": null, "branch": null },
|
||||
"features": {
|
||||
"libsurvive": {
|
||||
"feature_type": "Libsurvive",
|
||||
|
@ -34,4 +32,4 @@
|
|||
"can_be_built": true,
|
||||
"editable": true,
|
||||
"pull_on_build": true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue