From 9698f9be7cf05aeea2b77591b55d8a95f76055f8 Mon Sep 17 00:00:00 2001 From: Jas Singh Date: Sun, 1 Jun 2025 18:23:36 -0700 Subject: [PATCH] Improve monitor reconnect/disconnect logic. (#974) * WIP: Improve monitor reconnect logic * Organize revealer into a controller and clean up bindings of destroyed bars. * Improve monitor disconnect/reconnect logic. * Add JSDoc --- app.ts | 75 +------ src/components/bar/index.tsx | 17 +- src/components/bar/layout/BarLayout.tsx | 25 ++- src/components/bar/utils/monitors/index.ts | 55 ++++-- src/components/bar/utils/monitors/types.ts | 4 + .../notifications/Notification/index.tsx | 14 +- src/components/notifications/helpers.ts | 12 +- src/components/notifications/index.tsx | 6 +- src/components/osd/bar/helpers.ts | 15 +- src/components/osd/helpers.ts | 105 ++++------ src/components/osd/icon/helpers.ts | 15 +- src/components/osd/index.tsx | 12 +- src/components/osd/label/helpers.ts | 15 +- src/components/osd/revealer/index.tsx | 4 +- .../osd/revealer/revealerController.ts | 117 +++++++++++ src/components/osd/types.ts | 3 + src/core/initialization/index.ts | 99 ++++++++++ src/lib/performance/timer.ts | 70 +++++++ src/services/display/bar/autoHide.ts | 49 +++-- src/services/display/bar/refreshManager.ts | 117 +++++++++++ src/services/display/monitor/index.ts | 184 ++++++++---------- src/services/network/index.ts | 26 +-- 22 files changed, 706 insertions(+), 333 deletions(-) create mode 100644 src/components/bar/utils/monitors/types.ts create mode 100644 src/components/osd/revealer/revealerController.ts create mode 100644 src/components/osd/types.ts create mode 100644 src/core/initialization/index.ts create mode 100644 src/lib/performance/timer.ts create mode 100644 src/services/display/bar/refreshManager.ts diff --git a/app.ts b/app.ts index aa9ea1d..cbd9126 100644 --- a/app.ts +++ b/app.ts @@ -1,81 +1,12 @@ import './src/lib/session'; import './src/style'; import 'src/core/behaviors/bar'; -import AstalHyprland from 'gi://AstalHyprland?version=0.1'; -import { Bar } from './src/components/bar'; -import Notifications from './src/components/notifications'; -import SettingsDialog from './src/components/settings/index'; -import OSD from 'src/components/osd/index'; import { App } from 'astal/gtk3'; -import { execAsync } from 'astal'; -import { handleRealization } from 'src/components/menus/shared/dropdown/helpers/helpers'; -import { isDropdownMenu } from 'src/components/settings/constants.js'; -import { initializeSystemBehaviors } from 'src/core/behaviors'; import { runCLI } from 'src/services/cli/commander'; -import { DropdownMenus, StandardWindows } from 'src/components/menus'; -import { forMonitors } from 'src/components/bar/utils/monitors'; -import options from 'src/configuration'; -import { SystemUtilities } from 'src/core/system/SystemUtilities'; - -const hyprland = AstalHyprland.get_default(); -const initializeStartupScripts = (): void => { - execAsync(`python3 ${SRC_DIR}/scripts/bluetooth.py`).catch((err) => - console.error('Failed to initialize bluetooth script:', err), - ); -}; - -const initializeMenus = (): void => { - StandardWindows.forEach((window) => { - return window(); - }); - - DropdownMenus.forEach((window) => { - return window(); - }); - - DropdownMenus.forEach((window) => { - const windowName = window.name - .replace(/_default.*/, '') - .concat('menu') - .toLowerCase(); - - if (!isDropdownMenu(windowName)) { - return; - } - - handleRealization(windowName); - }); -}; +import { InitializationService } from 'src/core/initialization'; App.start({ instanceName: 'hyprpanel', - requestHandler(request: string, res: (response: unknown) => void) { - runCLI(request, res); - }, - async main() { - try { - initializeStartupScripts(); - - Notifications(); - OSD(); - - const barsForMonitors = await forMonitors(Bar); - barsForMonitors.forEach((bar: JSX.Element) => bar); - - SettingsDialog(); - initializeMenus(); - - initializeSystemBehaviors(); - } catch (error) { - console.error('Error during application initialization:', error); - } - }, -}); - -hyprland.connect('monitor-added', () => { - const { restartCommand } = options.hyprpanel; - - if (options.hyprpanel.restartAgs.get()) { - SystemUtilities.bash(restartCommand.get()); - } + requestHandler: (request: string, res: (response: unknown) => void) => runCLI(request, res), + main: () => InitializationService.initialize(), }); diff --git a/src/components/bar/index.tsx b/src/components/bar/index.tsx index bfcc164..a2fb01c 100644 --- a/src/components/bar/index.tsx +++ b/src/components/bar/index.tsx @@ -1,19 +1,24 @@ -import { GdkMonitorService } from 'src/services/display/monitor'; +import { JSXElement } from 'src/core/types'; import { BarLayout } from './layout/BarLayout'; import { getCoreWidgets } from './layout/coreWidgets'; import { WidgetRegistry } from './layout/WidgetRegistry'; -const gdkMonitorService = new GdkMonitorService(); const widgetRegistry = new WidgetRegistry(getCoreWidgets()); /** - * Factory function to create a Bar for a specific monitor + * Creates a bar widget for a specific monitor with proper error handling + * to prevent crashes when monitors become invalid. + * + * @param gdkMonitor - The GDK monitor index where the bar will be displayed + * @param hyprlandMonitor - The corresponding Hyprland monitor ID for workspace + * filtering and layout assignment + * @returns A JSX element representing the bar widget for the specified monitor */ -export const Bar = async (monitor: number): Promise => { +export const Bar = async (gdkMonitor: number, hyprlandMonitor?: number): Promise => { await widgetRegistry.initialize(); - const hyprlandMonitor = gdkMonitorService.mapGdkToHyprland(monitor); - const barLayout = new BarLayout(monitor, hyprlandMonitor, widgetRegistry); + const hyprlandId = hyprlandMonitor ?? gdkMonitor; + const barLayout = new BarLayout(gdkMonitor, hyprlandId, widgetRegistry); return barLayout.render(); }; diff --git a/src/components/bar/layout/BarLayout.tsx b/src/components/bar/layout/BarLayout.tsx index 814fe2e..9b1c17c 100644 --- a/src/components/bar/layout/BarLayout.tsx +++ b/src/components/bar/layout/BarLayout.tsx @@ -1,10 +1,11 @@ -import { App, Gtk } from 'astal/gtk3'; +import { App, Gdk, Gtk } from 'astal/gtk3'; import Astal from 'gi://Astal?version=3.0'; import { bind, Binding, Variable } from 'astal'; import { idleInhibit } from 'src/lib/window/visibility'; import { WidgetRegistry } from './WidgetRegistry'; import { getLayoutForMonitor, isLayoutEmpty } from '../utils/monitors'; import options from 'src/configuration'; +import { JSXElement } from 'src/core/types'; /** * Responsible for the bar UI layout and positioning @@ -46,7 +47,27 @@ export class BarLayout { this._initializeReactiveVariables(); } - public render(): JSX.Element { + public render(): JSXElement { + const display = Gdk.Display.get_default(); + if (!display) { + console.error('[BarLayout] No display available for bar creation'); + return null; + } + + const monitorCount = display.get_n_monitors(); + if (this._gdkMonitor < 0 || this._gdkMonitor >= monitorCount) { + console.error( + `[BarLayout] Invalid monitor index: ${this._gdkMonitor} (total monitors: ${monitorCount})`, + ); + return null; + } + + const monitor = display.get_monitor(this._gdkMonitor); + if (monitor === null) { + console.error(`[BarLayout] Monitor at index ${this._gdkMonitor} no longer exists`); + return null; + } + return ( { }; /** - * Generates an array of JSX elements for each monitor. + * Creates widgets for all available monitors with proper GDK to Hyprland monitor mapping. * - * This function creates an array of JSX elements by calling the provided widget function for each monitor. - * It uses the number of monitors available in the default Gdk display. - * - * @param widget A function that takes a monitor index and returns a JSX element. - * - * @returns An array of JSX elements, one for each monitor. + * @param widget - Function that creates a widget for a given monitor index + * @returns Array of created widgets for all available monitors */ -export async function forMonitors(widget: (monitor: number) => Promise): Promise { - const n = Gdk.Display.get_default()?.get_n_monitors() ?? 1; +export async function forMonitors( + widget: (monitor: number, hyprlandMonitor?: number) => Promise, +): Promise { + const display = Gdk.Display.get_default(); + if (display === null) { + console.error('[forMonitors] No display available'); + return []; + } - return Promise.all(range(n, 0).map(widget)); + const monitorCount = display.get_n_monitors(); + const gdkMonitorService = GdkMonitorService.getInstance(); + const monitorMappings: MonitorMapping[] = []; + + for (let gdkMonitorIndex = 0; gdkMonitorIndex < monitorCount; gdkMonitorIndex++) { + const monitor = display.get_monitor(gdkMonitorIndex); + if (monitor === null) { + console.warn(`[forMonitors] Skipping invalid monitor at index ${gdkMonitorIndex}`); + continue; + } + + const hyprlandId = gdkMonitorService.mapGdkToHyprland(gdkMonitorIndex); + + monitorMappings.push({ + gdkIndex: gdkMonitorIndex, + hyprlandId, + }); + } + + const monitorPromises = monitorMappings.map(async ({ gdkIndex, hyprlandId }) => { + try { + return await widget(gdkIndex, hyprlandId); + } catch (error) { + console.error(`[forMonitors] Failed to create widget for monitor ${gdkIndex}:`, error); + return null; + } + }); + const widgets = await Promise.all(monitorPromises); + + return widgets.filter((w): w is JSXElement => w !== null); } diff --git a/src/components/bar/utils/monitors/types.ts b/src/components/bar/utils/monitors/types.ts new file mode 100644 index 0000000..13ce8e9 --- /dev/null +++ b/src/components/bar/utils/monitors/types.ts @@ -0,0 +1,4 @@ +export interface MonitorMapping { + gdkIndex: number; + hyprlandId: number; +} diff --git a/src/components/notifications/Notification/index.tsx b/src/components/notifications/Notification/index.tsx index 6764bdc..d7d23b5 100644 --- a/src/components/notifications/Notification/index.tsx +++ b/src/components/notifications/Notification/index.tsx @@ -27,9 +27,13 @@ export const NotificationCard = ({ showActions, ...props }: NotificationCardProps): JSX.Element => { - const actionBox: IActionBox | null = notification.get_actions().length ? ( - - ) : null; + let actionBox: ActionBox | null; + + if (notification.get_actions().length) { + actionBox = ; + } else { + actionBox = null; + } return ( }; /** - * Tracks the active monitor and updates the provided variable. + * Tracks the currently focused monitor and updates the provided variable with its ID. + * Includes null safety to prevent crashes when monitors are disconnected or during DPMS events. * - * This function sets up a derived variable that updates the `curMonitor` variable with the ID of the focused monitor. - * - * @param curMonitor The variable to update with the active monitor ID. + * @param curMonitor - Variable that will be updated with the current monitor ID (defaults to 0 if no monitor is focused) */ export const trackActiveMonitor = (curMonitor: Variable): void => { Variable.derive([bind(hyprlandService, 'focusedMonitor')], (monitor) => { + if (monitor?.id === undefined) { + console.warn('No focused monitor available, defaulting to monitor 0'); + curMonitor.set(0); + return; + } curMonitor.set(monitor.id); }); }; diff --git a/src/components/notifications/index.tsx b/src/components/notifications/index.tsx index 2742892..1416b03 100644 --- a/src/components/notifications/index.tsx +++ b/src/components/notifications/index.tsx @@ -7,6 +7,7 @@ import AstalHyprland from 'gi://AstalHyprland?version=0.1'; import { GdkMonitorService } from 'src/services/display/monitor/index.js'; import { getPosition } from 'src/lib/window/positioning.js'; import { NotificationCard } from './Notification'; +import { App } from 'astal/gtk3'; const hyprlandService = AstalHyprland.get_default(); const { position, monitor, active_monitor, showActionsOnHover, displayedTotal } = options.notifications; @@ -20,15 +21,13 @@ trackPopupNotifications(popupNotifications); trackAutoTimeout(); export default (): JSX.Element => { - const gdkMonitorMapper = new GdkMonitorService(); + const gdkMonitorMapper = GdkMonitorService.getInstance(); const windowLayer = bind(tear).as((tear) => (tear ? Astal.Layer.TOP : Astal.Layer.OVERLAY)); const windowAnchor = bind(position).as(getPosition); const windowMonitor = Variable.derive( [bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)], (focusedMonitor, monitor, activeMonitor) => { - gdkMonitorMapper.reset(); - if (activeMonitor === true) { const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(focusedMonitor.id); return gdkMonitor; @@ -55,6 +54,7 @@ export default (): JSX.Element => { name={'notifications-window'} namespace={'notifications-window'} className={'notifications-window'} + application={App} layer={windowLayer} anchor={windowAnchor} exclusivity={Astal.Exclusivity.NORMAL} diff --git a/src/components/osd/bar/helpers.ts b/src/components/osd/bar/helpers.ts index 838932c..cd7bc8b 100644 --- a/src/components/osd/bar/helpers.ts +++ b/src/components/osd/bar/helpers.ts @@ -29,7 +29,7 @@ export const setupOsdBar = (self: LevelBar): void => { self.value = brightnessService.kbd; }); - Variable.derive([bind(audioService.defaultMicrophone, 'volume')], () => { + const micVolumeBinding = Variable.derive([bind(audioService.defaultMicrophone, 'volume')], () => { self.toggleClassName('overflow', audioService.defaultMicrophone.volume > 1); self.value = audioService.defaultMicrophone.volume <= 1 @@ -37,7 +37,7 @@ export const setupOsdBar = (self: LevelBar): void => { : audioService.defaultMicrophone.volume - 1; }); - Variable.derive([bind(audioService.defaultMicrophone, 'mute')], () => { + const micMuteBinding = Variable.derive([bind(audioService.defaultMicrophone, 'mute')], () => { self.toggleClassName( 'overflow', audioService.defaultMicrophone.volume > 1 && @@ -51,7 +51,7 @@ export const setupOsdBar = (self: LevelBar): void => { : audioService.defaultMicrophone.volume - 1; }); - Variable.derive([bind(audioService.defaultSpeaker, 'volume')], () => { + const speakerVolumeBinding = Variable.derive([bind(audioService.defaultSpeaker, 'volume')], () => { self.toggleClassName('overflow', audioService.defaultSpeaker.volume > 1); self.value = audioService.defaultSpeaker.volume <= 1 @@ -59,7 +59,7 @@ export const setupOsdBar = (self: LevelBar): void => { : audioService.defaultSpeaker.volume - 1; }); - Variable.derive([bind(audioService.defaultSpeaker, 'mute')], () => { + const speakerMuteBinding = Variable.derive([bind(audioService.defaultSpeaker, 'mute')], () => { self.toggleClassName( 'overflow', audioService.defaultSpeaker.volume > 1 && @@ -72,4 +72,11 @@ export const setupOsdBar = (self: LevelBar): void => { ? audioService.defaultSpeaker.volume : audioService.defaultSpeaker.volume - 1; }); + + self.connect('destroy', () => { + micVolumeBinding.drop(); + micMuteBinding.drop(); + speakerVolumeBinding.drop(); + speakerMuteBinding.drop(); + }); }; diff --git a/src/components/osd/helpers.ts b/src/components/osd/helpers.ts index 5c90c7a..fd3f7fc 100644 --- a/src/components/osd/helpers.ts +++ b/src/components/osd/helpers.ts @@ -1,79 +1,50 @@ -import { bind, timeout, Variable } from 'astal'; +import { bind, Variable } from 'astal'; import { Widget } from 'astal/gtk3'; import AstalHyprland from 'gi://AstalHyprland?version=0.1'; import AstalWp from 'gi://AstalWp?version=0.1'; import options from 'src/configuration'; import { GdkMonitorService } from 'src/services/display/monitor'; import BrightnessService from 'src/services/system/brightness'; +import { OsdRevealerController } from './revealer/revealerController'; const wireplumber = AstalWp.get_default() as AstalWp.Wp; const audioService = wireplumber.audio; const brightnessService = BrightnessService.getInstance(); const hyprlandService = AstalHyprland.get_default(); -const { enable, duration, active_monitor, monitor } = options.theme.osd; +const { enable, active_monitor, monitor } = options.theme.osd; -let count = 0; - -/* - * So the OSD doesn't show on startup for no reason - */ -let isStartingUp = true; -timeout(3000, () => { - isStartingUp = false; -}); +const osdController = OsdRevealerController.getInstance(); /** - * Handles the reveal state of a Widget.Revealer or Widget.Window. + * Determines which monitor the OSD should appear on based on user configuration. + * Safely handles null monitors and DPMS events to prevent crashes. * - * This function delegates the reveal handling to either `handleRevealRevealer` or `handleRevealWindow` based on the type of the widget. - * - * @param self The Widget.Revealer or Widget.Window instance. - * @param property The property to check, either 'revealChild' or 'visible'. - */ -const handleReveal = (self: Widget.Revealer): void => { - if (isStartingUp) { - return; - } - - if (!enable.get()) { - return; - } - - self.reveal_child = true; - - count++; - timeout(duration.get(), () => { - count--; - - if (count === 0) { - self.reveal_child = false; - } - }); -}; - -/** - * Retrieves the monitor index for the OSD. - * - * This function derives the monitor index for the OSD based on the focused monitor, default monitor, and active monitor settings. - * - * @returns A Variable representing the monitor index for the OSD. + * @returns Variable containing the GDK monitor index where OSD should be displayed (defaults to 0 if no valid monitor) */ export const getOsdMonitor = (): Variable => { - const gdkMonitorMapper = new GdkMonitorService(); + const gdkMonitorMapper = GdkMonitorService.getInstance(); return Variable.derive( [bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)], (currentMonitor, defaultMonitor, followMonitor) => { - gdkMonitorMapper.reset(); + try { + if (followMonitor === false) { + const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(defaultMonitor); + return gdkMonitor; + } + + if (!currentMonitor || currentMonitor.id === undefined || currentMonitor.id === null) { + console.warn('OSD: No focused monitor available, defaulting to monitor 0'); + return 0; + } - if (followMonitor === true) { const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(currentMonitor.id); return gdkMonitor; + } catch (error) { + console.error('OSD: Failed to map monitor, defaulting to 0:', error); + return 0; } - - const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(defaultMonitor); - return gdkMonitor; }, ); }; @@ -86,29 +57,29 @@ export const getOsdMonitor = (): Variable => { * @param self The Widget.Revealer instance to set up. */ export const revealerSetup = (self: Widget.Revealer): void => { - self.hook(enable, () => { - handleReveal(self); - }); + osdController.setRevealer(self); - self.hook(brightnessService, 'notify::screen', () => { - handleReveal(self); - }); + const handleReveal = (): void => { + osdController.show(); + }; - self.hook(brightnessService, 'notify::kbd', () => { - handleReveal(self); - }); + self.hook(enable, handleReveal); + self.hook(brightnessService, 'notify::screen', handleReveal); + self.hook(brightnessService, 'notify::kbd', handleReveal); - Variable.derive( + const microphoneBinding = Variable.derive( [bind(audioService.defaultMicrophone, 'volume'), bind(audioService.defaultMicrophone, 'mute')], - () => { - handleReveal(self); - }, + handleReveal, ); - Variable.derive( + const speakerBinding = Variable.derive( [bind(audioService.defaultSpeaker, 'volume'), bind(audioService.defaultSpeaker, 'mute')], - () => { - handleReveal(self); - }, + handleReveal, ); + + self.connect('destroy', () => { + microphoneBinding.drop(); + speakerBinding.drop(); + osdController.onRevealerDestroy(self); + }); }; diff --git a/src/components/osd/icon/helpers.ts b/src/components/osd/icon/helpers.ts index 3a69caa..197f52e 100644 --- a/src/components/osd/icon/helpers.ts +++ b/src/components/osd/icon/helpers.ts @@ -7,11 +7,6 @@ const wireplumber = AstalWp.get_default() as AstalWp.Wp; const audioService = wireplumber.audio; const brightnessService = BrightnessService.getInstance(); -type OSDIcon = { - micVariable: Variable; - speakerVariable: Variable; -}; - /** * Sets up the OSD icon for a given widget. * @@ -22,7 +17,7 @@ type OSDIcon = { * * @returns An object containing the micVariable and speakerVariable, which are derived variables for microphone and speaker status. */ -export const setupOsdIcon = (self: Widget.Label): OSDIcon => { +export const setupOsdIcon = (self: Widget.Label): void => { self.hook(brightnessService, 'notify::screen', () => { self.label = '󱍖'; }); @@ -45,8 +40,8 @@ export const setupOsdIcon = (self: Widget.Label): OSDIcon => { }, ); - return { - micVariable, - speakerVariable, - }; + self.connect('destroy', () => { + micVariable.drop(); + speakerVariable.drop(); + }); }; diff --git a/src/components/osd/index.tsx b/src/components/osd/index.tsx index 9c0cd34..78b77f4 100644 --- a/src/components/osd/index.tsx +++ b/src/components/osd/index.tsx @@ -1,6 +1,6 @@ import options from 'src/configuration'; import { bind } from 'astal'; -import { Astal } from 'astal/gtk3'; +import { App, Astal } from 'astal/gtk3'; import { getOsdMonitor } from './helpers'; import { getPosition } from 'src/lib/window/positioning'; import { OsdRevealer } from './revealer'; @@ -8,20 +8,26 @@ import { OsdRevealer } from './revealer'; const { location } = options.theme.osd; export default (): JSX.Element => { + const osdMonitorBinding = getOsdMonitor(); + return ( (tear ? Astal.Layer.TOP : Astal.Layer.OVERLAY))} anchor={bind(location).as((anchorPoint) => getPosition(anchorPoint))} setup={(self) => { - getOsdMonitor().subscribe(() => { + osdMonitorBinding().subscribe(() => { self.set_click_through(true); }); }} + onDestroy={() => { + osdMonitorBinding.drop(); + }} clickThrough > diff --git a/src/components/osd/label/helpers.ts b/src/components/osd/label/helpers.ts index a4d4e75..9885a8c 100644 --- a/src/components/osd/label/helpers.ts +++ b/src/components/osd/label/helpers.ts @@ -27,12 +27,12 @@ export const setupOsdLabel = (self: Widget.Label): void => { self.label = `${Math.round(brightnessService.kbd * 100)}`; }); - Variable.derive([bind(audioService.defaultMicrophone, 'volume')], () => { + const micVolumeBinding = Variable.derive([bind(audioService.defaultMicrophone, 'volume')], () => { self.toggleClassName('overflow', audioService.defaultMicrophone.volume > 1); self.label = `${Math.round(audioService.defaultMicrophone.volume * 100)}`; }); - Variable.derive([bind(audioService.defaultMicrophone, 'mute')], () => { + const micMuteBinding = Variable.derive([bind(audioService.defaultMicrophone, 'mute')], () => { self.toggleClassName( 'overflow', audioService.defaultMicrophone.volume > 1 && @@ -45,12 +45,12 @@ export const setupOsdLabel = (self: Widget.Label): void => { self.label = `${inputVolume}`; }); - Variable.derive([bind(audioService.defaultSpeaker, 'volume')], () => { + const speakerVolumeBinding = Variable.derive([bind(audioService.defaultSpeaker, 'volume')], () => { self.toggleClassName('overflow', audioService.defaultSpeaker.volume > 1); self.label = `${Math.round(audioService.defaultSpeaker.volume * 100)}`; }); - Variable.derive([bind(audioService.defaultSpeaker, 'mute')], () => { + const speakerMuteBinding = Variable.derive([bind(audioService.defaultSpeaker, 'mute')], () => { self.toggleClassName( 'overflow', audioService.defaultSpeaker.volume > 1 && @@ -62,4 +62,11 @@ export const setupOsdLabel = (self: Widget.Label): void => { : Math.round(audioService.defaultSpeaker.volume * 100); self.label = `${speakerVolume}`; }); + + self.connect('destroy', () => { + micVolumeBinding.drop(); + micMuteBinding.drop(); + speakerVolumeBinding.drop(); + speakerMuteBinding.drop(); + }); }; diff --git a/src/components/osd/revealer/index.tsx b/src/components/osd/revealer/index.tsx index 55ea4cc..c88d969 100644 --- a/src/components/osd/revealer/index.tsx +++ b/src/components/osd/revealer/index.tsx @@ -32,7 +32,9 @@ export const OsdRevealer = (): JSX.Element => { { + revealerSetup(self); + }} > {bind(orientation).as((currentOrientation) => { diff --git a/src/components/osd/revealer/revealerController.ts b/src/components/osd/revealer/revealerController.ts new file mode 100644 index 0000000..5166658 --- /dev/null +++ b/src/components/osd/revealer/revealerController.ts @@ -0,0 +1,117 @@ +import { timeout } from 'astal'; +import { Widget } from 'astal/gtk3'; +import AstalIO from 'gi://AstalIO?version=0.1'; +import options from 'src/configuration'; + +const { enable, duration } = options.theme.osd; + +/** + * Manages OSD revealer instances to prevent stale references and ensure proper cleanup + */ +export class OsdRevealerController { + private static _instance: OsdRevealerController; + + private _currentRevealer?: Widget.Revealer; + private _autoHideTimeout?: AstalIO.Time; + private _startupTimeout?: AstalIO.Time; + private _allowReveal = false; + + private constructor() { + this._startupTimeout = timeout(3000, () => { + this._allowReveal = true; + this._startupTimeout = undefined; + }); + } + + /** + * Gets the singleton instance of the OSD revealer controller + */ + public static getInstance(): OsdRevealerController { + if (this._instance === undefined) { + this._instance = new OsdRevealerController(); + } + + return this._instance; + } + + /** + * Registers a revealer widget as the active OSD display component + * Ensures proper cleanup of previous revealers before setting a new one + * + * @param revealer - The revealer widget to manage + */ + public setRevealer(revealer: Widget.Revealer): void { + if (this._currentRevealer && this._currentRevealer !== revealer) { + this._cleanup(); + } + + this._currentRevealer = revealer; + revealer.set_reveal_child(false); + } + + /** + * Reveals the OSD temporarily and sets up auto-hide behavior + * Respects enable state and startup delay before allowing reveal + */ + public show(): void { + const enableRevealer = enable.get(); + if (!this._allowReveal || this._currentRevealer === undefined || !enableRevealer) { + return; + } + + this._currentRevealer.set_reveal_child(true); + + if (this._autoHideTimeout !== undefined) { + this._autoHideTimeout.cancel(); + this._autoHideTimeout = undefined; + } + + const hideDelay = duration.get(); + const revealer = this._currentRevealer; + + this._autoHideTimeout = timeout(hideDelay, () => { + if (revealer !== undefined) { + revealer.set_reveal_child(false); + } + + this._autoHideTimeout = undefined; + }); + } + + /** + * Cancels any active auto-hide timeout to prevent stale callbacks + */ + private _cleanup(): void { + if (this._autoHideTimeout) { + this._autoHideTimeout.cancel(); + this._autoHideTimeout = undefined; + } + } + + /** + * Handles cleanup when a revealer widget is destroyed + * Ensures the controller doesn't hold references to destroyed widgets + * + * @param revealer - The revealer being destroyed + */ + public onRevealerDestroy(revealer: Widget.Revealer): void { + if (this._currentRevealer === revealer) { + this._cleanup(); + this._currentRevealer = undefined; + } + } + + /** + * Performs complete cleanup of the controller instance + * Cancels all active timeouts and clears widget references + */ + public destroy(): void { + this._cleanup(); + + if (this._startupTimeout) { + this._startupTimeout.cancel(); + this._startupTimeout = undefined; + } + this._currentRevealer = undefined; + } +} diff --git a/src/components/osd/types.ts b/src/components/osd/types.ts new file mode 100644 index 0000000..d84b3ef --- /dev/null +++ b/src/components/osd/types.ts @@ -0,0 +1,3 @@ +export interface RevealerSetupBindings { + cleanup: () => void; +} diff --git a/src/core/initialization/index.ts b/src/core/initialization/index.ts new file mode 100644 index 0000000..7b0d830 --- /dev/null +++ b/src/core/initialization/index.ts @@ -0,0 +1,99 @@ +import { execAsync } from 'astal'; +import { Bar } from 'src/components/bar'; +import Notifications from 'src/components/notifications'; +import SettingsDialog from 'src/components/settings/index'; +import OSD from 'src/components/osd/index'; +import { handleRealization } from 'src/components/menus/shared/dropdown/helpers/helpers'; +import { isDropdownMenu } from 'src/components/settings/constants.js'; +import { initializeSystemBehaviors } from 'src/core/behaviors'; +import { DropdownMenus, StandardWindows } from 'src/components/menus'; +import { forMonitors } from 'src/components/bar/utils/monitors'; +import { BarRefreshManager } from 'src/services/display/bar/refreshManager'; +import AstalHyprland from 'gi://AstalHyprland?version=0.1'; +import { Timer } from 'src/lib/performance/timer'; +import { JSXElement } from 'src/core/types'; + +/** + * Manages the complete initialization sequence for HyprPanel. + * Coordinates startup scripts, component initialization, and system behaviors. + */ +export class InitializationService { + /** + * Performs the complete application initialization sequence + */ + public static async initialize(): Promise { + try { + const overallTimer = new Timer('HyprPanel initialization'); + + await Timer.measureAsync('Startup scripts', () => this._initializeStartupScripts()); + + Timer.measureSync('Notifications', () => Notifications()); + Timer.measureSync('OSD', () => OSD()); + + await Timer.measureAsync('Bars', async () => { + const bars = await forMonitors(Bar); + bars.forEach((bar: JSXElement) => bar); + return bars; + }); + + Timer.measureSync('Settings dialog', () => SettingsDialog()); + Timer.measureSync('Menus', () => this._initializeMenus()); + Timer.measureSync('System behaviors', () => initializeSystemBehaviors()); + Timer.measureSync('Monitor handlers', () => this._setupMonitorHandlers()); + + overallTimer.end(); + } catch (error) { + console.error('Error during application initialization:', error); + } + } + + /** + * Initializes all startup scripts required by the application + */ + private static async _initializeStartupScripts(): Promise { + try { + execAsync(`python3 ${SRC_DIR}/scripts/bluetooth.py`).catch((err) => + console.error('Failed to initialize bluetooth script:', err), + ); + } catch (error) { + console.error('Failed to initialize bluetooth script:', error); + } + } + + /** + * Initializes all menu components + */ + private static _initializeMenus(): void { + StandardWindows.forEach((window) => { + return window(); + }); + + DropdownMenus.forEach((window) => { + return window(); + }); + + DropdownMenus.forEach((window) => { + const windowName = window.name + .replace(/_default.*/, '') + .concat('menu') + .toLowerCase(); + + if (!isDropdownMenu(windowName)) { + return; + } + + handleRealization(windowName); + }); + } + + /** + * Sets up monitor change event handlers + */ + private static _setupMonitorHandlers(): void { + const hyprland = AstalHyprland.get_default(); + const barRefreshManager = BarRefreshManager.getInstance(); + + hyprland.connect('monitor-added', () => barRefreshManager.handleMonitorChange('added')); + hyprland.connect('monitor-removed', () => barRefreshManager.handleMonitorChange('removed')); + } +} diff --git a/src/lib/performance/timer.ts b/src/lib/performance/timer.ts new file mode 100644 index 0000000..c358b6e --- /dev/null +++ b/src/lib/performance/timer.ts @@ -0,0 +1,70 @@ +import GLib from 'gi://GLib'; + +/** + * Performance timing utility for measuring execution time of operations + */ +export class Timer { + private _startTime: number; + private _label: string; + + constructor(label: string) { + this._label = label; + this._startTime = GLib.get_monotonic_time(); + } + + /** + * Stops the timer and logs the elapsed time with the configured label + * Returns the elapsed time in milliseconds for further processing + */ + public end(): number { + const elapsed = (GLib.get_monotonic_time() - this._startTime) / 1000; + console.log(`${this._label}: ${elapsed.toFixed(1)}ms`); + return elapsed; + } + + /** + * Retrieves the current elapsed time without stopping the timer + * Useful for intermediate measurements during long-running operations + */ + public elapsed(): number { + return (GLib.get_monotonic_time() - this._startTime) / 1000; + } + + /** + * Wraps an async function with automatic performance timing + * Logs execution time regardless of success or failure + * + * @param label - Description of the operation being measured + * @param fn - Async function to measure + */ + public static async measureAsync(label: string, fn: () => Promise): Promise { + const timer = new Timer(label); + try { + const result = await fn(); + timer.end(); + return result; + } catch (error) { + timer.end(); + throw error; + } + } + + /** + * Wraps a synchronous function with automatic performance timing + * Logs execution time regardless of success or failure + * + * @param label - Description of the operation being measured + * @param fn - Synchronous function to measure + */ + public static measureSync(label: string, fn: () => T): T { + const timer = new Timer(label); + try { + const result = fn(); + timer.end(); + return result; + } catch (error) { + timer.end(); + throw error; + } + } +} diff --git a/src/services/display/bar/autoHide.ts b/src/services/display/bar/autoHide.ts index 1f2b7c4..2562302 100644 --- a/src/services/display/bar/autoHide.ts +++ b/src/services/display/bar/autoHide.ts @@ -56,7 +56,7 @@ export class BarAutoHideService { if (hideMode === 'never') { this._showAllBars(); } else if (hideMode === 'single-window') { - this._updateBarVisibilityByWindowCount(); + this._handleSingleWindowAutoHide(); } }, ); @@ -64,7 +64,7 @@ export class BarAutoHideService { this._subscriptions.client = Variable.derive( [bind(this._hyprlandService, 'focusedClient')], (currentClient) => { - this._handleFullscreenClientVisibility(currentClient); + this._handleFullscreenAutoHide(currentClient); }, ); @@ -91,8 +91,17 @@ export class BarAutoHideService { private _setBarVisibility(monitorId: number, isVisible: boolean): void { const barName = `bar-${monitorId}`; - if (BarVisibility.get(barName)) { - App.get_window(barName)?.set_visible(isVisible); + if (!BarVisibility.get(barName)) { + return; + } + + const window = App.get_window(barName); + if (window && !window.get_window()?.is_destroyed()) { + try { + window.set_visible(isVisible); + } catch (error) { + console.warn(`[BarAutoHide] Failed to set visibility for ${barName}:`, error); + } } } @@ -101,7 +110,7 @@ export class BarAutoHideService { * * @param client - The Hyprland client whose fullscreen state to monitor */ - private _handleFullscreenClientVisibility(client: AstalHyprland.Client): void { + private _handleFullscreenAutoHide(client: AstalHyprland.Client): void { if (client === null) { return; } @@ -109,12 +118,25 @@ export class BarAutoHideService { const fullscreenBinding = bind(client, 'fullscreen'); Variable.derive([bind(fullscreenBinding)], (isFullScreen) => { - if (this._autoHide.get() === 'fullscreen') { - this._setBarVisibility(client.monitor.id, !Boolean(isFullScreen)); + if (this._autoHide.get() === 'fullscreen' && client.monitor?.id !== undefined) { + this._setBarVisibility(client.monitor?.id, !Boolean(isFullScreen)); } }); } + /** + * Updates bar visibility based on workspace window count + */ + private _handleSingleWindowAutoHide(): void { + const monitors = this._hyprlandService.get_monitors(); + const activeWorkspaces = monitors.map((monitor) => monitor.active_workspace); + + activeWorkspaces.forEach((workspace) => { + const hasOneClient = workspace.get_clients().length !== 1; + this._setBarVisibility(workspace.monitor.id, hasOneClient); + }); + } + /** * Shows bars on all monitors */ @@ -128,19 +150,6 @@ export class BarAutoHideService { }); } - /** - * Updates bar visibility based on workspace window count - */ - private _updateBarVisibilityByWindowCount(): void { - const monitors = this._hyprlandService.get_monitors(); - const activeWorkspaces = monitors.map((monitor) => monitor.active_workspace); - - activeWorkspaces.forEach((workspace) => { - const hasOneClient = workspace.get_clients().length !== 1; - this._setBarVisibility(workspace.monitor.id, hasOneClient); - }); - } - /** * Updates bar visibility based on workspace fullscreen state */ diff --git a/src/services/display/bar/refreshManager.ts b/src/services/display/bar/refreshManager.ts new file mode 100644 index 0000000..634c572 --- /dev/null +++ b/src/services/display/bar/refreshManager.ts @@ -0,0 +1,117 @@ +import { App } from 'astal/gtk3'; +import { Bar } from 'src/components/bar'; +import { forMonitors } from 'src/components/bar/utils/monitors'; +import { GdkMonitorService } from 'src/services/display/monitor'; +import Notifications from 'src/components/notifications'; +import OSD from 'src/components/osd/index'; + +/** + * Manages dynamic refresh of monitor-dependent components when monitor configuration changes. + * Handles bars, notifications, OSD, and other monitor-aware components. + * Includes debouncing, error recovery, and prevents concurrent refresh operations. + */ +export class BarRefreshManager { + private static _instance: BarRefreshManager | null = null; + private _refreshInProgress = false; + private _pendingRefresh = false; + private _monitorChangeTimeout: ReturnType | null = null; + + private constructor() {} + + /** + * Retrieves the singleton instance of the refresh manager + * Creates the instance on first access to ensure single point of control + */ + public static getInstance(): BarRefreshManager { + if (!BarRefreshManager._instance) { + BarRefreshManager._instance = new BarRefreshManager(); + } + return BarRefreshManager._instance; + } + + /** + * Processes monitor configuration change events with built-in debouncing + * Ensures smooth transitions during rapid monitor connect/disconnect scenarios + * + * @param event - The type of monitor change event that occurred + */ + public handleMonitorChange(event: string): void { + if (this._monitorChangeTimeout !== null) { + clearTimeout(this._monitorChangeTimeout); + } + + this._monitorChangeTimeout = setTimeout(() => { + this._refreshMonitors().catch((error) => { + console.error(`[MonitorChange] Failed to refresh bars for ${event}:`, error); + }); + this._monitorChangeTimeout = null; + }, 300); + } + + /** + * Orchestrates the complete refresh of monitor-dependent components + * Prevents concurrent refreshes and queues pending requests to avoid race conditions + */ + private async _refreshMonitors(): Promise { + if (this._refreshInProgress) { + this._pendingRefresh = true; + return; + } + + this._refreshInProgress = true; + + try { + this._destroyBars(); + this._destroyNotificationWindow(); + this._destroyOsdWindow(); + + const gdkMonitorService = GdkMonitorService.getInstance(); + gdkMonitorService.reset(); + + await forMonitors(Bar); + + Notifications(); + OSD(); + } catch (error) { + console.error('[MonitorRefresh] Error during component refresh:', error); + } finally { + this._refreshInProgress = false; + + if (this._pendingRefresh) { + this._pendingRefresh = false; + setTimeout(() => this._refreshMonitors(), 100); + } + } + } + + /** + * Destroys all existing bar windows across monitors + * Identifies bars by their naming convention to ensure complete cleanup + */ + private _destroyBars(): void { + const barWindows = App.get_windows().filter((window) => window.name.startsWith('bar-')); + barWindows.forEach((window) => window?.destroy()); + } + + /** + * Removes the notifications window from the display + * Ensures proper cleanup before recreating notifications on new monitor configuration + */ + private _destroyNotificationWindow(): void { + const notificationsWindow = App.get_window('notifications-window'); + if (notificationsWindow !== null) { + notificationsWindow.destroy(); + } + } + + /** + * Removes the OSD indicator window from the display + * Prepares for recreation on the appropriate monitor after configuration changes + */ + private _destroyOsdWindow(): void { + const osdWindow = App.get_window('indicator'); + if (osdWindow !== null) { + osdWindow.destroy(); + } + } +} diff --git a/src/services/display/monitor/index.ts b/src/services/display/monitor/index.ts index c0eadda..156d13c 100644 --- a/src/services/display/monitor/index.ts +++ b/src/services/display/monitor/index.ts @@ -4,24 +4,37 @@ import AstalHyprland from 'gi://AstalHyprland?version=0.1'; const hyprlandService = AstalHyprland.get_default(); /** - * The MonitorMapper class encapsulates the conversion logic between GDK and Hyprland monitor IDs. - * It maintains internal state for monitors that have already been used so that duplicate assignments are avoided. + * Singleton service that manages the conversion between GDK and Hyprland monitor IDs. + * Maintains persistent state to ensure consistent monitor mappings across the application lifecycle. */ export class GdkMonitorService { - private _usedGdkMonitors: Set; - private _usedHyprlandMonitors: Set; + private static _instance: GdkMonitorService; + private _usedHyprlandIds: Set; - constructor() { - this._usedGdkMonitors = new Set(); - this._usedHyprlandMonitors = new Set(); + private constructor() { + this._usedHyprlandIds = new Set(); } /** - * Resets the internal state for both GDK and Hyprland monitor mappings. + * Gets the singleton instance of GdkMonitorService. + * Creates the instance on first access and reuses it for all subsequent calls. + * + * @returns The singleton GdkMonitorService instance + */ + public static getInstance(): GdkMonitorService { + if (!GdkMonitorService._instance) { + GdkMonitorService._instance = new GdkMonitorService(); + } + return GdkMonitorService._instance; + } + + /** + * Resets the internal state for monitor mappings. + * Note: With singleton pattern, this should only be called when monitor + * configuration actually changes. */ public reset(): void { - this._usedGdkMonitors.clear(); - this._usedHyprlandMonitors.clear(); + this._usedHyprlandIds.clear(); } /** @@ -38,16 +51,25 @@ export class GdkMonitorService { } const gdkMonitor = gdkMonitors[monitor]; - const hyprlandMonitors = hyprlandService.get_monitors(); + if (!gdkMonitor) { + return monitor; + } - return this._matchMonitor( - hyprlandMonitors, + const hyprlandMonitors = hyprlandService.get_monitors(); + const validMonitors = hyprlandMonitors.filter((m) => m.model && m.model !== 'null'); + const tempUsedIds = new Set(); + const monitorsToUse = validMonitors.length > 0 ? validMonitors : hyprlandMonitors; + + const result = this._matchMonitor( + monitorsToUse, gdkMonitor, monitor, - this._usedHyprlandMonitors, (mon) => mon.id, (mon, gdkMon) => this._matchMonitorKey(mon, gdkMon), + tempUsedIds, ); + + return result; } /** @@ -71,79 +93,66 @@ export class GdkMonitorService { const foundHyprlandMonitor = hyprlandMonitors.find((mon) => mon.id === monitor) || hyprlandMonitors[0]; + const tempUsedIds = new Set(); + return this._matchMonitor( gdkCandidates, foundHyprlandMonitor, monitor, - this._usedGdkMonitors, (candidate) => candidate.id, (candidate, hyprlandMonitor) => this._matchMonitorKey(hyprlandMonitor, candidate.monitor), + tempUsedIds, ); } /** * Generic helper that finds the best matching candidate monitor based on: - * 1. A direct match (candidate matches the source and has the same id as the target). - * 2. A relaxed match (candidate matches the source, regardless of id). - * 3. A fallback match (first candidate that hasn’t been used). + * 1. A direct match (candidate matches the source and has the same id as the target, and hasn't been used). + * 2. A relaxed match (candidate matches the source, regardless of id, and hasn't been used). + * 3. No fallback - return target to preserve intended mapping. * * @param candidates - Array of candidate monitors. * @param source - The source monitor object to match against. * @param target - The desired monitor id. - * @param usedMonitors - A Set of already used candidate ids. * @param getId - Function to extract the id from a candidate. * @param compare - Function that determines if a candidate matches the source. + * @param usedIds - Set of already used IDs for this mapping batch. * @returns The chosen monitor id. */ private _matchMonitor( candidates: T[], source: U, target: number, - usedMonitors: Set, getId: (candidate: T) => number, compare: (candidate: T, source: U) => boolean, + usedIds: Set, ): number { - // Direct match: candidate matches the source and has the same id as the target. - const directMatch = candidates.find( - (candidate) => - compare(candidate, source) && - !usedMonitors.has(getId(candidate)) && - getId(candidate) === target, - ); + const directMatch = candidates.find((candidate) => { + const matches = compare(candidate, source); + const id = getId(candidate); + const isUsed = usedIds.has(id); + return matches && id === target && !isUsed; + }); if (directMatch !== undefined) { - usedMonitors.add(getId(directMatch)); - return getId(directMatch); + const result = getId(directMatch); + usedIds.add(result); + return result; } - // Relaxed match: candidate matches the source regardless of id. - const relaxedMatch = candidates.find( - (candidate) => compare(candidate, source) && !usedMonitors.has(getId(candidate)), - ); + const relaxedMatch = candidates.find((candidate) => { + const matches = compare(candidate, source); + const id = getId(candidate); + const isUsed = usedIds.has(id); + return matches && !isUsed; + }); if (relaxedMatch !== undefined) { - usedMonitors.add(getId(relaxedMatch)); - return getId(relaxedMatch); + const result = getId(relaxedMatch); + usedIds.add(result); + return result; } - // Fallback: use the first candidate that hasn't been used. - const fallback = candidates.find((candidate) => !usedMonitors.has(getId(candidate))); - - if (fallback !== undefined) { - usedMonitors.add(getId(fallback)); - return getId(fallback); - } - - // As a last resort, iterate over candidates. - for (const candidate of candidates) { - const candidateId = getId(candidate); - if (!usedMonitors.has(candidateId)) { - usedMonitors.add(candidateId); - return candidateId; - } - } - - console.warn(`Returning original monitor index as a last resort: ${target}`); return target; } @@ -155,6 +164,10 @@ export class GdkMonitorService { * @returns boolean indicating if the monitors match */ private _matchMonitorKey(hyprlandMonitor: AstalHyprland.Monitor, gdkMonitor: GdkMonitor): boolean { + if (!hyprlandMonitor.model || hyprlandMonitor.model === 'null') { + return false; + } + const isRotated90 = hyprlandMonitor.transform % 2 !== 0; const gdkScaleFactor = Math.ceil(hyprlandMonitor.scale); @@ -170,16 +183,6 @@ export class GdkMonitorService { const keyMatch = gdkMonitor.key === gdkScaleFactorKey || gdkMonitor.key === hyprlandScaleFactorKey; - this._logMonitorInfo( - gdkMonitor, - hyprlandMonitor, - isRotated90, - gdkScaleFactor, - gdkScaleFactorKey, - hyprlandScaleFactorKey, - keyMatch, - ); - return keyMatch; } @@ -205,56 +208,21 @@ export class GdkMonitorService { continue; } - const model = curMonitor.get_model() ?? ''; - const geometry = curMonitor.get_geometry(); - const scaleFactor = curMonitor.get_scale_factor(); + try { + const model = curMonitor.get_model() ?? ''; + const geometry = curMonitor.get_geometry(); + const scaleFactor = curMonitor.get_scale_factor(); - // GDK3 only supports integer scale factors - const key = `${model}_${geometry.width}x${geometry.height}_${scaleFactor}`; - gdkMonitors[i] = { key, model, used: false }; + const key = `${model}_${geometry.width}x${geometry.height}_${scaleFactor}`; + gdkMonitors[i] = { key, model, used: false }; + } catch (error) { + console.warn(`Failed to get properties for monitor ${i}:`, error); + gdkMonitors[i] = { key: `monitor_${i}`, model: 'Unknown', used: false }; + } } return gdkMonitors; } - - /** - * Logs detailed monitor information for debugging purposes - * @param gdkMonitor - GDK monitor object - * @param hyprlandMonitor - Hyprland monitor information - * @param isRotated90 - Whether the monitor is rotated 90 degrees - * @param gdkScaleFactor - The GDK monitor's scale factor - * @param gdkScaleFactorKey - Key used for scale factor matching - * @param hyprlandScaleFactorKey - Key used for general scale matching - * @param keyMatch - Whether the monitor keys match - */ - private _logMonitorInfo( - gdkMonitor: GdkMonitor, - hyprlandMonitor: AstalHyprland.Monitor, - isRotated90: boolean, - gdkScaleFactor: number, - gdkScaleFactorKey: string, - hyprlandScaleFactorKey: string, - keyMatch: boolean, - ): void { - console.debug('=== Monitor Matching Debug Info ==='); - console.debug('GDK Monitor'); - console.debug(` Key: ${gdkMonitor.key}`); - console.debug('Hyprland Monitor'); - console.debug(` ID: ${hyprlandMonitor.id}`); - console.debug(` Model: ${hyprlandMonitor.model}`); - console.debug(` Resolution: ${hyprlandMonitor.width}x${hyprlandMonitor.height}`); - console.debug(` Scale: ${hyprlandMonitor.scale}`); - console.debug(` Transform: ${hyprlandMonitor.transform}`); - console.debug('Calculated Values'); - console.debug(` Rotation: ${isRotated90 ? '90°' : '0°'}`); - console.debug(` GDK Scale Factor: ${gdkScaleFactor}`); - console.debug('Calculated Keys'); - console.debug(` GDK Scale Factor Key: ${gdkScaleFactorKey}`); - console.debug(` Hyprland Scale Factor Key: ${hyprlandScaleFactorKey}`); - console.debug('Match Result'); - console.debug(` ${keyMatch ? '✅ Monitors Match' : '❌ No Match'}`); - console.debug('===============================\n'); - } } type GdkMonitor = { diff --git a/src/services/network/index.ts b/src/services/network/index.ts index fb055ad..2193381 100644 --- a/src/services/network/index.ts +++ b/src/services/network/index.ts @@ -35,19 +35,6 @@ export class NetworkService { return this._instance; } - /** - * Sets up bindings to monitor network service changes - */ - private _setupBindings(): void { - Variable.derive([bind(this._astalNetwork, 'wifi')], () => { - this.wifi.onWifiServiceChanged(); - }); - - Variable.derive([bind(this._astalNetwork, 'wired')], () => { - this.ethernet.onWiredServiceChanged(); - }); - } - /** * Retrieves the appropriate WiFi icon based on the provided icon name. * @@ -67,4 +54,17 @@ export class NetworkService { return '󰤨'; } + + /** + * Sets up bindings to monitor network service changes + */ + private _setupBindings(): void { + Variable.derive([bind(this._astalNetwork, 'wifi')], () => { + this.wifi.onWifiServiceChanged(); + }); + + Variable.derive([bind(this._astalNetwork, 'wired')], () => { + this.ethernet.onWiredServiceChanged(); + }); + } }