-
-
+
\ No newline at end of file
diff --git a/dash-frontend/assets/lang/de.json b/dash-frontend/assets/lang/de.json
index 7cd575f..4188434 100644
--- a/dash-frontend/assets/lang/de.json
+++ b/dash-frontend/assets/lang/de.json
@@ -1,6 +1,6 @@
{
"HOME_SCREEN": "Startbildschirm",
- "MONADO_RUNTIME": "„Monado”-Laufzeitumgebung",
+ "MONADO_RUNTIME": "Monado-Laufzeitumgebung",
"APPLICATIONS": "Anwendungen",
"GAMES": "Spiele",
"SETTINGS": "Einstellungen",
diff --git a/dash-frontend/assets/lang/en.json b/dash-frontend/assets/lang/en.json
index eea46fb..7342d2a 100644
--- a/dash-frontend/assets/lang/en.json
+++ b/dash-frontend/assets/lang/en.json
@@ -127,7 +127,7 @@
"HOME_SCREEN": "Home",
"LIST_OF_PROCESSES": "Process list",
"LIST_OF_WINDOWS": "Window list",
- "MONADO_RUNTIME": "„Monado” runtime",
+ "MONADO_RUNTIME": "Monado runtime",
"NO_WINDOWS_FOUND": "No windows found",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolution"
diff --git a/dash-frontend/assets/lang/es.json b/dash-frontend/assets/lang/es.json
index c6f644c..06ee798 100644
--- a/dash-frontend/assets/lang/es.json
+++ b/dash-frontend/assets/lang/es.json
@@ -1,6 +1,6 @@
{
"HOME_SCREEN": "Inicio",
- "MONADO_RUNTIME": "„Monado” tiempo de ejecución",
+ "MONADO_RUNTIME": "Monado tiempo de ejecución",
"APPLICATIONS": "Aplicaciones",
"GAMES": "Juegos",
"SETTINGS": "Ajustes",
diff --git a/dash-frontend/assets/lang/pl.json b/dash-frontend/assets/lang/pl.json
index b76f57d..7f48b75 100644
--- a/dash-frontend/assets/lang/pl.json
+++ b/dash-frontend/assets/lang/pl.json
@@ -50,7 +50,7 @@
"USE_SKYBOX_HELP": "Wyświetlaj niebo, jeśli nie ma aplikacji sceny lub passthrough",
"USE_PASSTHROUGH_HELP": "Pozwól na passthrough, jeśli runtime XR to obsługuje",
"SCREEN_RENDER_DOWN_HELP": "Pomaga redukować aliasing na ekranach o wysokiej rozdzielczości",
- "SETS_ON_WATCH": "Lista zestawówna zegarku",
+ "SETS_ON_WATCH": "Lista zestawów na zegarku",
"TROUBLESHOOTING": "Rozwiązywanie problemów",
"CLEAR_SAVED_STATE": "Wyczyść zapisany stan",
"CLEAR_PIPEWIRE_TOKENS": "Wyczyść tokeny PipeWire",
diff --git a/dash-frontend/src/tab/monado.rs b/dash-frontend/src/tab/monado.rs
index f6cdd93..8540ee6 100644
--- a/dash-frontend/src/tab/monado.rs
+++ b/dash-frontend/src/tab/monado.rs
@@ -1,43 +1,163 @@
-use std::marker::PhantomData;
+use std::{collections::HashMap, marker::PhantomData, rc::Rc};
use wgui::{
assets::AssetPath,
+ components::checkbox::ComponentCheckbox,
+ globals::WguiGlobals,
layout::WidgetID,
- parser::{ParseDocumentParams, ParserState},
+ parser::{self, Fetchable, ParseDocumentParams, ParserState},
+ task::Tasks,
};
+use wlx_common::dash_interface;
use crate::{
frontend::Frontend,
tab::{Tab, TabType},
};
+#[derive(Debug)]
+enum Task {
+ Refresh,
+ FocusClient(String),
+}
+
pub struct TabMonado
{
#[allow(dead_code)]
- pub state: ParserState,
+ state: ParserState,
+ tasks: Tasks,
+
marker: PhantomData,
+
+ globals: WguiGlobals,
+ id_list_parent: WidgetID,
+
+ cells: Vec,
+
+ ticks: u32,
}
impl Tab for TabMonado {
fn get_type(&self) -> TabType {
TabType::Games
}
+
+ fn update(&mut self, frontend: &mut Frontend, data: &mut T) -> anyhow::Result<()> {
+ for task in self.tasks.drain() {
+ match task {
+ Task::Refresh => self.refresh(frontend, data)?,
+ Task::FocusClient(name) => self.focus_client(frontend, data, name)?,
+ }
+ }
+
+ // every few seconds
+ if self.ticks.is_multiple_of(500) {
+ self.tasks.push(Task::Refresh);
+ }
+
+ self.ticks += 1;
+
+ Ok(())
+ }
+}
+
+fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
+ ParseDocumentParams {
+ globals: globals.clone(),
+ path: AssetPath::BuiltIn("gui/tab/monado.xml"),
+ extra: Default::default(),
+ }
+}
+
+fn yesno(n: bool) -> &'static str {
+ match n {
+ true => "yes",
+ false => "no",
+ }
}
impl TabMonado {
pub fn new(frontend: &mut Frontend, parent_id: WidgetID) -> anyhow::Result {
- let state = wgui::parser::parse_from_assets(
- &ParseDocumentParams {
- globals: frontend.layout.state.globals.clone(),
- path: AssetPath::BuiltIn("gui/tab/monado.xml"),
- extra: Default::default(),
- },
- &mut frontend.layout,
- parent_id,
- )?;
+ let globals = frontend.layout.state.globals.clone();
+ let state = wgui::parser::parse_from_assets(&doc_params(&globals), &mut frontend.layout, parent_id)?;
+
+ let id_list_parent = state.get_widget_id("list_parent")?;
+
+ let tasks = Tasks::::new();
+
+ tasks.push(Task::Refresh);
Ok(Self {
state,
marker: PhantomData,
+ tasks,
+ globals,
+ id_list_parent,
+ ticks: 0,
+ cells: Vec::new(),
})
}
+
+ fn mount_client(&mut self, frontend: &mut Frontend, client: &dash_interface::MonadoClient) -> anyhow::Result<()> {
+ let mut par = HashMap::, Rc>::new();
+ par.insert(
+ "checked".into(),
+ if client.is_primary {
+ Rc::from("1")
+ } else {
+ Rc::from("0")
+ },
+ );
+ par.insert("name".into(), client.name.clone().into());
+ par.insert("flag_active".into(), yesno(client.is_active).into());
+ par.insert("flag_focused".into(), yesno(client.is_focused).into());
+ par.insert("flag_io_active".into(), yesno(client.is_io_active).into());
+ par.insert("flag_overlay".into(), yesno(client.is_overlay).into());
+ par.insert("flag_primary".into(), yesno(client.is_primary).into());
+ par.insert("flag_visible".into(), yesno(client.is_visible).into());
+
+ let state_cell = self.state.parse_template(
+ &doc_params(&self.globals),
+ "Cell",
+ &mut frontend.layout,
+ self.id_list_parent,
+ par,
+ )?;
+
+ let checkbox = state_cell.fetch_component_as::("checkbox")?;
+ checkbox.on_toggle({
+ let tasks = self.tasks.clone();
+ let client_name = client.name.clone();
+ Box::new(move |_common, e| {
+ if e.checked {
+ tasks.push(Task::FocusClient(client_name.clone()));
+ }
+ Ok(())
+ })
+ });
+
+ self.cells.push(state_cell);
+
+ Ok(())
+ }
+
+ fn refresh(&mut self, frontend: &mut Frontend, data: &mut T) -> anyhow::Result<()> {
+ log::debug!("refreshing monado client list");
+
+ let clients = frontend.interface.monado_client_list(data)?;
+
+ frontend.layout.remove_children(self.id_list_parent);
+ self.cells.clear();
+
+ for client in clients {
+ self.mount_client(frontend, &client)?;
+ }
+
+ Ok(())
+ }
+
+ fn focus_client(&mut self, frontend: &mut Frontend, data: &mut T, name: String) -> anyhow::Result<()> {
+ frontend.interface.monado_client_focus(data, &name)?;
+ self.tasks.push(Task::Refresh);
+ Ok(())
+ }
}
diff --git a/dash-frontend/src/util/steam_utils.rs b/dash-frontend/src/util/steam_utils.rs
index dad863e..0209dfd 100644
--- a/dash-frontend/src/util/steam_utils.rs
+++ b/dash-frontend/src/util/steam_utils.rs
@@ -129,7 +129,7 @@ pub fn stop(app_id: AppID, force_kill: bool) -> anyhow::Result<()> {
log::info!("Killing process with PID {} and its children", game.pid);
let _ = std::process::Command::new("pkill")
- .arg(if force_kill { "-9" } else { "-11" })
+ .arg(if force_kill { "-9" } else { "-15" })
.arg("-P")
.arg(format!("{}", game.pid))
.spawn()?;
diff --git a/scripts/translator/description.txt b/scripts/translator/description.txt
index ccfb26b..7b489a4 100644
--- a/scripts/translator/description.txt
+++ b/scripts/translator/description.txt
@@ -5,6 +5,7 @@ Glossary:
- wlx-overlay-s: The name of this software (also called WlxOverlay-S)
- WayVR: A Wayland compositor intended to be used in VR
- WayVR Dashboard: An application (and game) launcher which is displayed in front of the user
+- Monado: A VR compositor
- OpenVR: API made by Valve
- OpenXR: API made by Khronos
- OSC: OpenSoundControl
diff --git a/wgui/src/parser/mod.rs b/wgui/src/parser/mod.rs
index e5c7f76..99cb366 100644
--- a/wgui/src/parser/mod.rs
+++ b/wgui/src/parser/mod.rs
@@ -10,9 +10,16 @@ mod widget_rectangle;
mod widget_sprite;
use crate::{
- assets::{normalize_path, AssetPath, AssetPathOwned}, components::{Component, ComponentWeak}, drawing::{self}, globals::WguiGlobals, i18n::Translation, layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair}, log::LogErr, parser::{
+ assets::{AssetPath, AssetPathOwned, normalize_path},
+ components::{Component, ComponentWeak},
+ drawing::{self},
+ globals::WguiGlobals,
+ i18n::Translation,
+ layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair},
+ log::LogErr,
+ parser::{
component_button::parse_component_button,
- component_checkbox::{parse_component_checkbox, CheckboxKind},
+ component_checkbox::{CheckboxKind, parse_component_checkbox},
component_radio_group::parse_component_radio_group,
component_slider::parse_component_slider,
widget_div::parse_widget_div,
@@ -20,7 +27,9 @@ use crate::{
widget_label::parse_widget_label,
widget_rectangle::parse_widget_rectangle,
widget_sprite::parse_widget_sprite,
- }, widget::ConstructEssentials, windowing::context_menu
+ },
+ widget::ConstructEssentials,
+ windowing::context_menu,
};
use anyhow::Context;
use ouroboros::self_referencing;
@@ -215,7 +224,10 @@ impl ParserState {
template_parameters: HashMap, Rc>,
) -> anyhow::Result {
let Some(template) = self.data.templates.get(template_name) else {
- anyhow::bail!("{:?}: no template named \"{template_name}\" found", self.path.get_path_buf());
+ anyhow::bail!(
+ "{:?}: no template named \"{template_name}\" found",
+ self.path.get_path_buf().display()
+ );
};
let mut ctx = ParserContext {
@@ -280,7 +292,7 @@ impl ParserState {
for attrib in child.attributes() {
let (key, value) = (attrib.name(), attrib.value());
-
+
match key {
"text" => title = Some(Translation::from_raw_text(value)),
"translation" => title = Some(Translation::from_translation_key(value)),
@@ -291,7 +303,7 @@ impl ParserState {
if !other.starts_with('_') {
anyhow::bail!("unexpected \"{other}\" attribute");
}
- attribs.push(AttribPair::new(key, replace_vars(value, &template_params)));
+ attribs.push(AttribPair::new(key, replace_vars(value, template_params)));
}
}
}
@@ -305,14 +317,12 @@ impl ParserState {
});
}
other => {
- anyhow::bail!("{:?}: unexpected <{other}> tag", self.path.get_path_buf());
+ anyhow::bail!("{:?}: unexpected <{other}> tag", self.path.get_path_buf().display());
}
}
}
- Ok(
- cells,
- )
+ Ok(cells)
}
}
@@ -470,65 +480,71 @@ impl ParserContext<'_> {
insert_color_vars!(self, "faded", def.faded_color, def.translucent_alpha);
insert_color_vars!(self, "bg", def.bg_color, def.translucent_alpha);
}
-fn print_invalid_attrib(&self, tag_name: &str, key: &str, value: &str) {
- log::warn!("{}: <{tag_name}> value for \"{key}\" is invalid: \"{value}\"", self.doc_params.path.get_str());
-}
-
-fn print_missing_attrib(&self, tag_name: &str, attr: &str) {
- log::warn!("{}: <{tag_name}> is missing \"{attr}\".", self.doc_params.path.get_str());
-}
-
-fn parse_val(&self, tag_name: &str, key: &str, value: &str) -> Option {
- let Ok(val) = value.parse::() else {
- self.print_invalid_attrib(tag_name, key, value);
- return None;
- };
- Some(val)
-}
-
-fn parse_percent(&self, tag_name: &str, key: &str, value: &str) -> Option {
- let Some(val_str) = value.split('%').next() else {
- self.print_invalid_attrib(tag_name, key, value);
- return None;
- };
-
- let Ok(val) = val_str.parse::() else {
- self.print_invalid_attrib(tag_name, key, value);
- return None;
- };
- Some(val / 100.0)
-}
-
-fn parse_size_unit(&self, tag_name: &str, key: &str, value: &str) -> Option
-where
- T: taffy::prelude::FromPercent + taffy::prelude::FromLength,
-{
- if is_percent(value) {
- Some(taffy::prelude::percent(self.parse_percent(tag_name, key, value)?))
- } else {
- Some(taffy::prelude::length(parse_f32(value)?))
+ fn print_invalid_attrib(&self, tag_name: &str, key: &str, value: &str) {
+ log::warn!(
+ "{}: <{tag_name}> value for \"{key}\" is invalid: \"{value}\"",
+ self.doc_params.path.get_str()
+ );
}
-}
-fn parse_check_i32(&self, tag_name: &str, key: &str, value: &str, num: &mut i32) -> bool {
- if let Some(value) = parse_i32(value) {
- *num = value;
- true
- } else {
- self.print_invalid_attrib(tag_name, key, value);
- false
+ fn print_missing_attrib(&self, tag_name: &str, attr: &str) {
+ log::warn!(
+ "{}: <{tag_name}> is missing \"{attr}\".",
+ self.doc_params.path.get_str()
+ );
}
-}
-fn parse_check_f32(&self, tag_name: &str, key: &str, value: &str, num: &mut f32) -> bool {
- if let Some(value) = parse_f32(value) {
- *num = value;
- true
- } else {
- self.print_invalid_attrib(tag_name, key, value);
- false
+ fn parse_val(&self, tag_name: &str, key: &str, value: &str) -> Option {
+ let Ok(val) = value.parse::() else {
+ self.print_invalid_attrib(tag_name, key, value);
+ return None;
+ };
+ Some(val)
+ }
+
+ fn parse_percent(&self, tag_name: &str, key: &str, value: &str) -> Option {
+ let Some(val_str) = value.split('%').next() else {
+ self.print_invalid_attrib(tag_name, key, value);
+ return None;
+ };
+
+ let Ok(val) = val_str.parse::() else {
+ self.print_invalid_attrib(tag_name, key, value);
+ return None;
+ };
+ Some(val / 100.0)
+ }
+
+ fn parse_size_unit(&self, tag_name: &str, key: &str, value: &str) -> Option
+ where
+ T: taffy::prelude::FromPercent + taffy::prelude::FromLength,
+ {
+ if is_percent(value) {
+ Some(taffy::prelude::percent(self.parse_percent(tag_name, key, value)?))
+ } else {
+ Some(taffy::prelude::length(parse_f32(value)?))
+ }
+ }
+
+ fn parse_check_i32(&self, tag_name: &str, key: &str, value: &str, num: &mut i32) -> bool {
+ if let Some(value) = parse_i32(value) {
+ *num = value;
+ true
+ } else {
+ self.print_invalid_attrib(tag_name, key, value);
+ false
+ }
+ }
+
+ fn parse_check_f32(&self, tag_name: &str, key: &str, value: &str, num: &mut f32) -> bool {
+ if let Some(value) = parse_f32(value) {
+ *num = value;
+ true
+ } else {
+ self.print_invalid_attrib(tag_name, key, value);
+ false
+ }
}
-}
}
fn parse_i32(value: &str) -> Option {
@@ -583,7 +599,6 @@ fn require_tag_by_name<'a>(node: &roxmltree::Node<'a, 'a>, name: &str) -> anyhow
get_tag_by_name(node, name).ok_or_else(|| anyhow::anyhow!("Tag \"{name}\" not found"))
}
-
fn parse_widget_other_internal(
template: &Rc,
template_parameters: HashMap, Rc>,
@@ -617,7 +632,10 @@ fn parse_widget_other(
attribs: &[AttribPair],
) -> anyhow::Result<()> {
let Some(template) = ctx.get_template(xml_tag_name) else {
- log::error!("{}: Undefined tag named \"{xml_tag_name}\"", ctx.doc_params.path.get_str());
+ log::error!(
+ "{}: Undefined tag named \"{xml_tag_name}\"",
+ ctx.doc_params.path.get_str()
+ );
return Ok(()); // not critical
};
@@ -746,6 +764,7 @@ pub fn replace_vars(input: &str, vars: &HashMap, Rc>) -> Rc {
}
#[allow(clippy::manual_strip)]
+#[allow(clippy::single_match_else)]
fn process_attrib(
template_parameters: &HashMap, Rc>,
ctx: &ParserContext,
@@ -760,7 +779,7 @@ fn process_attrib(
None => {
log::warn!("{}: undefined variable \"{value}\"", ctx.doc_params.path.get_str());
AttribPair::new(key, "undefined")
- },
+ }
}
} else {
AttribPair::new(key, replace_vars(value, template_parameters))
@@ -797,7 +816,10 @@ fn process_attribs<'a>(
res.push(process_attrib(&file.template_parameters, ctx, macro_key, macro_value));
}
} else {
- log::warn!("{}: requested macro named \"{value}\" not found!", ctx.doc_params.path.get_str());
+ log::warn!(
+ "{}: requested macro named \"{value}\" not found!",
+ ctx.doc_params.path.get_str()
+ );
}
} else {
res.push(process_attrib(&file.template_parameters, ctx, key, value));
@@ -816,7 +838,10 @@ fn parse_tag_theme<'a>(ctx: &mut ParserContext, node: roxmltree::Node<'a, 'a>) {
}
"" => { /* ignore */ }
_ => {
- log::warn!("{}: <{child_name}> is not a valid child to .", ctx.doc_params.path.get_str());
+ log::warn!(
+ "{}: <{child_name}> is not a valid child to .",
+ ctx.doc_params.path.get_str()
+ );
}
}
}
@@ -837,7 +862,7 @@ fn parse_tag_template(file: &ParserFile, ctx: &mut ParserContext, node: roxmltre
}
}
}
-
+
let Some(name) = template_name else {
ctx.print_missing_attrib("template", "name");
return;
@@ -865,7 +890,11 @@ fn parse_tag_macro(file: &ParserFile, ctx: &mut ParserContext, node: roxmltree::
}
_ => {
if macro_attribs.insert(pair.attrib.clone(), pair.value).is_some() {
- log::warn!("{}: macro attrib \"{}\" already defined!", ctx.doc_params.path.get_str(), pair.attrib);
+ log::warn!(
+ "{}: macro attrib \"{}\" already defined!",
+ ctx.doc_params.path.get_str(),
+ pair.attrib
+ );
}
}
}
@@ -963,19 +992,29 @@ fn parse_child<'a>(
new_widget_id = Some(parse_widget_div(file, ctx, child_node, parent_id, &attribs, tag_name)?);
}
"rectangle" => {
- new_widget_id = Some(parse_widget_rectangle(file, ctx, child_node, parent_id, &attribs, tag_name)?);
+ new_widget_id = Some(parse_widget_rectangle(
+ file, ctx, child_node, parent_id, &attribs, tag_name,
+ )?);
}
"label" => {
- new_widget_id = Some(parse_widget_label(file, ctx, child_node, parent_id, &attribs, tag_name)?);
+ new_widget_id = Some(parse_widget_label(
+ file, ctx, child_node, parent_id, &attribs, tag_name,
+ )?);
}
"sprite" => {
- new_widget_id = Some(parse_widget_sprite(file, ctx, child_node, parent_id, &attribs, tag_name)?);
+ new_widget_id = Some(parse_widget_sprite(
+ file, ctx, child_node, parent_id, &attribs, tag_name,
+ )?);
}
"image" => {
- new_widget_id = Some(parse_widget_image(file, ctx, child_node, parent_id, &attribs, tag_name)?);
+ new_widget_id = Some(parse_widget_image(
+ file, ctx, child_node, parent_id, &attribs, tag_name,
+ )?);
}
"Button" => {
- new_widget_id = Some(parse_component_button(file, ctx, child_node, parent_id, &attribs, tag_name)?);
+ new_widget_id = Some(parse_component_button(
+ file, ctx, child_node, parent_id, &attribs, tag_name,
+ )?);
}
"Slider" => {
new_widget_id = Some(parse_component_slider(ctx, parent_id, &attribs, tag_name)?);
@@ -999,7 +1038,9 @@ fn parse_child<'a>(
)?);
}
"RadioGroup" => {
- new_widget_id = Some(parse_component_radio_group(file, ctx, child_node, parent_id, &attribs, tag_name)?);
+ new_widget_id = Some(parse_component_radio_group(
+ file, ctx, child_node, parent_id, &attribs, tag_name,
+ )?);
}
"" => { /* ignore */ }
other_tag_name => {
diff --git a/wlx-common/src/dash_interface.rs b/wlx-common/src/dash_interface.rs
index 97b2f4b..1c47efd 100644
--- a/wlx-common/src/dash_interface.rs
+++ b/wlx-common/src/dash_interface.rs
@@ -5,6 +5,17 @@ use wayvr_ipc::{
use crate::{config::GeneralConfig, desktop_finder::DesktopFinder};
+#[derive(Clone)]
+pub struct MonadoClient {
+ pub name: String,
+ pub is_primary: bool,
+ pub is_active: bool,
+ pub is_visible: bool,
+ pub is_focused: bool,
+ pub is_overlay: bool,
+ pub is_io_active: bool,
+}
+
pub trait DashInterface {
fn window_list(&mut self, data: &mut T) -> anyhow::Result>;
fn window_set_visible(&mut self, data: &mut T, handle: WvrWindowHandle, visible: bool) -> anyhow::Result<()>;
@@ -18,6 +29,8 @@ pub trait DashInterface {
) -> anyhow::Result;
fn process_list(&mut self, data: &mut T) -> anyhow::Result>;
fn process_terminate(&mut self, data: &mut T, handle: WvrProcessHandle) -> anyhow::Result<()>;
+ fn monado_client_list(&mut self, data: &mut T) -> anyhow::Result>;
+ fn monado_client_focus(&mut self, data: &mut T, name: &str) -> anyhow::Result<()>;
fn recenter_playspace(&mut self, data: &mut T) -> anyhow::Result<()>;
fn desktop_finder<'a>(&'a mut self, data: &'a mut T) -> &'a mut DesktopFinder;
fn general_config<'a>(&'a mut self, data: &'a mut T) -> &'a mut GeneralConfig;
diff --git a/wlx-common/src/dash_interface_emulated.rs b/wlx-common/src/dash_interface_emulated.rs
index bab67e7..d521663 100644
--- a/wlx-common/src/dash_interface_emulated.rs
+++ b/wlx-common/src/dash_interface_emulated.rs
@@ -3,7 +3,12 @@ use wayvr_ipc::{
packet_server::{WvrProcess, WvrProcessHandle, WvrWindow, WvrWindowHandle},
};
-use crate::{config::GeneralConfig, dash_interface::DashInterface, desktop_finder::DesktopFinder, gen_id};
+use crate::{
+ config::GeneralConfig,
+ dash_interface::{self, DashInterface},
+ desktop_finder::DesktopFinder,
+ gen_id,
+};
#[derive(Debug)]
pub struct EmuProcess {
@@ -56,6 +61,7 @@ pub struct DashInterfaceEmulated {
windows: EmuWindowVec,
desktop_finder: DesktopFinder,
general_config: GeneralConfig,
+ monado_clients: Vec,
}
impl DashInterfaceEmulated {
@@ -77,11 +83,42 @@ impl DashInterfaceEmulated {
// Use serde defaults
let general_config = serde_json::from_str("{}").unwrap();
+ let monado_clients = vec![
+ dash_interface::MonadoClient {
+ name: String::from("The Best VR Game 3000"),
+ is_active: true,
+ is_focused: true,
+ is_io_active: true,
+ is_overlay: false,
+ is_primary: true,
+ is_visible: true,
+ },
+ dash_interface::MonadoClient {
+ name: String::from("Second app"),
+ is_active: true,
+ is_focused: false,
+ is_io_active: true,
+ is_overlay: false,
+ is_primary: false,
+ is_visible: true,
+ },
+ dash_interface::MonadoClient {
+ name: String::from("Third app"),
+ is_active: true,
+ is_focused: false,
+ is_io_active: true,
+ is_overlay: false,
+ is_primary: false,
+ is_visible: true,
+ },
+ ];
+
Self {
processes,
windows,
desktop_finder,
general_config,
+ monado_clients,
}
}
}
@@ -187,4 +224,23 @@ impl DashInterface<()> for DashInterfaceEmulated {
fn config_changed(&mut self, _: &mut ()) {}
fn restart(&mut self, _data: &mut ()) {}
+
+ fn monado_client_list(&mut self, _data: &mut ()) -> anyhow::Result> {
+ Ok(self.monado_clients.clone())
+ }
+
+ fn monado_client_focus(&mut self, _data: &mut (), name: &str) -> anyhow::Result<()> {
+ for client in self.monado_clients.iter_mut() {
+ client.is_focused = false;
+ client.is_active = false;
+ client.is_primary = false;
+ }
+
+ if let Some(client) = self.monado_clients.iter_mut().find(|m| m.name == name) {
+ client.is_active = true;
+ client.is_focused = true;
+ client.is_primary = true;
+ }
+ Ok(())
+ }
}
diff --git a/wlx-overlay-s/src/assets/lang/de.json b/wlx-overlay-s/src/assets/lang/de.json
index faf3e35..c3cc2dc 100644
--- a/wlx-overlay-s/src/assets/lang/de.json
+++ b/wlx-overlay-s/src/assets/lang/de.json
@@ -2,8 +2,17 @@
"ANCHOR": {
"CENTER": "Zentrum"
},
- "BAR": {
- "ADD_MIRROR": "Neuen Spiegel-Overlay hinzufügen"
+ "BAR": {
+ "ADD_MIRROR": "Neuen Spiegel-Overlay hinzufügen",
+ "EDIT_MODE_TOGGLE": "Bearbeitungsmodus umschalten",
+ "ADD_NEW_SET": "Neues Set hinzufügen",
+ "DELETE_CURRENT_SET": "Aktuelles Set löschen",
+ "TOGGLE_VISIBILITY": "Sichtbarkeit umschalten",
+ "RESET_POSITION": "Position zurücksetzen",
+ "RELOAD_FROM_DISK": "XML-Datei von der Festplatte neu laden",
+ "CLOSE_MIRROR": "Spiegel schließen",
+ "CLOSE_APP": "App schließen",
+ "FORCE_CLOSE_APP": "App zwangsweise schließen"
},
"WATCH": {
"RECENTER": "Spielbereich neu zentrieren",
@@ -14,7 +23,8 @@
"ADD_NEW_SET": "Neuen Satz hinzufügen",
"SWITCH_TO_SET": "Zum Satz wechseln",
"TOGGLE_FOR_CURRENT_SET": "Sichtbarkeit im aktuellen Satz umschalten",
- "LONG_PRESS_TO_DELETE_SET": "Lange drücken, um Satz zu löschen"
+ "LONG_PRESS_TO_DELETE_SET": "Lange drücken, um Satz zu löschen",
+ "CLEANUP_MIRRORS": "Spiegel entfernen, die\nderzeit nicht sichtbar sind"
},
"EDIT_MODE": {
"ADJUST_CURVATURE": "Krümmung anpassen",
@@ -83,6 +93,8 @@
"EMPTY_SET": "Leeres Set!",
"LETS_ADD_OVERLAYS": "Lass uns ein paar Overlays von der Uhr hinzufügen!",
"FIXING_FLOOR": "Boden wird in 5 Sekunden fixiert...",
- "ONE_CONTROLLER_ON_FLOOR": "Lege einen Controller auf den Boden!"
+ "ONE_CONTROLLER_ON_FLOOR": "Lege einen Controller auf den Boden!",
+ "CANNOT_ADD_SET": "Satz kann nicht hinzugefügt werden!",
+ "MAXIMUM_SETS_REACHED": "Maximale Anzahl an Sets erreicht."
}
-}
+}
\ No newline at end of file
diff --git a/wlx-overlay-s/src/assets/lang/es.json b/wlx-overlay-s/src/assets/lang/es.json
index 6ba8581..5cd77da 100644
--- a/wlx-overlay-s/src/assets/lang/es.json
+++ b/wlx-overlay-s/src/assets/lang/es.json
@@ -3,7 +3,16 @@
"CENTER": "Centro"
},
"BAR": {
- "ADD_MIRROR": "Agregar una nueva superposición de espejo"
+ "ADD_MIRROR": "Agregar una nueva superposición de espejo",
+ "EDIT_MODE_TOGGLE": "Activar/desactivar el modo de edición",
+ "ADD_NEW_SET": "Añadir nuevo set",
+ "DELETE_CURRENT_SET": "Eliminar set actual",
+ "TOGGLE_VISIBILITY": "Alternar visibilidad",
+ "RESET_POSITION": "Restablecer posición",
+ "RELOAD_FROM_DISK": "Volver a cargar XML desde el disco",
+ "CLOSE_MIRROR": "Cerrar espejo",
+ "CLOSE_APP": "Cerrar aplicación",
+ "FORCE_CLOSE_APP": "Forzar cierre de la aplicación"
},
"WATCH": {
"RECENTER": "Recentrar el área de juego",
@@ -14,7 +23,8 @@
"ADD_NEW_SET": "Añadir un nuevo conjunto",
"SWITCH_TO_SET": "Cambiar al conjunto",
"TOGGLE_FOR_CURRENT_SET": "Alternar visibilidad en el conjunto actual",
- "LONG_PRESS_TO_DELETE_SET": "Mantén presionado para eliminar el conjunto"
+ "LONG_PRESS_TO_DELETE_SET": "Mantén presionado para eliminar el conjunto",
+ "CLEANUP_MIRRORS": "Eliminar los espejos que\nno son actualmente visibles"
},
"EDIT_MODE": {
"ADJUST_CURVATURE": "Ajustar curvatura",
@@ -83,6 +93,8 @@
"EMPTY_SET": "¡Conjunto vacío!",
"LETS_ADD_OVERLAYS": "¡Añadamos algunos overlays desde el reloj!",
"FIXING_FLOOR": "Fijando el suelo en 5 segundos...",
- "ONE_CONTROLLER_ON_FLOOR": "¡Coloca un mando en el suelo!"
+ "ONE_CONTROLLER_ON_FLOOR": "¡Coloca un mando en el suelo!",
+ "CANNOT_ADD_SET": "¡No se puede agregar el conjunto!",
+ "MAXIMUM_SETS_REACHED": "Se ha alcanzado el número máximo de sets."
}
-}
+}
\ No newline at end of file
diff --git a/wlx-overlay-s/src/assets/lang/ja.json b/wlx-overlay-s/src/assets/lang/ja.json
index be5d4ec..04f2364 100644
--- a/wlx-overlay-s/src/assets/lang/ja.json
+++ b/wlx-overlay-s/src/assets/lang/ja.json
@@ -2,8 +2,17 @@
"ANCHOR": {
"CENTER": "センター"
},
- "BAR": {
- "ADD_MIRROR": "新しいミラーを追加"
+ "BAR": {
+ "ADD_MIRROR": "新しいミラーを追加",
+ "EDIT_MODE_TOGGLE": "編集モードの切り替え",
+ "ADD_NEW_SET": "新しいセットを追加",
+ "DELETE_CURRENT_SET": "現在のセットを削除",
+ "TOGGLE_VISIBILITY": "表示/非表示の切り替え",
+ "RESET_POSITION": "位置をリセット",
+ "RELOAD_FROM_DISK": "ディスクからXMLを再読み込み",
+ "CLOSE_MIRROR": "ミラーを閉じる",
+ "CLOSE_APP": "アプリを閉じる",
+ "FORCE_CLOSE_APP": "アプリを強制終了"
},
"WATCH": {
"RECENTER": "プレイスペースをリセンター",
@@ -14,7 +23,8 @@
"ADD_NEW_SET": "新しいセットを追加",
"SWITCH_TO_SET": "セットに切り替える",
"TOGGLE_FOR_CURRENT_SET": "現在のセットで表示を切り替え",
- "LONG_PRESS_TO_DELETE_SET": "長押しでセットを削除"
+ "LONG_PRESS_TO_DELETE_SET": "長押しでセットを削除",
+ "CLEANUP_MIRRORS": "現在表示されていないミラーを削除"
},
"EDIT_MODE": {
"ADJUST_CURVATURE": "曲率の調整",
@@ -81,6 +91,8 @@
"EMPTY_SET": "空のセットです!",
"LETS_ADD_OVERLAYS": "ウォッチからオーバーレイを追加しましょう!",
"FIXING_FLOOR": "5秒後にフロアを固定します...",
- "ONE_CONTROLLER_ON_FLOOR": "コントローラーを床に置いてください!"
+ "ONE_CONTROLLER_ON_FLOOR": "コントローラーを床に置いてください!",
+ "CANNOT_ADD_SET": "セットを追加できません!",
+ "MAXIMUM_SETS_REACHED": "最大セット数に達しました。"
}
-}
+}
\ No newline at end of file
diff --git a/wlx-overlay-s/src/assets/lang/pl.json b/wlx-overlay-s/src/assets/lang/pl.json
index e394feb..95d9597 100644
--- a/wlx-overlay-s/src/assets/lang/pl.json
+++ b/wlx-overlay-s/src/assets/lang/pl.json
@@ -2,8 +2,17 @@
"ANCHOR": {
"CENTER": "Centrum"
},
- "BAR": {
- "ADD_MIRROR": "Dodaj nowy widok lustrzany"
+ "BAR": {
+ "ADD_MIRROR": "Dodaj nowy widok lustrzany",
+ "EDIT_MODE_TOGGLE": "Przełącz tryb edycji",
+ "ADD_NEW_SET": "Dodaj nowy zestaw",
+ "DELETE_CURRENT_SET": "Usuń aktualny zestaw",
+ "TOGGLE_VISIBILITY": "Przełącz widoczność",
+ "RESET_POSITION": "Zresetuj pozycję",
+ "RELOAD_FROM_DISK": "Przeładuj XML z dysku",
+ "CLOSE_MIRROR": "Zamknij lustro",
+ "CLOSE_APP": "Zamknij aplikację",
+ "FORCE_CLOSE_APP": "Wymuś zamknięcie aplikacji"
},
"WATCH": {
"RECENTER": "Wyśrodkuj przestrzeń gry",
@@ -14,7 +23,8 @@
"ADD_NEW_SET": "Dodaj nowy zestaw",
"SWITCH_TO_SET": "Przełącz na zestaw",
"TOGGLE_FOR_CURRENT_SET": "Przełącz widoczność w bieżącym zestawie",
- "LONG_PRESS_TO_DELETE_SET": "Przytrzymaj, aby usunąć zestaw"
+ "LONG_PRESS_TO_DELETE_SET": "Przytrzymaj, aby usunąć zestaw",
+ "CLEANUP_MIRRORS": "Usuń lustra, które\nnie są obecnie widoczne"
},
"EDIT_MODE": {
"ADJUST_CURVATURE": "Dostosuj zakrzywienie",
@@ -81,6 +91,8 @@
"EMPTY_SET": "Pusty zestaw!",
"LETS_ADD_OVERLAYS": "Dodajmy kilka nakładek z zegarka!",
"FIXING_FLOOR": "Naprawianie podłogi za 5 sekund...",
- "ONE_CONTROLLER_ON_FLOOR": "Umieść jeden kontroler na podłodze!"
+ "ONE_CONTROLLER_ON_FLOOR": "Umieść jeden kontroler na podłodze!",
+ "CANNOT_ADD_SET": "Nie można dodać zestawu!",
+ "MAXIMUM_SETS_REACHED": "Osiągnięto maksymalną liczbę zestawów."
}
-}
+}
\ No newline at end of file
diff --git a/wlx-overlay-s/src/backend/openxr/blocker.rs b/wlx-overlay-s/src/backend/openxr/blocker.rs
index 96e8c94..af20824 100644
--- a/wlx-overlay-s/src/backend/openxr/blocker.rs
+++ b/wlx-overlay-s/src/backend/openxr/blocker.rs
@@ -14,14 +14,18 @@ impl InputBlocker {
}
}
- pub fn update(&mut self, state: &AppState, watch_id: OverlayID, monado: &mut Monado) {
- if !state.session.config.block_game_input {
+ pub fn update(&mut self, app: &mut AppState, watch_id: OverlayID) {
+ let Some(monado) = &mut app.monado else {
+ return; // monado not available
+ };
+
+ if !app.session.config.block_game_input {
return;
}
- let any_hovered = state.input_state.pointers.iter().any(|p| {
+ let any_hovered = app.input_state.pointers.iter().any(|p| {
p.interaction.hovered_id.is_some_and(|id| {
- id != watch_id || !state.session.config.block_game_input_ignore_watch
+ id != watch_id || !app.session.config.block_game_input_ignore_watch
})
});
diff --git a/wlx-overlay-s/src/backend/openxr/input.rs b/wlx-overlay-s/src/backend/openxr/input.rs
index 9c9e392..abbc6e2 100644
--- a/wlx-overlay-s/src/backend/openxr/input.rs
+++ b/wlx-overlay-s/src/backend/openxr/input.rs
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
use wlx_common::config_io;
use crate::{
- backend::input::{Haptics, Pointer, TrackedDevice, TrackedDeviceRole},
+ backend::input::{Haptics, InputState, Pointer, TrackedDevice, TrackedDeviceRole},
state::{AppSession, AppState},
};
@@ -227,12 +227,12 @@ impl OpenXrInputSource {
fn update_device_battery_status(
device: &mut mnd::Device,
role: TrackedDeviceRole,
- app: &mut AppState,
+ input_state: &mut InputState,
) {
if let Ok(status) = device.battery_status()
&& status.present
{
- app.input_state.devices.push(TrackedDevice {
+ input_state.devices.push(TrackedDevice {
soc: Some(status.charge),
charging: status.charging,
role,
@@ -247,7 +247,11 @@ impl OpenXrInputSource {
}
}
- pub fn update_devices(app: &mut AppState, monado: &mut mnd::Monado) -> bool {
+ pub fn update_devices(app: &mut AppState) -> bool {
+ let Some(monado) = &mut app.monado else {
+ return false; // monado not available
+ };
+
let old_len = app.input_state.devices.len();
app.input_state.devices.clear();
@@ -267,13 +271,14 @@ impl OpenXrInputSource {
),
];
let mut seen = Vec::::with_capacity(32);
+
for (mnd_role, wlx_role) in roles {
let device = monado.device_from_role(mnd_role);
if let Ok(mut device) = device
&& !seen.contains(&device.index)
{
seen.push(device.index);
- Self::update_device_battery_status(&mut device, wlx_role, app);
+ Self::update_device_battery_status(&mut device, wlx_role, &mut app.input_state);
}
}
if let Ok(devices) = monado.devices() {
@@ -284,7 +289,7 @@ impl OpenXrInputSource {
} else {
TrackedDeviceRole::None
};
- Self::update_device_battery_status(&mut device, role, app);
+ Self::update_device_battery_status(&mut device, role, &mut app.input_state);
}
}
}
diff --git a/wlx-overlay-s/src/backend/openxr/mod.rs b/wlx-overlay-s/src/backend/openxr/mod.rs
index 2601064..1a7b15d 100644
--- a/wlx-overlay-s/src/backend/openxr/mod.rs
+++ b/wlx-overlay-s/src/backend/openxr/mod.rs
@@ -7,7 +7,6 @@ use std::{
use glam::{Affine3A, Vec3};
use input::OpenXrInputSource;
-use libmonado::Monado;
use openxr as xr;
use skybox::create_skybox;
use vulkano::{Handle, VulkanObject};
@@ -98,17 +97,15 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
let mut delete_queue = vec![];
- let mut monado = Monado::auto_connect()
- .map_err(|e| log::warn!("Will not use libmonado: {e}"))
- .ok();
+ app.monado_init();
- let mut playspace = monado.as_mut().and_then(|m| {
+ let mut playspace = app.monado.as_mut().and_then(|m| {
playspace::PlayspaceMover::new(m)
.map_err(|e| log::warn!("Will not use Monado playspace mover: {e}"))
.ok()
});
- let mut blocker = monado.is_some().then(blocker::InputBlocker::new);
+ let mut blocker = app.monado.is_some().then(blocker::InputBlocker::new);
let (session, mut frame_wait, mut frame_stream) = unsafe {
let raw_session = helpers::create_overlay_session(
@@ -223,10 +220,8 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
}
}
- if next_device_update <= Instant::now()
- && let Some(monado) = &mut monado
- {
- let changed = OpenXrInputSource::update_devices(&mut app, monado);
+ if app.monado.is_some() && next_device_update <= Instant::now() {
+ let changed = OpenXrInputSource::update_devices(&mut app);
if changed {
overlays.devices_changed(&mut app)?;
}
@@ -278,11 +273,7 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
app.input_state.post_update(&app.session);
if let Some(ref mut blocker) = blocker {
- blocker.update(
- &app,
- watch_id,
- monado.as_mut().unwrap(), // safe
- );
+ blocker.update(&mut app, watch_id);
}
if app
@@ -307,11 +298,7 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
watch_fade(&mut app, overlays.mut_by_id(watch_id).unwrap()); // want panic
if let Some(ref mut space_mover) = playspace {
- space_mover.update(
- &mut overlays,
- &app,
- monado.as_mut().unwrap(), // safe
- );
+ space_mover.update(&mut overlays, &mut app);
}
for o in overlays.values_mut() {
@@ -489,8 +476,8 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
overlays.handle_task(&mut app, task)?;
}
TaskType::Playspace(task) => {
- if let (Some(playspace), Some(monado)) = (playspace.as_mut(), monado.as_mut()) {
- playspace.handle_task(&app, monado, task);
+ if let Some(playspace) = playspace.as_mut() {
+ playspace.handle_task(&mut app, task);
}
}
#[cfg(feature = "openvr")]
diff --git a/wlx-overlay-s/src/backend/openxr/playspace.rs b/wlx-overlay-s/src/backend/openxr/playspace.rs
index 1c402ca..24bd3c0 100644
--- a/wlx-overlay-s/src/backend/openxr/playspace.rs
+++ b/wlx-overlay-s/src/backend/openxr/playspace.rs
@@ -43,7 +43,11 @@ impl PlayspaceMover {
})
}
- pub fn handle_task(&mut self, app: &AppState, monado: &mut Monado, task: PlayspaceTask) {
+ pub fn handle_task(&mut self, app: &mut AppState, task: PlayspaceTask) {
+ let Some(monado) = &mut app.monado else {
+ return; // monado not available
+ };
+
match task {
PlayspaceTask::FixFloor => {
self.fix_floor(&app.input_state, monado);
@@ -60,9 +64,12 @@ impl PlayspaceMover {
pub fn update(
&mut self,
overlays: &mut OverlayWindowManager,
- app: &AppState,
- monado: &mut Monado,
+ app: &mut AppState,
) {
+ let Some(monado) = &mut app.monado else {
+ return; // monado not available
+ };
+
for pointer in &app.input_state.pointers {
if pointer.now.space_reset {
if !pointer.before.space_reset {
diff --git a/wlx-overlay-s/src/overlays/dashboard.rs b/wlx-overlay-s/src/overlays/dashboard.rs
index feae803..6d7a1b2 100644
--- a/wlx-overlay-s/src/overlays/dashboard.rs
+++ b/wlx-overlay-s/src/overlays/dashboard.rs
@@ -16,7 +16,7 @@ use wgui::{
widget::EventResult,
};
use wlx_common::{
- dash_interface::DashInterface,
+ dash_interface::{self, DashInterface},
overlays::{BackendAttrib, BackendAttribValue},
};
use wlx_common::{
@@ -444,4 +444,92 @@ impl DashInterface for DashInterfaceLive {
RUNNING.store(false, Ordering::Relaxed);
RESTART.store(true, Ordering::Relaxed);
}
+
+ fn monado_client_list(
+ &mut self,
+ app: &mut AppState,
+ ) -> anyhow::Result> {
+ let Some(monado) = &mut app.monado else {
+ return Ok(Vec::new()); // no monado available
+ };
+
+ let clients = monado_list_clients_filtered(monado)?;
+
+ let mut res = Vec::::new();
+
+ for mut client in clients {
+ let name = client.name()?;
+ let state = client.state()?;
+
+ res.push(dash_interface::MonadoClient {
+ name,
+ is_primary: state.contains(libmonado::ClientState::ClientPrimaryApp),
+ is_active: state.contains(libmonado::ClientState::ClientSessionActive),
+ is_visible: state.contains(libmonado::ClientState::ClientSessionVisible),
+ is_focused: state.contains(libmonado::ClientState::ClientSessionFocused),
+ is_overlay: state.contains(libmonado::ClientState::ClientSessionOverlay),
+ is_io_active: state.contains(libmonado::ClientState::ClientIoActive),
+ });
+ }
+
+ Ok(res)
+ }
+
+ fn monado_client_focus(&mut self, app: &mut AppState, name: &str) -> anyhow::Result<()> {
+ let Some(monado) = &mut app.monado else {
+ return Ok(()); // no monado avoilable
+ };
+
+ monado_client_focus(monado, name)?;
+
+ // Restart monado (BUG!)
+ // https://gitlab.freedesktop.org/monado/monado/-/issues/497
+ app.monado_init();
+ Ok(())
+ }
+}
+
+const CLIENT_NAME_BLACKLIST: [&str; 2] = ["wlx-overlay-s", "libmonado"];
+
+fn monado_list_clients_filtered(
+ monado: &mut libmonado::Monado,
+) -> anyhow::Result>> {
+ let mut clients: Vec<_> = monado.clients()?.into_iter().collect();
+
+ let clients: Vec<_> = clients
+ .iter_mut()
+ .filter_map(|client| {
+ let Ok(name) = client.name() else {
+ return None;
+ };
+
+ for cell in CLIENT_NAME_BLACKLIST {
+ if cell == name {
+ // blacklisted!
+ return None;
+ }
+ }
+
+ Some(client.clone())
+ })
+ .collect();
+
+ Ok(clients)
+}
+
+fn monado_client_focus(monado: &mut libmonado::Monado, name: &str) -> anyhow::Result<()> {
+ let clients = monado_list_clients_filtered(monado)?;
+
+ for mut client in clients {
+ let client_name = client.name()?;
+ if client_name != name {
+ continue;
+ }
+
+ log::info!("Monado focus set to {client_name}");
+ client.set_primary()?;
+ return Ok(());
+ }
+
+ Ok(())
}
diff --git a/wlx-overlay-s/src/state.rs b/wlx-overlay-s/src/state.rs
index 976d2c4..ab0fa01 100644
--- a/wlx-overlay-s/src/state.rs
+++ b/wlx-overlay-s/src/state.rs
@@ -64,6 +64,9 @@ pub struct AppState {
#[cfg(feature = "wayvr")]
pub wvr_server: Option,
+
+ #[cfg(feature = "openxr")]
+ pub monado: Option,
}
#[allow(unused_mut)]
@@ -163,8 +166,20 @@ impl AppState {
#[cfg(feature = "wayvr")]
wvr_server,
+
+ #[cfg(feature = "openxr")]
+ monado: None,
})
}
+
+ #[cfg(feature = "openxr")]
+ pub fn monado_init(&mut self) {
+ log::debug!("Connecting to Monado IPC");
+ self.monado = None; // stop connection first
+ self.monado = libmonado::Monado::auto_connect()
+ .map_err(|e| log::warn!("Will not use libmonado: {e}"))
+ .ok();
+ }
}
pub struct AppSession {