Compare commits

..

31 commits

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

25
Cargo.lock generated
View file

@ -445,6 +445,16 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "delicious-adwaita"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53548c789a95211e0ce6d26c213067002b9b4360f8de69046d84de78ad9da3f"
dependencies = [
"gtk4",
"libadwaita",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@ -563,10 +573,11 @@ dependencies = [
[[package]] [[package]]
name = "envision" name = "envision"
version = "3.0.1" version = "3.1.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ash", "ash",
"delicious-adwaita",
"gettext-rs", "gettext-rs",
"git2", "git2",
"gtk4", "gtk4",
@ -1073,9 +1084,9 @@ dependencies = [
[[package]] [[package]]
name = "gtk4" name = "gtk4"
version = "0.9.4" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9376d14d7e33486c54823a42bef296e882b9f25cb4c52b52f4d1d57bbadb5b6d" checksum = "af1c491051f030994fd0cde6f3c44f3f5640210308cff1298c7673c47408091d"
dependencies = [ dependencies = [
"cairo-rs", "cairo-rs",
"field-offset", "field-offset",
@ -1106,9 +1117,9 @@ dependencies = [
[[package]] [[package]]
name = "gtk4-sys" name = "gtk4-sys"
version = "0.9.4" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e653b0a9001ba9be1ffddb9373bfe9a111f688222f5aeee2841481300d91b55a" checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6"
dependencies = [ dependencies = [
"cairo-sys-rs", "cairo-sys-rs",
"gdk-pixbuf-sys", "gdk-pixbuf-sys",
@ -1523,9 +1534,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "libadwaita" name = "libadwaita"
version = "0.7.1" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8611ee9fb85e7606c362b513afcaf5b59853f79e4d98caaaf581d99465014247" checksum = "500135d29c16aabf67baafd3e7741d48e8b8978ca98bac39e589165c8dc78191"
dependencies = [ dependencies = [
"gdk4", "gdk4",
"gio", "gio",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "envision" name = "envision"
version = "3.1.0" version = "3.1.1"
edition = "2021" edition = "2021"
authors = [ authors = [
"Gabriele Musco <gabmus@disroot.org>", "Gabriele Musco <gabmus@disroot.org>",
@ -44,3 +44,4 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
tracing = "0.1.41" tracing = "0.1.41"
tracing-appender = "0.2.3" tracing-appender = "0.2.3"
serde_yaml = "0.9.34" serde_yaml = "0.9.34"
delicious-adwaita = { version = "0.3.0", features = ["all_themes"] }

View file

@ -30,6 +30,15 @@
<url type="bugtracker">@REPO_URL@/issues</url> <url type="bugtracker">@REPO_URL@/issues</url>
<content_rating type="oars-1.0" /> <content_rating type="oars-1.0" />
<releases> <releases>
<release version="3.1.1" date="2025-04-22">
<description>
<p>Fixes</p>
<ul>
<li>add libusb and libusb-dev deps</li>
<li>Revert &quot;disable and blacklist wayvr dashboard plugin&quot;</li>
</ul>
</description>
</release>
<release version="3.1.0" date="2025-04-08"> <release version="3.1.0" date="2025-04-08">
<description> <description>
<p>What's new</p> <p>What's new</p>

View file

@ -1,7 +1,7 @@
project( project(
'envision', 'envision',
'rust', 'rust',
version: '3.1.0', # version number row version: '3.1.1', # version number row
meson_version: '>= 0.59', meson_version: '>= 0.59',
license: 'AGPL-3.0-or-later', license: 'AGPL-3.0-or-later',
) )

View file

@ -2,57 +2,6 @@
set -ev set -ev
PREFIX=$1
CACHE_DIR=$2
if [[ -z $PREFIX ]] || [[ -z $CACHE_DIR ]]; then
echo "Usage: $0 PREFIX CACHE_DIR"
exit 1
fi
ONNX_RELEASES=$(curl -sSL "https://api.github.com/repos/microsoft/onnxruntime/releases")
NUM_RELEASES=$(echo "$ONNX_RELEASES" | jq -r '[ select (.[]!=null) ] | length')
for (( IDX=0; IDX<NUM_RELEASES; IDX++ )); do
ASSETS_LEN=$(echo "$ONNX_RELEASES" | jq -r ".[$IDX].assets_url" | xargs -n 1 curl -sSL | jq -r '[ select (.[]!=null) ] | length')
if [[ $ASSETS_LEN -gt 0 ]]; then
ONNX_VER=$(echo "$ONNX_RELEASES" | jq -r ".[$IDX].tag_name" | tr -d v)
break
fi
done
if [[ -z $ONNX_VER ]]; then
echo "Failed to find a suitable ONNX Runtime release."
exit 1
fi
SYS_ARCH=$(uname -m)
if [[ $SYS_ARCH == x*64 ]]; then
ARCH="x64"
elif [[ $SYS_ARCH == arm64 ]] || [[ $ARCH == aarch64 ]]; then
ARCH="aarch64"
else
echo "CPU architecture '$SYS_ARCH' is not supported"
exit 1
fi
ONNX="onnxruntime-linux-${ARCH}-${ONNX_VER}"
ONNX_URL="https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VER}/${ONNX}.tgz"
mkdir -p "$CACHE_DIR"
curl -sSL "$ONNX_URL" -o "${CACHE_DIR}/onnxruntime.tgz"
tar xf "${CACHE_DIR}/onnxruntime.tgz" --directory="${CACHE_DIR}"
mkdir -p "${PREFIX}/lib"
mkdir -p "${PREFIX}/include"
cp -r "${CACHE_DIR}/${ONNX}/include/"* "${PREFIX}/include/"
cp -r "${CACHE_DIR}/${ONNX}/lib/"* "${PREFIX}/lib/"
if [[ -z $XDG_DATA_HOME ]]; then if [[ -z $XDG_DATA_HOME ]]; then
DATA_HOME=$HOME/.local/share DATA_HOME=$HOME/.local/share
else else

View file

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

View file

@ -5,7 +5,10 @@ use crate::{
ui::job_worker::job::WorkerJob, ui::job_worker::job::WorkerJob,
util::file_utils::rm_rf, util::file_utils::rm_rf,
}; };
use std::collections::{HashMap, VecDeque}; use std::{
collections::{HashMap, VecDeque},
num::NonZero,
};
pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> { pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<WorkerJob> {
let mut jobs = VecDeque::<WorkerJob>::new(); let mut jobs = VecDeque::<WorkerJob>::new();
@ -40,11 +43,24 @@ pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<W
env: Some({ env: Some({
let mut cmake_env: HashMap<String, String> = HashMap::new(); let mut cmake_env: HashMap<String, String> = HashMap::new();
for (k, v) in [ for (k, v) in [
("CMAKE_BUILD_PARALLEL_LEVEL", "2"), // The basalt build uses a lot of RAM, so we have to limit the number of
("CMAKE_BUILD_TYPE", "RelWithDebInfo"), // build processes to not starve the system of memory
("BUILD_TESTS", "off"), // Limit to 6 build processes at most
(
"CMAKE_BUILD_PARALLEL_LEVEL",
std::cmp::min(
6,
std::thread::available_parallelism()
.map(NonZero::get)
.unwrap_or(2),
)
.to_string(),
),
("CMAKE_BUILD_TYPE", "RelWithDebInfo".into()),
("CMAKE_POLICY_VERSION_MINIMUM", "3.5".into()),
("BUILD_TESTS", "off".into()),
] { ] {
cmake_env.insert(k.to_string(), v.to_string()); cmake_env.insert(k.to_string(), v);
} }
cmake_env cmake_env
}), }),

View file

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

View file

@ -2,12 +2,11 @@ use crate::{
build_tools::{cmake::Cmake, git::Git}, build_tools::{cmake::Cmake, git::Git},
profile::Profile, profile::Profile,
termcolor::TermColor, termcolor::TermColor,
ui::job_worker::job::{FuncWorkerData, FuncWorkerOut, WorkerJob}, ui::job_worker::job::WorkerJob,
util::file_utils::{copy_file, rm_rf}, util::file_utils::rm_rf,
}; };
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
fs::create_dir_all,
path::Path, path::Path,
}; };
@ -37,16 +36,22 @@ pub fn get_build_vapor_jobs(profile: &Profile, clean_build: bool) -> VecDeque<Wo
jobs.extend(git.get_pre_build_jobs(profile.pull_on_build)); jobs.extend(git.get_pre_build_jobs(profile.pull_on_build));
let build_dir = profile.ovr_comp.path.join("build"); let build_dir = profile.ovr_comp.path.join("build");
let install_dir = build_dir.join("install_pfx");
let cmake = Cmake { let cmake = Cmake {
env: None, env: None,
vars: Some({ vars: Some({
let mut cmake_vars: HashMap<String, String> = HashMap::new(); let mut cmake_vars: HashMap<String, String> = HashMap::new();
for (k, v) in [ for (k, v) in [
("VAPOR_LOG_SILENT=ON", "ON"), ("VAPOR_LOG_SILENT", "ON"),
("USE_SYSTEM_OPENXR", "OFF"),
("CMAKE_BUILD_TYPE", "RelWithDebInfo"), ("CMAKE_BUILD_TYPE", "RelWithDebInfo"),
] { ] {
cmake_vars.insert(k.to_string(), v.to_string()); cmake_vars.insert(k.to_string(), v.to_string());
} }
cmake_vars.insert(
"CMAKE_INSTALL_PREFIX".into(),
install_dir.to_string_lossy().to_string(),
);
cmake_vars cmake_vars
}), }),
source_dir: profile.ovr_comp.path.clone(), source_dir: profile.ovr_comp.path.clone(),
@ -57,29 +62,7 @@ pub fn get_build_vapor_jobs(profile: &Profile, clean_build: bool) -> VecDeque<Wo
jobs.push_back(cmake.get_prepare_job()); jobs.push_back(cmake.get_prepare_job());
} }
jobs.push_back(cmake.get_build_job()); jobs.push_back(cmake.get_build_job());
jobs.push_back(WorkerJob::Func(FuncWorkerData { jobs.push_back(cmake.get_install_job());
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 jobs
} }

View file

@ -45,6 +45,10 @@ const fn default_win_size() -> [i32; 2] {
DEFAULT_WIN_SIZE DEFAULT_WIN_SIZE
} }
fn default_theme_name() -> String {
"Follow system".into()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub selected_profile_uuid: String, pub selected_profile_uuid: String,
@ -54,6 +58,8 @@ pub struct Config {
pub win_size: [i32; 2], pub win_size: [i32; 2],
#[serde(default)] #[serde(default)]
pub plugins: HashMap<String, PluginConfig>, pub plugins: HashMap<String, PluginConfig>,
#[serde(default = "default_theme_name")]
pub theme_name: String,
} }
impl Default for Config { impl Default for Config {
@ -65,6 +71,7 @@ impl Default for Config {
user_profiles: Vec::default(), user_profiles: Vec::default(),
win_size: DEFAULT_WIN_SIZE, win_size: DEFAULT_WIN_SIZE,
plugins: HashMap::default(), plugins: HashMap::default(),
theme_name: default_theme_name(),
} }
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@ pub mod mercury_deps;
pub mod monado_deps; pub mod monado_deps;
pub mod openhmd_deps; pub mod openhmd_deps;
pub mod wivrn_deps; pub mod wivrn_deps;
pub mod xrizer_deps;
use crate::linux_distro::LinuxDistro; use crate::linux_distro::LinuxDistro;
use std::{collections::HashMap, env, fmt::Display, path::Path}; use std::{collections::HashMap, env, fmt::Display, path::Path};
@ -108,6 +109,24 @@ impl Display for DependencyCheckResult {
} }
} }
pub trait DepcheckResultGetMissing {
fn filter_missing_deps(self) -> Vec<Dependency>;
}
impl DepcheckResultGetMissing for Vec<DependencyCheckResult> {
fn filter_missing_deps(self) -> Vec<Dependency> {
self.into_iter()
.filter_map(|res| {
if !res.found {
Some(res.dependency)
} else {
None
}
})
.collect()
}
}
fn shared_obj_paths() -> Vec<String> { fn shared_obj_paths() -> Vec<String> {
vec![ vec![
"/lib".into(), "/lib".into(),
@ -117,6 +136,24 @@ fn shared_obj_paths() -> Vec<String> {
"/usr/local/lib64".into(), "/usr/local/lib64".into(),
"/usr/lib/x86_64-linux-gnu".into(), "/usr/lib/x86_64-linux-gnu".into(),
"/usr/lib/aarch64-linux-gnu".into(), "/usr/lib/aarch64-linux-gnu".into(),
// Debian puts libclang in /usr/lib/llvm-[llvm major version]/lib.
"/usr/lib/llvm-15/lib".into(),
"/usr/lib/llvm-16/lib".into(),
"/usr/lib/llvm-19/lib".into(),
// Fedora puts libclang in /usr/lib64/llvm[llvm major version]/lib as well as /usr/lib64.
"/usr/lib64/llvm15/lib".into(),
"/usr/lib64/llvm16/lib".into(),
"/usr/lib64/llvm17/lib".into(),
"/usr/lib64/llvm18/lib".into(),
"/usr/lib64/llvm19/lib".into(),
"/usr/lib64/llvm20/lib".into(),
// Gentoo puts libclang in /usr/lib/llvm/[llvm major version]/lib64.
"/usr/lib/llvm/15/lib64".into(),
"/usr/lib/llvm/16/lib64".into(),
"/usr/lib/llvm/17/lib64".into(),
"/usr/lib/llvm/18/lib64".into(),
"/usr/lib/llvm/19/lib64".into(),
"/usr/lib/llvm/20/lib64".into(),
"/lib/x86_64-linux-gnu".into(), "/lib/x86_64-linux-gnu".into(),
"/lib/aarch64-linux-gnu".into(), "/lib/aarch64-linux-gnu".into(),
"/app/lib".into(), "/app/lib".into(),
@ -140,6 +177,8 @@ fn include_paths() -> Vec<String> {
"/usr/include/ffmpeg/libpostproc".into(), "/usr/include/ffmpeg/libpostproc".into(),
"/usr/include/ffmpeg/libswresample".into(), "/usr/include/ffmpeg/libswresample".into(),
"/usr/include/ffmpeg/libswscale".into(), "/usr/include/ffmpeg/libswscale".into(),
// opensuse puts wayland-client.h here
"/usr/include/wayland".into(),
] ]
} }

View file

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

View file

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

View file

@ -4,7 +4,7 @@ use super::{
dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr, dep_vulkan_headers, dep_libudev, dep_libx11, dep_libxcb, dep_ninja, dep_openxr, dep_vulkan_headers,
dep_vulkan_icd_loader, dep_vulkan_icd_loader,
}, },
DepType, Dependency, DependencyCheckResult, DepType, DepcheckResultGetMissing, Dependency, DependencyCheckResult,
}; };
use crate::{ use crate::{
depcheck::common::{dep_libgl, dep_libxrandr}, depcheck::common::{dep_libgl, dep_libxrandr},
@ -270,9 +270,5 @@ pub fn check_wivrn_deps() -> Vec<DependencyCheckResult> {
} }
pub fn get_missing_wivrn_deps() -> Vec<Dependency> { pub fn get_missing_wivrn_deps() -> Vec<Dependency> {
check_wivrn_deps() check_wivrn_deps().filter_missing_deps()
.iter()
.filter(|res| !res.found)
.map(|res| res.dependency.clone())
.collect()
} }

View file

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

View file

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

View file

@ -65,7 +65,7 @@ fn env_var_descriptions() -> Vec<(&'static str, &'static str)> {
fn env_var_descriptions_as_paragraph() -> String { fn env_var_descriptions_as_paragraph() -> String {
ENV_VAR_DESCRIPTIONS ENV_VAR_DESCRIPTIONS
.iter() .iter()
.map(|(k, v)| format!("<span size=\"large\" weight=\"bold\">{}</span>\n{}", k, v)) .map(|(k, v)| format!("<span size=\"large\" weight=\"bold\">{k}</span>\n{v}"))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n\n") .join("\n\n")
} }

View file

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

View file

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

View file

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

View file

@ -11,9 +11,16 @@ use relm4::{
gtk::{self, gdk, gio, glib, prelude::*}, gtk::{self, gdk, gio, glib, prelude::*},
MessageBroker, RelmApp, MessageBroker, RelmApp,
}; };
use std::env; use std::{
use steam_linux_runtime_injector::restore_runtime_entrypoint; env,
use tracing::warn; fs::{read_dir, remove_file},
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
};
use steam_linux_runtime_injector::{
restore_sniper_runtime_entrypoint, restore_soldier_runtime_entrypoint,
};
use tracing::{error, warn};
use tracing_subscriber::{ use tracing_subscriber::{
filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
}; };
@ -63,7 +70,50 @@ fn restore_steam_xr_files() {
} }
} }
} }
restore_runtime_entrypoint(); restore_sniper_runtime_entrypoint();
restore_soldier_runtime_entrypoint();
}
const LOGS_MAX_SIZE_BYTES: u64 = 1000000000; // 1GB
fn remove_old_logs(dir: &Path, log_files: Option<Vec<PathBuf>>) -> anyhow::Result<()> {
let log_files: Vec<PathBuf> = log_files
.map::<anyhow::Result<Vec<PathBuf>>, _>(Ok)
.unwrap_or_else(|| {
let mut files: Vec<PathBuf> = read_dir(dir)?
.filter_map(|de| {
let p = de.ok()?.path();
if p.is_file() && !p.is_symlink() {
Some(p)
} else {
None
}
})
.collect();
files.sort_unstable();
Ok(files)
})?;
let total_size = log_files
.iter()
.filter_map(|p| Some(p.metadata().ok()?.size()))
.reduce(u64::saturating_add)
.unwrap_or(0);
// if size is under threshold, finish
if total_size < LOGS_MAX_SIZE_BYTES {
return Ok(());
}
// keep a minimum of 3 logs
if log_files.len() <= 3 {
return Ok(());
}
remove_file(log_files.first().ok_or_else(||
anyhow::Error::msg(
"Could not get first item in log files list, but they should be more than 3! This is a bug!"
)
)?)?;
remove_old_logs(dir, Some(log_files))
} }
fn main() -> Result<()> { fn main() -> Result<()> {
@ -71,6 +121,8 @@ fn main() -> Result<()> {
panic!("{APP_NAME} cannot run as root"); panic!("{APP_NAME} cannot run as root");
} }
restore_steam_xr_files(); restore_steam_xr_files();
// deferring error logging for this since tracing isn't initialized yet
let old_logs_removal_res = remove_old_logs(&get_logs_dir(), None);
let rolling_log_writer = tracing_appender::rolling::daily(get_logs_dir(), "log"); let rolling_log_writer = tracing_appender::rolling::daily(get_logs_dir(), "log");
let (non_blocking_appender, _appender_guard) = let (non_blocking_appender, _appender_guard) =
@ -90,6 +142,10 @@ fn main() -> Result<()> {
) )
.init(); .init();
if let Err(e) = old_logs_removal_res {
error!("Failed to remove old log files: {e}");
}
// Prepare i18n // Prepare i18n
gettextrs::setlocale(LocaleCategory::LcAll, ""); gettextrs::setlocale(LocaleCategory::LcAll, "");
gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALE_DIR).expect("Unable to bind the text domain"); gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALE_DIR).expect("Unable to bind the text domain");
@ -104,7 +160,7 @@ fn main() -> Result<()> {
} }
let provider = gtk::CssProvider::new(); let provider = gtk::CssProvider::new();
provider.load_from_resource(&format!("{}/style.css", RESOURCES_BASE_PATH)); provider.load_from_resource(&format!("{RESOURCES_BASE_PATH}/style.css"));
if let Some(display) = gdk::Display::default() { if let Some(display) = gdk::Display::default() {
gtk::style_context_add_provider_for_display( gtk::style_context_add_provider_for_display(
&display, &display,

View file

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

View file

@ -2,7 +2,8 @@ use crate::{
depcheck::{ depcheck::{
basalt_deps::get_missing_basalt_deps, libsurvive_deps::get_missing_libsurvive_deps, basalt_deps::get_missing_basalt_deps, libsurvive_deps::get_missing_libsurvive_deps,
mercury_deps::get_missing_mercury_deps, monado_deps::get_missing_monado_deps, mercury_deps::get_missing_mercury_deps, monado_deps::get_missing_monado_deps,
openhmd_deps::get_missing_openhmd_deps, wivrn_deps::get_missing_wivrn_deps, Dependency, openhmd_deps::get_missing_openhmd_deps, wivrn_deps::get_missing_wivrn_deps,
xrizer_deps::get_missing_xrizer_deps, Dependency,
}, },
file_builders::active_runtime_json::ActiveRuntime, file_builders::active_runtime_json::ActiveRuntime,
paths::{get_data_dir, BWRAP_SYSTEM_PREFIX, SYSTEM_PREFIX}, paths::{get_data_dir, BWRAP_SYSTEM_PREFIX, SYSTEM_PREFIX},
@ -14,7 +15,7 @@ use serde::{Deserialize, Serialize};
use std::{ use std::{
collections::HashMap, collections::HashMap,
fmt::Display, fmt::Display,
fs::File, fs::{remove_dir_all, File},
io::BufReader, io::BufReader,
path::{Path, PathBuf}, path::{Path, PathBuf},
slice::Iter, slice::Iter,
@ -282,6 +283,15 @@ impl OvrCompatibilityModuleType {
pub fn iter() -> Iter<'static, Self> { pub fn iter() -> Iter<'static, Self> {
[Self::Opencomposite, Self::Xrizer, Self::Vapor].iter() [Self::Opencomposite, Self::Xrizer, Self::Vapor].iter()
} }
pub fn get_missing_deps(&self) -> Vec<Dependency> {
match self {
OvrCompatibilityModuleType::Xrizer => get_missing_xrizer_deps(),
OvrCompatibilityModuleType::Opencomposite | OvrCompatibilityModuleType::Vapor => {
Vec::default()
}
}
}
} }
impl FromStr for OvrCompatibilityModuleType { impl FromStr for OvrCompatibilityModuleType {
@ -331,9 +341,8 @@ impl ProfileOvrCompatibilityModule {
/// this should correspond to the build output directory /// this should correspond to the build output directory
pub fn runtime_dir(&self) -> PathBuf { pub fn runtime_dir(&self) -> PathBuf {
match self.mod_type { match self.mod_type {
OvrCompatibilityModuleType::Opencomposite | OvrCompatibilityModuleType::Vapor => { OvrCompatibilityModuleType::Opencomposite => self.path.join("build"),
self.path.join("build") OvrCompatibilityModuleType::Vapor => self.path.join("build/install_pfx/lib/VapoR"),
}
OvrCompatibilityModuleType::Xrizer => self.path.join("target/release"), OvrCompatibilityModuleType::Xrizer => self.path.join("target/release"),
} }
} }
@ -450,6 +459,29 @@ impl Profile {
get_data_dir().join("prefixes").join(uuid) get_data_dir().join("prefixes").join(uuid)
} }
/// deletes files and folders associated to this profile (mostly repo clones)
pub fn delete_files(&self) -> Vec<std::io::Result<()>> {
[
Some(&self.xrservice_path),
Some(&self.ovr_comp.path),
self.features.libsurvive.path.as_ref(),
self.features.basalt.path.as_ref(),
self.features.openhmd.path.as_ref(),
]
.iter()
.map(|dir| match dir {
Some(dir) => {
if dir.try_exists().unwrap_or_default() {
remove_dir_all(dir)
} else {
Ok(())
}
}
None => Ok(()),
})
.collect()
}
pub fn xr_runtime_json_env_var(&self) -> String { pub fn xr_runtime_json_env_var(&self) -> String {
format!( format!(
"XR_RUNTIME_JSON=\"{prefix}/share/openxr/1/openxr_{runtime}.json\"", "XR_RUNTIME_JSON=\"{prefix}/share/openxr/1/openxr_{runtime}.json\"",
@ -714,7 +746,7 @@ impl Profile {
if self.features.mercury_enabled { if self.features.mercury_enabled {
missing_deps.extend(get_missing_mercury_deps()); missing_deps.extend(get_missing_mercury_deps());
} }
// no listed deps for opencomp missing_deps.extend(self.ovr_comp.mod_type.get_missing_deps());
} }
missing_deps.sort_unstable(); missing_deps.sort_unstable();
missing_deps.dedup(); // dedup only works if sorted, hence the above missing_deps.dedup(); // dedup only works if sorted, hence the above

View file

@ -21,7 +21,7 @@ pub fn lighthouse_profile() -> Profile {
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix)); environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
Profile { Profile {
uuid: "lighthouse-default".into(), uuid: "lighthouse-default".into(),
name: format!("Lighthouse Driver - {name} Default", name = APP_NAME), name: format!("Lighthouse Driver - {APP_NAME} Default"),
xrservice_path: data_monado_path(), xrservice_path: data_monado_path(),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,
ovr_comp: ProfileOvrCompatibilityModule { ovr_comp: ProfileOvrCompatibilityModule {

View file

@ -21,7 +21,7 @@ pub fn openhmd_profile() -> Profile {
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix)); environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
Profile { Profile {
uuid: "openhmd-default".into(), uuid: "openhmd-default".into(),
name: format!("OpenHMD - {name} Default", name = APP_NAME), name: format!("OpenHMD - {APP_NAME} Default"),
xrservice_path: data_monado_path(), xrservice_path: data_monado_path(),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,
ovr_comp: ProfileOvrCompatibilityModule { ovr_comp: ProfileOvrCompatibilityModule {

View file

@ -22,7 +22,7 @@ pub fn simulated_profile() -> Profile {
); );
Profile { Profile {
uuid: "simulated-default".into(), uuid: "simulated-default".into(),
name: format!("Simulated Driver - {name} Default", name = APP_NAME), name: format!("Simulated Driver - {APP_NAME} Default"),
xrservice_path: data_monado_path(), xrservice_path: data_monado_path(),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,
ovr_comp: ProfileOvrCompatibilityModule { ovr_comp: ProfileOvrCompatibilityModule {

View file

@ -23,7 +23,7 @@ pub fn survive_profile() -> Profile {
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix)); environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
Profile { Profile {
uuid: "survive-default".into(), uuid: "survive-default".into(),
name: format!("Survive - {name} Default", name = APP_NAME), name: format!("Survive - {APP_NAME} Default"),
xrservice_path: data_monado_path(), xrservice_path: data_monado_path(),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,
ovr_comp: ProfileOvrCompatibilityModule { ovr_comp: ProfileOvrCompatibilityModule {

View file

@ -19,7 +19,7 @@ pub fn wivrn_profile() -> Profile {
environment.insert("U_PACING_APP_USE_MIN_FRAME_PERIOD".into(), "1".into()); environment.insert("U_PACING_APP_USE_MIN_FRAME_PERIOD".into(), "1".into());
Profile { Profile {
uuid: "wivrn-default".into(), uuid: "wivrn-default".into(),
name: format!("WiVRn - {name} Default", name = APP_NAME), name: format!("WiVRn - {APP_NAME} Default"),
xrservice_path: data_wivrn_path(), xrservice_path: data_wivrn_path(),
xrservice_type: XRServiceType::Wivrn, xrservice_type: XRServiceType::Wivrn,
ovr_comp: ProfileOvrCompatibilityModule { ovr_comp: ProfileOvrCompatibilityModule {

View file

@ -21,7 +21,7 @@ pub fn wmr_profile() -> Profile {
environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix)); environment.insert("LD_LIBRARY_PATH".into(), prepare_ld_library_path(&prefix));
Profile { Profile {
uuid: "wmr-default".into(), uuid: "wmr-default".into(),
name: format!("WMR - {name} Default", name = APP_NAME), name: format!("WMR - {APP_NAME} Default"),
xrservice_path: data_monado_path(), xrservice_path: data_monado_path(),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,
ovr_comp: ProfileOvrCompatibilityModule { ovr_comp: ProfileOvrCompatibilityModule {

View file

@ -2,7 +2,7 @@ use crate::{
paths::get_backup_dir, paths::get_backup_dir,
profile::Profile, profile::Profile,
util::{ util::{
file_utils::{copy_file, get_writer}, file_utils::{copy_file, get_writer, mark_as_executable},
steam_library_folder::SteamLibraryFolder, steam_library_folder::SteamLibraryFolder,
}, },
}; };
@ -15,45 +15,85 @@ use std::{
}; };
use tracing::error; use tracing::error;
pub const PRESSURE_VESSEL_STEAM_APPID: u32 = 1628350; pub const SNIPER_RUNTIME_STEAM_APPID: u32 = 1628350;
pub const SOLDIER_RUNTIME_STEAM_APPID: u32 = 1391110;
fn get_runtime_entrypoint_path() -> Option<PathBuf> { fn get_sniper_runtime_entrypoint_path() -> Option<PathBuf> {
match SteamLibraryFolder::get_folders() { match SteamLibraryFolder::get_folders() {
Ok(libraryfolders) => libraryfolders Ok(libraryfolders) => libraryfolders
.iter() .iter()
.find(|(_, folder)| folder.apps.contains_key(&PRESSURE_VESSEL_STEAM_APPID)) .find(|(_, folder)| folder.apps.contains_key(&SNIPER_RUNTIME_STEAM_APPID))
.map(|(_, folder)| { .map(|(_, folder)| {
PathBuf::from(&folder.path) PathBuf::from(&folder.path)
.join("steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point") .join("steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point")
}), }),
Err(e) => { Err(e) => {
error!("unable to get runtime entrypoint path: {e}"); error!("unable to get sniper runtime entrypoint path: {e}");
None
}
}
}
fn get_soldier_runtime_entrypoint_path() -> Option<PathBuf> {
match SteamLibraryFolder::get_folders() {
Ok(libraryfolders) => libraryfolders
.iter()
.find(|(_, folder)| folder.apps.contains_key(&SOLDIER_RUNTIME_STEAM_APPID))
.map(|(_, folder)| {
PathBuf::from(&folder.path)
.join("steamapps/common/SteamLinuxRuntime_soldier/_v2-entry-point")
}),
Err(e) => {
error!("unable to get soldier runtime entrypoint path: {e}");
None None
} }
} }
} }
lazy_static! { lazy_static! {
static ref STEAM_RUNTIME_ENTRYPOINT_PATH: Option<PathBuf> = get_runtime_entrypoint_path(); static ref STEAM_SNIPER_RUNTIME_ENTRYPOINT_PATH: Option<PathBuf> =
get_sniper_runtime_entrypoint_path();
static ref STEAM_SOLDIER_RUNTIME_ENTRYPOINT_PATH: Option<PathBuf> =
get_soldier_runtime_entrypoint_path();
} }
fn get_backup_runtime_entrypoint_location() -> PathBuf { fn get_backup_sniper_runtime_entrypoint_location() -> PathBuf {
get_backup_dir().join("_v2-entry-point.bak") get_backup_dir().join("_v2-entry-point.bak")
} }
fn backup_runtime_entrypoint(path: &Path) { fn get_backup_soldier_runtime_entrypoint_location() -> PathBuf {
copy_file(path, &get_backup_runtime_entrypoint_location()); get_backup_dir().join("_v2-entry-point.soldier.bak")
} }
pub fn restore_runtime_entrypoint() { fn backup_sniper_runtime_entrypoint(path: &Path) {
if let Some(path) = STEAM_RUNTIME_ENTRYPOINT_PATH.as_ref() { copy_file(path, &get_backup_sniper_runtime_entrypoint_location());
let backup = get_backup_runtime_entrypoint_location(); }
fn backup_soldier_runtime_entrypoint(path: &Path) {
copy_file(path, &get_backup_soldier_runtime_entrypoint_location());
}
pub fn restore_sniper_runtime_entrypoint() {
if let Some(path) = STEAM_SNIPER_RUNTIME_ENTRYPOINT_PATH.as_ref() {
let backup = get_backup_sniper_runtime_entrypoint_location();
if Path::new(&backup).is_file() { if Path::new(&backup).is_file() {
copy_file(&backup, path); copy_file(&backup, path);
let _ = mark_as_executable(path);
} }
} }
} }
pub fn restore_soldier_runtime_entrypoint() {
if let Some(path) = STEAM_SOLDIER_RUNTIME_ENTRYPOINT_PATH.as_ref() {
let backup = get_backup_soldier_runtime_entrypoint_location();
if Path::new(&backup).is_file() {
copy_file(&backup, path);
let _ = mark_as_executable(path);
}
}
}
/// this implementation is identical for both sniper and soldier runtimes
fn append_to_runtime_entrypoint(data: &str, path: &Path) -> anyhow::Result<()> { fn append_to_runtime_entrypoint(data: &str, path: &Path) -> anyhow::Result<()> {
let existing = read_to_string(path)?; let existing = read_to_string(path)?;
let new = existing.replace( let new = existing.replace(
@ -65,10 +105,12 @@ fn append_to_runtime_entrypoint(data: &str, path: &Path) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
pub fn set_runtime_entrypoint_launch_opts_from_profile(profile: &Profile) -> anyhow::Result<()> { pub fn set_sniper_runtime_entrypoint_launch_opts_from_profile(
restore_runtime_entrypoint(); profile: &Profile,
if let Some(dest) = STEAM_RUNTIME_ENTRYPOINT_PATH.as_ref() { ) -> anyhow::Result<()> {
backup_runtime_entrypoint(dest); restore_sniper_runtime_entrypoint();
if let Some(dest) = STEAM_SNIPER_RUNTIME_ENTRYPOINT_PATH.as_ref() {
backup_sniper_runtime_entrypoint(dest);
append_to_runtime_entrypoint( append_to_runtime_entrypoint(
&profile &profile
.get_env_vars() .get_env_vars()
@ -78,8 +120,31 @@ pub fn set_runtime_entrypoint_launch_opts_from_profile(profile: &Profile) -> any
.join("\n"), .join("\n"),
dest, dest,
)?; )?;
mark_as_executable(dest)?;
return Ok(()); return Ok(());
} }
bail!("Could not find valid runtime entrypoint"); bail!("Could not find valid sniper runtime entrypoint");
}
pub fn set_soldier_runtime_entrypoint_launch_opts_from_profile(
profile: &Profile,
) -> anyhow::Result<()> {
restore_soldier_runtime_entrypoint();
if let Some(dest) = STEAM_SOLDIER_RUNTIME_ENTRYPOINT_PATH.as_ref() {
backup_soldier_runtime_entrypoint(dest);
append_to_runtime_entrypoint(
&profile
.get_env_vars()
.iter()
.map(|ev| "export ".to_string() + ev)
.collect::<Vec<String>>()
.join("\n"),
dest,
)?;
mark_as_executable(dest)?;
return Ok(());
}
bail!("Could not find valid soldier runtime entrypoint");
} }

View file

@ -41,8 +41,11 @@ use crate::{
profile::{OvrCompatibilityModuleType, Profile, XRServiceType}, profile::{OvrCompatibilityModuleType, Profile, XRServiceType},
stateless_action, stateless_action,
steam_linux_runtime_injector::{ steam_linux_runtime_injector::{
restore_runtime_entrypoint, set_runtime_entrypoint_launch_opts_from_profile, restore_sniper_runtime_entrypoint, restore_soldier_runtime_entrypoint,
set_sniper_runtime_entrypoint_launch_opts_from_profile,
set_soldier_runtime_entrypoint_launch_opts_from_profile,
}, },
termcolor::TermColor,
util::file_utils::{ util::file_utils::{
setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd, verify_cap_sys_nice_eip, setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd, verify_cap_sys_nice_eip,
}, },
@ -51,6 +54,7 @@ use crate::{
xr_devices::XRDevice, xr_devices::XRDevice,
}; };
use adw::{prelude::*, ResponseAppearance}; use adw::{prelude::*, ResponseAppearance};
use delicious_adwaita::{theme::Theme, ThemeEngine};
use gtk::glib::{self, clone}; use gtk::glib::{self, clone};
use notify_rust::NotificationHandle; use notify_rust::NotificationHandle;
use relm4::{ use relm4::{
@ -96,6 +100,8 @@ pub struct App {
inhibit_fail_notif: Option<NotificationHandle>, inhibit_fail_notif: Option<NotificationHandle>,
pluginstore: Option<AsyncController<PluginStore>>, pluginstore: Option<AsyncController<PluginStore>>,
theme_engine: ThemeEngine,
} }
#[derive(Debug)] #[derive(Debug)]
@ -113,7 +119,8 @@ pub enum Msg {
StartWithDebug, StartWithDebug,
RestartXRService, RestartXRService,
ProfileSelected(Profile), ProfileSelected(Profile),
DeleteProfile, /// bool param: delete files
DeleteProfile(bool),
SaveProfile(Profile), SaveProfile(Profile),
RunSetCap, RunSetCap,
OpenLibsurviveSetup, OpenLibsurviveSetup,
@ -129,6 +136,8 @@ pub enum Msg {
WivrnCheckPairMode, WivrnCheckPairMode,
OpenPluginStore, OpenPluginStore,
UpdateConfigPlugins(HashMap<String, PluginConfig>), UpdateConfigPlugins(HashMap<String, PluginConfig>),
ShowThemeManager,
SaveThemeConfig,
NoOp, NoOp,
} }
@ -227,13 +236,17 @@ impl App {
); );
worker.start(); worker.start();
self.xrservice_worker = Some(worker); self.xrservice_worker = Some(worker);
let set_sniper_launch_opts_res =
set_sniper_runtime_entrypoint_launch_opts_from_profile(&prof);
let set_soldier_launch_opts_res =
set_soldier_runtime_entrypoint_launch_opts_from_profile(&prof);
self.main_view self.main_view
.sender() .sender()
.emit(MainViewMsg::XRServiceActiveChanged( .emit(MainViewMsg::XRServiceActiveChanged(
true, true,
Some(self.get_selected_profile()), Some(self.get_selected_profile()),
// show launch opts only if setting the runtime entrypoint fails // show launch opts only if setting the runtime entrypoint fails
set_runtime_entrypoint_launch_opts_from_profile(&prof).is_err(), set_sniper_launch_opts_res.is_err() || set_soldier_launch_opts_res.is_err(),
)); ));
self.debug_view self.debug_view
.sender() .sender()
@ -249,10 +262,6 @@ impl App {
.plugins .plugins
.values() .values()
.filter_map(|cp| { .filter_map(|cp| {
// disable potentially unsafe wayvr_dashboard
if cp.plugin.appid.contains("wayvr_dashboard") {
return None;
}
if cp.enabled && cp.plugin.validate() { if cp.enabled && cp.plugin.validate() {
if let Err(e) = cp.plugin.mark_as_executable() { if let Err(e) = cp.plugin.mark_as_executable() {
error!( error!(
@ -301,7 +310,8 @@ impl App {
} }
pub fn restore_openxr_openvr_files(&self) { pub fn restore_openxr_openvr_files(&self) {
restore_runtime_entrypoint(); restore_sniper_runtime_entrypoint();
restore_soldier_runtime_entrypoint();
if let Err(e) = remove_current_active_runtime() { if let Err(e) = remove_current_active_runtime() {
alert( alert(
"Could not remove profile active runtime", "Could not remove profile active runtime",
@ -370,7 +380,7 @@ impl AsyncComponent for App {
set_content: Some(&adw::NavigationPage::new(model.debug_view.widget(), "Debug View")), set_content: Some(&adw::NavigationPage::new(model.debug_view.widget(), "Debug View")),
set_show_content: false, set_show_content: false,
set_collapsed: !model.config.debug_view_enabled, set_collapsed: !model.config.debug_view_enabled,
} },
}, },
connect_close_request[sender] => move |win| { connect_close_request[sender] => move |win| {
sender.input(Msg::SaveWinSize(win.width(), win.height())); sender.input(Msg::SaveWinSize(win.width(), win.height()));
@ -394,6 +404,27 @@ impl AsyncComponent for App {
) { ) {
match message { match message {
Msg::NoOp => {} Msg::NoOp => {}
Msg::ShowThemeManager => {
let dialog = self
.theme_engine
.theme_chooser_dialog(Theme::default_themes().as_ref());
dialog.set_content_height(2000);
dialog.present(Some(&self.app_win));
dialog.connect_closed(clone!(
#[strong]
sender,
move |_| {
sender.input(Msg::SaveThemeConfig);
}
));
}
Msg::SaveThemeConfig => {
let name = self.theme_engine.current_theme_name();
if self.config.theme_name != name {
self.config.theme_name = name;
self.config.save();
}
}
Msg::OnServiceLog(rows) => { Msg::OnServiceLog(rows) => {
if !rows.is_empty() { if !rows.is_empty() {
self.debug_view self.debug_view
@ -524,7 +555,7 @@ impl AsyncComponent for App {
jobs.extend(get_build_basalt_jobs(&profile, clean_build)); jobs.extend(get_build_basalt_jobs(&profile, clean_build));
} }
if profile.features.mercury_enabled { if profile.features.mercury_enabled {
jobs.extend(get_build_mercury_jobs(&profile)); jobs.extend(get_build_mercury_jobs());
} }
jobs.extend(match profile.xrservice_type { jobs.extend(match profile.xrservice_type {
XRServiceType::Monado => get_build_monado_jobs(&profile, clean_build), XRServiceType::Monado => get_build_monado_jobs(&profile, clean_build),
@ -630,6 +661,10 @@ impl AsyncComponent for App {
if dep_pkexec().check() { if dep_pkexec().check() {
self.setcap_confirm_dialog.present(Some(&self.app_win)); self.setcap_confirm_dialog.present(Some(&self.app_win));
} else { } else {
self.build_window
.sender()
.emit(BuildWindowMsg::UpdateContent(vec![TermColor::Red
.colorize("pkexec not found, cannot set capabilities\n")]));
alert_w_widget( alert_w_widget(
"pkexec not found", "pkexec not found",
Some(&format!( Some(&format!(
@ -652,7 +687,7 @@ impl AsyncComponent for App {
self.build_window self.build_window
.sender() .sender()
.emit(BuildWindowMsg::UpdateBuildStatus(BuildStatus::Error( .emit(BuildWindowMsg::UpdateBuildStatus(BuildStatus::Error(
format!("Exit status {}", errcode), format!("Exit status {errcode}"),
))); )));
} }
}; };
@ -662,9 +697,16 @@ impl AsyncComponent for App {
w.stop(); w.stop();
} }
} }
Msg::DeleteProfile => { Msg::DeleteProfile(delete_files) => {
let todel = self.get_selected_profile(); let todel = self.get_selected_profile();
if todel.editable { if todel.editable {
if delete_files {
for res in todel.delete_files() {
if let Err(e) = res {
error!("Error deleting profile directory: {e}");
}
}
}
self.config.user_profiles.retain(|p| p.uuid != todel.uuid); self.config.user_profiles.retain(|p| p.uuid != todel.uuid);
self.config.save(); self.config.save();
self.profiles = self.config.profiles(); self.profiles = self.config.profiles();
@ -704,6 +746,7 @@ impl AsyncComponent for App {
} }
Msg::RunSetCap => { Msg::RunSetCap => {
if !dep_pkexec().check() { if !dep_pkexec().check() {
// there's a precheck ahead of this, this should likely never happen
error!("pkexec not found, skipping setcap"); error!("pkexec not found, skipping setcap");
} else { } else {
let profile = self.get_selected_profile(); let profile = self.get_selected_profile();
@ -721,8 +764,26 @@ impl AsyncComponent for App {
if let Err(e) = setcap_cap_sys_nice_eip(&profile).await { if let Err(e) = setcap_cap_sys_nice_eip(&profile).await {
setcap_failed_dialog(); setcap_failed_dialog();
error!("failed running setcap: {e}"); error!("failed running setcap: {e}");
self.build_window
.sender()
.emit(BuildWindowMsg::UpdateContent(vec![
TermColor::Red.colorize("Setting capabilities failed\n")
]));
} else if !verify_cap_sys_nice_eip(&profile).await { } else if !verify_cap_sys_nice_eip(&profile).await {
setcap_failed_dialog(); setcap_failed_dialog();
error!("setcap succeeded but capabilities were reset");
self.build_window
.sender()
.emit(BuildWindowMsg::UpdateContent(vec![TermColor::Red
.colorize(
"Setting capabilities succeeded, but capabilities have been reset\n",
)]));
} else {
self.build_window
.sender()
.emit(BuildWindowMsg::UpdateContent(vec![
TermColor::Green.colorize("Capabilities set correctly\n")
]));
} }
} }
} }
@ -970,6 +1031,17 @@ impl AsyncComponent for App {
} }
) )
); );
stateless_action!(
actions,
ThemeManagerAction,
clone!(
#[strong]
sender,
move |_| {
sender.input(Msg::ShowThemeManager);
}
)
);
// this bypasses the macro because I need the underlying gio action // this bypasses the macro because I need the underlying gio action
// to enable/disable it in update() // to enable/disable it in update()
let configure_wivrn_action = { let configure_wivrn_action = {
@ -1011,7 +1083,7 @@ impl AsyncComponent for App {
MainViewOutMsg::DoStartStopXRService => Msg::DoStartStopXRService, MainViewOutMsg::DoStartStopXRService => Msg::DoStartStopXRService,
MainViewOutMsg::RestartXRService => Msg::RestartXRService, MainViewOutMsg::RestartXRService => Msg::RestartXRService,
MainViewOutMsg::ProfileSelected(uuid) => Msg::ProfileSelected(uuid), MainViewOutMsg::ProfileSelected(uuid) => Msg::ProfileSelected(uuid),
MainViewOutMsg::DeleteProfile => Msg::DeleteProfile, MainViewOutMsg::DeleteProfile(delete_files) => Msg::DeleteProfile(delete_files),
MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p), MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p),
MainViewOutMsg::OpenLibsurviveSetup => Msg::OpenLibsurviveSetup, MainViewOutMsg::OpenLibsurviveSetup => Msg::OpenLibsurviveSetup,
MainViewOutMsg::BuildProfile(clean) => Msg::BuildProfile(clean), MainViewOutMsg::BuildProfile(clean) => Msg::BuildProfile(clean),
@ -1038,6 +1110,17 @@ impl AsyncComponent for App {
.detach(), .detach(),
split_view: None, split_view: None,
setcap_confirm_dialog, setcap_confirm_dialog,
theme_engine: ThemeEngine::new_with_theme(&{
if config.theme_name == "Follow system" {
Theme::default()
} else {
Theme::default_themes()
.into_iter()
.find(|t| t.name == config.theme_name)
.unwrap_or_default()
}
})
.unwrap(),
config, config,
profiles, profiles,
xrservice_worker: None, xrservice_worker: None,
@ -1126,6 +1209,7 @@ new_stateless_action!(pub QuitAction, AppActionGroup, "quit");
new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool); new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool);
new_stateless_action!(pub ConfigureWivrnAction, AppActionGroup, "configurewivrn"); new_stateless_action!(pub ConfigureWivrnAction, AppActionGroup, "configurewivrn");
new_stateless_action!(pub PluginStoreAction, AppActionGroup, "store"); new_stateless_action!(pub PluginStoreAction, AppActionGroup, "store");
new_stateless_action!(pub ThemeManagerAction, AppActionGroup, "thememanager");
new_stateless_action!(pub DebugOpenDataAction, AppActionGroup, "debugopendata"); new_stateless_action!(pub DebugOpenDataAction, AppActionGroup, "debugopendata");
new_stateless_action!(pub DebugOpenPrefixAction, AppActionGroup, "debugopenprefix"); new_stateless_action!(pub DebugOpenPrefixAction, AppActionGroup, "debugopenprefix");

View file

@ -1,3 +1,5 @@
use crate::termcolor::TermColor;
use super::{term_widget::TermWidget, SENDER_IO_ERR_MSG}; use super::{term_widget::TermWidget, SENDER_IO_ERR_MSG};
use adw::prelude::*; use adw::prelude::*;
use relm4::prelude::*; use relm4::prelude::*;
@ -91,7 +93,7 @@ impl SimpleComponent for BuildWindow {
BuildStatus::Building => String::default(), BuildStatus::Building => String::default(),
BuildStatus::Done => "Build done, you can close this window".into(), BuildStatus::Done => "Build done, you can close this window".into(),
BuildStatus::Error(code) => { BuildStatus::Error(code) => {
format!("Build failed: \"{c}\"", c = code) format!("Build failed: \"{code}\"")
} }
}.as_str(), }.as_str(),
#[track = "model.changed(BuildWindow::build_status())"] #[track = "model.changed(BuildWindow::build_status())"]
@ -164,8 +166,18 @@ impl SimpleComponent for BuildWindow {
label.remove_css_class("success"); label.remove_css_class("success");
label.remove_css_class("error"); label.remove_css_class("error");
match status { match status {
BuildStatus::Done => label.add_css_class("success"), BuildStatus::Done => {
BuildStatus::Error(_) => label.add_css_class("error"), label.add_css_class("success");
sender.input(BuildWindowMsg::UpdateContent(vec![
TermColor::Blue.colorize("Build completed!\n")
]));
}
BuildStatus::Error(_) => {
label.add_css_class("error");
sender.input(BuildWindowMsg::UpdateContent(vec![
TermColor::Blue.colorize("Build failed!\n")
]));
}
_ => {} _ => {}
} }
if status != BuildStatus::Building { if status != BuildStatus::Building {

View file

@ -21,6 +21,7 @@ use crate::{
paths::{get_data_dir, get_home_dir}, paths::{get_data_dir, get_home_dir},
profile::{LighthouseDriver, Profile, XRServiceType}, profile::{LighthouseDriver, Profile, XRServiceType},
stateless_action, stateless_action,
ui::app::ThemeManagerAction,
util::{ util::{
file_utils::{get_writer, mount_has_nosuid}, file_utils::{get_writer, mount_has_nosuid},
steamvr_utils::chaperone_info_exists, steamvr_utils::chaperone_info_exists,
@ -112,7 +113,8 @@ pub enum MainViewOutMsg {
DoStartStopXRService, DoStartStopXRService,
RestartXRService, RestartXRService,
ProfileSelected(Profile), ProfileSelected(Profile),
DeleteProfile, /// bool param: delete files
DeleteProfile(bool),
SaveProfile(Profile), SaveProfile(Profile),
OpenLibsurviveSetup, OpenLibsurviveSetup,
/// params: clean /// params: clean
@ -158,6 +160,7 @@ impl AsyncComponent for MainView {
"Configure _WiVRn" => ConfigureWivrnAction, "Configure _WiVRn" => ConfigureWivrnAction,
}, },
section! { section! {
"Change _Theme" => ThemeManagerAction,
"_About" => AboutAction, "_About" => AboutAction,
}, },
}, },
@ -931,6 +934,13 @@ impl AsyncComponent for MainView {
let profile_delete_confirm_dialog = adw::AlertDialog::builder() let profile_delete_confirm_dialog = adw::AlertDialog::builder()
.heading("Are you sure you want to delete this profile?") .heading("Are you sure you want to delete this profile?")
.extra_child(
&gtk::CheckButton::builder()
.label("Delete all files and folders associated with profile")
.halign(gtk::Align::Center)
.hexpand(true)
.build(),
)
.build(); .build();
profile_delete_confirm_dialog.add_response("no", "_No"); profile_delete_confirm_dialog.add_response("no", "_No");
profile_delete_confirm_dialog.add_response("yes", "_Yes"); profile_delete_confirm_dialog.add_response("yes", "_Yes");
@ -942,10 +952,19 @@ impl AsyncComponent for MainView {
clone!( clone!(
#[strong] #[strong]
sender, sender,
move |_, res| { move |dialog, res| {
let delete_files_checkbox = dialog
.extra_child()
.and_then(|child| child.downcast::<gtk::CheckButton>().ok());
let delete_files = delete_files_checkbox
.as_ref()
.is_some_and(|c| c.is_active());
if let Some(check) = delete_files_checkbox {
check.set_active(false);
}
if res == "yes" { if res == "yes" {
sender sender
.output(Self::Output::DeleteProfile) .output(Self::Output::DeleteProfile(delete_files))
.expect("Sender output failed"); .expect("Sender output failed");
} }
} }

View file

@ -31,6 +31,7 @@ pub enum AddCustomPluginWinMsg {
Present, Present,
Close, Close,
OnNameChange(String), OnNameChange(String),
OnArgsChange(String),
OnExecPathChange(Option<String>), OnExecPathChange(Option<String>),
Add, Add,
} }
@ -95,7 +96,11 @@ impl SimpleComponent for AddCustomPluginWin {
"", "",
clone!( clone!(
#[strong] sender, #[strong] sender,
move |row| sender.input(Self::Input::OnNameChange(row.text().to_string())) move |row| sender.input(
Self::Input::OnNameChange(
row.text().to_string()
)
)
) )
), ),
add: &file_row( add: &file_row(
@ -105,9 +110,23 @@ impl SimpleComponent for AddCustomPluginWin {
Some(model.parent.clone()), Some(model.parent.clone()),
clone!( clone!(
#[strong] sender, #[strong] sender,
move |path_s| sender.input(Self::Input::OnExecPathChange(path_s)) move |path_s| sender.input(
Self::Input::OnExecPathChange(path_s)
)
) )
) ),
add: &entry_row(
"Plugin Arguments",
"",
clone!(
#[strong] sender,
move |row| sender.input(
Self::Input::OnArgsChange(
row.text().to_string()
)
)
)
),
}, },
}, },
}, },
@ -139,6 +158,17 @@ impl SimpleComponent for AddCustomPluginWin {
self.plugin.name = name; self.plugin.name = name;
self.set_can_add(self.plugin.validate()); self.set_can_add(self.plugin.validate());
} }
Self::Input::OnArgsChange(args) => {
let args = args.trim().to_string();
self.plugin.args = if args.is_empty() {
None
} else {
// it's fine to have them joined
// since they will ultimately be
// passed as a joined string
Some(vec![args])
}
}
Self::Input::OnExecPathChange(ep) => { Self::Input::OnExecPathChange(ep) => {
self.plugin.exec_path = ep.map(PathBuf::from); self.plugin.exec_path = ep.map(PathBuf::from);
self.set_can_add(self.plugin.validate()); self.set_can_add(self.plugin.validate());

View file

@ -163,10 +163,10 @@ impl Plugin {
/// urls to manifest json files representing plugins. /// urls to manifest json files representing plugins.
/// each manifest should be json and the link should always point to the latest version /// each manifest should be json and the link should always point to the latest version
const MANIFESTS: [&str;2] = [ const MANIFESTS: [&str;3] = [
"https://github.com/galister/wlx-overlay-s/raw/refs/heads/meta/com.github.galister.wlx-overlay-s.json", "https://github.com/galister/wlx-overlay-s/raw/refs/heads/meta/com.github.galister.wlx-overlay-s.json",
"https://github.com/StardustXR/telescope/raw/refs/heads/main/envision/org.stardustxr.telescope.json", "https://github.com/StardustXR/telescope/raw/refs/heads/main/envision/org.stardustxr.telescope.json",
// wayvr dashboard potentially unsafe "https://github.com/olekolek1000/wayvr-dashboard/raw/refs/heads/meta/dev.oo8.wayvr_dashboard.json",
]; ];
pub async fn refresh_plugins() -> anyhow::Result<Vec<anyhow::Result<Plugin>>> { pub async fn refresh_plugins() -> anyhow::Result<Vec<anyhow::Result<Plugin>>> {

View file

@ -213,7 +213,7 @@ pub fn file_row<F: Fn(Option<String>) + 'static + Clone>(
let filedialog = gtk::FileDialog::builder() let filedialog = gtk::FileDialog::builder()
.modal(true) .modal(true)
.title(format!("Select {}", title)) .title(format!("Select {title}"))
.build(); .build();
row.connect_activated(clone!( row.connect_activated(clone!(
@ -255,7 +255,7 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
let (row, path_label) = filedialog_row_base(title, description, value, cb.clone()); let (row, path_label) = filedialog_row_base(title, description, value, cb.clone());
let filedialog = gtk::FileDialog::builder() let filedialog = gtk::FileDialog::builder()
.modal(true) .modal(true)
.title(format!("Select Path for {}", title)) .title(format!("Select Path for {title}"))
.build(); .build();
row.connect_activated(clone!( row.connect_activated(clone!(

View file

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

View file

@ -112,7 +112,7 @@ impl AsyncComponent for WivrnWiredStartBox {
Self::Input::UpdateSelectedProfile(p) => self.set_selected_profile(p), Self::Input::UpdateSelectedProfile(p) => self.set_selected_profile(p),
Self::Input::StartWivrnClient => { Self::Input::StartWivrnClient => {
if !dep_adb().check() { if !dep_adb().check() {
alert("ADB is not installed", Some(&format!("Please install ADB on your computer to start the WiVRn client from {}.", APP_NAME)), Some(&self.root_win)); alert("ADB is not installed", Some(&format!("Please install ADB on your computer to start the WiVRn client from {APP_NAME}.")), Some(&self.root_win));
return; return;
} }
self.set_start_client_status(StartClientStatus::InProgress); self.set_start_client_status(StartClientStatus::InProgress);

View file

@ -1,4 +1,4 @@
use crate::{async_process::async_process, profile::Profile}; use crate::{async_process::async_process, depcheck::common::dep_getcap_setcap, profile::Profile};
use anyhow::bail; use anyhow::bail;
use nix::{ use nix::{
errno::Errno, errno::Errno,
@ -79,9 +79,29 @@ pub fn set_file_readonly(path: &Path, readonly: bool) -> anyhow::Result<()> {
Ok(fs::set_permissions(path, perms)?) Ok(fs::set_permissions(path, perms)?)
} }
pub fn setcap_executable() -> Option<String> {
if dep_getcap_setcap().check() {
Some("setcap".into())
} else if Path::new("/sbin/setcap").try_exists().unwrap_or_default() {
Some("/sbin/setcap".into())
} else {
None
}
}
pub fn getcap_executable() -> Option<String> {
if dep_getcap_setcap().check() {
Some("getcap".into())
} else if Path::new("/sbin/getcap").try_exists().unwrap_or_default() {
Some("/sbin/getcap".into())
} else {
None
}
}
pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> { pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
vec![ vec![
"setcap".into(), setcap_executable().unwrap_or("setcap".into()),
"CAP_SYS_NICE=eip".into(), "CAP_SYS_NICE=eip".into(),
profile profile
.prefix .prefix
@ -93,24 +113,29 @@ pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
pub async fn verify_cap_sys_nice_eip(profile: &Profile) -> bool { pub async fn verify_cap_sys_nice_eip(profile: &Profile) -> bool {
let xrservice_binary = profile.xrservice_binary().to_string_lossy().to_string(); let xrservice_binary = profile.xrservice_binary().to_string_lossy().to_string();
match async_process("getcap", Some(&[&xrservice_binary]), None).await { if let Some(getcap_exec) = getcap_executable() {
Err(e) => { match async_process(&getcap_exec, Some(&[&xrservice_binary]), None).await {
error!("failed to run `getcap {xrservice_binary}`: {e:?}"); Err(e) => {
false error!("failed to run `getcap {xrservice_binary}`: {e:?}");
}
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 false
} else { }
out.stdout.to_lowercase().contains("cap_sys_nice=eip") Ok(out) => {
debug!("getcap {xrservice_binary} stdout: {}", out.stdout);
debug!("getcap {xrservice_binary} stderr: {}", out.stderr);
if out.exit_code != 0 {
error!(
"command `getcap {xrservice_binary}` failed with status code {}",
out.exit_code
);
false
} else {
out.stdout.to_lowercase().contains("cap_sys_nice=eip")
}
} }
} }
} else {
error!("getcap executable does not exist");
false
} }
} }

View file

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