const Clutter = imports.gi.Clutter; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; const St = imports.gi.St; const Main = imports.ui.main; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Globals = Me.imports.globals; const { CursorManager } = Me.imports.cursormanager; const { Logger } = Me.imports.logger; const { MonitorManager } = Me.imports.monitormanager; const { SystemBackground } = Me.imports.systembackground; const { isValidKeepAlive } = Me.imports.time; const { IPC_FILE_PATH, XREffect } = Me.imports.xrEffect; const NESTED_MONITOR_PRODUCT = 'MetaMonitor'; const SUPPORTED_MONITOR_PRODUCTS = [ 'VITURE', 'nreal air', 'Air', 'Air 2', 'Air 2 Pro', 'Air 2 Ultra', 'SmartGlasses', // TCL/RayNeo 'Rokid Max', 'Rokid Air', NESTED_MONITOR_PRODUCT ]; class BreezyDesktopExtension { constructor(extensionPath) { this.path = extensionPath; this.settings = ExtensionUtils.getSettings(); // Set/destroyed by enable/disable this._cursor_manager = null; this._monitor_manager = null; this._xr_effect = null; this._overlay = null; this._target_monitor = null; this._is_effect_running = false; this._distance_binding = null; this._distance_connection = null; this._follow_threshold_connection = null; this._widescreen_mode_settings_connection = null; this._widescreen_mode_effect_state_connection = null; this._supported_device_detected_connected = null; this._start_binding = null; this._end_binding = null; this._curved_display_binding = null; this._display_size_binding = null; this._look_ahead_override_binding = null; this._disable_anti_aliasing_binding = null; this._optimal_monitor_config_binding = null; this._headset_as_primary_binding = null; this._actor_added_connection = null; this._actor_removed_connection = null; if (!Globals.logger) { Globals.logger = new Logger({ title: 'breezydesktop', debug: this.settings.get_boolean('debug') }); Globals.logger.logVersion(); } } enable() { Globals.logger.log_debug('BreezyDesktopExtension enable'); try { Globals.extension_dir = this.path; this.settings.bind('debug', Globals.logger, 'debug', Gio.SettingsBindFlags.DEFAULT); this._monitor_manager = new MonitorManager({ use_optimal_monitor_config: this.settings.get_boolean('use-optimal-monitor-config'), headset_as_primary: this.settings.get_boolean('headset-as-primary'), use_highest_refresh_rate: this.settings.get_boolean('use-highest-refresh-rate'), extension_path: this.path }); this._monitor_manager.setChangeHook(this._handle_monitor_change.bind(this)); this._monitor_manager.enable(); this._optimal_monitor_config_binding = this.settings.bind('use-optimal-monitor-config', this._monitor_manager, 'use-optimal-monitor-config', Gio.SettingsBindFlags.DEFAULT); this._headset_as_primary_binding = this.settings.bind('headset-as-primary', this._monitor_manager, 'headset-as-primary', Gio.SettingsBindFlags.DEFAULT); this._setup(); } catch (e) { Globals.logger.log(`ERROR: BreezyDesktopExtension enable ${e.message}\n${e.stack}`); } } _poll_for_ready() { Globals.logger.log_debug('BreezyDesktopExtension _poll_for_ready'); var target_monitor = this._target_monitor; var is_effect_running = this._is_effect_running; this._running_poller_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, (() => { try { if (is_effect_running) { this._running_poller_id = undefined; return GLib.SOURCE_REMOVE; } if (this._check_driver_running() && target_monitor) { // Don't enable the effect yet if monitor updates are needed. // _setup will be triggered again since a !ready result means it will trigger monitor changes, // so we can remove this timeout_add no matter what. if (this._target_monitor_ready(target_monitor)) { Globals.logger.log('Driver is running, supported monitor connected. Enabling XR effect.'); this._effect_enable(); } this._running_poller_id = undefined; return GLib.SOURCE_REMOVE; } else { return GLib.SOURCE_CONTINUE; } } catch (e) { Globals.logger.log(`ERROR: BreezyDesktopExtension _poll_for_ready ${e.message}\n${e.stack}`); this._running_poller_id = undefined; return GLib.SOURCE_REMOVE; } }).bind(this)); } _find_supported_monitor() { try { Globals.logger.log_debug('BreezyDesktopExtension _find_supported_monitor'); const target_monitor = this._monitor_manager.getMonitorPropertiesList()?.find( monitor => SUPPORTED_MONITOR_PRODUCTS.includes(monitor.product) || this.settings.get_string('custom-monitor-product') === monitor.product); if (target_monitor !== undefined) { Globals.logger.log(`Identified supported monitor: ${target_monitor.product} on ${target_monitor.connector}`); return { monitor: this._monitor_manager.getMonitors()[target_monitor.index], connector: target_monitor.connector, refreshRate: target_monitor.refreshRate, is_dummy: target_monitor.product === NESTED_MONITOR_PRODUCT }; } if (this.settings.get_boolean('developer-mode')) { Globals.logger.log_debug('BreezyDesktopExtension _find_supported_monitor - Using dummy monitor'); // allow testing XR devices with just USB, no video needed return { monitor: this._monitor_manager.getMonitors()[0], connector: 'dummy', refreshRate: 60, is_dummy: true }; } Globals.logger.log_debug('BreezyDesktopExtension _find_supported_monitor - No supported monitor found'); return null; } catch (e) { Globals.logger.log(`ERROR: BreezyDesktopExtension _find_supported_monitor ${e.message}\n${e.stack}`); return null; } } // Assumes target_monitor is set, and was returned by _find_supported_monitor. // A false result means we'll expect _handle_monitor_change to be triggered, so active polling // can be disabled. _target_monitor_ready(target_monitor) { if (target_monitor.is_dummy) return true; const needs_sbs_mode_switch = this.settings.get_boolean('fast-sbs-mode-switching') && this._needs_widescreen_monitor_update(); return !needs_sbs_mode_switch && !this._monitor_manager.needsOptimalModeCheck(target_monitor.connector); } _setup() { Globals.logger.log_debug('BreezyDesktopExtension _setup'); if (this._is_effect_running) { Globals.logger.log('Reset triggered, disabling XR effect'); this._effect_disable(true); } const target_monitor = this._find_supported_monitor(); // if target_monitor isn't set, do nothing and wait for MonitorManager to call this again if (target_monitor && this._running_poller_id === undefined) { this._target_monitor = target_monitor; if (this._check_driver_running()) { // Don't enable the effect yet if monitor updates are needed. // _setup will be triggered again since a !ready result means it will trigger monitor changes if (this._target_monitor_ready(target_monitor)) { Globals.logger.log('Ready, enabling XR effect'); this._effect_enable(); } else { Globals.logger.log_debug('BreezyDesktopExtension _setup - driver running but async monitor action needed'); } } else { Globals.logger.log_debug('BreezyDesktopExtension _setup - driver not running, starting poller'); this._poll_for_ready(); } } else { if (!target_monitor) { Globals.logger.log_debug(`BreezyDesktopExtension _setup - Doing nothing, no supported monitor found`); } else { Globals.logger.log_debug(`BreezyDesktopExtension _setup - Doing nothing, target monitor found, waiting for poller to pick it up`); } } } _check_driver_running() { try { if (!Globals.ipc_file) Globals.ipc_file = Gio.file_new_for_path(IPC_FILE_PATH); if (Globals.ipc_file.query_exists(null)) { const file_info = Globals.ipc_file.query_info(Gio.FILE_ATTRIBUTE_TIME_MODIFIED, Gio.FileQueryInfoFlags.NONE, null); const file_modified_time = file_info.get_attribute_uint64(Gio.FILE_ATTRIBUTE_TIME_MODIFIED); // when the driver is running, the IMU file is updated at least 60x per second, do a strict check return isValidKeepAlive(file_modified_time, true); } } catch (e) { Globals.logger.log(`ERROR: BreezyDesktopExtension _check_driver_running ${e.message}\n${e.stack}`); } return false; } _needs_widescreen_monitor_update() { Globals.logger.log_debug('BreezyDesktopExtension _needs_widescreen_monitor_update'); const state = this._read_state(); const sbs_enabled = state['sbs_mode_enabled'] === 'true'; const widescreen_setting_enabled = this.settings.get_boolean('widescreen-mode'); if (widescreen_setting_enabled !== sbs_enabled) { Globals.logger.log_debug('BreezyDesktopExtension _needs_widescreen_monitor_update - true'); this._request_sbs_mode_change(widescreen_setting_enabled); return true; } return false; } _effect_enable() { Globals.logger.log_debug('BreezyDesktopExtension _effect_enable'); this._running_poller_id = undefined; if (!this._is_effect_running) { this._is_effect_running = true; try { const targetMonitor = this._target_monitor.monitor; const refreshRate = targetMonitor.refreshRate ?? 60; this._cursor_manager = new CursorManager(Main.layoutManager.uiGroup, refreshRate); this._cursor_manager.enable(); const overlayContent = new Clutter.Actor({clip_to_allocation: true}); this._overlay = new St.Bin({ child: overlayContent }); this._overlay.set_position(targetMonitor.x, targetMonitor.y); this._overlay.set_size(targetMonitor.width, targetMonitor.height); global.stage.add_child(this._overlay); Shell.util_set_hidden_from_pick(this._overlay, true); this._background = new SystemBackground(); overlayContent.add_child(this._background); const uiClone = new Clutter.Clone({ source: Main.layoutManager.uiGroup, clip_to_allocation: true }); uiClone.x = -targetMonitor.x; uiClone.y = -targetMonitor.y; overlayContent.add_child(uiClone); // In GS 45, use of "actor" was renamed to "child". const clutterContainer = Clutter.Container !== undefined; this._actor_added_connection = global.stage.connect( clutterContainer ? 'actor-added' : 'child-added', this._handle_sibling_update.bind(this), ); this._actor_removed_connection = global.stage.connect( clutterContainer ? 'actor-removed' : 'child-removed', this._handle_sibling_update.bind(this), ); this._xr_effect = new XREffect({ target_monitor: targetMonitor, target_framerate: refreshRate, 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'), look_ahead_override: this.settings.get_int('look-ahead-override'), disable_anti_aliasing: this.settings.get_boolean('disable-anti-aliasing') }); this._update_follow_threshold(this.settings); // this gets triggered before _effect_enable if in fast-sbs-mode-switching mode if (!this.settings.get_boolean('fast-sbs-mode-switching')) this._update_widescreen_mode_from_settings(this.settings); this._widescreen_mode_effect_state_connection = this._xr_effect.connect('notify::widescreen-mode-state', this._update_widescreen_mode_from_state.bind(this)); this._supported_device_detected_connected = this._xr_effect.connect('notify::supported-device-detected', this._handle_supported_device_change.bind(this)); this._distance_binding = this.settings.bind('display-distance', this._xr_effect, 'display-distance', Gio.SettingsBindFlags.DEFAULT) this._distance_connection = this.settings.connect('changed::display-distance', this._update_display_distance.bind(this)) this._follow_threshold_connection = this.settings.connect('changed::follow-threshold', this._update_follow_threshold.bind(this)) this._widescreen_mode_settings_connection = this.settings.connect('changed::widescreen-mode', this._update_widescreen_mode_from_settings.bind(this)) this._start_binding = this.settings.bind('toggle-display-distance-start', this._xr_effect, 'toggle-display-distance-start', Gio.SettingsBindFlags.DEFAULT) this._end_binding = this.settings.bind('toggle-display-distance-end', this._xr_effect, 'toggle-display-distance-end', Gio.SettingsBindFlags.DEFAULT) this._curved_display_binding = this.settings.bind('curved-display', this._xr_effect, 'curved-display', Gio.SettingsBindFlags.DEFAULT) this._display_size_binding = this.settings.bind('display-size', this._xr_effect, 'display-size', Gio.SettingsBindFlags.DEFAULT); this._look_ahead_override_binding = this.settings.bind('look-ahead-override', this._xr_effect, 'look-ahead-override', Gio.SettingsBindFlags.DEFAULT); this._disable_anti_aliasing_binding = this.settings.bind('disable-anti-aliasing', this._xr_effect, 'disable-anti-aliasing', Gio.SettingsBindFlags.DEFAULT); this._overlay.add_effect_with_name('xr-desktop', this._xr_effect); Meta.disable_unredirect_for_display(global.display); 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)); this._add_settings_keybinding('toggle-follow-shortcut', this._toggle_follow_mode.bind(this)); } catch (e) { Globals.logger.log(`ERROR: BreezyDesktopExtension _effect_enable ${e.message}\n${e.stack}`); this._effect_disable(); } } } _handle_sibling_update() { Globals.logger.log_debug('BreezyDesktopExtension _handle_sibling_update()'); global.stage.set_child_above_sibling(this._overlay, null); } _add_settings_keybinding(settings_key, bind_to_function) { try { 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}`, () => { try { // 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 ); } catch (e) { Globals.logger.log(`ERROR: BreezyDesktopExtension _add_settings_keybinding settings binding lambda ${e.message}\n${e.stack}`); } }); } catch (e) { Globals.logger.log(`ERROR: BreezyDesktopExtension _add_settings_keybinding ${e.message}\n${e.stack}`); } } _write_control(key, value) { try { const file = Gio.file_new_for_path('/dev/shm/xr_driver_control'); const stream = file.replace(null, false, Gio.FileCreateFlags.NONE, null); stream.write(`${key}=${value}`, null); stream.close(null); } catch (e) { Globals.logger.log(`ERROR: BreezyDesktopExtension _write_control ${e.message}\n${e.stack}`); } } _read_state() { const state = {}; try { const file = Gio.file_new_for_path('/dev/shm/xr_driver_state'); if (file.query_exists(null)) { const data = file.load_contents(null); if (data[0]) { const bytes = new Uint8Array(data[1]); const decoder = new TextDecoder(); const contents = decoder.decode(bytes); const lines = contents.split('\n'); for (const line of lines) { const [k, v] = line.split('='); state[k] = v; } } } } catch (e) { Globals.logger.log(`ERROR: BreezyDesktopExtension _read_state ${e.message}\n${e.stack}`); } return state; } _update_display_distance(settings, event) { const value = settings.get_double('display-distance'); Globals.logger.log_debug(`BreezyDesktopExtension _update_display_distance ${value}`); if (value !== undefined) this._write_control('breezy_desktop_display_distance', value); } _update_follow_threshold(settings, event) { const value = settings.get_double('follow-threshold'); Globals.logger.log_debug(`BreezyDesktopExtension _update_follow_threshold ${value}`); if (value !== undefined) this._write_control('breezy_desktop_follow_threshold', value); } // requests sbs_mode change and monitors to ensure the state reflects the setting _request_sbs_mode_change(value) { Globals.logger.log_debug(`BreezyDesktopExtension _request_sbs_mode_change ${value}`); this._write_control('sbs_mode', value ? 'enable' : 'disable'); if (!this._sbs_mode_update_timeout) { var attempts = 0; this._sbs_mode_update_timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 3000, (() => { const state = this._read_state(); const sbs_enabled = state['sbs_mode_enabled'] === 'true'; if (sbs_enabled === value) { Globals.logger.log_debug('BreezyDesktopExtension _request_sbs_mode_change - successfully updated'); this._sbs_mode_update_timeout = undefined; if (this.settings.get_boolean('fast-sbs-mode-switching')) { // setup and polling were halted if this is enabled, so we have to re-trigger setup this._setup(); } return GLib.SOURCE_REMOVE; } if (attempts++ < 3) { this._write_control('sbs_mode', value ? 'enable' : 'disable'); return GLib.SOURCE_CONTINUE; } // the state never updated to reflect our request, revert the setting Globals.logger.log('Failed to update sbs_mode state, reverting setting'); this.settings.set_boolean('widescreen-mode', !value); this._sbs_mode_update_timeout = undefined; return GLib.SOURCE_REMOVE; }).bind(this)); } } _update_widescreen_mode_from_settings(settings, event) { const value = settings.get_boolean('widescreen-mode'); Globals.logger.log_debug(`BreezyDesktopExtension _update_widescreen_mode_from_settings ${value}`); if (value !== undefined && value !== this._xr_effect.widescreen_mode_state) { this._request_sbs_mode_change(value); } else Globals.logger.log_debug('effect.widescreen_mode_state already matched setting'); } _update_widescreen_mode_from_state(effect, _pspec) { // kill our state checker if it's running if (this._sbs_mode_update_timeout) { Globals.logger.log_debug('BreezyDesktopExtension _update_widescreen_mode_from_state - clearing timeout'); GLib.source_remove(this._sbs_mode_update_timeout); this._sbs_mode_update_timeout = undefined; } const value = effect.widescreen_mode_state; Globals.logger.log_debug(`BreezyDesktopExtension _update_widescreen_mode_from_state ${value}`); if (value !== this.settings.get_boolean('widescreen-mode')) this.settings.set_boolean('widescreen-mode', value); else Globals.logger.log_debug('settings.widescreen-mode already matched state'); } _handle_monitor_change() { Globals.logger.log('Monitor change detected'); this._setup(); } _handle_supported_device_change(effect, _pspec) { const value = effect.supported_device_detected; Globals.logger.log_debug(`BreezyDesktopExtension _handle_supported_device_change ${value}`); // this will disable the effect and begin polling for a ready state again if (!value && this._is_effect_running) { Globals.logger.log('Supported device disconnected'); this._setup(); } } _recenter_display() { Globals.logger.log_debug('BreezyDesktopExtension _recenter_display'); this._write_control('recenter_screen', 'true'); } _toggle_follow_mode() { Globals.logger.log_debug('BreezyDesktopExtension _toggle_follow_mode'); this._write_control('toggle_breezy_desktop_smooth_follow', 'true'); } // for_setup should be true if our intention is to immediately re-enable the extension _effect_disable(for_setup = false) { try { Globals.logger.log_debug('BreezyDesktopExtension _effect_disable'); this._is_effect_running = false; if (this._running_poller_id) { const poller_id = this._running_poller_id; this._running_poller_id = undefined; GLib.source_remove(poller_id); } Main.wm.removeKeybinding('recenter-display-shortcut'); Main.wm.removeKeybinding('toggle-display-distance-shortcut'); Main.wm.removeKeybinding('toggle-follow-shortcut'); Meta.enable_unredirect_for_display(global.display); if (this._actor_added_connection) { global.stage.disconnect(this._actor_added_connection); this._actor_added_connection = null; } if (this._actor_removed_connection) { global.stage.disconnect(this._actor_removed_connection); this._actor_removed_connection = null; } if (this._overlay) { if (this._xr_effect) this._xr_effect.cleanup(); this._overlay.remove_effect_by_name('xr-desktop'); global.stage.remove_child(this._overlay); this._overlay.destroy(); this._overlay = null; } if (this._distance_binding) { this.settings.unbind(this._distance_binding); this._distance_binding = null; } if (this._distance_connection) { this.settings.disconnect(this._distance_connection); this._distance_connection = null; } if (this._follow_threshold_connection) { this.settings.disconnect(this._follow_threshold_connection); this._follow_threshold_connection = null; } if (this._widescreen_mode_settings_connection) { this.settings.disconnect(this._widescreen_mode_settings_connection); this._widescreen_mode_settings_connection = null; } if (this._start_binding) { this.settings.unbind(this._start_binding); this._start_binding = null; } if (this._end_binding) { this.settings.unbind(this._end_binding); this._end_binding = null; } if (this._curved_display_binding) { this.settings.unbind(this._curved_display_binding); this._curved_display_binding = null; } if (this._display_size_binding) { this.settings.unbind(this._display_size_binding); this._display_size_binding = null; } if (this._look_ahead_override_binding) { this.settings.unbind(this._look_ahead_override_binding); this._look_ahead_override_binding = null; } if (this._disable_anti_aliasing_binding) { this.settings.unbind(this._disable_anti_aliasing_binding); this._disable_anti_aliasing_binding = null; } if (this._xr_effect) { if (this._widescreen_mode_effect_state_connection) { this._xr_effect.disconnect(this._widescreen_mode_effect_state_connection); this._widescreen_mode_effect_state_connection = null; } if (this._supported_device_detected_connected) { this._xr_effect.disconnect(this._supported_device_detected_connected); this._supported_device_detected_connected = null; } this._xr_effect = null; } if (this._cursor_manager) { this._cursor_manager.disable(); this._cursor_manager = null; } // this should always be done at the end of this function after the widescreen settings binding is removed, // so it doesn't reset the setting to false if (!for_setup && this.settings.get_boolean('widescreen-mode')) { Globals.logger.log('Disabling SBS mode due to disabling effect'); this._write_control('sbs_mode', 'disable'); } } catch (e) { Globals.logger.log(`ERROR: BreezyDesktopExtension _effect_disable ${e.message}\n${e.stack}`); } } disable() { try { Globals.logger.log_debug('BreezyDesktopExtension disable'); this._effect_disable(); this._target_monitor = null; if (this._monitor_manager) { if (this._optimal_monitor_config_binding) { this.settings.unbind(this._optimal_monitor_config_binding); this._optimal_monitor_config_binding = null } if (this._headset_as_primary_binding) { this.settings.unbind(this._headset_as_primary_binding); this._headset_as_primary_binding = null; } this._monitor_manager.disable(); this._monitor_manager = null; } } catch (e) { Globals.logger.log(`ERROR: BreezyDesktopExtension disable ${e.message}\n${e.stack}`); } } } function init(meta) { return new BreezyDesktopExtension(meta.path); }