diff --git a/.gitignore b/.gitignore
index 92b8c92..37d93f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/vulkan/build/
/build/
+__pycache__
diff --git a/gnome/bin/setup-45.sh b/gnome/bin/setup-45.sh
old mode 100644
new mode 100755
index 8504b54..8139e0d
--- a/gnome/bin/setup-45.sh
+++ b/gnome/bin/setup-45.sh
@@ -58,6 +58,11 @@ if [ ! -L $extensions_dir/breezydesktop@org.xronlinux ]; then
chown -R $user:$group $extensions_dir/breezydesktop@org.xronlinux
fi
+glib-compile-schemas --targetdir=$extensions_dir/breezydesktop@org.xronlinux/schemas/ $extensions_dir/breezydesktop@org.xronlinux/schemas/
+
+sudo cp $extensions_dir/breezydesktop@org.xronlinux/schemas/org.gnome.shell.extensions.breezy-desktop.gschema.xml /usr/share/glib-2.0/schemas/
+sudo glib-compile-schemas /usr/share/glib-2.0/schemas/
+
echo "Breezy Desktop extension is installed. Please log out, log back in, \
and then run the following command to enable it:\
gnome-extensions enable breezydesktop@org.xronlinux"
diff --git a/gnome/breezydesktop@org.xronlinux/extension.js b/gnome/breezydesktop@org.xronlinux/extension.js
index 33ca3fd..5a1f9ab 100644
--- a/gnome/breezydesktop@org.xronlinux/extension.js
+++ b/gnome/breezydesktop@org.xronlinux/extension.js
@@ -23,6 +23,8 @@ const SUPPORTED_MONITOR_PRODUCTS = [
export default class BreezyDesktopExtension extends Extension {
constructor(metadata, uuid) {
super(metadata, uuid);
+
+ this.settings = this.getSettings();
// Set/destroyed by enable/disable
this._cursor_manager = null;
@@ -129,26 +131,22 @@ export default class BreezyDesktopExtension extends Extension {
this._xr_effect = new XREffect({
target_monitor: this._target_monitor,
- target_framerate: this._refresh_rate ?? 60
+ target_framerate: this._refresh_rate ?? 60,
+ display_distance: this.settings.get_double('display-distance'),
+ toggle_display_distance_start: this.settings.get_double('toggle-display-distance-start'),
+ toggle_display_distance_end: this.settings.get_double('toggle-display-distance-end'),
});
+ this.settings.bind('effect-enable', this._xr_effect, 'effect-enable', Gio.SettingsBindFlags.DEFAULT)
+ this.settings.bind('display-distance', this._xr_effect, 'display-distance', Gio.SettingsBindFlags.DEFAULT)
+ this.settings.bind('toggle-display-distance-start', this._xr_effect, 'toggle-display-distance-start', Gio.SettingsBindFlags.DEFAULT)
+ this.settings.bind('toggle-display-distance-end', this._xr_effect, 'toggle-display-distance-end', Gio.SettingsBindFlags.DEFAULT)
+
this._overlay.add_effect_with_name('xr-desktop', this._xr_effect);
Meta.disable_unredirect_for_display(global.display);
- Main.wm.addKeybinding(
- 'shortcut-recenter',
- this.getSettings(),
- Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
- Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW | Shell.ActionMode.POPUP,
- this._recenter_display.bind(this)
- );
- Main.wm.addKeybinding(
- 'shortcut-change-distance',
- this.getSettings(),
- Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
- Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW | Shell.ActionMode.POPUP,
- this._xr_effect._change_distance.bind(this._xr_effect)
- );
+ this._add_settings_keybinding('recenter-display-shortcut', this._recenter_display.bind(this));
+ this._add_settings_keybinding('toggle-display-distance-shortcut', this._xr_effect._change_distance.bind(this._xr_effect));
} catch (e) {
console.error('Error enabling XR effect', e);
this._effect_disable();
@@ -156,6 +154,31 @@ export default class BreezyDesktopExtension extends Extension {
}
}
+ _add_settings_keybinding(settings_key, bind_to_function) {
+ Main.wm.addKeybinding(
+ settings_key,
+ this.settings,
+ Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
+ Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW | Shell.ActionMode.POPUP,
+ bind_to_function
+ );
+
+ // Connect to the 'changed' signal for the keybinding property
+ this.settings.connect(`changed::${settings_key}`, () => {
+ // Remove the old keybinding
+ Main.wm.removeKeybinding(settings_key);
+
+ // Add the updated keybinding
+ Main.wm.addKeybinding(
+ settings_key,
+ this.settings,
+ Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
+ Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW | Shell.ActionMode.POPUP,
+ bind_to_function
+ );
+ });
+ }
+
_recenter_display() {
const file = Gio.file_new_for_path('/dev/shm/xr_driver_control');
const stream = file.replace(null, false, Gio.FileCreateFlags.NONE, null);
@@ -168,8 +191,8 @@ export default class BreezyDesktopExtension extends Extension {
if (this._running_poller_id) GLib.source_remove(this._running_poller_id);
- Main.wm.removeKeybinding('shortcut-recenter');
- Main.wm.removeKeybinding('shortcut-change-distance');
+ Main.wm.removeKeybinding('recenter-display-shortcut');
+ Main.wm.removeKeybinding('toggle-display-distance-shortcut');
Meta.enable_unredirect_for_display(global.display);
if (this._overlay) {
diff --git a/gnome/breezydesktop@org.xronlinux/schemas/gschemas.compiled b/gnome/breezydesktop@org.xronlinux/schemas/gschemas.compiled
index dd50967..ff1fbd4 100644
Binary files a/gnome/breezydesktop@org.xronlinux/schemas/gschemas.compiled and b/gnome/breezydesktop@org.xronlinux/schemas/gschemas.compiled differ
diff --git a/gnome/breezydesktop@org.xronlinux/schemas/org.gnome.shell.extensions.breezy-desktop.gschema.xml b/gnome/breezydesktop@org.xronlinux/schemas/org.gnome.shell.extensions.breezy-desktop.gschema.xml
index 3788b5a..55561be 100644
--- a/gnome/breezydesktop@org.xronlinux/schemas/org.gnome.shell.extensions.breezy-desktop.gschema.xml
+++ b/gnome/breezydesktop@org.xronlinux/schemas/org.gnome.shell.extensions.breezy-desktop.gschema.xml
@@ -1,22 +1,58 @@
-
+
- space']]]>
+ true
+
+ Enable XR effect
+
+ Enable XR effect
+
+
+
+
+ space', 'Ctrl+Super+Space']]]>
Re-center display
Shortcut to re-center the virtual display.
-
+
- Return']]]>
+ Return', 'Ctrl+Super+Return']]]>
Trigger change to display distance
Shortcut to change the display distance.
+
+
+ 1.05
+
+ Display distance
+
+ How far away the display appears. Farther will look smaller, closer will look larger.
+
+
+
+
+ 0.85
+
+ Display distance start
+
+ Start distance when using the "change distance" shortcut.
+
+
+
+
+ 1.05
+
+ Display distance end
+
+ End distance when using the "toggle display distance" shortcut.
+
+
\ No newline at end of file
diff --git a/gnome/breezydesktop@org.xronlinux/xrEffect.js b/gnome/breezydesktop@org.xronlinux/xrEffect.js
index f238c31..a567c2b 100644
--- a/gnome/breezydesktop@org.xronlinux/xrEffect.js
+++ b/gnome/breezydesktop@org.xronlinux/xrEffect.js
@@ -25,8 +25,6 @@ import { getShaderSource } from "./shader.js";
import { toSec } from "./time.js";
export const IPC_FILE_PATH = "/dev/shm/breezy_desktop_imu";
-const display_distance_nearest = 0.85;
-const display_distance_furthest = 1.05;
// the driver should be using the same data layout version
const DATA_LAYOUT_VERSION = 2;
@@ -123,7 +121,7 @@ function setIntermittentUniformVariables() {
const validKeepalive = Math.abs(toSec(currentDateMS) - toSec(imuDateMS)) < 5;
const imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA);
const imuResetState = imuData[0] === 0.0 && imuData[1] === 0.0 && imuData[2] === 0.0 && imuData[3] === 1.0;
- const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive && !imuResetState;
+ const enabled = this.effect_enable && dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive && !imuResetState;
if (enabled) {
const displayRes = dataViewUintArray(dataView, DISPLAY_RES);
@@ -171,6 +169,13 @@ function setIntermittentUniformVariables() {
export const XREffect = GObject.registerClass({
Properties: {
+ 'effect-enable': GObject.ParamSpec.boolean(
+ 'effect-enable',
+ 'Effect enable',
+ 'Whether this effect is enabled',
+ GObject.ParamFlags.READWRITE,
+ true
+ ),
'target-monitor': GObject.ParamSpec.jsobject(
'target-monitor',
'Target Monitor',
@@ -182,6 +187,33 @@ export const XREffect = GObject.registerClass({
'Target Framerate',
'Target framerate for this effect',
GObject.ParamFlags.READWRITE, 60, 240, 60
+ ),
+ 'display-distance': GObject.ParamSpec.double(
+ 'display-distance',
+ 'Display Distance',
+ 'How far away the display appears',
+ GObject.ParamFlags.READWRITE,
+ 0.2,
+ 2.5,
+ 1.05
+ ),
+ 'toggle-display-distance-start': GObject.ParamSpec.double(
+ 'toggle-display-distance-start',
+ 'Display distance start',
+ 'Start distance when using the "change distance" shortcut.',
+ GObject.ParamFlags.READWRITE,
+ 0.2,
+ 2.5,
+ 1.05
+ ),
+ 'toggle-display-distance-end': GObject.ParamSpec.double(
+ 'toggle-display-distance-end',
+ 'Display distance end',
+ 'End distance when using the "change distance" shortcut.',
+ GObject.ParamFlags.READWRITE,
+ 0.2,
+ 2.5,
+ 1.05
)
}
}, class XREffect extends Shell.GLSLEffect {
@@ -190,28 +222,24 @@ export const XREffect = GObject.registerClass({
this._frametime = Math.floor(1000 / this.target_framerate);
- // slightly zoomed out by default
- this._display_distance = display_distance_furthest;
- this._display_distance_near = false;
+ this._is_display_distance_at_end = false;
this._distance_ease_timeline = null;
}
_change_distance() {
- if (this._distance_ease_timeline?.is_playing()) this._distance_ease_timeline.stop();
- this._distance_ease_start = this._display_distance;
+ if (this._distance_ease_timeline?.is_playing()) this._distance_ease_timeline.stop();
+
+ this._distance_ease_start = this.display_distance;
this._distance_ease_timeline = Clutter.Timeline.new_for_actor(this.get_actor(), 250);
- if (this._display_distance_near) {
- this._distance_ease_timeline.connect('new-frame', () => {
- this._display_distance = this._distance_ease_start + this._distance_ease_timeline.get_progress() * (display_distance_furthest - this._distance_ease_start);
- });
- this._display_distance_near = false;
- } else {
- this._distance_ease_timeline.connect('new-frame', () => {
- this._display_distance = this._distance_ease_start - this._distance_ease_timeline.get_progress() * (this._distance_ease_start - display_distance_nearest);
- });
- this._display_distance_near = true;
- }
+ const toggle_display_distance_target = this._is_display_distance_at_end ?
+ this.toggle_display_distance_start : this.toggle_display_distance_end;
+ this._distance_ease_timeline.connect('new-frame', () => {
+ this.display_distance = this._distance_ease_start +
+ this._distance_ease_timeline.get_progress() *
+ (toggle_display_distance_target - this._distance_ease_start);
+ });
+ this._is_display_distance_at_end = !this._is_display_distance_at_end;
this._distance_ease_timeline.start();
}
@@ -252,7 +280,7 @@ export const XREffect = GObject.registerClass({
}
if (this._dataView.byteLength === DATA_VIEW_LENGTH) {
- setSingleFloat(this, 'display_north_offset', this._display_distance);
+ setSingleFloat(this, 'display_north_offset', this.display_distance);
setSingleFloat(this, 'look_ahead_ms', lookAheadMS(this._dataView));
setUniformMatrix(this, 'imu_quat_data', 4, this._dataView, IMU_QUAT_DATA);
} else if (this._dataView.byteLength !== 0) {
diff --git a/ui/BreezyDesktop.py b/ui/BreezyDesktop.py
new file mode 100644
index 0000000..6c6f3f5
--- /dev/null
+++ b/ui/BreezyDesktop.py
@@ -0,0 +1,93 @@
+import gi
+import sys
+import threading
+
+gi.require_version("Gtk", "4.0")
+gi.require_version('Adw', '1')
+
+from gi.repository import Adw, Gio, Gtk
+
+from XRDriverIPC import XRDriverIPC
+from ShortcutDialog import bind_shortcut_settings
+
+class Logger:
+ def info(self, message):
+ print(message)
+
+ def error(self, message):
+ print(message)
+
+class MainWindow(Gtk.ApplicationWindow):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.set_title("Breezy Desktop")
+
+ builder = Gtk.Builder()
+ builder.add_from_file("./breezy-desktop.ui")
+ self.set_child(builder.get_object("main"))
+ self.connected_device_info = builder.get_object("connected-device-info")
+ self.connected_device_label = builder.get_object("connected-device-label")
+ self.connected_device_settings = builder.get_object("connected-device-settings")
+ self.connected_device_shortcuts = builder.get_object("connected-device-shortcuts")
+ self.no_connected_device = builder.get_object("no-connected-device")
+
+ self.settings = Gio.Settings.new_with_path("org.gnome.shell.extensions.breezy-desktop", "/org/gnome/shell/extensions/breezy-desktop/")
+ self.ipc = XRDriverIPC(logger = Logger())
+ self._refresh_state()
+
+ bind_shortcut_settings(self, self.settings, [
+ builder.get_object('reassign-recenter-display-shortcut-button'),
+ builder.get_object('reassign-toggle-display-distance-shortcut-button'),
+ ])
+
+ self.bind_set_distance_toggle([
+ builder.get_object('set-toggle-display-distance-start-button'),
+ builder.get_object('set-toggle-display-distance-end-button')
+ ])
+ display_distance_slider = builder.get_object('display-distance-slider')
+ self.settings.bind('display-distance', display_distance_slider, 'value', Gio.SettingsBindFlags.DEFAULT)
+
+ effect_enable_switch = builder.get_object('effect-enable')
+ self.settings.bind('effect-enable', effect_enable_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
+
+ def _refresh_state(self):
+ self.state = self.ipc.retrieve_driver_state()
+ if self.state.get('connected_device_brand') and self.state.get('connected_device_model'):
+ self.connected_device_info.set_visible(True)
+ self.connected_device_settings.set_visible(True)
+ self.connected_device_shortcuts.set_visible(True)
+ self.no_connected_device.set_visible(False)
+ self.connected_device_label.set_markup(f"{self.state['connected_device_brand']} {self.state['connected_device_model']}")
+ else:
+ self.connected_device_info.set_visible(False)
+ self.connected_device_settings.set_visible(False)
+ self.connected_device_shortcuts.set_visible(False)
+ self.no_connected_device.set_visible(True)
+ threading.Timer(1.0, self._refresh_state).start()
+
+
+ def bind_set_distance_toggle(self, widgets):
+ for widget in widgets:
+ widget.connect('clicked', lambda *args, widget=widget: on_set_display_distance_toggle(self.settings, widget))
+ reload_display_distance_toggle_button(self.settings, widget)
+
+def reload_display_distance_toggle_button(settings, widget):
+ distance = settings.get_double(widget.get_name())
+ if distance: widget.set_label(str(distance))
+
+def on_set_display_distance_toggle(settings, widget):
+ distance = settings.get_double('display-distance')
+ settings.set_double(widget.get_name(), distance)
+ reload_display_distance_toggle_button(settings, widget)
+
+class BreezyDesktop(Adw.Application):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.connect('activate', self.on_activate)
+
+ def on_activate(self, app):
+ self.win = MainWindow(application=app)
+ self.win.present()
+
+app = BreezyDesktop(application_id="com.example.GtkApplication")
+app.run(sys.argv)
\ No newline at end of file
diff --git a/ui/Pipfile b/ui/Pipfile
new file mode 100644
index 0000000..35280f8
--- /dev/null
+++ b/ui/Pipfile
@@ -0,0 +1,12 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+
+[dev-packages]
+pygobject-stubs = "*"
+
+[requires]
+python_version = "3.11"
diff --git a/ui/Pipfile.lock b/ui/Pipfile.lock
new file mode 100644
index 0000000..f9343d4
--- /dev/null
+++ b/ui/Pipfile.lock
@@ -0,0 +1,28 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "f363f820b1e601b5678bb4d65939db586a4db4fb008ee873697aff3df08d0877"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.11"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {},
+ "develop": {
+ "pygobject-stubs": {
+ "hashes": [
+ "sha256:e1c7e32658213ae711d8afc5ea083a434231b8b588d1de23f50d5705a9c8eefe"
+ ],
+ "index": "pypi",
+ "version": "==2.11.0"
+ }
+ }
+}
diff --git a/ui/ShortcutDialog.py b/ui/ShortcutDialog.py
new file mode 100644
index 0000000..8531888
--- /dev/null
+++ b/ui/ShortcutDialog.py
@@ -0,0 +1,104 @@
+from gi.repository import Gtk, Gdk
+
+# ported from https://github.com/velitasali/gnome-shell-extension-awesome-tiles
+class ShortcutDialog:
+ def __init__(self, settings, settings_key):
+ self.settings = settings
+ self.settings_key = settings_key
+
+ self._builder = Gtk.Builder()
+ self._builder.add_from_file('./shortcut-dialog.ui')
+
+ self.widget = self._builder.get_object('dialog')
+
+ self.event_controller = self._builder.get_object('event-controller')
+ self.key_pressed_connect_id = self.event_controller.connect('key-pressed', self._on_key_pressed)
+
+ def _on_key_pressed(self, widget, keyval, keycode, state):
+ mask = state & Gtk.accelerator_get_default_mod_mask()
+ mask &= ~Gdk.ModifierType.LOCK_MASK
+
+ done = True
+ if mask == 0 and keyval == Gdk.KEY_Escape:
+ self.widget.visible = False
+ elif keyval == Gdk.KEY_BackSpace:
+ self.settings.set_strv(self.settings_key, [])
+ self.widget.close()
+ elif is_binding_valid(mask, keycode, keyval) and is_accel_valid(state, keyval):
+ binding = Gtk.accelerator_name_with_keycode(
+ None,
+ keyval,
+ keycode,
+ state
+ )
+ label = Gtk.accelerator_get_label(keyval, state)
+
+ # hacky way to store the label, causes warnings from the WM
+ self.settings.set_strv(self.settings_key, [binding, label])
+
+ self.widget.close()
+ else:
+ done = False
+
+ if done and self.key_pressed_connect_id:
+ self.event_controller.disconnect(self.key_pressed_connect_id)
+ self.key_pressed_connect_id = None
+
+ return Gdk.EVENT_STOP
+
+def is_binding_valid(mask, keycode, keyval):
+ if mask == 0 or mask == Gdk.ModifierType.SHIFT_MASK and keycode != 0:
+ if keyval >= Gdk.KEY_a and keyval <= Gdk.KEY_z or \
+ keyval >= Gdk.KEY_A and keyval <= Gdk.KEY_Z or \
+ keyval >= Gdk.KEY_0 and keyval <= Gdk.KEY_9 or \
+ keyval >= Gdk.KEY_kana_fullstop and keyval <= Gdk.KEY_semivoicedsound or \
+ keyval >= Gdk.KEY_Arabic_comma and keyval <= Gdk.KEY_Arabic_sukun or \
+ keyval >= Gdk.KEY_Serbian_dje and keyval <= Gdk.KEY_Cyrillic_HARDSIGN or \
+ keyval >= Gdk.KEY_Greek_ALPHAaccent and keyval <= Gdk.KEY_Greek_omega or \
+ keyval >= Gdk.KEY_hebrew_doublelowline and keyval <= Gdk.KEY_hebrew_taf or \
+ keyval >= Gdk.KEY_Thai_kokai and keyval <= Gdk.KEY_Thai_lekkao or \
+ keyval >= Gdk.KEY_Hangul_Kiyeog and keyval <= Gdk.KEY_Hangul_J_YeorinHieuh or \
+ keyval == Gdk.KEY_space and mask == 0 or \
+ is_keyval_forbidden(keyval):
+ return False
+ return True
+
+def is_keyval_forbidden(keyval):
+ forbidden_keyvals = [
+ Gdk.KEY_Home,
+ Gdk.KEY_Left,
+ Gdk.KEY_Up,
+ Gdk.KEY_Right,
+ Gdk.KEY_Down,
+ Gdk.KEY_Page_Up,
+ Gdk.KEY_Page_Down,
+ Gdk.KEY_End,
+ Gdk.KEY_Tab,
+ Gdk.KEY_KP_Enter,
+ Gdk.KEY_Return,
+ Gdk.KEY_Mode_switch
+ ]
+ return keyval in forbidden_keyvals
+
+def is_accel_valid(mask, keyval):
+ return Gtk.accelerator_valid(keyval, mask) or (keyval == Gdk.KEY_Tab and mask != 0)
+
+def bind_shortcut_settings(window, settings, widgets):
+ for widget in widgets:
+ settings.connect('changed::' + widget.get_name(), lambda *args, widget=widget: reload_shortcut_widget(settings, widget))
+ widget.connect('clicked', lambda *args, widget=widget: on_assign_shortcut(window, settings, widget))
+
+ reload_shortcut_widgets(settings, widgets)
+
+def on_assign_shortcut(window, settings, widget):
+ dialog = ShortcutDialog(settings, widget.get_name())
+ dialog.widget.set_transient_for(window)
+ dialog.widget.present()
+
+def reload_shortcut_widget(settings, widget):
+ shortcut = settings.get_strv(widget.get_name())
+ widget.set_label(shortcut[1] if len(shortcut) > 1 else 'Disabled')
+
+def reload_shortcut_widgets(settings, widgets):
+ for widget in widgets:
+ reload_shortcut_widget(settings, widget)
\ No newline at end of file
diff --git a/ui/XRDriverIPC.py b/ui/XRDriverIPC.py
new file mode 100644
index 0000000..ba4bd14
--- /dev/null
+++ b/ui/XRDriverIPC.py
@@ -0,0 +1,279 @@
+import json
+import os
+import pwd
+import stat
+import subprocess
+import time
+
+# write-only file that the driver reads (but never writes) to get user-specified control flags
+CONTROL_FLAGS_FILE_PATH = '/dev/shm/xr_driver_control'
+
+# read-only file that the driver writes (but never reads) to with its current state
+DRIVER_STATE_FILE_PATH = '/dev/shm/xr_driver_state'
+
+CONTROL_FLAGS = ['recenter_screen', 'recalibrate', 'sbs_mode', 'refresh_device_license']
+SBS_MODE_VALUES = ['unset', 'enable', 'disable']
+MANAGED_EXTERNAL_MODES = ['virtual_display', 'sideview', 'none']
+VR_LITE_OUTPUT_MODES = ['mouse', 'joystick']
+
+def parse_boolean(value, default):
+ if not value:
+ return default
+
+ return value.lower() == 'true'
+
+
+def parse_int(value, default):
+ return int(value) if value.isdigit() else default
+
+def parse_float(value, default):
+ try:
+ return float(value)
+ except ValueError:
+ return default
+
+def parse_string(value, default):
+ return value if value else default
+
+def parse_array(value, default):
+ return value.split(",") if value else default
+
+
+CONFIG_PARSER_INDEX = 0
+CONFIG_DEFAULT_VALUE_INDEX = 1
+CONFIG_ENTRIES = {
+ 'disabled': [parse_boolean, True],
+ 'output_mode': [parse_string, 'mouse'],
+ 'external_mode': [parse_array, 'none'],
+ 'mouse_sensitivity': [parse_int, 30],
+ 'display_zoom': [parse_float, 1.0],
+ 'look_ahead': [parse_int, 0],
+ 'sbs_display_size': [parse_float, 1.0],
+ 'sbs_display_distance': [parse_float, 1.0],
+ 'sbs_content': [parse_boolean, False],
+ 'sbs_mode_stretched': [parse_boolean, False],
+ 'sideview_position': [parse_string, 'center'],
+ 'sideview_display_size': [parse_float, 1.0],
+ 'virtual_display_smooth_follow_enabled': [parse_boolean, False],
+ 'sideview_smooth_follow_enabled': [parse_boolean, False]
+}
+
+class XRDriverIPC:
+ def __init__(self, logger, user=None, user_home=None):
+ self.breezy_installed = False
+ self.breezy_installing = False
+ self.user = user if user else pwd.getpwuid( os.getuid() )[0]
+ self.user_home = user_home if user_home else os.path.expanduser("~")
+ self.config_file_path = os.path.join(self.user_home, ".xreal_driver_config")
+ self.config_script_path = os.path.join(self.user_home, "bin/xreal_driver_config")
+ self.logger = logger
+
+ def retrieve_config(self):
+ config = {}
+ for key, value in CONFIG_ENTRIES.items():
+ config[key] = value[CONFIG_DEFAULT_VALUE_INDEX]
+
+ try:
+ with open(self.config_file_path, 'r') as f:
+ for line in f:
+ try:
+ if not line.strip():
+ continue
+
+ key, value = line.strip().split('=')
+ if key in CONFIG_ENTRIES:
+ parser = CONFIG_ENTRIES[key][CONFIG_PARSER_INDEX]
+ default_val = CONFIG_ENTRIES[key][CONFIG_DEFAULT_VALUE_INDEX]
+ config[key] = parser(value, default_val)
+ except Exception as e:
+ self.logger.error(f"Error parsing line {line}: {e}")
+ except FileNotFoundError as e:
+ self.logger.error(f"Config file not found {e}")
+ return config
+
+ config['ui_view'] = self.build_ui_view(config)
+
+ return config
+
+ def write_config(self, config):
+ try:
+ output = ""
+
+ # Since the UI doesn't refresh the config before it updates, the external_mode can get out of sync with
+ # what's on disk. To avoid losing external_mode values, we retrieve the previous configs to preserve
+ # any non-managed external modes.
+ old_config = self._retrieve_config(self)
+
+ # remove the UI's "view" data, translate back to config values, and merge them in
+ view = config.pop('ui_view', None)
+ config.update(self.headset_mode_to_config(view['headset_mode'], view['is_joystick_mode'], old_config['external_mode']))
+
+ for key, value in config.items():
+ if key != "updated":
+ if isinstance(value, bool):
+ output += f'{key}={str(value).lower()}\n'
+ elif isinstance(value, int):
+ output += f'{key}={value}\n'
+ elif isinstance(value, list):
+ output += f'{key}={",".join(value)}\n'
+ else:
+ output += f'{key}={value}\n'
+
+ temp_file = "temp.txt"
+
+ # Write to a temporary file
+ with open(temp_file, 'w') as f:
+ f.write(output)
+
+ # Atomically replace the old config file with the new one
+ os.replace(temp_file, self.config_file_path)
+ os.chmod(self.config_file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
+
+ config['ui_view'] = self.build_ui_view(self, config)
+
+ return config
+ except Exception as e:
+ self.logger.error(f"Error writing config {e}")
+ raise e
+
+ # like a SQL "view," these are computed values that are commonly used in the UI
+ def build_ui_view(self, config):
+ view = {}
+ view['headset_mode'] = self.config_to_headset_mode(config)
+ view['is_joystick_mode'] = config['output_mode'] == 'joystick'
+ return view
+
+ def filter_to_other_external_modes(self, external_modes):
+ return [mode for mode in external_modes if mode not in MANAGED_EXTERNAL_MODES]
+
+ def headset_mode_to_config(self, headset_mode, joystick_mode, old_external_modes):
+ new_external_modes = self.filter_to_other_external_modes(old_external_modes)
+
+ config = {}
+ if headset_mode == "virtual_display":
+ # TODO - uncomment this when the driver can support multiple external_mode values
+ # new_external_modes.append("virtual_display")
+ new_external_modes = ["virtual_display"]
+ config['output_mode'] = "external_only"
+ config['disabled'] = False
+ elif headset_mode == "vr_lite":
+ config['output_mode'] = "joystick" if joystick_mode else "mouse"
+ config['disabled'] = False
+ elif headset_mode == "sideview":
+ # TODO - uncomment this when the driver can support multiple external_mode values
+ # new_external_modes.append("sideview")
+ new_external_modes = ["sideview"]
+ config['output_mode'] = "external_only"
+ config['disabled'] = False
+ else:
+ config['output_mode'] = "external_only"
+
+ has_external_mode = len(new_external_modes) > 0
+ if not has_external_mode:
+ new_external_modes.append("none")
+ config['external_mode'] = new_external_modes
+
+ return config
+
+ def config_to_headset_mode(self, config):
+ if not config or config['disabled']:
+ return "disabled"
+
+ if config['output_mode'] in VR_LITE_OUTPUT_MODES:
+ return "vr_lite"
+
+ managed_mode = next((mode for mode in MANAGED_EXTERNAL_MODES if mode in config['external_mode']), None)
+ if managed_mode and managed_mode != "none":
+ return managed_mode
+
+ return "disabled"
+
+ def write_control_flags(self, control_flags):
+ try:
+ output = ""
+ for key, value in control_flags.items():
+ if key in CONTROL_FLAGS:
+ if key == 'sbs_mode':
+ if value not in SBS_MODE_VALUES:
+ self.logger.error(f"Invalid value {value} for sbs_mode flag")
+ continue
+ elif not isinstance(value, bool):
+ self.logger.error(f"Invalid value {value} for {key} flag")
+ continue
+ output += f'{key}={str(value).lower()}\n'
+
+ with open(CONTROL_FLAGS_FILE_PATH, 'w') as f:
+ f.write(output)
+ except Exception as e:
+ self.logger.error(f"Error writing control flags {e}")
+
+ def retrieve_driver_state(self):
+ state = {}
+ state['heartbeat'] = 0
+ state['connected_device_brand'] = None
+ state['connected_device_model'] = None
+ state['calibration_setup'] = "AUTOMATIC"
+ state['calibration_state'] = "NOT_CALIBRATED"
+ state['sbs_mode_enabled'] = False
+ state['sbs_mode_supported'] = False
+ state['firmware_update_recommended'] = False
+ state['device_license'] = {}
+
+ try:
+ with open(DRIVER_STATE_FILE_PATH, 'r') as f:
+ output = f.read()
+ for line in output.splitlines():
+ try:
+ if not line.strip():
+ continue
+
+ key, value = line.strip().split('=')
+ if key == 'heartbeat':
+ state[key] = parse_int(value, 0)
+ elif key in ['calibration_setup', 'calibration_state', 'connected_device_brand', 'connected_device_model']:
+ state[key] = value
+ elif key in ['sbs_mode_enabled', 'sbs_mode_supported', 'firmware_update_recommended']:
+ state[key] = parse_boolean(value, False)
+ elif key == 'device_license':
+ state[key] = json.loads(value)
+ except Exception as e:
+ self.logger.error(f"Error parsing key-value pair {key}={value}: {e}")
+ except FileNotFoundError:
+ pass
+
+ # state is stale, just send the license
+ if state['heartbeat'] == 0 or (time.time() - state['heartbeat']) > 5:
+ return {
+ 'heartbeat': state['heartbeat'],
+ 'device_license': state['device_license']
+ }
+
+ return state
+
+ async def request_token(self, email):
+ self.logger.info(f"Requesting a new token for {email}")
+
+ # Set the USER environment variable for this command
+ env_copy = os.environ.copy()
+ env_copy["USER"] = self.user
+
+ try:
+ output = subprocess.check_output([self.config_script_path, "--request-token", email], stderr=subprocess.STDOUT, env=env_copy)
+ return output.strip() == b"Token request sent"
+ except subprocess.CalledProcessError as exc:
+ self.logger.error(f"Error running config script {exc.output}")
+ return False
+
+ async def verify_token(self, token):
+ self.logger.info(f"Verifying token {token}")
+
+ # Set the USER environment variable for this command
+ env_copy = os.environ.copy()
+ env_copy["USER"] = self.user
+
+ try:
+ output = subprocess.check_output([self.config_script_path, "--verify-token", token], stderr=subprocess.STDOUT, env=env_copy)
+ return output.strip() == b"Token verified"
+ except subprocess.CalledProcessError as exc:
+ self.logger.error(f"Error running config script {exc.output}")
+ return False
\ No newline at end of file
diff --git a/ui/breezy-desktop.ui b/ui/breezy-desktop.ui
new file mode 100644
index 0000000..cbcab64
--- /dev/null
+++ b/ui/breezy-desktop.ui
@@ -0,0 +1,142 @@
+
+
+
+
+
diff --git a/ui/shortcut-dialog.ui b/ui/shortcut-dialog.ui
new file mode 100644
index 0000000..2f2cc5d
--- /dev/null
+++ b/ui/shortcut-dialog.ui
@@ -0,0 +1,43 @@
+
+
+
+
+ 1
+ 440
+ 200
+
+
+ vertical
+ 2
+ 16
+ 16
+ 16
+ 16
+
+
+ 1
+ Press your keyboard shortcut or 'Backspace' to disable...
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file