diff --git a/src/backend/input.rs b/src/backend/input.rs index 1a9b107c..a1dc5427 100644 --- a/src/backend/input.rs +++ b/src/backend/input.rs @@ -1,14 +1,17 @@ use std::{collections::VecDeque, time::Instant}; use glam::{Affine3A, Vec2, Vec3A}; -use ovr_overlay::TrackedDeviceIndex; use tinyvec::array_vec; +#[cfg(feature = "openvr")] +use ovr_overlay::TrackedDeviceIndex; + use crate::state::AppState; use super::{common::OverlayContainer, overlay::OverlayData}; pub struct TrackedDevice { + #[cfg(feature = "openvr")] pub index: TrackedDeviceIndex, pub valid: bool, pub soc: Option, diff --git a/src/backend/openvr/mod.rs b/src/backend/openvr/mod.rs index 2dac6133..71f07266 100644 --- a/src/backend/openvr/mod.rs +++ b/src/backend/openvr/mod.rs @@ -62,7 +62,7 @@ pub fn openvr_run(running: Arc) -> Result<(), BackendError> { }; let mut state = { - let graphics = WlxGraphics::new(instance_extensions, device_extensions_fn); + let graphics = WlxGraphics::new_openvr(instance_extensions, device_extensions_fn); AppState::from_graphics(graphics) }; diff --git a/src/backend/openvr/overlay.rs b/src/backend/openvr/overlay.rs index cefbf177..23eb38cc 100644 --- a/src/backend/openvr/overlay.rs +++ b/src/backend/openvr/overlay.rs @@ -225,7 +225,6 @@ impl OverlayData { m_pQueue: graphics.queue.handle().as_raw() as *mut _, m_nQueueFamilyIndex: graphics.queue.queue_family_index(), }; - log::info!( "{}: UploadTex {:?}, {}x{}, {:?}", self.state.name, @@ -233,10 +232,8 @@ impl OverlayData { texture.m_nWidth, texture.m_nHeight, image.usage() - ); if let Err(e) = overlay.set_image_vulkan(handle, &mut texture) { panic!("Failed to set overlay texture: {}", e); } - log::info!("{}: Uploaded texture", self.state.name); } } diff --git a/src/backend/openxr/mod.rs b/src/backend/openxr/mod.rs index de7ec28a..1bb4af9c 100644 --- a/src/backend/openxr/mod.rs +++ b/src/backend/openxr/mod.rs @@ -54,7 +54,7 @@ pub fn openxr_run(running: Arc) -> Result<(), BackendError> { log::info!("Using environment blend mode: {:?}", environment_blend_mode); let mut app_state = { - let graphics = WlxGraphics::new_xr(xr_instance.clone(), system); + let graphics = WlxGraphics::new_openxr(xr_instance.clone(), system); AppState::from_graphics(graphics) }; diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..b20b700a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,121 @@ +use crate::config_io; +use crate::config_io::get_conf_d_path; +use crate::load_with_fallback; +use crate::overlays::keyboard; +use log::error; +use serde::Deserialize; +use serde::Serialize; + +pub fn def_pw_tokens() -> Vec<(String, String)> { + Vec::new() +} + +fn def_grab_threshold() -> f32 { + 0.6 +} + +fn def_trigger_threshold() -> f32 { + 0.65 +} + +fn def_click_freeze_time_ms() -> u32 { + 300 +} + +fn def_true() -> bool { + true +} + +fn def_one() -> f32 { + 1.0 +} + +#[derive(Deserialize, Serialize)] +pub struct GeneralConfig { + #[serde(default = "def_grab_threshold")] + pub grab_threshold: f32, + + #[serde(default = "def_trigger_threshold")] + pub trigger_threshold: f32, + + #[serde(default = "def_click_freeze_time_ms")] + pub click_freeze_time_ms: u32, + + #[serde(default = "def_true")] + pub keyboard_sound_enabled: bool, + + #[serde(default = "def_one")] + pub keyboard_scale: f32, + + #[serde(default = "def_one")] + pub desktop_view_scale: f32, + + #[serde(default = "def_one")] + pub watch_scale: f32, + + #[serde(default = "def_pw_tokens")] + pub pw_tokens: Vec<(String, String)>, +} + +impl GeneralConfig { + fn sanitize_range(name: &str, val: f32, from: f32, to: f32) { + if !val.is_normal() || val < from || val > to { + panic!( + "GeneralConfig: {} needs to be between {} and {}", + name, from, to + ); + } + } + + pub fn load_from_disk() -> GeneralConfig { + let config = load_general(); + config.post_load(); + config + } + + fn post_load(&self) { + GeneralConfig::sanitize_range("grab_threshold", self.grab_threshold, 0.0, 1.0); + GeneralConfig::sanitize_range("trigger_threshold", self.trigger_threshold, 0.0, 1.0); + GeneralConfig::sanitize_range("keyboard_scale", self.keyboard_scale, 0.0, 5.0); + GeneralConfig::sanitize_range("desktop_view_scale", self.desktop_view_scale, 0.0, 5.0); + GeneralConfig::sanitize_range("watch_scale", self.watch_scale, 0.0, 5.0); + } +} + +pub fn load_keyboard() -> keyboard::Layout { + let yaml_data = load_with_fallback!("keyboard.yaml", "res/keyboard.yaml"); + serde_yaml::from_str(&yaml_data).expect("Failed to parse keyboard.yaml") +} + +pub fn load_general() -> GeneralConfig { + let mut yaml_data = String::new(); + + // Add files from conf.d directory + let path_conf_d = get_conf_d_path(); + if let Ok(paths_unsorted) = std::fs::read_dir(path_conf_d) { + // Sort paths alphabetically + let mut paths: Vec<_> = paths_unsorted.map(|r| r.unwrap()).collect(); + paths.sort_by_key(|dir| dir.path()); + for path in paths { + if !path.file_type().unwrap().is_file() { + continue; + } + + println!("Loading config file {}", path.path().to_string_lossy()); + + if let Ok(data) = std::fs::read_to_string(path.path()) { + yaml_data.push('\n'); // Just in case, if end of the config file was not newline + yaml_data.push_str(data.as_str()); + } else { + // Shouldn't happen anyways + error!("Failed to load {}", path.path().to_string_lossy()); + } + } + } + + if yaml_data.is_empty() { + yaml_data.push_str(load_with_fallback!("config.yaml", "res/config.yaml").as_str()); + } + + serde_yaml::from_str(&yaml_data).expect("Failed to parse config.yaml") +} diff --git a/src/config_io.rs b/src/config_io.rs new file mode 100644 index 00000000..8a88f140 --- /dev/null +++ b/src/config_io.rs @@ -0,0 +1,71 @@ +use log::error; +use once_cell::sync::Lazy; +use std::{ + fs::{self, create_dir}, + path::PathBuf, +}; + +const FALLBACK_CONFIG_PATH: &str = "/tmp/wlxoverlay"; + +pub static CONFIG_ROOT_PATH: Lazy = Lazy::new(|| { + if let Ok(xdg_dirs) = xdg::BaseDirectories::new() { + let mut dir = xdg_dirs.get_config_home(); + dir.push("wlxoverlay"); + return dir; + } + //Return fallback config path + error!( + "Err: Failed to find config path, using {}", + FALLBACK_CONFIG_PATH + ); + PathBuf::from(FALLBACK_CONFIG_PATH) +}); + +pub fn get_conf_d_path() -> PathBuf { + let mut config_root = CONFIG_ROOT_PATH.clone(); + config_root.push("conf.d"); + config_root +} + +// Make sure config directory is present and return root config path +pub fn ensure_config_root() -> PathBuf { + let path = CONFIG_ROOT_PATH.clone(); + let _ = create_dir(&path); + + let path_conf_d = get_conf_d_path(); + let _ = create_dir(path_conf_d); + path +} + +fn get_config_file_path(filename: &str) -> PathBuf { + let mut config_root = CONFIG_ROOT_PATH.clone(); + config_root.push(filename); + config_root +} + +pub fn load(filename: &str) -> Option { + let path = get_config_file_path(filename); + println!("Loading config {}", path.to_string_lossy()); + + if let Ok(data) = fs::read_to_string(path) { + Some(data) + } else { + None + } +} + +#[macro_export] +macro_rules! load_with_fallback { + ($filename: expr, $fallback: expr) => { + if let Some(data) = config_io::load($filename) { + data + } else { + println!( + "Config {}/{} does not exist, using internal fallback", + config_io::CONFIG_ROOT_PATH.to_string_lossy(), + $filename + ); + include_str!($fallback).to_string() + } + }; +} diff --git a/src/graphics.rs b/src/graphics.rs index 71d04231..559b256f 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -26,8 +26,8 @@ use vulkano::{ allocator::StandardDescriptorSetAllocator, DescriptorSet, WriteDescriptorSet, }, device::{ - physical::{PhysicalDevice, PhysicalDeviceType}, - Device, DeviceCreateInfo, DeviceExtensions, Features, Queue, QueueCreateInfo, QueueFlags, + physical::PhysicalDevice, Device, DeviceCreateInfo, DeviceExtensions, Queue, + QueueCreateInfo, QueueFlags, }, format::Format, image::{ @@ -37,7 +37,7 @@ use vulkano::{ Image, ImageCreateInfo, ImageLayout, ImageTiling, ImageType, ImageUsage, SampleCount, SubresourceLayout, }, - instance::{Instance, InstanceCreateFlags, InstanceCreateInfo, InstanceExtensions}, + instance::{Instance, InstanceCreateInfo}, memory::{ allocator::{ AllocationCreateInfo, GenericMemoryAllocatorCreateInfo, MemoryAllocator, @@ -108,7 +108,7 @@ pub struct WlxGraphics { impl WlxGraphics { #[cfg(feature = "openxr")] - pub fn new_xr(xr_instance: openxr::Instance, system: openxr::SystemId) -> Arc { + pub fn new_openxr(xr_instance: openxr::Instance, system: openxr::SystemId) -> Arc { use std::ffi::{self, c_char, CString}; use vulkano::Handle; @@ -269,7 +269,8 @@ impl WlxGraphics { Arc::new(me) } - pub fn new( + #[cfg(feature = "openvr")] + pub fn new_openvr( vk_instance_extensions: InstanceExtensions, mut vk_device_extensions_fn: impl FnMut(&PhysicalDevice) -> DeviceExtensions, ) -> Arc { @@ -901,7 +902,7 @@ impl WlxPipeline { ) -> Self { let render_pass_description = RenderPassCreateInfo { attachments: vec![AttachmentDescription { - format: format, + format, samples: SampleCount::Sample1, load_op: AttachmentLoadOp::Clear, store_op: AttachmentStoreOp::Store, diff --git a/src/gui/mod.rs b/src/gui/mod.rs index b72ac78a..5759c1ee 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -466,6 +466,7 @@ impl OverlayRenderer for Canvas { fn view(&mut self) -> Option> { Some(self.view_final.clone()) } + fn extent(&self) -> [u32; 3] { self.view_final.image().extent().clone() } diff --git a/src/main.rs b/src/main.rs index ca8a97d4..4845a1f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ #[allow(dead_code)] mod backend; +mod config; +mod config_io; mod graphics; mod gui; mod hid; @@ -33,11 +35,12 @@ fn main() { #[cfg(all(feature = "openxr", feature = "openvr"))] auto_run(running); + // TODO: Handle error messages if using cherry-picked features #[cfg(all(feature = "openvr", not(feature = "openxr")))] - crate::backend::openvr::openvr_run(running); + let _ = crate::backend::openvr::openvr_run(running); #[cfg(all(feature = "openxr", not(feature = "openvr")))] - crate::backend::openxr::openxr_run(running); + let _ = crate::backend::openxr::openxr_run(running); #[cfg(not(any(feature = "openxr", feature = "openvr")))] compile_error!("You must enable at least one backend feature (openxr or openvr)"); diff --git a/src/overlays/keyboard.rs b/src/overlays/keyboard.rs index 95031d6a..31b2c95b 100644 --- a/src/overlays/keyboard.rs +++ b/src/overlays/keyboard.rs @@ -1,9 +1,6 @@ use std::{ collections::HashMap, - env::var, - fs, io::Cursor, - path::PathBuf, process::{Child, Command}, str::FromStr, sync::Arc, @@ -11,6 +8,7 @@ use std::{ use crate::{ backend::overlay::{OverlayData, OverlayState}, + config, gui::{color_parse, CanvasBuilder, Control}, hid::{KeyModifier, VirtualKey, KEYS_TO_MODS}, state::AppState, @@ -246,18 +244,13 @@ enum KeyButtonData { }, } -static KEYBOARD_YAML: Lazy = Lazy::new(|| { - let home = &var("HOME").unwrap(); - [home, ".config/wlxoverlay/keyboard.yaml"].iter().collect() //TODO other paths -}); - static LAYOUT: Lazy = Lazy::new(Layout::load_from_disk); static MACRO_REGEX: Lazy = Lazy::new(|| Regex::new(r"^([A-Za-z0-1_-]+)(?: +(UP|DOWN))?$").unwrap()); #[derive(Debug, Deserialize, Serialize)] -struct Layout { +pub struct Layout { name: String, row_size: f32, key_sizes: Vec>, @@ -269,16 +262,8 @@ struct Layout { impl Layout { fn load_from_disk() -> Layout { - let mut yaml = fs::read_to_string(KEYBOARD_YAML.as_path()).ok(); - - if yaml.is_none() { - yaml = Some(include_str!("../res/keyboard.yaml").to_string()); - } - - let mut layout: Layout = - serde_yaml::from_str(&yaml.unwrap()).expect("Failed to parse keyboard.yaml"); + let mut layout = config::load_keyboard(); layout.post_load(); - layout } diff --git a/src/overlays/screen.rs b/src/overlays/screen.rs index a1363d93..01466a0e 100644 --- a/src/overlays/screen.rs +++ b/src/overlays/screen.rs @@ -1,11 +1,14 @@ use core::slice; +use serde::{Deserialize, Serialize}; use std::{ + collections::BTreeMap, + error::Error, f32::consts::PI, - path::Path, + ops::Deref, + path::PathBuf, ptr, sync::{mpsc::Receiver, Arc}, time::{Duration, Instant}, - usize, }; use vulkano::{ buffer::Subbuffer, @@ -30,6 +33,8 @@ use crate::{ input::{InteractionHandler, PointerHit, PointerMode}, overlay::{OverlayData, OverlayRenderer, OverlayState, SplitOverlayBackend}, }, + config::def_pw_tokens, + config_io, graphics::{fourcc_to_vk, Vert2Uv, WlxGraphics, WlxPipeline}, hid::{MOUSE_LEFT, MOUSE_MIDDLE, MOUSE_RIGHT}, state::{AppSession, AppState}, @@ -342,7 +347,12 @@ impl OverlayRenderer for ScreenRenderer { } } -fn try_create_screen(wl: &WlxClient, id: u32, session: &AppSession) -> Option> +fn try_create_screen( + wl: &WlxClient, + id: u32, + pw_token_store: &mut BTreeMap, + session: &AppSession, +) -> Option> where O: Default, { @@ -366,9 +376,18 @@ where if capture.is_none() { log::info!("{}: Using Pipewire capture", &output.name); - let file_name = format!("{}.token", &output.name); - let full_path = Path::new(&session.config_path).join(file_name); - let token = std::fs::read_to_string(full_path).ok(); + + let display_name = output.name.deref(); + + // Find existing token by display + let token = pw_token_store.get(display_name).map(|s| s.as_str()); + + if let Some(t) = token { + println!( + "Found existing Pipewire token for display {}: {}", + display_name, t + ); + } capture = ScreenRenderer::new_pw( output, @@ -431,6 +450,44 @@ where } } +#[derive(Deserialize, Serialize, Default)] +pub struct TokenConf { + #[serde(default = "def_pw_tokens")] + pub pw_tokens: Vec<(String, String)>, +} + +fn get_pw_token_path() -> PathBuf { + let mut path = config_io::get_conf_d_path(); + path.push("pw_tokens.yaml"); + path +} + +pub fn save_pw_token_config(tokens: &BTreeMap) -> Result<(), Box> { + let mut conf = TokenConf::default(); + + for (name, token) in tokens { + conf.pw_tokens.push((name.clone(), token.clone())); + } + + let yaml = serde_yaml::to_string(&conf)?; + std::fs::write(get_pw_token_path(), yaml)?; + + Ok(()) +} + +pub fn load_pw_token_config() -> Result, Box> { + let mut map: BTreeMap = BTreeMap::new(); + + let yaml = std::fs::read_to_string(get_pw_token_path())?; + let conf: TokenConf = serde_yaml::from_str(yaml.as_str())?; + + for (name, token) in conf.pw_tokens { + map.insert(name, token); + } + + Ok(map) +} + pub fn get_screens_wayland(session: &AppSession) -> (Vec>, Vec2) where O: Default, @@ -438,11 +495,28 @@ where let mut overlays = vec![]; let wl = WlxClient::new().unwrap(); + // Load existing Pipewire tokens from file + let mut pw_tokens: BTreeMap = if let Ok(conf) = load_pw_token_config() { + conf + } else { + BTreeMap::new() + }; + + let pw_tokens_copy = pw_tokens.clone(); + for id in wl.outputs.keys() { - if let Some(overlay) = try_create_screen(&wl, *id, &session) { + if let Some(overlay) = try_create_screen(&wl, *id, &mut pw_tokens, session) { overlays.push(overlay); } } + + if pw_tokens_copy != pw_tokens { + // Token list changed, re-create token config file + if let Err(err) = save_pw_token_config(&pw_tokens) { + log::error!("Failed to save Pipewire token config: {}", err); + } + } + let extent = wl.get_desktop_extent(); (overlays, Vec2::new(extent.0 as f32, extent.1 as f32)) } diff --git a/src/res/config.yaml b/src/res/config.yaml new file mode 100644 index 00000000..4652e754 --- /dev/null +++ b/src/res/config.yaml @@ -0,0 +1,25 @@ +# Grab threshold for changing position of overlays +# Allowed values: 0.0 - 1.0 +# Default - 0.6 +# +# 1.0 - The hand must grab tighter to start dragging, recommended on Index controller +grab_threshold: 0.6 + +# Threshold for registering trigger (click) event +# Allowed values: 0.0 - 1.0 +# Default - 0.65 +trigger_threshold: 0.65 + +# For how much time mouse motion events should be stopped after clicking? +# Prevents accidental dragging various GUI elements or links, making it easier to click +# Default: 300 +click_freeze_time_ms: 300 + +# Default: true +keyboard_sound_enabled: true + +# Alter default scale of various overlays +# Default: 1.0 +keyboard_scale: 1.0 +desktop_view_scale: 1.0 +watch_scale: 1.0 diff --git a/src/state.rs b/src/state.rs index c2f10fcd..0baf9700 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,10 +1,12 @@ -use std::{env::VarError, path::Path, sync::Arc}; +use std::{path::PathBuf, sync::Arc}; use glam::{Quat, Vec3}; use vulkano::{command_buffer::CommandBufferUsage, format::Format, image::view::ImageView}; use crate::{ backend::{common::TaskContainer, input::InputState}, + config::GeneralConfig, + config_io, graphics::WlxGraphics, gui::font::FontCache, hid::HidProvider, @@ -74,7 +76,8 @@ impl AppState { } pub struct AppSession { - pub config_path: String, + pub config_root_path: PathBuf, + pub config: GeneralConfig, pub show_screens: Vec, pub show_keyboard: bool, @@ -103,21 +106,13 @@ pub struct AppSession { impl AppSession { pub fn load() -> Self { - let config_path = std::env::var("XDG_CONFIG_HOME") - .or_else(|_| std::env::var("HOME").map(|home| format!("{}/.config", home))) - .or_else(|_| { - log::warn!("Err: $XDG_CONFIG_HOME and $HOME are not set, using /tmp/wlxoverlay"); - Ok::("/tmp".to_string()) - }) - .map(|config| Path::new(&config).join("wlxoverlay")) - .ok() - .and_then(|path| path.to_str().map(|path| path.to_string())) - .unwrap(); - - let _ = std::fs::create_dir(&config_path); + let config_root_path = config_io::ensure_config_root(); + println!("Config root path: {}", config_root_path.to_string_lossy()); + let config = GeneralConfig::load_from_disk(); AppSession { - config_path, + config_root_path, + config, show_screens: vec!["DP-3".to_string()], keyboard_volume: 0.5, show_keyboard: false,