Compare commits

..

No commits in common. "main" and "1.1.0" have entirely different histories.
main ... 1.1.0

74 changed files with 700 additions and 3347 deletions

View file

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

174
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
version = 3
[[package]]
name = "addr2line"
@ -420,15 +420,6 @@ dependencies = [
"libc",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
@ -563,7 +554,7 @@ dependencies = [
[[package]]
name = "envision"
version = "3.0.1"
version = "1.1.0"
dependencies = [
"anyhow",
"ash",
@ -583,12 +574,8 @@ dependencies = [
"rusb",
"serde",
"serde_json",
"serde_yaml",
"sha2",
"tokio",
"tracing",
"tracing-appender",
"tracing-subscriber",
"tracker",
"uuid",
"vte4",
@ -1711,15 +1698,6 @@ dependencies = [
"libc",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "memchr"
version = "2.7.4"
@ -1842,16 +1820,6 @@ dependencies = [
"zbus 4.4.0",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@ -1977,12 +1945,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "pango"
version = "0.20.6"
@ -2218,17 +2180,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
"regex-automata",
"regex-syntax",
]
[[package]]
@ -2239,15 +2192,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@ -2556,19 +2503,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sha1"
version = "0.10.6"
@ -2591,15 +2525,6 @@ dependencies = [
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -2788,16 +2713,6 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "time"
version = "0.3.36"
@ -2805,12 +2720,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
@ -2819,16 +2732,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.7.6"
@ -2941,18 +2844,6 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
dependencies = [
"crossbeam-channel",
"thiserror",
"time",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
@ -2971,49 +2862,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-serde"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
dependencies = [
"serde",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
]
[[package]]
@ -3077,12 +2925,6 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.9.0"
@ -3122,12 +2964,6 @@ dependencies = [
"rand",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"

View file

@ -1,16 +1,7 @@
[package]
name = "envision"
version = "3.1.0"
version = "1.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
@ -40,7 +31,3 @@ sha2 = "0.10.8"
tokio = { version = "1.39.3", features = ["process"] }
notify-rust = "4.11.3"
zbus = { version = "5.1.1", features = ["tokio"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
tracing = "0.1.41"
tracing-appender = "0.2.3"
serde_yaml = "0.9.34"

View file

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

View file

@ -2,173 +2,22 @@
<component type="desktop-application">
<id>@APP_ID@</id>
<metadata_license>CC0</metadata_license>
<project_license>AGPL-3.0-or-later</project_license>
<project_license>AGPL-3.0</project_license>
<name translatable="no">@PRETTY_NAME@</name>
<summary>Orchestrator for the free XR stack</summary>
<summary>GUI for Monado</summary>
<description>
<p>Orchestrator for the free XR stack</p>
<p>GUI for Monado</p> <!-- temporary -->
</description>
<screenshots>
<!--screenshots>
<screenshot type="default">
<image>https://gitlab.com/gabmus/envision/raw/main/data/screenshots/01.png</image>
<image>https://gitlab.com/gabmus/envision/raw/main/misc/screenshots/screenshot1.png</image>
<caption>Main window</caption>
</screenshot>
<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>
</screenshots-->
<url type="homepage">@REPO_URL@</url>
<url type="bugtracker">@REPO_URL@/issues</url>
<content_rating type="oars-1.0" />
<releases>
<release version="3.1.0" date="2025-04-08">
<description>
<p>What's new</p>
<ul>
<li>don&#x27;t set openvrpaths as read only during profile startup</li>
<li>small design changes to build window ui</li>
<li>add support for vapor openvr compatibility module</li>
<li>remove monado vulkan layers check for nvidia</li>
</ul>
<p>Fixes</p>
<ul>
<li>disable and blacklist wayvr dashboard plugin</li>
<li>monado dependencies: use wayland-protocols-devel on Fedora</li>
</ul>
<p>Other changes</p>
<ul>
<li>clippy</li>
</ul>
</description>
</release>
<release version="3.0.1" date="2025-03-02">
<description>
<p>Fixes</p>
<ul>
<li>libnotify headers path in wivrn depcheck</li>
</ul>
</description>
</release>
<release version="3.0.0" date="2025-03-01">
<description>
<p>Breaking changes</p>
<ul>
<li>plugin store</li>
</ul>
<p>What's new</p>
<ul>
<li>Add WayVR Dashboard to the plugin list</li>
<li>wivrn: replace pulse dependency with pipewire</li>
<li>set wivrn launch options in the default profile</li>
<li>support for plugin dependencies and wayvr dashboards (using unreleased api)</li>
<li>launch options for plugins</li>
<li>homepage and author in plugin details</li>
<li>write rolling logs to file</li>
<li>add xrizer as an option for openvr compatibility module</li>
<li>switch wlx manifest</li>
<li>fetch plugins manifests online</li>
<li>add telescope to plugin store</li>
<li>ask to build profile after editing it</li>
<li>add cpu to debug info</li>
<li>make env var description selectable</li>
<li>press enter on env var entry to add</li>
<li>clearer messaging around setcap failures; getcap after setcap</li>
<li>version command line option</li>
<li>single stage ci with tests, clippy and fmt check all in one</li>
<li>use ubuntu for the ci</li>
</ul>
<p>Fixes</p>
<ul>
<li>onnxruntime build error when latest release has no artifacts</li>
<li>typo in install wivrn box</li>
<li>typo in XRT_COMPOSITOR_SCALE_PERCENTAGE</li>
<li>add plugin to config via function instead of signal</li>
<li>refresh all rows on plugin install (fixes dependencies showing up as not installed)</li>
<li>always mark plugin executable as executable</li>
<li>wrap single plugin cmd parts in single quotes</li>
<li>use correct wayland-protocols package name for open suse</li>
<li>typo in plugin schema</li>
<li>remove canonicalize from get steamvr bin dir path function</li>
<li>actually return steamvr dir in get_steamvr_base_dir</li>
<li>canonicalize some steamvr related paths to hopefully resolve symlinks</li>
<li>get ovr compatibility module runtime dir from profile ovr compatibility module struct</li>
<li>use exists() to verify existance of socket file</li>
<li>correct wording of lighthouse calibration</li>
<li>get steamvr bin dir by parsing libraryfolders.vdf</li>
<li>switch to searching for the xml for deb based distros</li>
<li>Include not shared object wayland-protocols</li>
<li>add libbsd deps for monado</li>
<li>add wayland drm-lease protocols dep for monado</li>
<li>use boost dev packages</li>
<li>debian package name for gstreamer plugins base</li>
<li>print active runtime related informative logs as debug</li>
</ul>
<p>Other changes</p>
<ul>
<li>plugins: point to stardust hosted manifest</li>
<li>cargo: revert back to serde_yaml over unsound advisory</li>
</ul>
</description>
</release>
<release version="2.0.1" date="2024-12-11">
<description>
<p>Fixes</p>
<ul>
<li>add screenshots to appdata</li>
</ul>
</description>
</release>
<release version="2.0.0" date="2024-12-09">
<description>
<p>Breaking changes</p>
<ul>
<li>enable support for different openvr compatibility modules other than opencomposite</li>
</ul>
<p>What's new</p>
<ul>
<li>add metadata to Cargo.toml; get developers from Cargo.toml authors; rectify SPDX id for license as AGPL-3.0-or-later</li>
<li>refactor builders cmake vars and env to use inner blocks</li>
<li>disable wivrnctl; refactor cmake vars in wivrn builder</li>
<li>make left and right qwerty controllers appear as no controller detected</li>
<li>try to find libmonado and openxr shared objects by reading openxr config</li>
<li>prefer symlinks over generating files for openxr active runtime json file</li>
<li>move steam library folders parser to own module; function to find steam openxr json; format</li>
<li>proper logging framework</li>
</ul>
<p>Fixes</p>
<ul>
<li>build profile can be specified manually</li>
<li>update wivrn libmonado path to wirvn&#x2F;libmonado_wivrn.so</li>
<li>create openxr config dir when starting profile</li>
<li>add libnotify-dev dependency for wivrn</li>
<li>openssl dep is an include</li>
<li>add openssl-devel dep for wivrn</li>
<li>negative logic and early return in start xrservice func</li>
<li>use let err instead of match in restore xr files func</li>
</ul>
<p>Other changes</p>
<ul>
<li>format</li>
</ul>
</description>
</release>
<release version="1.1.1" date="2024-11-29">
<description>
<p>Fixes</p>
<ul>
<li>remove wivrn pairing mode timer</li>
</ul>
</description>
</release>
<release version="1.1.0" date="2024-11-28">
<description>
<p>What's new</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

View file

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

1
dist/arch/PKGBUILD vendored
View file

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

View file

@ -1,9 +1,9 @@
project(
'envision',
'rust',
version: '3.1.0', # version number row
version: '1.1.0', # version number row
meson_version: '>= 0.59',
license: 'AGPL-3.0-or-later',
license: 'AGPL-3.0',
)
i18n = import('i18n')
@ -38,30 +38,17 @@ iconsdir = datadir / 'icons'
podir = meson.project_source_root() / 'po'
gettext_package = meson.project_name()
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
profile = ''
version_suffix = ''
application_id = base_id
endif
elif opt_profile == 'development'
# are we building a tagged version?
if run_command('git', 'describe', '--tags', '--exact-match').returncode() != 0
profile = 'Devel'
version_suffix = '-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)
elif opt_profile == 'release'
else
profile = ''
version_suffix = ''
application_id = base_id

View file

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

View file

@ -11,22 +11,7 @@ if [[ -z $PREFIX ]] || [[ -z $CACHE_DIR ]]; then
exit 1
fi
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
ONNX_VER=$(curl -sSL "https://api.github.com/repos/microsoft/onnxruntime/releases/latest" | jq -r .tag_name | tr -d v)
SYS_ARCH=$(uname -m)
if [[ $SYS_ARCH == x*64 ]]; then

View file

@ -35,39 +35,28 @@ 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({
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
}),
env: Some(cmake_env),
vars: Some(cmake_vars),
source_dir: profile.features.basalt.path.as_ref().unwrap().clone(),
build_dir: build_dir.clone(),
};

View file

@ -44,30 +44,24 @@ 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({
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
}),
vars: Some(cmake_vars),
source_dir: profile.features.libsurvive.path.as_ref().unwrap().clone(),
build_dir: build_dir.clone(),
};

View file

@ -43,43 +43,37 @@ 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({
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
}),
vars: Some(cmake_vars),
source_dir: profile.xrservice_path.clone(),
build_dir: build_dir.clone(),
};

View file

@ -19,15 +19,13 @@ pub fn get_build_opencomposite_jobs(profile: &Profile, clean_build: bool) -> Vec
let git = Git {
repo: profile
.ovr_comp
.repo
.opencomposite_repo
.as_ref()
.unwrap_or(&"https://gitlab.com/znixian/OpenOVR.git".into())
.clone(),
dir: profile.ovr_comp.path.clone(),
dir: profile.opencomposite_path.clone(),
branch: profile
.ovr_comp
.branch
.opencomposite_branch
.as_ref()
.unwrap_or(&"openxr".into())
.clone(),
@ -35,20 +33,14 @@ 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.ovr_comp.path.join("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 cmake = Cmake {
env: None,
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(),
vars: Some(cmake_vars),
source_dir: profile.opencomposite_path.clone(),
build_dir: build_dir.clone(),
};
if !Path::new(&build_dir).is_dir() || clean_build {

View file

@ -1,85 +0,0 @@
use crate::{
build_tools::{cmake::Cmake, git::Git},
profile::Profile,
termcolor::TermColor,
ui::job_worker::job::{FuncWorkerData, FuncWorkerOut, WorkerJob},
util::file_utils::{copy_file, rm_rf},
};
use std::{
collections::{HashMap, VecDeque},
fs::create_dir_all,
path::Path,
};
pub fn get_build_vapor_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> {
let mut jobs = VecDeque::<WorkerJob>::new();
jobs.push_back(WorkerJob::new_printer(
"Building VapoR...",
Some(TermColor::Blue),
));
let git = Git {
repo: profile
.ovr_comp
.repo
.as_ref()
.unwrap_or(&"https://github.com/micheal65536/VapoR.git".into())
.clone(),
dir: profile.ovr_comp.path.clone(),
branch: profile
.ovr_comp
.branch
.as_ref()
.unwrap_or(&"master".into())
.clone(),
};
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
let build_dir = profile.ovr_comp.path.join("build");
let cmake = Cmake {
env: None,
vars: Some({
let mut cmake_vars: HashMap<String, String> = HashMap::new();
for (k, v) in [
("VAPOR_LOG_SILENT=ON", "ON"),
("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
] {
cmake_vars.insert(k.to_string(), v.to_string());
}
cmake_vars
}),
source_dir: profile.ovr_comp.path.clone(),
build_dir: build_dir.clone(),
};
if !Path::new(&build_dir).is_dir() || clean_build {
rm_rf(&build_dir);
jobs.push_back(cmake.get_prepare_job());
}
jobs.push_back(cmake.get_build_job());
jobs.push_back(WorkerJob::Func(FuncWorkerData {
func: Box::new(move || {
let dest_dir = build_dir.join("bin/linux64");
if let Err(e) = create_dir_all(&dest_dir) {
return FuncWorkerOut {
success: false,
out: vec![format!(
"failed to create dir {}: {e}",
dest_dir.to_string_lossy()
)],
};
}
copy_file(
&build_dir.join("src/vrclient.so"),
&dest_dir.join("vrclient.so"),
);
FuncWorkerOut {
success: true,
out: Vec::default(),
}
}),
}));
jobs
}

View file

@ -34,29 +34,23 @@ 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({
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
}),
vars: Some(cmake_vars),
source_dir: profile.xrservice_path.clone(),
build_dir: build_dir.clone(),
};

View file

@ -1,50 +0,0 @@
use crate::{
build_tools::git::Git, profile::Profile, termcolor::TermColor, ui::job_worker::job::WorkerJob,
util::file_utils::rm_rf,
};
use std::{collections::VecDeque, path::Path};
pub fn get_build_xrizer_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> {
let mut jobs = VecDeque::<WorkerJob>::new();
jobs.push_back(WorkerJob::new_printer(
"Building xrizer...",
Some(TermColor::Blue),
));
let git = Git {
repo: profile
.ovr_comp
.repo
.as_ref()
.unwrap_or(&"https://github.com/Supreeeme/xrizer".into())
.clone(),
dir: profile.ovr_comp.path.clone(),
branch: profile
.ovr_comp
.branch
.as_ref()
.unwrap_or(&"main".into())
.clone(),
};
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
let build_dir = profile.ovr_comp.path.join("target");
if !Path::new(&build_dir).is_dir() || clean_build {
rm_rf(&build_dir);
}
jobs.push_back(WorkerJob::new_cmd(
None,
"sh".into(),
Some(vec![
"-c".into(),
format!(
"cd '{}' && cargo xbuild --release",
profile.ovr_comp.path.to_string_lossy()
),
]),
));
jobs
}

View file

@ -4,6 +4,4 @@ pub mod build_mercury;
pub mod build_monado;
pub mod build_opencomposite;
pub mod build_openhmd;
pub mod build_vapor;
pub mod build_wivrn;
pub mod build_xrizer;

View file

@ -7,38 +7,15 @@ 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] {
@ -52,8 +29,6 @@ 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 {
@ -62,9 +37,8 @@ impl Default for Config {
// TODO: using an empty string here is ugly
selected_profile_uuid: "".to_string(),
debug_view_enabled: false,
user_profiles: Vec::default(),
user_profiles: vec![],
win_size: DEFAULT_WIN_SIZE,
plugins: HashMap::default(),
}
}
}
@ -91,42 +65,10 @@ impl Config {
}
fn from_path(path: &Path) -> Self {
let mut this: Self = File::open(path)
File::open(path)
.ok()
.and_then(|file| serde_json::from_reader(BufReader::new(file)).ok())
.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
.unwrap_or_default()
}
fn save_to_path(&self, path: &Path) -> Result<(), serde_json::Error> {

View file

@ -16,6 +16,10 @@ pub const LOCALE_DIR: &str = "@LOCALEDIR@";
pub const BUILD_PROFILE: &str = "@PROFILE@";
pub const BUILD_DATETIME: &str = "@BUILD_DATETIME@";
pub fn get_developers() -> Vec<String> {
vec!["Gabriele Musco <gabmus@disroot.org>".into()]
}
pub fn get_artists() -> Vec<String> {
vec!["App Icon: Yannick (@Yandr)".into()]
}

View file

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

View file

@ -16,7 +16,6 @@ pub enum DepType {
Executable,
Include,
UdevRule,
Share,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -50,7 +49,6 @@ 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);
@ -147,10 +145,6 @@ fn udev_rules_paths() -> Vec<String> {
vec!["/usr/lib/udev/rules.d".into()]
}
fn share_paths() -> Vec<String> {
vec!["/usr/share".into()]
}
#[cfg(test)]
mod tests {
use super::{DepType, Dependency};

View file

@ -30,30 +30,6 @@ fn monado_deps() -> Vec<Dependency> {
(LinuxDistro::Suse, "wayland-devel".into()),
]),
},
Dependency {
name: "wayland-protocols".into(),
dep_type: DepType::Share,
filename: "wayland-protocols/staging/drm-lease/drm-lease-v1.xml".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "wayland-protocols".into()),
(LinuxDistro::Debian, "wayland-protocols".into()),
(LinuxDistro::Fedora, "wayland-protocols-devel".into()),
(LinuxDistro::Gentoo, "dev-libs/wayland-protocols".into()),
(LinuxDistro::Suse, "wayland-protocols-devel".into()),
]),
},
Dependency {
name: "libbsd".into(),
dep_type: DepType::SharedObject,
filename: "libbsd.so".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "libbsd".into()),
(LinuxDistro::Debian, "libbsd-dev".into()),
(LinuxDistro::Fedora, "libbsd-devel".into()),
(LinuxDistro::Gentoo, "dev-libs/libbsd".into()),
(LinuxDistro::Suse, "libbsd-devel".into()),
]),
},
dep_cmake(),
dep_eigen(),
dep_git(),

View file

@ -78,15 +78,15 @@ fn wivrn_deps() -> Vec<Dependency> {
]),
},
Dependency {
name: "libpipewire-dev".into(),
name: "libpulse-dev".into(),
dep_type: DepType::Include,
filename: "pipewire-0.3/pipewire/pipewire.h".into(),
filename: "pulse/context.h".into(),
packages: HashMap::from([
(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()),
(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()),
]),
},
dep_eigen(),
@ -169,10 +169,7 @@ 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,
"libgstreamer-plugins-base1.0-dev".into(),
),
(LinuxDistro::Debian, "libgstreamer1.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()),
@ -238,30 +235,6 @@ fn wivrn_deps() -> Vec<Dependency> {
(LinuxDistro::Suse, "glib2-devel".into()),
]),
},
Dependency {
name: "openssl-dev".into(),
dep_type: DepType::Include,
filename: "openssl/ssl3.h".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "openssl".into()),
(LinuxDistro::Alpine, "openssl-dev".into()),
(LinuxDistro::Debian, "libssl-dev".into()),
(LinuxDistro::Fedora, "openssl-devel".into()),
(LinuxDistro::Suse, "openssl-devel".into()),
]),
},
Dependency {
name: "libnotify-dev".into(),
dep_type: DepType::Include,
filename: "libnotify/notify.h".into(),
packages: HashMap::from([
(LinuxDistro::Arch, "libnotify".into()),
(LinuxDistro::Alpine, "libnotify-dev".into()),
(LinuxDistro::Debian, "libnotify-dev".into()),
(LinuxDistro::Fedora, "libnotify-devel".into()),
(LinuxDistro::Suse, "libnotify-devel".into()),
]),
},
]
}

