libmonado_rs/
lib.rs

1mod space;
2mod sys;
3
4pub use semver::Version;
5pub use space::*;
6pub use sys::ClientState;
7pub use sys::MndProperty;
8pub use sys::MndResult;
9
10use dlopen2::wrapper::Container;
11use flagset::FlagSet;
12use semver::VersionReq;
13use serde::Deserialize;
14use std::env;
15use std::ffi::c_char;
16use std::ffi::CStr;
17use std::ffi::CString;
18use std::ffi::OsStr;
19use std::fmt::Debug;
20use std::fs;
21use std::path::PathBuf;
22use std::ptr;
23use std::vec;
24use sys::MndRootPtr;
25use sys::MonadoApi;
26
27fn crate_api_version() -> VersionReq {
28	VersionReq::parse("^1.3.0").unwrap()
29}
30fn get_api_version(api: &Container<MonadoApi>) -> Version {
31	let mut major = 0;
32	let mut minor = 0;
33	let mut patch = 0;
34	unsafe { api.mnd_api_get_version(&mut major, &mut minor, &mut patch) };
35
36	Version::new(major as u64, minor as u64, patch as u64)
37}
38
39#[derive(Debug, Clone, Deserialize)]
40struct RuntimeJSON {
41	runtime: RuntimeInfo,
42}
43#[derive(Debug, Clone, Deserialize)]
44struct RuntimeInfo {
45	#[serde(rename = "library_path")]
46	_library_path: PathBuf,
47	#[serde(rename = "MND_libmonado_path")]
48	libmonado_path: Option<PathBuf>,
49}
50
51#[derive(Debug, Clone, Copy)]
52pub struct BatteryStatus {
53	pub present: bool,
54	pub charging: bool,
55	pub charge: f32,
56}
57
58#[derive(Debug, Clone, Copy)]
59pub enum DeviceRole {
60	Head,
61	Eyes,
62	Left,
63	Right,
64	Gamepad,
65	HandTrackingLeft,
66	HandTrackingRight,
67}
68
69impl From<DeviceRole> for &'static str {
70	fn from(value: DeviceRole) -> Self {
71		match value {
72			DeviceRole::Head => "head",
73			DeviceRole::Eyes => "eyes",
74			DeviceRole::Left => "left",
75			DeviceRole::Right => "right",
76			DeviceRole::Gamepad => "gamepad",
77			DeviceRole::HandTrackingLeft => "hand-tracking-left",
78			DeviceRole::HandTrackingRight => "hand-tracking-right",
79		}
80	}
81}
82
83pub struct Monado {
84	api: Container<MonadoApi>,
85	root: MndRootPtr,
86}
87impl Monado {
88	pub fn auto_connect() -> Result<Self, String> {
89		if let Ok(libmonado_path) = env::var("LIBMONADO_PATH") {
90			match fs::metadata(&libmonado_path) {
91				Ok(metadata) if metadata.is_file() => {
92					return Self::create(libmonado_path).map_err(|e| format!("{e:?}"))
93				}
94				_ => return Err("LIBMONADO_PATH does not point to a valid file".into()),
95			}
96		}
97
98		let override_runtime = std::env::var_os("XDG_RUNTIME_JSON").map(PathBuf::from);
99		let possible_config_files = xdg::BaseDirectories::new()
100			.ok()
101			.into_iter()
102			.flat_map(|b| b.find_config_files("openxr/1/active_runtime.json"));
103		let override_runtime = override_runtime
104			.into_iter()
105			.chain(possible_config_files)
106			.find_map(|p| {
107				Some((
108					serde_json::from_str::<RuntimeJSON>(&std::fs::read_to_string(&p).ok()?).ok()?,
109					p,
110				))
111			});
112
113		let Some((runtime_json, mut runtime_path)) = override_runtime else {
114			return Err("Couldn't find the actively running runtime".to_string());
115		};
116		runtime_path.pop();
117		let Some(libmonado_path) = runtime_json.runtime.libmonado_path else {
118			return Err("Couldn't find libmonado path in active runtime json".to_string());
119		};
120
121		let path = runtime_path.join(libmonado_path);
122		Self::create(path).map_err(|e| format!("{e:?}"))
123	}
124	pub fn create<S: AsRef<OsStr>>(libmonado_so: S) -> Result<Self, MndResult> {
125		let api = unsafe { Container::<MonadoApi>::load(libmonado_so) }
126			.map_err(|_| MndResult::ErrorConnectingFailed)?;
127		if !crate_api_version().matches(&get_api_version(&api)) {
128			return Err(MndResult::ErrorInvalidVersion);
129		}
130		let mut root = std::ptr::null_mut();
131		unsafe {
132			api.mnd_root_create(&mut root).to_result()?;
133		}
134		Ok(Monado { api, root })
135	}
136
137	pub fn get_api_version(&self) -> Version {
138		get_api_version(&self.api)
139	}
140	pub fn recenter_local_spaces(&self) -> Result<(), MndResult> {
141		unsafe {
142			self.api
143				.mnd_root_recenter_local_spaces(self.root)
144				.to_result()
145		}
146	}
147
148	pub fn clients(&self) -> Result<impl IntoIterator<Item = Client<'_>>, MndResult> {
149		unsafe {
150			self.api
151				.mnd_root_update_client_list(self.root)
152				.to_result()?
153		};
154		let mut count = 0;
155		unsafe {
156			self.api
157				.mnd_root_get_number_clients(self.root, &mut count)
158				.to_result()?
159		};
160		let mut clients: Vec<Option<Client>> = vec::from_elem(None, count as usize);
161		for (index, client) in clients.iter_mut().enumerate() {
162			let mut id = 0;
163			unsafe {
164				self.api
165					.mnd_root_get_client_id_at_index(self.root, index as u32, &mut id)
166					.to_result()?
167			};
168			client.replace(Client { monado: self, id });
169		}
170		Ok(clients.into_iter().flatten())
171	}
172
173	fn device_index_from_role_str(&self, role_name: &str) -> Result<u32, MndResult> {
174		let c_name = CString::new(role_name).unwrap();
175		let mut index = -1;
176
177		unsafe {
178			self.api
179				.mnd_root_get_device_from_role(self.root, c_name.as_ptr(), &mut index)
180				.to_result()?
181		};
182		if index == -1 {
183			return Err(MndResult::ErrorInvalidValue);
184		}
185		Ok(index as u32)
186	}
187
188	// Get device id from role name
189	//
190	// @param root Opaque libmonado state
191	// @param role_name Name of the role
192	// @param out_index Pointer to populate with device id
193	fn device_from_role_str<'m>(&'m self, role_name: &str) -> Result<Device<'m>, MndResult> {
194		let index = self.device_index_from_role_str(role_name)?;
195		let mut c_name: *const c_char = std::ptr::null_mut();
196		let mut name_id = 0;
197		unsafe {
198			self.api
199				.mnd_root_get_device_info(self.root, index, &mut name_id, &mut c_name)
200				.to_result()?
201		};
202		let name = unsafe {
203			CStr::from_ptr(c_name)
204				.to_str()
205				.map_err(|_| MndResult::ErrorInvalidValue)?
206				.to_owned()
207		};
208
209		Ok(Device {
210			monado: self,
211			index,
212			name_id,
213			name,
214		})
215	}
216
217	pub fn device_index_from_role(&self, role: DeviceRole) -> Result<u32, MndResult> {
218		self.device_index_from_role_str(role.into())
219	}
220
221	pub fn device_from_role(&self, role: DeviceRole) -> Result<Device<'_>, MndResult> {
222		self.device_from_role_str(role.into())
223	}
224
225	pub fn devices(&self) -> Result<impl IntoIterator<Item = Device<'_>>, MndResult> {
226		let mut count = 0;
227		unsafe {
228			self.api
229				.mnd_root_get_device_count(self.root, &mut count)
230				.to_result()?
231		};
232		let mut devices: Vec<Option<Device>> = vec::from_elem(None, count as usize);
233		for (index, device) in devices.iter_mut().enumerate() {
234			let index = index as u32;
235			let mut name_id = 0;
236			let mut c_name: *const c_char = std::ptr::null_mut();
237			unsafe {
238				self.api
239					.mnd_root_get_device_info(self.root, index, &mut name_id, &mut c_name)
240					.to_result()?
241			};
242			let name = unsafe {
243				CStr::from_ptr(c_name)
244					.to_str()
245					.map_err(|_| MndResult::ErrorInvalidValue)?
246					.to_owned()
247			};
248			device.replace(Device {
249				monado: self,
250				index,
251				name_id,
252				name,
253			});
254		}
255		Ok(devices.into_iter().flatten())
256	}
257}
258impl Drop for Monado {
259	fn drop(&mut self) {
260		unsafe { self.api.mnd_root_destroy(&mut self.root) }
261	}
262}
263
264#[derive(Clone)]
265pub struct Client<'m> {
266	monado: &'m Monado,
267	id: u32,
268}
269impl Client<'_> {
270	pub fn name(&mut self) -> Result<String, MndResult> {
271		let mut string = std::ptr::null();
272		unsafe {
273			self.monado
274				.api
275				.mnd_root_get_client_name(self.monado.root, self.id, &mut string)
276				.to_result()?
277		};
278		let c_string = unsafe { CStr::from_ptr(string) };
279		c_string
280			.to_str()
281			.map_err(|_| MndResult::ErrorInvalidValue)
282			.map(ToString::to_string)
283	}
284	pub fn state(&mut self) -> Result<FlagSet<ClientState>, MndResult> {
285		let mut state = 0;
286		unsafe {
287			self.monado
288				.api
289				.mnd_root_get_client_state(self.monado.root, self.id, &mut state)
290				.to_result()?
291		};
292		Ok(unsafe { FlagSet::new_unchecked(state) })
293	}
294	pub fn set_primary(&mut self) -> Result<(), MndResult> {
295		unsafe {
296			self.monado
297				.api
298				.mnd_root_set_client_primary(self.monado.root, self.id)
299				.to_result()
300		}
301	}
302	pub fn set_focused(&mut self) -> Result<(), MndResult> {
303		unsafe {
304			self.monado
305				.api
306				.mnd_root_set_client_focused(self.monado.root, self.id)
307				.to_result()
308		}
309	}
310	pub fn set_io_active(&mut self, active: bool) -> Result<(), MndResult> {
311		let state = self.state()?;
312		if state.contains(ClientState::ClientIoActive) != active {
313			unsafe {
314				self.monado
315					.api
316					.mnd_root_toggle_client_io_active(self.monado.root, self.id)
317					.to_result()?;
318			}
319		}
320		Ok(())
321	}
322}
323
324#[derive(Clone)]
325pub struct Device<'m> {
326	monado: &'m Monado,
327	pub index: u32,
328	/// non-unique numeric representation of device name, see: xrt_device_name
329	pub name_id: u32,
330	pub name: String,
331}
332impl Device<'_> {
333	pub fn battery_status(&self) -> Result<BatteryStatus, MndResult> {
334		let mut present: bool = Default::default();
335		let mut charging: bool = Default::default();
336		let mut charge: f32 = Default::default();
337		unsafe {
338			self.monado
339				.api
340				.mnd_root_get_device_battery_status(
341					self.monado.root,
342					self.index,
343					&mut present,
344					&mut charging,
345					&mut charge,
346				)
347				.to_result()?;
348		}
349		Ok(BatteryStatus {
350			present,
351			charging,
352			charge,
353		})
354	}
355	pub fn serial(&self) -> Result<String, MndResult> {
356		self.get_info_string(MndProperty::PropertySerialString)
357	}
358	pub fn get_info_bool(&self, property: MndProperty) -> Result<bool, MndResult> {
359		let mut value: bool = Default::default();
360		unsafe {
361			self.monado
362				.api
363				.mnd_root_get_device_info_bool(self.monado.root, self.index, property, &mut value)
364				.to_result()?
365		}
366		Ok(value)
367	}
368	pub fn get_info_u32(&self, property: MndProperty) -> Result<u32, MndResult> {
369		let mut value: u32 = Default::default();
370		unsafe {
371			self.monado
372				.api
373				.mnd_root_get_device_info_u32(self.monado.root, self.index, property, &mut value)
374				.to_result()?
375		}
376		Ok(value)
377	}
378	pub fn get_info_i32(&self, property: MndProperty) -> Result<i32, MndResult> {
379		let mut value: i32 = Default::default();
380		unsafe {
381			self.monado
382				.api
383				.mnd_root_get_device_info_i32(self.monado.root, self.index, property, &mut value)
384				.to_result()?
385		}
386		Ok(value)
387	}
388	pub fn get_info_f32(&self, property: MndProperty) -> Result<f32, MndResult> {
389		let mut value: f32 = Default::default();
390		unsafe {
391			self.monado
392				.api
393				.mnd_root_get_device_info_float(self.monado.root, self.index, property, &mut value)
394				.to_result()?
395		}
396		Ok(value)
397	}
398	pub fn get_info_string(&self, property: MndProperty) -> Result<String, MndResult> {
399		let mut cstr_ptr = ptr::null_mut();
400
401		unsafe {
402			self.monado
403				.api
404				.mnd_root_get_device_info_string(
405					self.monado.root,
406					self.index,
407					property,
408					&mut cstr_ptr,
409				)
410				.to_result()?
411		}
412
413		unsafe { Ok(CStr::from_ptr(cstr_ptr).to_string_lossy().to_string()) }
414	}
415}
416impl Debug for Device<'_> {
417	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418		f.debug_struct("Device")
419			.field("id", &self.name_id)
420			.field("name", &self.name)
421			.finish()
422	}
423}
424
425#[test]
426fn test_dump_info() {
427	let monado = Monado::auto_connect().unwrap();
428	dbg!(monado.get_api_version());
429	println!();
430
431	for mut client in monado.clients().unwrap() {
432		dbg!(client.name().unwrap(), client.state().unwrap());
433		println!();
434	}
435	for device in monado.devices().unwrap() {
436		let _ = dbg!(device.name_id, &device.name, device.serial());
437		println!();
438	}
439	for tracking_origin in monado.tracking_origins().unwrap() {
440		dbg!(
441			tracking_origin.id,
442			&tracking_origin.name,
443			tracking_origin.get_offset().unwrap()
444		);
445		println!();
446	}
447}