View file

@ -3,7 +3,7 @@ use lazy_static::lazy_static;
fn env_var_descriptions() -> Vec<(&'static str, &'static str)> {
vec![
(
"XRT_COMPOSITOR_SCALE_PERCENTAGE",
"XRT_COMPOSITOR_SCALE_PECENTAGE",
"Render resolution percentage. A percentage higher than the native resolution (>100) will help with antialiasing and image clarity."
),
(

View file

@ -1,17 +1,14 @@
use crate::{
paths::SYSTEM_PREFIX,
paths::{get_backup_dir, SYSTEM_PREFIX},
profile::Profile,
util::file_utils::{deserialize_file, get_writer, set_file_readonly},
util::file_utils::{copy_file, deserialize_file, get_writer, set_file_readonly},
xdg::XDG,
};
use anyhow::bail;
use serde::{Deserialize, Serialize};
use std::{
fs::{create_dir_all, remove_file, rename},
os::unix::fs::symlink,
fs::remove_file,
path::{Path, PathBuf},
};
use tracing::{debug, warn};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActiveRuntimeInnerRuntime {
@ -37,6 +34,29 @@ 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)
}
@ -58,6 +78,29 @@ 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!(
@ -94,67 +137,18 @@ 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();
if dest.is_dir() {
bail!("{} is a directory", dest.to_string_lossy());
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_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");
}
dump_current_active_runtime(&ar)?;
set_file_readonly(&dest, true)?;
Ok(())
}

View file

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

View file

@ -1,3 +1,5 @@
use std::path::{Path, PathBuf};
use crate::{
paths::get_backup_dir,
profile::Profile,
@ -5,7 +7,6 @@ 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 {
@ -85,7 +86,6 @@ 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,26 @@ pub fn build_profile_openvrpaths(profile: &Profile) -> OpenVrPaths {
external_drivers: None,
jsonid: "vrpathreg".into(),
log: vec![datadir.join("Steam/logs")],
runtime: vec![profile.ovr_comp.runtime_dir()],
runtime: vec![profile.opencomposite_path.join("build")],
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 super::{dump_openvrpaths_to_path, get_openvrpaths_from_path, OpenVrPaths};
use std::path::Path;
use super::{dump_openvrpaths_to_path, get_openvrpaths_from_path, OpenVrPaths};
#[test]
fn can_read_openvrpaths_vrpath_steamvr() {
let ovrp = get_openvrpaths_from_path(Path::new("./test/files/openvrpaths.vrpath")).unwrap();

View file

@ -1,13 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WayVrDashboardConfigFragmentInner {
pub exec: String,
pub args: Option<String>,
pub env: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WayVrDashboardConfigFragment {
pub dashboard: WayVrDashboardConfigFragmentInner,
}

View file

@ -49,13 +49,13 @@ impl LinuxDistro {
Ok(_) if buf.starts_with("PRETTY_NAME=\"") => {
return buf
.split('=')
.next_back()
.last()
.map(|b| b.trim().trim_matches('"').trim().to_string());
}
Ok(_) if buf.starts_with("NAME=\"") => {
name = buf
.split('=')
.next_back()
.last()
.map(|b| b.trim().trim_matches('"').trim().to_string());
}
_ => {}
@ -79,7 +79,7 @@ impl LinuxDistro {
{
let name = buf
.split('=')
.next_back()
.last()
.unwrap_or_default()
.trim()
.trim_matches('"')

View file

@ -1,11 +1,10 @@
use anyhow::Result;
use constants::{resources, APP_ID, APP_NAME, GETTEXT_PACKAGE, LOCALE_DIR, RESOURCES_BASE_PATH};
use file_builders::{
active_runtime_json::restore_active_runtime_backup,
active_runtime_json::{get_current_active_runtime, set_current_active_runtime_to_steam},
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::*},
@ -13,10 +12,6 @@ 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,
@ -27,7 +22,6 @@ 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;
@ -52,15 +46,22 @@ 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 Err(e) = restore_active_runtime_backup() {
warn!("failed to restore active runtime to steam: {e}");
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 Some(ovrp) = openvrpaths {
if !file_builders::openvrpaths_vrpath::is_steam(&ovrp) {
if let Err(e) = set_current_openvrpaths_to_steam() {
warn!("failed to restore openvrpaths to steam: {e}");
}
match set_current_openvrpaths_to_steam() {
Ok(_) => {}
Err(e) => eprintln!("Warning: failed to restore openvrpaths to steam: {e}"),
};
}
}
restore_runtime_entrypoint();
@ -72,24 +73,6 @@ fn main() -> Result<()> {
}
restore_steam_xr_files();
let rolling_log_writer = tracing_appender::rolling::daily(get_logs_dir(), "log");
let (non_blocking_appender, _appender_guard) =
tracing_appender::non_blocking(rolling_log_writer);
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer().pretty().with_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
),
)
.with(
tracing_subscriber::fmt::layer()
.json()
.with_writer(non_blocking_appender),
)
.init();
// Prepare i18n
gettextrs::setlocale(LocaleCategory::LcAll, "");
gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALE_DIR).expect("Unable to bind the text domain");

View file

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

View file

@ -1,6 +1,4 @@
use anyhow::bail;
use crate::{constants::CMD_NAME, util::steam_library_folder::SteamLibraryFolder, xdg::XDG};
use crate::{constants::CMD_NAME, xdg::XDG};
use std::{
env,
fs::create_dir_all,
@ -56,10 +54,6 @@ 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() {
@ -89,26 +83,7 @@ pub fn get_exec_prefix() -> PathBuf {
.into()
}
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")
pub fn get_steamvr_bin_dir_path() -> PathBuf {
XDG.get_data_home()
.join("Steam/steamapps/common/SteamVR/bin/linux64")
}

View file

@ -4,9 +4,8 @@ use crate::{
mercury_deps::get_missing_mercury_deps, monado_deps::get_missing_monado_deps,
openhmd_deps::get_missing_openhmd_deps, wivrn_deps::get_missing_wivrn_deps, Dependency,
},
file_builders::active_runtime_json::ActiveRuntime,
paths::{get_data_dir, BWRAP_SYSTEM_PREFIX, SYSTEM_PREFIX},
util::file_utils::{deserialize_file, get_writer},
util::file_utils::get_writer,
xdg::XDG,
};
use nix::NixPath;
@ -18,7 +17,6 @@ use std::{
io::BufReader,
path::{Path, PathBuf},
slice::Iter,
str::FromStr,
};
use uuid::Uuid;
@ -45,14 +43,7 @@ impl XRServiceType {
pub fn libmonado_path(&self) -> &'static str {
match self {
Self::Monado => "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",
Self::Wivrn => "wivrn/libmonado.so",
}
}
@ -260,97 +251,6 @@ 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,
@ -361,15 +261,9 @@ 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
@ -382,6 +276,7 @@ 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,
}
@ -393,7 +288,6 @@ 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);
@ -429,27 +323,23 @@ impl Default for Profile {
mercury_enabled: false,
},
environment: HashMap::new(),
prefix: Self::default_prefix_path(&uuid),
prefix: get_data_dir().join("prefixes").join(&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\"",
@ -468,8 +358,8 @@ impl Profile {
pub fn env_vars_full(&self) -> Vec<String> {
vec![
// format!(
// "VR_OVERRIDE={}",
// self.ovr_comp.runtime_dir(),
// "VR_OVERRIDE={opencomp}/build",
// opencomp = self.opencomposite_path,
// ),
self.xr_runtime_json_env_var(),
format!(
@ -527,8 +417,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(),
@ -560,6 +450,7 @@ 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(),
@ -567,16 +458,7 @@ impl Profile {
opencomposite_path: profile_dir.join("opencomposite"),
skip_dependency_check: self.skip_dependency_check,
xrservice_launch_options: self.xrservice_launch_options.clone(),
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,
..Default::default()
};
if dup.environment.contains_key("LD_LIBRARY_PATH") {
dup.environment.insert(
@ -662,37 +544,21 @@ impl Profile {
}
/// absolute path to a given shared object in the profile prefix
pub fn find_so<P: AsRef<Path>>(&self, rel_path: P) -> Option<PathBuf> {
pub fn find_so(&self, rel_path: &str) -> Option<PathBuf> {
["lib", "lib64"]
.into_iter()
.map(|lib| self.prefix.join(lib).join(rel_path.as_ref()))
.map(|lib| self.prefix.join(lib).join(rel_path))
.find(|path| path.is_file())
}
/// absolute path to the libmonado shared object
pub fn libmonado_so(&self) -> Option<PathBuf> {
// 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())
self.find_so(self.xrservice_type.libmonado_path())
}
/// absolute path to the libopenxr shared object
pub fn libopenxr_so(&self) -> Option<PathBuf> {
// 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()))
self.find_so(self.xrservice_type.libopenxr_path())
}
pub fn missing_dependencies(&self) -> Vec<Dependency> {
@ -720,12 +586,6 @@ impl Profile {
missing_deps.dedup(); // dedup only works if sorted, hence the above
missing_deps
}
/// the file that will become active_runtime.json, as installed in the
/// prefix
pub fn openxr_json_path(&self) -> PathBuf {
self.prefix.join(self.xrservice_type.openxr_json_rel_path())
}
}
pub fn prepare_ld_library_path(prefix: &Path) -> String {
@ -739,10 +599,7 @@ mod tests {
path::{Path, PathBuf},
};
use crate::profile::{
OvrCompatibilityModuleType, ProfileFeature, ProfileFeatureType, ProfileFeatures,
ProfileOvrCompatibilityModule, XRServiceType,
};
use crate::profile::{ProfileFeature, ProfileFeatureType, ProfileFeatures, XRServiceType};
use super::Profile;
@ -752,7 +609,7 @@ mod tests {
assert_eq!(profile.name, "Demo profile");
assert_eq!(profile.xrservice_path, PathBuf::from("/home/user/monado"));
assert_eq!(
profile.ovr_comp.path,
profile.opencomposite_path,
PathBuf::from("/home/user/opencomposite")
);
assert_eq!(profile.prefix, PathBuf::from("/home/user/envisionprefix"));
@ -783,12 +640,7 @@ mod tests {
name: "Demo profile".into(),
xrservice_path: PathBuf::from("/home/user/monado"),
xrservice_type: XRServiceType::Monado,
ovr_comp: ProfileOvrCompatibilityModule {
path: PathBuf::from("/home/user/opencomposite"),
repo: None,
branch: None,
mod_type: OvrCompatibilityModuleType::default(),
},
opencomposite_path: PathBuf::from("/home/user/opencomposite"),
features: ProfileFeatures {
libsurvive: ProfileFeature {
feature_type: ProfileFeatureType::Libsurvive,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,33 +1,75 @@
use crate::{
paths::get_backup_dir,
paths::{get_backup_dir, get_home_dir},
profile::Profile,
util::{
file_utils::{copy_file, get_writer},
steam_library_folder::SteamLibraryFolder,
},
util::file_utils::{copy_file, get_writer},
};
use anyhow::bail;
use lazy_static::lazy_static;
use serde::Deserialize;
use std::{
collections::HashMap,
fs::read_to_string,
io::Write,
path::{Path, PathBuf},
};
use tracing::error;
#[derive(Deserialize)]
struct LibraryFolder {
pub path: String,
pub apps: HashMap<u32, usize>,
}
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 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")
}),
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")
})
}
Err(e) => {
error!("unable to get runtime entrypoint path: {e}");
eprintln!("Error getting steam root path: {e}");
None
}
}
@ -83,3 +125,22 @@ pub fn set_runtime_entrypoint_launch_opts_from_profile(profile: &Profile) -> any
}
bail!("Could not find valid runtime entrypoint");
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::parse_steam_libraryfolders_vdf;
#[test]
fn deserialize_steam_libraryfolders_vdf() {
let lf = parse_steam_libraryfolders_vdf(Path::new("./test/files/steam_libraryfolders.vdf"))
.unwrap();
assert_eq!(lf.len(), 1);
let first = lf.get(&0).unwrap();
assert_eq!(first.path, "/home/gabmus/.local/share/Steam");
assert_eq!(first.apps.len(), 10);
assert_eq!(first.apps.get(&228980).unwrap(), &29212173);
assert_eq!(first.apps.get(&632360).unwrap(), &0);
}
}

View file

@ -1,7 +1,7 @@
use crate::{
constants::{
get_artists, APP_ID, APP_NAME, BUILD_DATETIME, ISSUES_URL, REPO_URL, SINGLE_DEVELOPER,
VERSION,
get_artists, get_developers, APP_ID, APP_NAME, BUILD_DATETIME, ISSUES_URL, REPO_URL,
SINGLE_DEVELOPER, VERSION,
},
device_prober::PhysicalXRDevice,
linux_distro::LinuxDistro,
@ -20,20 +20,13 @@ pub fn create_about_dialog() -> adw::AboutDialog {
.website(REPO_URL)
.issue_url(ISSUES_URL)
.developer_name(SINGLE_DEVELOPER)
.developers(
env!("CARGO_PKG_AUTHORS")
.split(':')
.map(|s| s.to_string())
.collect::<Vec<String>>(),
)
.developers(get_developers())
.artists(get_artists())
.build()
}
const UNKNOWN: &str = "UNKNOWN";
pub fn populate_debug_info(dialog: &adw::AboutDialog, vkinfo: Option<&VulkanInfo>) {
if !dialog.debug_info().is_empty() {
if dialog.debug_info().len() > 0 {
return;
}
let distro_family = LinuxDistro::get();
@ -44,10 +37,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: {}",
@ -57,29 +50,23 @@ 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())
),
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())
env::var("XDG_CURRENT_DESKTOP").unwrap_or("unknown".into())
),
format!(
"GPUs: {}",
vkinfo
.map(|i| i.gpu_names.join(", "))
.unwrap_or(UNKNOWN.into())
.unwrap_or("unknown".into())
),
format!(
"Monado Vulkan Layers: {}",
vkinfo
.map(|i| i.has_monado_vulkan_layers.to_string())
.unwrap_or("unknown".into())
),
format!("Detected XR Devices: {}", {
let devs = PhysicalXRDevice::from_usb();

View file

@ -11,7 +11,6 @@ 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},
};
@ -20,16 +19,14 @@ 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_vapor::get_build_vapor_jobs, build_wivrn::get_build_wivrn_jobs,
build_xrizer::get_build_xrizer_jobs,
build_wivrn::get_build_wivrn_jobs,
},
config::{Config, PluginConfig},
config::Config,
constants::APP_NAME,
depcheck::common::dep_pkexec,
file_builders::{
active_runtime_json::{
remove_current_active_runtime, restore_active_runtime_backup,
set_current_active_runtime_to_profile,
set_current_active_runtime_to_profile, set_current_active_runtime_to_steam,
},
openvrpaths_vrpath::{
set_current_openvrpaths_to_profile, set_current_openvrpaths_to_steam,
@ -38,14 +35,12 @@ use crate::{
linux_distro::LinuxDistro,
openxr_prober::is_openxr_ready,
paths::get_data_dir,
profile::{OvrCompatibilityModuleType, Profile, XRServiceType},
profile::{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, verify_cap_sys_nice_eip,
},
util::file_utils::{setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd},
vulkaninfo::VulkanInfo,
wivrn_dbus,
xr_devices::XRDevice,
@ -58,12 +53,7 @@ use relm4::{
new_action_group, new_stateful_action, new_stateless_action,
prelude::*,
};
use std::{
collections::{HashMap, VecDeque},
fs::remove_file,
time::Duration,
};
use tracing::error;
use std::{collections::VecDeque, fs::remove_file, time::Duration};
pub struct App {
application: adw::Application,
@ -80,7 +70,7 @@ pub struct App {
config: Config,
xrservice_worker: Option<JobWorker>,
plugins_worker: Option<JobWorker>,
autostart_worker: Option<JobWorker>,
restart_xrservice: bool,
build_worker: Option<JobWorker>,
profiles: Vec<Profile>,
@ -95,14 +85,13 @@ pub struct App {
vkinfo: Option<VulkanInfo>,
inhibit_fail_notif: Option<NotificationHandle>,
pluginstore: Option<AsyncController<PluginStore>>,
}
#[derive(Debug)]
pub enum Msg {
OnServiceLog(Vec<String>),
OnServiceExit(i32),
OnPluginsExit(i32),
OnAutostartExit(i32),
OnBuildLog(Vec<String>),
OnBuildExit(i32),
ClockTicking,
@ -127,8 +116,6 @@ pub enum Msg {
StartProber,
OnProberExit(bool),
WivrnCheckPairMode,
OpenPluginStore,
UpdateConfigPlugins(HashMap<String, PluginConfig>),
NoOp,
}
@ -160,7 +147,7 @@ impl App {
} {
Ok(n) => Some(n),
Err(e) => {
error!("failed to send desktop notification: {e:?}");
eprintln!("Failed to send desktop notification: {e:?}");
None
}
}
@ -176,7 +163,57 @@ 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 prof.can_start() {
if let Err(e) = set_current_active_runtime_to_profile(&prof) {
alert(
"Failed to start XR Service",
Some(&format!(
"Error setting current active runtime to profile: {e}"
)),
Some(&self.app_win.clone().upcast::<gtk::Window>()),
);
return;
}
if let Err(e) = set_current_openvrpaths_to_profile(&prof) {
alert(
"Failed to start XR Service",
Some(&format!(
"Error setting current openvrpaths file to profile: {e}"
)),
Some(&self.app_win.clone().upcast::<gtk::Window>()),
);
return;
};
self.debug_view.sender().emit(DebugViewMsg::ClearLog);
self.xr_devices = vec![];
remove_file(prof.xrservice_type.ipc_file_path())
.is_err()
.then(|| println!("Failed to remove xrservice IPC file"));
let worker = JobWorker::xrservice_worker_wrap_from_profile(
&prof,
sender.input_sender(),
|msg| match msg {
JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows),
JobWorkerOut::Exit(code) => Msg::OnServiceExit(code),
},
debug,
);
worker.start();
self.xrservice_worker = Some(worker);
self.main_view
.sender()
.emit(MainViewMsg::XRServiceActiveChanged(
true,
Some(self.get_selected_profile()),
// show launch opts only if setting the runtime entrypoint fails
set_runtime_entrypoint_launch_opts_from_profile(&prof).is_err(),
));
self.debug_view
.sender()
.emit(DebugViewMsg::XRServiceActiveChanged(true));
self.set_inhibit_session(true);
sender.input(Msg::StartProber);
} else {
alert(
"Failed to start profile",
Some(concat!(
@ -185,133 +222,32 @@ 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();
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() {
if let Some(autostart_cmd) = &prof.autostart_command {
let mut jobs = VecDeque::new();
jobs.push_back(WorkerJob::new_cmd(
Some(prof.environment.clone()),
"sh".into(),
Some(vec!["-c".into(), plugins_cmd]),
Some(vec!["-c".into(), autostart_cmd.clone()]),
));
let plugins_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
let autostart_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows),
JobWorkerOut::Exit(code) => Msg::OnPluginsExit(code),
JobWorkerOut::Exit(code) => Msg::OnAutostartExit(code),
});
plugins_worker.start();
self.plugins_worker = Some(plugins_worker);
autostart_worker.start();
self.autostart_worker = Some(autostart_worker);
}
}
pub fn restore_openxr_openvr_files(&self) {
restore_runtime_entrypoint();
if let Err(e) = remove_current_active_runtime() {
if let Err(e) = set_current_active_runtime_to_steam() {
alert(
"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",
"Could not restore Steam active runtime",
Some(&format!("{e}")),
Some(&self.app_win.clone().upcast::<gtk::Window>()),
);
@ -326,17 +262,27 @@ impl App {
}
pub fn shutdown_xrservice(&mut self) {
if let Some(w) = self.plugins_worker.as_ref() {
w.stop();
if let Some(worker) = self.autostart_worker.as_ref() {
worker.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;
}
if let Some(w) = self.xrservice_worker.as_ref() {
w.stop();
self.set_inhibit_session(false);
if let Some(worker) = self.xrservice_worker.as_ref() {
worker.stop();
}
self.libmonado = None;
self.main_view
.sender()
.emit(MainViewMsg::XRServiceActiveChanged(false, None, false));
self.debug_view
.sender()
.emit(DebugViewMsg::XRServiceActiveChanged(false));
self.xr_devices = vec![];
}
}
@ -402,8 +348,6 @@ 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()
@ -411,8 +355,6 @@ 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!(
@ -427,7 +369,7 @@ impl AsyncComponent for App {
self.start_xrservice(sender, false);
}
}
Msg::OnPluginsExit(_) => self.plugins_worker = None,
Msg::OnAutostartExit(_) => self.autostart_worker = None,
Msg::ClockTicking => {
self.main_view.sender().emit(MainViewMsg::ClockTicking);
let xrservice_worker_is_alive = self
@ -467,7 +409,7 @@ impl AsyncComponent for App {
.emit(MainViewMsg::SetWivrnSupportsPairing(true));
}
Err(e) => {
error!("failed to get wivrn pairing mode: {e:?}");
eprintln!("Error: failed to get wivrn pairing mode: {e:?}");
self.main_view
.sender()
.emit(MainViewMsg::SetWivrnSupportsPairing(false));
@ -531,17 +473,7 @@ impl AsyncComponent for App {
XRServiceType::Wivrn => get_build_wivrn_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)
}
});
jobs.extend(get_build_opencomposite_jobs(&profile, clean_build));
let missing_deps = profile.missing_dependencies();
if !(self.skip_depcheck || profile.skip_dependency_check || missing_deps.is_empty())
{
@ -698,32 +630,13 @@ 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() {
error!("pkexec not found, skipping setcap");
println!("pkexec not found, skipping setcap");
} else {
let profile = self.get_selected_profile();
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();
}
setcap_cap_sys_nice_eip(&profile).await;
}
}
Msg::ProfileSelected(prof) => {
@ -843,21 +756,6 @@ 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();
}
}
}
@ -959,17 +857,6 @@ 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 = {
@ -991,7 +878,7 @@ impl AsyncComponent for App {
match VulkanInfo::get() {
Ok(info) => Some(info),
Err(e) => {
error!("failed to get Vulkan info: {e:#?}");
eprintln!("Failed to get Vulkan info: {e:#?}");
None
}
}
@ -1006,6 +893,7 @@ 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,
@ -1014,7 +902,6 @@ 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()
@ -1041,7 +928,7 @@ impl AsyncComponent for App {
config,
profiles,
xrservice_worker: None,
plugins_worker: None,
autostart_worker: None,
build_worker: None,
xr_devices: vec![],
restart_xrservice: false,
@ -1052,7 +939,6 @@ impl AsyncComponent for App {
openxr_prober_worker: None,
xrservice_ready: false,
inhibit_fail_notif: None,
pluginstore: None,
};
let widgets = view_output!();
@ -1125,7 +1011,6 @@ new_stateless_action!(pub BuildProfileCleanAction, AppActionGroup, "buildprofile
new_stateless_action!(pub QuitAction, AppActionGroup, "quit");
new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool);
new_stateless_action!(pub ConfigureWivrnAction, AppActionGroup, "configurewivrn");
new_stateless_action!(pub PluginStoreAction, AppActionGroup, "store");
new_stateless_action!(pub DebugOpenDataAction, AppActionGroup, "debugopendata");
new_stateless_action!(pub DebugOpenPrefixAction, AppActionGroup, "debugopenprefix");

View file

@ -88,54 +88,43 @@ impl SimpleComponent for BuildWindow {
gtk::Label {
#[track = "model.changed(BuildWindow::build_status())"]
set_markup: match &model.build_status {
BuildStatus::Building => String::default(),
BuildStatus::Done => "Build done, you can close this window".into(),
BuildStatus::Building => "Build in progress...".to_string(),
BuildStatus::Done => "Build done, you can close this window".to_string(),
BuildStatus::Error(code) => {
format!("Build failed: \"{c}\"", c = code)
}
}.as_str(),
#[track = "model.changed(BuildWindow::build_status())"]
set_visible: match &model.build_status {
BuildStatus::Building => false,
BuildStatus::Done | BuildStatus::Error(_) => true,
},
add_css_class: "title-2",
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
set_justify: gtk::Justification::Center,
},
gtk::Button {
#[track = "model.changed(BuildWindow::build_status())"]
set_visible: matches!(&model.build_status, BuildStatus::Building),
add_css_class: "destructive-action",
add_css_class: "circular",
set_icon_name: "window-close-symbolic",
set_tooltip_text: Some("Cancel build"),
connect_clicked[sender] => move |_| {
sender.output(Self::Output::CancelBuild).expect(SENDER_IO_ERR_MSG);
}
},
},
model.term.container.clone(),
},
add_bottom_bar: bottom_bar = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
add_bottom_bar: bottom_bar = &gtk::Button {
add_css_class: "pill",
set_halign: gtk::Align::Center,
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();
},
set_label: "Close",
set_margin_all: 12,
#[track = "model.changed(BuildWindow::can_close())"]
set_sensitive: model.can_close,
connect_clicked[win] => move |_| {
win.close();
},
// this
gtk::Button {
#[track = "model.changed(BuildWindow::build_status())"]
set_visible: matches!(&model.build_status, BuildStatus::Building),
add_css_class: "destructive-action",
add_css_class: "pill",
set_label: "Cancel build",
connect_clicked[sender] => move |_| {
sender.output(Self::Output::CancelBuild).expect(SENDER_IO_ERR_MSG);
}
},
// ^^^
},
}
}
}
}

View file

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

View file

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

View file

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

View file

@ -155,7 +155,7 @@ impl Worker for InternalJobWorker {
}
}
pub const LAUNCH_OPTS_CMD_PLACEHOLDER: &str = "%command%";
const LAUNCH_OPTS_CMD_PLACEHOLDER: &str = "%command%";
impl InternalJobWorker {
pub fn xrservice_worker_from_profile(
@ -193,6 +193,9 @@ impl InternalJobWorker {
} else {
launch_opts
};
if !launch_opts.contains(" --no-instructions") {
launch_opts.push_str(" --no-instructions");
}
let (command, args) = match launch_opts.is_empty() {
false => (
"sh".into(),

View file

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

View file

@ -2,7 +2,7 @@ use super::{
alert::alert,
app::{
AboutAction, BuildProfileAction, BuildProfileCleanAction, ConfigureWivrnAction,
DebugViewToggleAction, PluginStoreAction,
DebugViewToggleAction,
},
devices_box::{DevicesBox, DevicesBoxMsg},
install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg},
@ -12,7 +12,6 @@ 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,
@ -25,6 +24,7 @@ use crate::{
file_utils::{get_writer, mount_has_nosuid},
steamvr_utils::chaperone_info_exists,
},
vulkaninfo::VulkanInfo,
wivrn_dbus,
xr_devices::XRDevice,
};
@ -36,7 +36,6 @@ use relm4::{
prelude::*,
};
use std::{fs::read_to_string, io::Write};
use tracing::{error, warn};
#[tracker::track]
pub struct MainView {
@ -60,8 +59,6 @@ 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>,
@ -74,6 +71,8 @@ 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,
@ -104,7 +103,6 @@ pub enum MainViewMsg {
SetWivrnPairingMode(bool),
StopWivrnPairingMode,
StartWivrnPairingMode,
QueryProfileRebuild,
}
#[derive(Debug)]
@ -115,14 +113,13 @@ 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 {
@ -150,7 +147,6 @@ impl AsyncComponent for MainView {
menu! {
app_menu: {
section! {
"Plugin_s" => PluginStoreAction,
// value inside action is ignored
"_Debug View" => DebugViewToggleAction,
"_Build Profile" => BuildProfileAction,
@ -393,8 +389,8 @@ impl AsyncComponent for MainView {
set_visible: match mount_has_nosuid(&model.selected_profile.prefix) {
Ok(b) => b,
Err(_) => {
warn!(
"nosuid detection: could not get stat on path {}",
eprintln!(
"Warning (nosuid detection): could not get stat on path {}",
model.selected_profile.prefix.to_string_lossy());
false
},
@ -449,7 +445,35 @@ impl AsyncComponent for MainView {
set_label: concat!(
"SteamVR room configuration not found.\n",
"To use the SteamVR lighthouse driver, you ",
"will need to run SteamVR Quick Calibration.",
"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."
),
add_css_class: "warning",
set_xalign: 0.0,
@ -603,7 +627,7 @@ impl AsyncComponent for MainView {
self.set_wivrn_pin(Some(pin));
}
Err(e) => {
error!("failed to get wivrn pairing pin: {e}");
eprintln!("Error: failed to get wivrn pairing pin: {e:?}");
}
};
} else {
@ -613,12 +637,12 @@ impl AsyncComponent for MainView {
}
Self::Input::StopWivrnPairingMode => {
if let Err(e) = wivrn_dbus::disable_pairing().await {
error!("failed to stop wivrn pairing mode: {e}");
eprintln!("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}");
eprintln!("Error: failed to start wivrn pairing mode: {e:?}");
}
}
Self::Input::StartStopClicked => {
@ -696,10 +720,6 @@ impl AsyncComponent for MainView {
}
}));
}
Self::Input::QueryProfileRebuild => {
self.query_profile_rebuild_dialog
.present(Some(&self.root_win));
}
Self::Input::SetSelectedProfile(index) => {
self.profiles_dropdown
.as_ref()
@ -737,7 +757,7 @@ impl AsyncComponent for MainView {
Self::Input::SaveProfile(prof) => {
sender
.output(Self::Output::SaveProfile(prof))
.expect(SENDER_IO_ERR_MSG);
.expect("Sender output failed");
}
Self::Input::DuplicateProfile => {
if self.selected_profile.can_be_built {
@ -906,29 +926,6 @@ impl AsyncComponent for MainView {
),
);
let query_profile_rebuild_dialog = adw::AlertDialog::builder()
.heading("Do you want to build this profile now?")
.body("This will trigger a clean build")
.build();
query_profile_rebuild_dialog.add_response("no", "_No");
query_profile_rebuild_dialog.add_response("yes", "_Yes");
query_profile_rebuild_dialog.set_response_appearance("yes", ResponseAppearance::Suggested);
query_profile_rebuild_dialog.connect_response(
None,
clone!(
#[strong]
sender,
move |_, res| {
if res == "yes" {
sender
.output(Self::Output::BuildProfile(true))
.expect(SENDER_IO_ERR_MSG);
}
}
),
);
let profile_delete_confirm_dialog = adw::AlertDialog::builder()
.heading("Are you sure you want to delete this profile?")
.build();
@ -1063,7 +1060,6 @@ impl AsyncComponent for MainView {
selected_profile: init.selected_profile.clone(),
profile_not_editable_dialog,
profile_delete_confirm_dialog,
query_profile_rebuild_dialog,
root_win: init.root_win.clone(),
steamvr_calibration_box,
openhmd_calibration_box,
@ -1071,6 +1067,7 @@ impl AsyncComponent for MainView {
xrservice_ready: false,
profile_delete_action,
profile_export_action,
vkinfo: init.vkinfo,
wivrn_pairing_mode: false,
wivrn_supports_pairing: false,
wivrn_pin: None,

View file

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

View file

@ -3,7 +3,6 @@ use relm4::{
gtk::{self, prelude::*},
ComponentParts, ComponentSender, SimpleComponent,
};
use tracing::{debug, error};
#[tracker::track]
pub struct OpenHmdCalibrationBox {
@ -60,10 +59,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) {
error!("Failed to remove openhmd config: {e}");
eprintln!("Failed to remove openhmd config: {e}");
}
} else {
debug!("trying to delete openhmd calibration config, but file is missing")
println!("info: trying to delete openhmd calibration config, but file is missing")
}
}
},

View file

@ -1,169 +0,0 @@
use std::path::PathBuf;
use crate::{
constants::APP_ID,
ui::{
preference_rows::{entry_row, file_row},
SENDER_IO_ERR_MSG,
},
};
use super::Plugin;
use adw::prelude::*;
use gtk::glib::clone;
use relm4::prelude::*;
#[tracker::track]
pub struct AddCustomPluginWin {
#[tracker::do_not_track]
parent: gtk::Window,
#[tracker::do_not_track]
win: Option<adw::Dialog>,
/// this is true when enough fields are populated, allowing the creation
/// of the plugin object to add
can_add: bool,
#[tracker::do_not_track]
plugin: Plugin,
}
#[derive(Debug)]
pub enum AddCustomPluginWinMsg {
Present,
Close,
OnNameChange(String),
OnExecPathChange(Option<String>),
Add,
}
#[derive(Debug)]
pub enum AddCustomPluginWinOutMsg {
Add(Plugin),
}
#[derive(Debug)]
pub struct AddCustomPluginWinInit {
pub parent: gtk::Window,
}
#[relm4::component(pub)]
impl SimpleComponent for AddCustomPluginWin {
type Init = AddCustomPluginWinInit;
type Input = AddCustomPluginWinMsg;
type Output = AddCustomPluginWinOutMsg;
view! {
#[name(win)]
adw::Dialog {
set_can_close: true,
#[wrap(Some)]
set_child: inner = &adw::ToolbarView {
set_top_bar_style: adw::ToolbarStyle::Flat,
set_bottom_bar_style: adw::ToolbarStyle::Flat,
set_vexpand: true,
set_hexpand: true,
add_top_bar: top_bar = &adw::HeaderBar {
set_show_end_title_buttons: false,
set_show_start_title_buttons: false,
pack_start: cancel_btn = &gtk::Button {
set_label: "Cancel",
add_css_class: "destructive-action",
connect_clicked[sender] => move |_| {
sender.input(Self::Input::Close)
},
},
pack_end: add_btn = &gtk::Button {
set_label: "Add",
add_css_class: "suggested-action",
#[track = "model.changed(AddCustomPluginWin::can_add())"]
set_sensitive: model.can_add,
connect_clicked[sender] => move |_| {
sender.input(Self::Input::Add)
},
},
#[wrap(Some)]
set_title_widget: title_label = &adw::WindowTitle {
set_title: "Add Custom Plugin",
},
},
#[wrap(Some)]
set_content: content = &adw::PreferencesPage {
set_hexpand: true,
set_vexpand: true,
add: grp = &adw::PreferencesGroup {
add: &entry_row(
"Plugin Name",
"",
clone!(
#[strong] sender,
move |row| sender.input(Self::Input::OnNameChange(row.text().to_string()))
)
),
add: &file_row(
"Plugin Executable",
None,
None,
Some(model.parent.clone()),
clone!(
#[strong] sender,
move |path_s| sender.input(Self::Input::OnExecPathChange(path_s))
)
)
},
},
},
}
}
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
self.reset();
match message {
Self::Input::Present => self.win.as_ref().unwrap().present(Some(&self.parent)),
Self::Input::Close => {
self.win.as_ref().unwrap().close();
}
Self::Input::Add => {
if self.plugin.validate() {
sender
.output(Self::Output::Add(self.plugin.clone()))
.expect(SENDER_IO_ERR_MSG);
self.win.as_ref().unwrap().close();
}
}
Self::Input::OnNameChange(name) => {
self.plugin.appid = if !name.is_empty() {
format!("{APP_ID}.customPlugin.{name}")
} else {
String::default()
};
self.plugin.name = name;
self.set_can_add(self.plugin.validate());
}
Self::Input::OnExecPathChange(ep) => {
self.plugin.exec_path = ep.map(PathBuf::from);
self.set_can_add(self.plugin.validate());
}
}
}
fn init(
init: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let mut model = Self {
tracker: 0,
win: None,
parent: init.parent,
can_add: false,
plugin: Plugin {
short_description: Some("Custom Plugin".into()),
..Default::default()
},
};
let widgets = view_output!();
model.win = Some(widgets.win.clone());
ComponentParts { model, widgets }
}
}

View file

@ -1,189 +0,0 @@
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)
}

View file

@ -1,552 +0,0 @@
use super::{
add_custom_plugin_win::{
AddCustomPluginWin, AddCustomPluginWinInit, AddCustomPluginWinMsg, AddCustomPluginWinOutMsg,
},
refresh_plugins,
store_detail::{StoreDetail, StoreDetailMsg, StoreDetailOutMsg},
store_row_factory::{StoreRowModel, StoreRowModelInit, StoreRowModelMsg, StoreRowModelOutMsg},
Plugin,
};
use crate::{
config::PluginConfig,
downloader::download_file_async,
ui::{alert::alert, SENDER_IO_ERR_MSG},
};
use adw::prelude::*;
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
use std::{collections::HashMap, fs::remove_file};
use tracing::{debug, error};
#[tracker::track]
pub struct PluginStore {
#[tracker::do_not_track]
win: Option<adw::Window>,
#[tracker::do_not_track]
plugin_rows: Option<AsyncFactoryVecDeque<StoreRowModel>>,
#[tracker::do_not_track]
details: AsyncController<StoreDetail>,
#[tracker::do_not_track]
main_stack: Option<gtk::Stack>,
#[tracker::do_not_track]
config_plugins: HashMap<String, PluginConfig>,
refreshing: bool,
locked: bool,
plugins: Vec<Plugin>,
#[tracker::do_not_track]
add_custom_plugin_win: Option<Controller<AddCustomPluginWin>>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum PluginStoreSignalSource {
Row,
Detail,
}
#[derive(Debug)]
pub enum PluginStoreMsg {
Present,
/// sets state and calls DoRefresh
Refresh,
/// called by Refresh
DoRefresh,
Install(Plugin),
InstallDownload(Vec<Plugin>),
Remove(Plugin),
SetEnabled(PluginStoreSignalSource, Plugin, bool),
ShowDetails(usize),
ShowPluginList,
PresentAddCustomPluginWin,
AddCustomPlugin(Plugin),
}
#[derive(Debug)]
pub struct PluginStoreInit {
pub config_plugins: HashMap<String, PluginConfig>,
}
#[derive(Debug)]
pub enum PluginStoreOutMsg {
UpdateConfigPlugins(HashMap<String, PluginConfig>),
}
impl PluginStore {
fn refresh_plugin_rows(&mut self) {
let mut guard = self.plugin_rows.as_mut().unwrap().guard();
guard.clear();
self.plugins.iter().for_each(|plugin| {
guard.push_back(StoreRowModelInit {
plugin: plugin.clone(),
enabled: self
.config_plugins
.get(&plugin.appid)
.is_some_and(|cp| cp.enabled),
needs_update: self
.config_plugins
.get(&plugin.appid)
.is_some_and(|cp| cp.plugin.version != plugin.version),
});
});
}
fn add_plugin_to_config(&mut self, sender: &relm4::AsyncComponentSender<Self>, plugin: Plugin) {
self.config_plugins
.insert(plugin.appid.clone(), PluginConfig::from(&plugin));
sender
.output(PluginStoreOutMsg::UpdateConfigPlugins(
self.config_plugins.clone(),
))
.expect(SENDER_IO_ERR_MSG);
}
}
#[relm4::component(pub async)]
impl AsyncComponent for PluginStore {
type Init = PluginStoreInit;
type Input = PluginStoreMsg;
type Output = PluginStoreOutMsg;
type CommandOutput = ();
view! {
#[name(win)]
adw::Window {
set_title: Some("Plugins"),
#[name(main_stack)]
gtk::Stack {
add_child = &adw::ToolbarView {
set_top_bar_style: adw::ToolbarStyle::Flat,
add_top_bar: headerbar = &adw::HeaderBar {
pack_start: add_custom_plugin_btn = &gtk::Button {
set_icon_name: "list-add-symbolic",
set_tooltip_text: Some("Add custom plugin"),
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
set_sensitive: !(model.refreshing || model.locked),
connect_clicked[sender] => move |_| {
sender.input(Self::Input::PresentAddCustomPluginWin)
},
},
pack_end: refreshbtn = &gtk::Button {
set_icon_name: "view-refresh-symbolic",
set_tooltip_text: Some("Refresh"),
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
set_sensitive: !(model.refreshing || model.locked),
connect_clicked[sender] => move |_| {
sender.input(Self::Input::Refresh);
},
},
},
#[wrap(Some)]
set_content: inner = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_vexpand: true,
gtk::Stack {
set_hexpand: true,
set_vexpand: true,
add_child = &gtk::ScrolledWindow {
set_hscrollbar_policy: gtk::PolicyType::Never,
set_hexpand: true,
set_vexpand: true,
adw::Clamp {
#[name(listbox)]
gtk::ListBox {
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
set_sensitive: !(model.refreshing || model.locked),
add_css_class: "boxed-list",
set_valign: gtk::Align::Start,
set_margin_all: 12,
set_selection_mode: gtk::SelectionMode::None,
connect_row_activated[sender] => move |_, row| {
sender.input(
Self::Input::ShowDetails(
row.index() as usize
)
);
},
}
}
} -> {
set_name: "pluginlist"
},
add_child = &gtk::Spinner {
set_hexpand: true,
set_vexpand: true,
set_width_request: 32,
set_height_request: 32,
set_valign: gtk::Align::Center,
set_halign: gtk::Align::Center,
#[track = "model.changed(PluginStore::refreshing())"]
set_spinning: model.refreshing,
} -> {
set_name: "spinner"
},
add_child = &adw::StatusPage {
set_hexpand: true,
set_vexpand: true,
set_title: "No Plugins Found",
set_description: Some("Make sure you're connected to the internet and refresh"),
set_icon_name: Some("application-x-addon-symbolic"),
} -> {
set_name: "emptystate"
},
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::plugins())"]
set_visible_child_name: if model.refreshing {
"spinner"
} else if model.plugins.is_empty() {
"emptystate"
} else {
"pluginlist"
},
},
}
} -> {
set_name: "listview"
},
add_child = &adw::Bin {
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
set_sensitive: !(model.refreshing || model.locked),
set_child: Some(details_view),
} -> {
set_name: "detailsview",
},
set_visible_child_name: "listview",
}
}
}
async fn update(
&mut self,
message: Self::Input,
sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
self.reset();
match message {
Self::Input::Present => {
self.win.as_ref().unwrap().present();
sender.input(Self::Input::Refresh);
}
Self::Input::AddCustomPlugin(plugin) => {
if self.config_plugins.contains_key(&plugin.appid) {
alert(
"Failed to add custom plugin",
Some("A plugin with the same name already exists"),
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()),
);
return;
}
self.add_plugin_to_config(&sender, plugin);
sender.input(Self::Input::Refresh);
}
Self::Input::PresentAddCustomPluginWin => {
let add_win = AddCustomPluginWin::builder()
.launch(AddCustomPluginWinInit {
parent: self.win.as_ref().unwrap().clone().upcast(),
})
.forward(sender.input_sender(), |msg| match msg {
AddCustomPluginWinOutMsg::Add(plugin) => {
Self::Input::AddCustomPlugin(plugin)
}
});
add_win.sender().emit(AddCustomPluginWinMsg::Present);
self.add_custom_plugin_win = Some(add_win);
}
Self::Input::Refresh => {
self.set_refreshing(true);
sender.input(Self::Input::DoRefresh);
}
Self::Input::DoRefresh => {
let mut plugins = match refresh_plugins().await {
Err(e) => {
error!("failed to refresh plugins: {e}");
Vec::new()
}
Ok(results) => results
.into_iter()
.filter_map(|res| match res {
Ok(plugin) => Some(plugin),
Err(e) => {
error!("failed to refresh single plugin manifest: {e}");
None
}
})
.collect(),
};
{
let appids_from_web = plugins
.iter()
.map(|p| p.appid.clone())
.collect::<Vec<String>>();
// add all plugins that are in config but not retrieved
plugins.extend(self.config_plugins.values().filter_map(|cp| {
if appids_from_web.contains(&cp.plugin.appid) {
None
} else {
Some(Plugin::from(cp))
}
}));
}
self.set_plugins(plugins);
self.refresh_plugin_rows();
self.set_refreshing(false);
}
Self::Input::Install(plugin) => {
self.set_locked(true);
let mut plugins = vec![plugin];
for dep in plugins[0].dependencies.clone().unwrap_or_default() {
if let Some(dep_plugin) = self
.plugins
.iter()
.find(|plugin| plugin.appid == dep)
.cloned()
{
plugins.push(dep_plugin);
} else {
error!(
"unable to find dependency for {}: {}",
plugins[0].appid, dep
);
alert(
"Missing dependencies",
Some(&format!(
"{} depends on unknown plugin {}",
plugins[0].name, dep
)),
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
);
return;
}
}
sender.input(Self::Input::InstallDownload(plugins))
}
Self::Input::InstallDownload(plugins) => {
for plugin in plugins {
let mut plugin = plugin.clone();
match plugin.exec_url.as_ref() {
Some(url) => {
let exec_path = plugin.canonical_exec_path();
if let Err(e) = download_file_async(url, &exec_path).await {
alert(
"Download failed",
Some(&format!(
"Downloading {} {} failed:\n\n{e}",
plugin.name,
plugin
.version
.as_ref()
.unwrap_or(&"(no version)".to_string())
)),
Some(
&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>(),
),
);
} else {
plugin.exec_path = Some(exec_path);
if let Err(e) = plugin.enable() {
error!("failed to enable plugin {}: {e}", plugin.appid);
alert(
"Failed to enable plugin",
Some(&e.to_string()),
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
);
return;
}
self.add_plugin_to_config(&sender, plugin.clone());
}
}
None => {
alert(
"Download failed",
Some(&format!(
"Downloading {} {} failed:\n\nNo executable url provided for this plugin, this is likely a bug!",
plugin.name,
plugin.version.as_ref().unwrap_or(&"(no version)".to_string()))
),
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>())
);
}
};
self.refresh_plugin_rows();
self.details
.emit(StoreDetailMsg::Refresh(plugin.appid, true, false));
}
self.set_locked(false);
}
Self::Input::Remove(plugin) => {
self.set_locked(true);
if let Err(e) = plugin.disable() {
error!("failed to disable plugin {}: {e}", plugin.appid);
alert(
"Failed to disable plugin",
Some(&e.to_string()),
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
);
return;
}
if let Some(exec) = plugin.executable() {
// delete executable only if it's not a custom plugin
if exec.is_file() && plugin.exec_url.is_some() {
if let Err(e) = remove_file(&exec) {
alert(
"Failed removing plugin",
Some(&format!(
"Could not remove plugin executable {}:\n\n{e}",
exec.to_string_lossy()
)),
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()),
);
}
}
}
self.config_plugins.remove(&plugin.appid);
self.set_plugins(
self.plugins
.clone()
.into_iter()
.filter(|p| !(p.appid == plugin.appid && p.exec_url.is_none()))
.collect(),
);
sender
.output(Self::Output::UpdateConfigPlugins(
self.config_plugins.clone(),
))
.expect(SENDER_IO_ERR_MSG);
self.refresh_plugin_rows();
self.details
.emit(StoreDetailMsg::Refresh(plugin.appid, false, false));
self.set_locked(false);
}
Self::Input::SetEnabled(signal_sender, plugin, enabled) => {
if let Some(cp) = self.config_plugins.get_mut(&plugin.appid) {
if let Err(e) = plugin.set_enabled(enabled) {
error!(
"failed to {} plugin {}: {e}",
if enabled { "enable" } else { "disable" },
plugin.appid
);
alert(
&format!(
"Failed to {} plugin",
if enabled { "enable" } else { "disable" }
),
Some(&e.to_string()),
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
);
return;
}
cp.enabled = enabled;
if signal_sender == PluginStoreSignalSource::Detail {
if let Some(row) = self
.plugin_rows
.as_mut()
.unwrap()
.guard()
.iter()
.find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid))
.flatten()
{
row.input_sender.emit(StoreRowModelMsg::Refresh(
enabled,
cp.plugin.version != plugin.version,
));
} else {
error!("could not find corresponding listbox row")
}
}
if signal_sender == PluginStoreSignalSource::Row {
self.details.emit(StoreDetailMsg::Refresh(
plugin.appid,
enabled,
cp.plugin.version != plugin.version,
));
}
} else {
debug!(
"failed to set plugin {} enabled: could not find in hashmap",
plugin.appid
)
}
sender
.output(Self::Output::UpdateConfigPlugins(
self.config_plugins.clone(),
))
.expect(SENDER_IO_ERR_MSG);
}
// we use index here because it's the listbox not the row that can
// send this signal, so I don't directly have the plugin object
Self::Input::ShowDetails(index) => {
if let Some(plugin) = self.plugins.get(index) {
self.details.sender().emit(StoreDetailMsg::SetPlugin(
plugin.clone(),
self.config_plugins
.get(&plugin.appid)
.is_some_and(|cp| cp.enabled),
self.config_plugins
.get(&plugin.appid)
.is_some_and(|cp| cp.plugin.version != plugin.version),
));
self.main_stack
.as_ref()
.unwrap()
.set_visible_child_name("detailsview");
} else {
error!("plugins list index out of range!")
}
}
Self::Input::ShowPluginList => {
self.main_stack
.as_ref()
.unwrap()
.set_visible_child_name("listview");
}
}
}
async fn init(
init: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let mut model = Self {
tracker: 0,
refreshing: false,
locked: false,
win: None,
plugins: Vec::default(),
plugin_rows: None,
details: StoreDetail::builder()
.launch(())
.forward(sender.input_sender(), move |msg| match msg {
StoreDetailOutMsg::GoBack => Self::Input::ShowPluginList,
StoreDetailOutMsg::Install(plugin) => Self::Input::Install(plugin),
StoreDetailOutMsg::Remove(plugin) => Self::Input::Remove(plugin),
StoreDetailOutMsg::SetEnabled(plugin, enabled) => {
Self::Input::SetEnabled(PluginStoreSignalSource::Detail, plugin, enabled)
}
}),
config_plugins: init.config_plugins,
main_stack: None,
add_custom_plugin_win: None,
};
let details_view = model.details.widget();
let widgets = view_output!();
model.win = Some(widgets.win.clone());
model.plugin_rows = Some(
AsyncFactoryVecDeque::builder()
.launch(widgets.listbox.clone())
.forward(sender.input_sender(), move |msg| match msg {
StoreRowModelOutMsg::Install(appid) => Self::Input::Install(appid),
StoreRowModelOutMsg::Remove(appid) => Self::Input::Remove(appid),
StoreRowModelOutMsg::SetEnabled(plugin, enabled) => {
Self::Input::SetEnabled(PluginStoreSignalSource::Row, plugin, enabled)
}
}),
);
model.main_stack = Some(widgets.main_stack.clone());
AsyncComponentParts { model, widgets }
}
}

View file

@ -1,373 +0,0 @@
use super::Plugin;
use crate::{downloader::cache_file, ui::SENDER_IO_ERR_MSG};
use adw::prelude::*;
use relm4::prelude::*;
use tracing::{error, warn};
#[tracker::track]
pub struct StoreDetail {
plugin: Option<Plugin>,
enabled: bool,
#[tracker::do_not_track]
carousel: Option<adw::Carousel>,
#[tracker::do_not_track]
icon: Option<gtk::Image>,
needs_update: bool,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum StoreDetailMsg {
SetPlugin(Plugin, bool, bool),
SetIcon,
SetScreenshots,
Refresh(String, bool, bool),
Install,
Remove,
SetEnabled(bool),
OpenHomepage,
}
#[derive(Debug)]
pub enum StoreDetailOutMsg {
Install(Plugin),
Remove(Plugin),
GoBack,
SetEnabled(Plugin, bool),
}
#[relm4::component(pub async)]
impl AsyncComponent for StoreDetail {
type Init = ();
type Input = StoreDetailMsg;
type Output = StoreDetailOutMsg;
type CommandOutput = ();
view! {
adw::ToolbarView {
set_top_bar_style: adw::ToolbarStyle::Flat,
add_top_bar: headerbar = &adw::HeaderBar {
#[wrap(Some)]
set_title_widget: title = &adw::WindowTitle {
#[track = "model.changed(Self::plugin())"]
set_title: model
.plugin
.as_ref()
.map(|p| p.name.as_str())
.unwrap_or_default(),
},
pack_start: backbtn = &gtk::Button {
set_icon_name: "go-previous-symbolic",
set_tooltip_text: Some("Back"),
connect_clicked[sender] => move |_| {
sender.output(Self::Output::GoBack).expect(SENDER_IO_ERR_MSG);
}
},
},
#[wrap(Some)]
set_content: inner = &gtk::ScrolledWindow {
set_hscrollbar_policy: gtk::PolicyType::Never,
set_hexpand: true,
set_vexpand: true,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_vexpand: true,
set_margin_top: 12,
set_margin_bottom: 48,
set_margin_start: 12,
set_margin_end: 12,
set_spacing: 24,
adw::Clamp { // icon, name, buttons
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_vexpand: false,
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
#[name(icon)]
gtk::Image {
set_icon_name: Some("application-x-addon-symbolic"),
set_margin_end: 12,
set_pixel_size: 96,
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_valign: gtk::Align::Center,
set_spacing: 6,
gtk::Label {
add_css_class: "title-2",
set_xalign: 0.0,
#[track = "model.changed(Self::plugin())"]
set_text: model
.plugin
.as_ref()
.map(|p| p.name.as_str())
.unwrap_or_default(),
set_ellipsize: gtk::pango::EllipsizeMode::None,
set_wrap: true,
},
gtk::Label {
add_css_class: "dim-label",
set_xalign: 0.0,
#[track = "model.changed(Self::plugin())"]
set_visible: model
.plugin
.as_ref()
.is_some_and(|p| p.author.is_some()),
#[track = "model.changed(Self::plugin())"]
set_text: model
.plugin
.as_ref()
.and_then(
|p| p.author.as_deref()
)
.unwrap_or_default(),
set_ellipsize: gtk::pango::EllipsizeMode::None,
set_wrap: true,
},
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_halign: gtk::Align::Center,
set_valign: gtk::Align::Center,
set_spacing: 6,
gtk::Button {
#[track = "model.changed(Self::plugin())"]
set_visible: !model
.plugin
.as_ref()
.is_some_and(|p| p.is_installed()),
set_label: "Install",
add_css_class: "suggested-action",
connect_clicked[sender] => move |_| {
sender.input(Self::Input::Install);
}
},
gtk::Button {
#[track = "model.changed(Self::plugin())"]
set_visible: model
.plugin
.as_ref()
.is_some_and(|p| p.is_installed()),
set_label: "Remove",
add_css_class: "destructive-action",
connect_clicked[sender] => move |_| {
sender.input(Self::Input::Remove);
}
},
gtk::Button {
#[track = "model.changed(Self::plugin()) || model.changed(Self::needs_update())"]
set_visible: model
.plugin
.as_ref()
.is_some_and(|p| p.is_installed()) && model.needs_update,
add_css_class: "suggested-action",
set_label: "Update",
set_valign: gtk::Align::Center,
set_halign: gtk::Align::Center,
connect_clicked[sender] => move |_| {
sender.input(Self::Input::Install);
}
},
gtk::Switch {
#[track = "model.changed(Self::plugin())"]
set_visible: model.plugin.as_ref()
.is_some_and(|p| p.is_installed()),
#[track = "model.changed(Self::enabled())"]
set_active: model.enabled,
set_tooltip_text: Some("Plugin enabled"),
set_valign: gtk::Align::Center,
set_halign: gtk::Align::Center,
connect_state_set[sender] => move |_, state| {
sender.input(Self::Input::SetEnabled(state));
gtk::glib::Propagation::Proceed
}
},
gtk::Button {
#[track = "model.changed(Self::plugin())"]
set_visible: model.plugin.as_ref()
.is_some_and(|p| p.homepage_url.is_some()),
set_label: "Homepage",
connect_clicked[sender] => move |_| {
sender.input(Self::Input::OpenHomepage);
}
}
}
}
},
},
gtk::Box { // screenshots
set_orientation: gtk::Orientation::Vertical,
set_spacing: 12,
#[name(carousel)]
adw::Carousel {
set_allow_mouse_drag: true,
set_allow_scroll_wheel: false,
set_spacing: 24,
},
adw::CarouselIndicatorDots {
set_carousel: Some(&carousel),
},
},
adw::Clamp { // description
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
gtk::Label {
set_xalign: 0.0,
#[track = "model.changed(Self::plugin())"]
set_text: model
.plugin
.as_ref()
.and_then(|p| p
.description
.as_deref()
).unwrap_or(""),
set_ellipsize: gtk::pango::EllipsizeMode::None,
set_wrap: true,
set_justify: gtk::Justification::Fill,
},
},
},
}
}
}
}
async fn update(
&mut self,
message: Self::Input,
sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
self.reset();
match message {
Self::Input::OpenHomepage => {
if let Some(plugin) = self.plugin.as_ref() {
if let Some(homepage) = plugin.homepage_url.as_ref() {
if let Err(e) = gtk::gio::AppInfo::launch_default_for_uri(
homepage,
gtk::gio::AppLaunchContext::NONE,
) {
error!("opening uri {homepage}: {e}");
};
}
}
}
Self::Input::SetPlugin(p, enabled, needs_update) => {
self.set_plugin(Some(p));
self.set_enabled(enabled);
self.set_needs_update(needs_update);
sender.input(Self::Input::SetIcon);
sender.input(Self::Input::SetScreenshots);
}
Self::Input::SetIcon => {
if let Some(plugin) = self.plugin.as_ref() {
if let Some(url) = plugin.icon_url.as_ref() {
match cache_file(url, None).await {
Ok(dest) => {
self.icon.as_ref().unwrap().set_from_file(Some(dest));
}
Err(e) => {
warn!("Failed downloading icon '{url}': {e}");
}
};
} else {
self.icon
.as_ref()
.unwrap()
.set_icon_name(Some("application-x-addon-symbolic"));
}
}
}
Self::Input::SetScreenshots => {
let carousel = self.carousel.as_ref().unwrap().clone();
while let Some(child) = carousel.first_child() {
carousel.remove(&child);
}
if let Some(plugin) = self.plugin.as_ref() {
carousel
.parent()
.unwrap()
.set_visible(!plugin.screenshots.is_empty());
for url in plugin.screenshots.iter() {
match cache_file(url, None).await {
Ok(dest) => {
let pic = gtk::Picture::builder()
.height_request(300)
.css_classes(["card"])
.overflow(gtk::Overflow::Hidden)
.valign(gtk::Align::Center)
.build();
pic.set_filename(Some(dest));
let clamp = adw::Clamp::builder().child(&pic).build();
carousel.append(&clamp);
}
Err(e) => {
warn!("failed downloading screenshot '{url}': {e}");
}
};
}
}
}
Self::Input::Refresh(appid, enabled, needs_update) => {
if self.plugin.as_ref().is_some_and(|p| p.appid == appid) {
self.mark_all_changed();
self.set_enabled(enabled);
self.set_needs_update(needs_update);
}
}
Self::Input::Install => {
if let Some(plugin) = self.plugin.as_ref() {
sender
.output(Self::Output::Install(plugin.clone()))
.expect(SENDER_IO_ERR_MSG);
}
}
Self::Input::Remove => {
if let Some(plugin) = self.plugin.as_ref() {
sender
.output(Self::Output::Remove(plugin.clone()))
.expect(SENDER_IO_ERR_MSG);
if plugin.exec_url.is_none() {
sender
.output(Self::Output::GoBack)
.expect(SENDER_IO_ERR_MSG);
}
}
}
Self::Input::SetEnabled(enabled) => {
self.set_enabled(enabled);
if let Some(plugin) = self.plugin.as_ref() {
sender
.output(Self::Output::SetEnabled(plugin.clone(), enabled))
.expect(SENDER_IO_ERR_MSG);
}
}
}
}
async fn init(
_init: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let mut model = Self {
tracker: 0,
plugin: None,
enabled: false,
carousel: None,
icon: None,
needs_update: false,
};
let widgets = view_output!();
model.carousel = Some(widgets.carousel.clone());
model.icon = Some(widgets.icon.clone());
AsyncComponentParts { model, widgets }
}
}

View file

@ -1,225 +0,0 @@
use super::Plugin;
use crate::{downloader::cache_file, ui::SENDER_IO_ERR_MSG};
use gtk::prelude::*;
use relm4::{
factory::AsyncFactoryComponent, prelude::DynamicIndex, AsyncFactorySender, RelmWidgetExt,
};
use tracing::error;
#[derive(Debug)]
#[tracker::track]
pub struct StoreRowModel {
#[no_eq]
pub plugin: Plugin,
#[tracker::do_not_track]
icon: Option<gtk::Image>,
#[tracker::do_not_track]
pub input_sender: relm4::Sender<StoreRowModelMsg>,
pub enabled: bool,
pub needs_update: bool,
}
#[derive(Debug)]
pub struct StoreRowModelInit {
pub plugin: Plugin,
pub enabled: bool,
pub needs_update: bool,
}
#[derive(Debug)]
pub enum StoreRowModelMsg {
LoadIcon,
/// params: enabled, needs_update
Refresh(bool, bool),
SetEnabled(bool),
}
#[derive(Debug)]
pub enum StoreRowModelOutMsg {
Install(Plugin),
Remove(Plugin),
SetEnabled(Plugin, bool),
}
#[relm4::factory(async pub)]
impl AsyncFactoryComponent for StoreRowModel {
type Init = StoreRowModelInit;
type Input = StoreRowModelMsg;
type Output = StoreRowModelOutMsg;
type CommandOutput = ();
type ParentWidget = gtk::ListBox;
view! {
root = gtk::ListBoxRow {
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
set_vexpand: false,
set_spacing: 12,
set_margin_all: 12,
#[name(icon)]
gtk::Image {
set_icon_name: Some("application-x-addon-symbolic"),
set_icon_size: gtk::IconSize::Large,
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 6,
set_hexpand: true,
set_vexpand: true,
gtk::Label {
add_css_class: "title-3",
set_hexpand: true,
set_xalign: 0.0,
set_text: &self.plugin.name,
set_ellipsize: gtk::pango::EllipsizeMode::None,
set_wrap: true,
},
gtk::Label {
add_css_class: "dim-label",
set_hexpand: true,
set_xalign: 0.0,
set_text: self.plugin.short_description
.as_deref()
.unwrap_or(""),
set_ellipsize: gtk::pango::EllipsizeMode::None,
set_wrap: true,
},
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 6,
set_vexpand: true,
set_valign: gtk::Align::Center,
set_halign: gtk::Align::Center,
gtk::Button {
#[track = "self.changed(StoreRowModel::plugin())"]
set_visible: !self.plugin.is_installed(),
set_icon_name: "folder-download-symbolic",
add_css_class: "suggested-action",
set_tooltip_text: Some("Install"),
set_valign: gtk::Align::Center,
set_halign: gtk::Align::Center,
connect_clicked[sender, plugin] => move |_| {
sender
.output(Self::Output::Install(
plugin.clone(),
))
.expect(SENDER_IO_ERR_MSG);
}
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 6,
set_valign: gtk::Align::Center,
set_halign: gtk::Align::Center,
gtk::Button {
#[track = "self.changed(StoreRowModel::plugin())"]
set_visible: self.plugin.is_installed(),
set_icon_name: "user-trash-symbolic",
add_css_class: "destructive-action",
set_tooltip_text: Some("Remove"),
set_valign: gtk::Align::Center,
set_halign: gtk::Align::Center,
connect_clicked[sender, plugin] => move |_| {
sender
.output(Self::Output::Remove(
plugin.clone(),
))
.expect(SENDER_IO_ERR_MSG);
}
},
gtk::Button {
#[track = "self.changed(StoreRowModel::plugin()) || self.changed(StoreRowModel::needs_update())"]
set_visible: self.plugin.is_installed() && self.needs_update,
set_icon_name: "view-refresh-symbolic",
add_css_class: "suggested-action",
set_tooltip_text: Some("Update"),
set_valign: gtk::Align::Center,
set_halign: gtk::Align::Center,
connect_clicked[sender, plugin] => move |_| {
sender
.output(Self::Output::Install(
plugin.clone(),
))
.expect(SENDER_IO_ERR_MSG);
}
},
},
gtk::Switch {
#[track = "self.changed(StoreRowModel::plugin())"]
set_visible: self.plugin.is_installed(),
#[track = "self.changed(StoreRowModel::enabled())"]
set_active: self.enabled,
set_valign: gtk::Align::Center,
set_halign: gtk::Align::Center,
set_tooltip_text: Some("Plugin enabled"),
connect_state_set[sender] => move |_, state| {
sender.input(Self::Input::SetEnabled(state));
gtk::glib::Propagation::Proceed
}
},
},
}
}
}
async fn update(&mut self, message: Self::Input, sender: AsyncFactorySender<Self>) {
self.reset();
match message {
Self::Input::LoadIcon => {
if let Some(url) = self.plugin.icon_url.as_ref() {
match cache_file(url, None).await {
Ok(dest) => {
self.icon.as_ref().unwrap().set_from_file(Some(dest));
}
Err(e) => {
error!("failed downloading icon '{url}': {e}");
}
};
}
}
Self::Input::SetEnabled(state) => {
self.set_enabled(state);
sender
.output(Self::Output::SetEnabled(self.plugin.clone(), state))
.expect(SENDER_IO_ERR_MSG);
}
Self::Input::Refresh(enabled, needs_update) => {
self.mark_all_changed();
self.set_enabled(enabled);
self.set_needs_update(needs_update);
}
}
}
async fn init_model(
init: Self::Init,
_index: &DynamicIndex,
sender: AsyncFactorySender<Self>,
) -> Self {
Self {
tracker: 0,
plugin: init.plugin,
enabled: init.enabled,
icon: None,
input_sender: sender.input_sender().clone(),
needs_update: init.needs_update,
}
}
fn init_widgets(
&mut self,
_index: &DynamicIndex,
root: Self::Root,
_returned_widget: &<Self::ParentWidget as relm4::factory::FactoryView>::ReturnedWidget,
sender: AsyncFactorySender<Self>,
) -> Self::Widgets {
let plugin = self.plugin.clone(); // for use in a signal handler
let widgets = view_output!();
self.icon = Some(widgets.icon.clone());
sender.input(Self::Input::LoadIcon);
widgets
}
}

View file

@ -156,12 +156,13 @@ pub fn spin_row<F: Fn(&gtk::Adjustment) + 'static>(
row
}
fn filedialog_row_base<F: Fn(Option<String>) + 'static + Clone>(
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, gtk::Label) {
) -> adw::ActionRow {
let row = adw::ActionRow::builder()
.title(title)
.subtitle_lines(0)
@ -173,14 +174,14 @@ fn filedialog_row_base<F: Fn(Option<String>) + 'static + Clone>(
row.set_subtitle(d);
}
let path_label = gtk::Label::builder()
let path_label = &gtk::Label::builder()
.label(match value.as_ref() {
None => "(None)",
Some(p) => p.as_str(),
})
.wrap(true)
.build();
row.add_suffix(&path_label);
row.add_suffix(path_label);
let clear_btn = gtk::Button::builder()
.icon_name("edit-clear-symbolic")
@ -199,60 +200,6 @@ fn filedialog_row_base<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))
@ -273,8 +220,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_string_lossy().to_string();
path_label.set_text(&path_s);
let path_s = path.to_str().unwrap().to_string();
path_label.set_text(path_s.as_str());
cb(Some(path_s))
}
}

View file

@ -6,13 +6,12 @@ use super::{
};
use crate::{
env_var_descriptions::ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH,
profile::{LighthouseDriver, OvrCompatibilityModuleType, Profile, XRServiceType},
profile::{LighthouseDriver, 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 {
@ -130,6 +129,14 @@ 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,
@ -209,43 +216,31 @@ impl SimpleComponent for ProfileEditor {
),
},
add: model.xrservice_cmake_flags_rows.widget(),
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: opencompgrp = &adw::PreferencesGroup {
set_title: "OpenComposite",
set_description: Some("OpenVR driver built on top of OpenXR"),
add: &path_row(
"OpenVR Module Path", None,
Some(model.profile.borrow().ovr_comp.path.clone().to_string_lossy().to_string()),
"OpenComposite Path", None,
Some(model.profile.borrow().opencomposite_path.clone().to_string_lossy().to_string()),
Some(init.root_win.clone()),
clone!(#[strong] prof, move |n_path| {
prof.borrow_mut().ovr_comp.path = n_path.unwrap_or_default().into();
prof.borrow_mut().opencomposite_path = n_path.unwrap_or_default().into();
})
),
add: &entry_row(
"OpenVR Compatibility Repo",
model.profile.borrow().ovr_comp.repo.clone().unwrap_or_default().as_str(),
"OpenComposite Repo",
model.profile.borrow().opencomposite_repo.clone().unwrap_or_default().as_str(),
clone!(#[strong] prof, move |row| {
let n_val = row.text().to_string();
prof.borrow_mut().ovr_comp.repo = (!n_val.is_empty()).then_some(n_val);
prof.borrow_mut().opencomposite_repo = (!n_val.is_empty()).then_some(n_val);
})
),
add: &entry_row(
"OpenVR Compatibility Branch",
model.profile.borrow().ovr_comp.branch.clone().unwrap_or_default().as_str(),
"OpenComposite Branch",
model.profile.borrow().opencomposite_branch.clone().unwrap_or_default().as_str(),
clone!(#[strong] prof, move |row| {
let n_val = row.text().to_string();
prof.borrow_mut().ovr_comp.branch = (!n_val.is_empty()).then_some(n_val);
prof.borrow_mut().opencomposite_branch = (!n_val.is_empty()).then_some(n_val);
})
),
},
@ -504,14 +499,14 @@ impl SimpleComponent for ProfileEditor {
.halign(gtk::Align::End)
.build();
let on_add = clone!(
add_btn.connect_clicked(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() {
@ -520,13 +515,7 @@ 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
}};
}
@ -539,30 +528,17 @@ 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(env_var_prefs_group)
.launch(
adw::PreferencesGroup::builder()
.title("Environment Variables")
.description(ENV_VAR_DESCRIPTIONS_AS_PARAGRAPH.as_str())
.header_suffix(&add_env_var_btn)
.build(),
)
.forward(sender.input_sender(), |msg| match msg {
EnvVarModelOutMsg::Changed(name, value) => {
ProfileEditorMsg::EnvVarChanged(name, value)

View file

@ -10,10 +10,10 @@ use relm4::{
};
use std::{
collections::{HashMap, VecDeque},
path::Path,
thread::sleep,
time::Duration,
};
use tracing::error;
#[tracker::track]
pub struct SteamVrCalibrationBox {
@ -144,59 +144,55 @@ impl SimpleComponent for SteamVrCalibrationBox {
}
Self::Input::RunCalibration => {
self.set_calibration_result(None);
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);
}
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),
})
};
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 {
error!("calibration exited with code {code}");
eprintln!("Calibration exited with code {code}");
}
self.calibration_running = false;
}

View file

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

View file

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

View file

@ -7,7 +7,6 @@ use crate::{
};
use gtk::prelude::*;
use relm4::prelude::*;
use tracing::error;
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum StartClientStatus {
@ -154,14 +153,14 @@ impl AsyncComponent for WivrnWiredStartBox {
.into(),
))
} else {
error!("ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr);
eprintln!("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) => {
error!("failed to run ADB: {e}");
eprintln!("Error: failed to run ADB: {e}");
StartClientStatus::Done(Some(
"Failed to run ADB".into()
))

View file

@ -7,10 +7,8 @@ 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() {
@ -38,7 +36,7 @@ pub fn get_reader(path: &Path) -> Option<BufReader<File>> {
}
match File::open(path) {
Err(e) => {
error!("Error opening {}: {}", path.to_string_lossy(), e);
eprintln!("Error opening {}: {}", path.to_string_lossy(), e);
None
}
Ok(fd) => Some(BufReader::new(fd)),
@ -50,7 +48,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) => {
error!("Failed to deserialize {}: {}", path.to_string_lossy(), e);
eprintln!("Failed to deserialize {}: {}", path.to_string_lossy(), e);
None
}
Ok(res) => Some(res),
@ -58,25 +56,16 @@ pub fn deserialize_file<T: serde::de::DeserializeOwned>(path: &Path) -> Option<T
}
}
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()
);
}
pub fn set_file_readonly(path: &Path, readonly: bool) -> Result<(), std::io::Error> {
if !path.is_file() {
debug!(
"trying to set readonly on a file that does not exist: {}",
path.to_string_lossy()
);
eprintln!("WARN: trying to set readonly on a file that does not exist");
return Ok(());
}
let mut perms = fs::metadata(path)
.expect("Could not get metadata for file")
.permissions();
perms.set_readonly(readonly);
Ok(fs::set_permissions(path, perms)?)
fs::set_permissions(path, perms)
}
pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
@ -91,37 +80,16 @@ pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
]
}
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) {
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 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() {
error!("failed to remove path {}", path.to_string_lossy());
eprintln!("Failed to remove path {}", path.to_string_lossy());
}
}
@ -132,13 +100,11 @@ pub fn copy_file(source: &Path, dest: &Path) {
.unwrap_or_else(|_| panic!("Failed to create dir {}", parent.to_str().unwrap()));
}
}
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| {
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(|_| {
panic!(
"Failed to copy {} to {}: {e}",
"Failed to copy {} to {}",
source.to_string_lossy(),
dest.to_string_lossy()
)
@ -152,17 +118,6 @@ pub fn mount_has_nosuid(path: &Path) -> Result<bool, Errno> {
}
}
pub fn mark_as_executable(path: &Path) -> anyhow::Result<()> {
if !path.is_file() {
bail!("Path '{}' is not a file", path.to_string_lossy())
} else {
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(perms.mode() | 0o111);
fs::set_permissions(path, perms)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::mount_has_nosuid;

View file

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

View file

@ -1,69 +0,0 @@
use crate::paths::get_home_dir;
use anyhow::bail;
use serde::Deserialize;
use std::{
collections::HashMap,
fs::read_to_string,
path::{Path, PathBuf},
};
#[derive(Deserialize)]
pub struct SteamLibraryFolder {
pub path: String,
pub apps: HashMap<u32, usize>,
}
fn get_steam_main_dir_path() -> anyhow::Result<PathBuf> {
let steam_root: PathBuf = get_home_dir().join(".steam/root").canonicalize()?;
if steam_root.is_dir() {
Ok(steam_root)
} else {
bail!(
"Canonical steam root '{}' is not a dir nor a symlink!",
steam_root.to_string_lossy()
)
}
}
impl SteamLibraryFolder {
pub fn get_folders() -> anyhow::Result<HashMap<u32, Self>> {
let libraryfolders_path = get_steam_main_dir_path()?
.join("steamapps/libraryfolders.vdf")
.canonicalize()?;
if !libraryfolders_path.is_file() {
bail!(
"Steam libraryfolders.vdf does not exist in its canonical location {}",
libraryfolders_path.to_string_lossy()
);
}
Self::get_folders_from_path(&libraryfolders_path)
}
/// Do not use this: use get_folders() instead as it always uses Steam's
/// canonical root path. This is intended to be directly used only for
/// unit tests
pub fn get_folders_from_path(p: &Path) -> anyhow::Result<HashMap<u32, Self>> {
Ok(keyvalues_serde::from_str(read_to_string(p)?.as_str())?)
}
}
#[cfg(test)]
mod tests {
use super::SteamLibraryFolder;
use std::path::Path;
#[test]
fn deserialize_steam_libraryfolders_vdf() {
let lf = SteamLibraryFolder::get_folders_from_path(Path::new(
"./test/files/steam_libraryfolders.vdf",
))
.unwrap();
assert_eq!(lf.len(), 1);
let first = lf.get(&0).unwrap();
assert_eq!(first.path, "/home/gabmus/.local/share/Steam");
assert_eq!(first.apps.len(), 10);
assert_eq!(first.apps.get(&228980).unwrap(), &29212173);
assert_eq!(first.apps.get(&632360).unwrap(), &0);
}
}

View file

@ -5,10 +5,12 @@ 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
@ -23,19 +25,40 @@ 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| {
Some(
unsafe { instance.get_physical_device_properties(d) }
.device_name_as_c_str()
.ok()?
.to_string_lossy()
.to_string(),
)
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())
})
.collect();
unsafe { instance.destroy_instance(None) };
Ok(Self { gpu_names })
Ok(Self {
gpu_names,
has_nvidia_gpu,
has_monado_vulkan_layers,
})
}
}

View file

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

View file

@ -1,6 +1,5 @@
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 {
@ -281,8 +280,8 @@ impl XRDevice {
if let Some(target) = devs.get_mut(&index) {
target.roles.push(role.into());
} else {
error!(
"could not find device index {index} for role {}",
eprintln!(
"Could not find device index {index} for role {}",
XRDeviceRole::from(role)
)
}

View file

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