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
This commit is contained in:
75
app.ts
75
app.ts
@@ -1,81 +1,12 @@
|
|||||||
import './src/lib/session';
|
import './src/lib/session';
|
||||||
import './src/style';
|
import './src/style';
|
||||||
import 'src/core/behaviors/bar';
|
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 { 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 { runCLI } from 'src/services/cli/commander';
|
||||||
import { DropdownMenus, StandardWindows } from 'src/components/menus';
|
import { InitializationService } from 'src/core/initialization';
|
||||||
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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
App.start({
|
App.start({
|
||||||
instanceName: 'hyprpanel',
|
instanceName: 'hyprpanel',
|
||||||
requestHandler(request: string, res: (response: unknown) => void) {
|
requestHandler: (request: string, res: (response: unknown) => void) => runCLI(request, res),
|
||||||
runCLI(request, res);
|
main: () => InitializationService.initialize(),
|
||||||
},
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
import { GdkMonitorService } from 'src/services/display/monitor';
|
import { JSXElement } from 'src/core/types';
|
||||||
import { BarLayout } from './layout/BarLayout';
|
import { BarLayout } from './layout/BarLayout';
|
||||||
import { getCoreWidgets } from './layout/coreWidgets';
|
import { getCoreWidgets } from './layout/coreWidgets';
|
||||||
import { WidgetRegistry } from './layout/WidgetRegistry';
|
import { WidgetRegistry } from './layout/WidgetRegistry';
|
||||||
|
|
||||||
const gdkMonitorService = new GdkMonitorService();
|
|
||||||
const widgetRegistry = new WidgetRegistry(getCoreWidgets());
|
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<JSX.Element> => {
|
export const Bar = async (gdkMonitor: number, hyprlandMonitor?: number): Promise<JSXElement> => {
|
||||||
await widgetRegistry.initialize();
|
await widgetRegistry.initialize();
|
||||||
|
|
||||||
const hyprlandMonitor = gdkMonitorService.mapGdkToHyprland(monitor);
|
const hyprlandId = hyprlandMonitor ?? gdkMonitor;
|
||||||
const barLayout = new BarLayout(monitor, hyprlandMonitor, widgetRegistry);
|
const barLayout = new BarLayout(gdkMonitor, hyprlandId, widgetRegistry);
|
||||||
|
|
||||||
return barLayout.render();
|
return barLayout.render();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 Astal from 'gi://Astal?version=3.0';
|
||||||
import { bind, Binding, Variable } from 'astal';
|
import { bind, Binding, Variable } from 'astal';
|
||||||
import { idleInhibit } from 'src/lib/window/visibility';
|
import { idleInhibit } from 'src/lib/window/visibility';
|
||||||
import { WidgetRegistry } from './WidgetRegistry';
|
import { WidgetRegistry } from './WidgetRegistry';
|
||||||
import { getLayoutForMonitor, isLayoutEmpty } from '../utils/monitors';
|
import { getLayoutForMonitor, isLayoutEmpty } from '../utils/monitors';
|
||||||
import options from 'src/configuration';
|
import options from 'src/configuration';
|
||||||
|
import { JSXElement } from 'src/core/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible for the bar UI layout and positioning
|
* Responsible for the bar UI layout and positioning
|
||||||
@@ -46,7 +47,27 @@ export class BarLayout {
|
|||||||
this._initializeReactiveVariables();
|
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 (
|
return (
|
||||||
<window
|
<window
|
||||||
inhibit={bind(idleInhibit)}
|
inhibit={bind(idleInhibit)}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Gdk } from 'astal/gtk3';
|
import { Gdk } from 'astal/gtk3';
|
||||||
import { range } from 'src/lib/array/helpers';
|
|
||||||
import { BarLayout, BarLayouts } from 'src/lib/options/types';
|
import { BarLayout, BarLayouts } from 'src/lib/options/types';
|
||||||
|
import { GdkMonitorService } from 'src/services/display/monitor';
|
||||||
|
import { MonitorMapping } from './types';
|
||||||
|
import { JSXElement } from 'src/core/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the bar layout configuration for a specific monitor
|
* Returns the bar layout configuration for a specific monitor
|
||||||
@@ -43,17 +45,48 @@ export const isLayoutEmpty = (layout: BarLayout): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* @param widget - Function that creates a widget for a given monitor index
|
||||||
* It uses the number of monitors available in the default Gdk display.
|
* @returns Array of created widgets for all available monitors
|
||||||
*
|
|
||||||
* @param widget A function that takes a monitor index and returns a JSX element.
|
|
||||||
*
|
|
||||||
* @returns An array of JSX elements, one for each monitor.
|
|
||||||
*/
|
*/
|
||||||
export async function forMonitors(widget: (monitor: number) => Promise<JSX.Element>): Promise<JSX.Element[]> {
|
export async function forMonitors(
|
||||||
const n = Gdk.Display.get_default()?.get_n_monitors() ?? 1;
|
widget: (monitor: number, hyprlandMonitor?: number) => Promise<JSXElement>,
|
||||||
|
): Promise<JSXElement[]> {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/components/bar/utils/monitors/types.ts
Normal file
4
src/components/bar/utils/monitors/types.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface MonitorMapping {
|
||||||
|
gdkIndex: number;
|
||||||
|
hyprlandId: number;
|
||||||
|
}
|
||||||
@@ -27,9 +27,13 @@ export const NotificationCard = ({
|
|||||||
showActions,
|
showActions,
|
||||||
...props
|
...props
|
||||||
}: NotificationCardProps): JSX.Element => {
|
}: NotificationCardProps): JSX.Element => {
|
||||||
const actionBox: IActionBox | null = notification.get_actions().length ? (
|
let actionBox: ActionBox | null;
|
||||||
<Actions notification={notification} showActions={showActions} />
|
|
||||||
) : null;
|
if (notification.get_actions().length) {
|
||||||
|
actionBox = <Actions notification={notification} showActions={showActions} />;
|
||||||
|
} else {
|
||||||
|
actionBox = null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<eventbox
|
<eventbox
|
||||||
@@ -63,11 +67,11 @@ interface NotificationCardProps extends Widget.BoxProps {
|
|||||||
showActions: boolean;
|
showActions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IActionBox extends Gtk.Widget {
|
interface ActionBox extends Gtk.Widget {
|
||||||
revealChild?: boolean;
|
revealChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationContentProps {
|
interface NotificationContentProps {
|
||||||
actionBox: IActionBox | null;
|
actionBox: ActionBox | null;
|
||||||
notification: AstalNotifd.Notification;
|
notification: AstalNotifd.Notification;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,18 @@ export const notifHasImg = (notification: AstalNotifd.Notification): boolean =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 - Variable that will be updated with the current monitor ID (defaults to 0 if no monitor is focused)
|
||||||
*
|
|
||||||
* @param curMonitor The variable to update with the active monitor ID.
|
|
||||||
*/
|
*/
|
||||||
export const trackActiveMonitor = (curMonitor: Variable<number>): void => {
|
export const trackActiveMonitor = (curMonitor: Variable<number>): void => {
|
||||||
Variable.derive([bind(hyprlandService, 'focusedMonitor')], (monitor) => {
|
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);
|
curMonitor.set(monitor.id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
|||||||
import { GdkMonitorService } from 'src/services/display/monitor/index.js';
|
import { GdkMonitorService } from 'src/services/display/monitor/index.js';
|
||||||
import { getPosition } from 'src/lib/window/positioning.js';
|
import { getPosition } from 'src/lib/window/positioning.js';
|
||||||
import { NotificationCard } from './Notification';
|
import { NotificationCard } from './Notification';
|
||||||
|
import { App } from 'astal/gtk3';
|
||||||
|
|
||||||
const hyprlandService = AstalHyprland.get_default();
|
const hyprlandService = AstalHyprland.get_default();
|
||||||
const { position, monitor, active_monitor, showActionsOnHover, displayedTotal } = options.notifications;
|
const { position, monitor, active_monitor, showActionsOnHover, displayedTotal } = options.notifications;
|
||||||
@@ -20,15 +21,13 @@ trackPopupNotifications(popupNotifications);
|
|||||||
trackAutoTimeout();
|
trackAutoTimeout();
|
||||||
|
|
||||||
export default (): JSX.Element => {
|
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 windowLayer = bind(tear).as((tear) => (tear ? Astal.Layer.TOP : Astal.Layer.OVERLAY));
|
||||||
const windowAnchor = bind(position).as(getPosition);
|
const windowAnchor = bind(position).as(getPosition);
|
||||||
const windowMonitor = Variable.derive(
|
const windowMonitor = Variable.derive(
|
||||||
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
|
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
|
||||||
(focusedMonitor, monitor, activeMonitor) => {
|
(focusedMonitor, monitor, activeMonitor) => {
|
||||||
gdkMonitorMapper.reset();
|
|
||||||
|
|
||||||
if (activeMonitor === true) {
|
if (activeMonitor === true) {
|
||||||
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(focusedMonitor.id);
|
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(focusedMonitor.id);
|
||||||
return gdkMonitor;
|
return gdkMonitor;
|
||||||
@@ -55,6 +54,7 @@ export default (): JSX.Element => {
|
|||||||
name={'notifications-window'}
|
name={'notifications-window'}
|
||||||
namespace={'notifications-window'}
|
namespace={'notifications-window'}
|
||||||
className={'notifications-window'}
|
className={'notifications-window'}
|
||||||
|
application={App}
|
||||||
layer={windowLayer}
|
layer={windowLayer}
|
||||||
anchor={windowAnchor}
|
anchor={windowAnchor}
|
||||||
exclusivity={Astal.Exclusivity.NORMAL}
|
exclusivity={Astal.Exclusivity.NORMAL}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const setupOsdBar = (self: LevelBar): void => {
|
|||||||
self.value = brightnessService.kbd;
|
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.toggleClassName('overflow', audioService.defaultMicrophone.volume > 1);
|
||||||
self.value =
|
self.value =
|
||||||
audioService.defaultMicrophone.volume <= 1
|
audioService.defaultMicrophone.volume <= 1
|
||||||
@@ -37,7 +37,7 @@ export const setupOsdBar = (self: LevelBar): void => {
|
|||||||
: audioService.defaultMicrophone.volume - 1;
|
: audioService.defaultMicrophone.volume - 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
Variable.derive([bind(audioService.defaultMicrophone, 'mute')], () => {
|
const micMuteBinding = Variable.derive([bind(audioService.defaultMicrophone, 'mute')], () => {
|
||||||
self.toggleClassName(
|
self.toggleClassName(
|
||||||
'overflow',
|
'overflow',
|
||||||
audioService.defaultMicrophone.volume > 1 &&
|
audioService.defaultMicrophone.volume > 1 &&
|
||||||
@@ -51,7 +51,7 @@ export const setupOsdBar = (self: LevelBar): void => {
|
|||||||
: audioService.defaultMicrophone.volume - 1;
|
: 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.toggleClassName('overflow', audioService.defaultSpeaker.volume > 1);
|
||||||
self.value =
|
self.value =
|
||||||
audioService.defaultSpeaker.volume <= 1
|
audioService.defaultSpeaker.volume <= 1
|
||||||
@@ -59,7 +59,7 @@ export const setupOsdBar = (self: LevelBar): void => {
|
|||||||
: audioService.defaultSpeaker.volume - 1;
|
: audioService.defaultSpeaker.volume - 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
Variable.derive([bind(audioService.defaultSpeaker, 'mute')], () => {
|
const speakerMuteBinding = Variable.derive([bind(audioService.defaultSpeaker, 'mute')], () => {
|
||||||
self.toggleClassName(
|
self.toggleClassName(
|
||||||
'overflow',
|
'overflow',
|
||||||
audioService.defaultSpeaker.volume > 1 &&
|
audioService.defaultSpeaker.volume > 1 &&
|
||||||
@@ -72,4 +72,11 @@ export const setupOsdBar = (self: LevelBar): void => {
|
|||||||
? audioService.defaultSpeaker.volume
|
? audioService.defaultSpeaker.volume
|
||||||
: audioService.defaultSpeaker.volume - 1;
|
: audioService.defaultSpeaker.volume - 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.connect('destroy', () => {
|
||||||
|
micVolumeBinding.drop();
|
||||||
|
micMuteBinding.drop();
|
||||||
|
speakerVolumeBinding.drop();
|
||||||
|
speakerMuteBinding.drop();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,79 +1,50 @@
|
|||||||
import { bind, timeout, Variable } from 'astal';
|
import { bind, Variable } from 'astal';
|
||||||
import { Widget } from 'astal/gtk3';
|
import { Widget } from 'astal/gtk3';
|
||||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||||
import options from 'src/configuration';
|
import options from 'src/configuration';
|
||||||
import { GdkMonitorService } from 'src/services/display/monitor';
|
import { GdkMonitorService } from 'src/services/display/monitor';
|
||||||
import BrightnessService from 'src/services/system/brightness';
|
import BrightnessService from 'src/services/system/brightness';
|
||||||
|
import { OsdRevealerController } from './revealer/revealerController';
|
||||||
|
|
||||||
const wireplumber = AstalWp.get_default() as AstalWp.Wp;
|
const wireplumber = AstalWp.get_default() as AstalWp.Wp;
|
||||||
const audioService = wireplumber.audio;
|
const audioService = wireplumber.audio;
|
||||||
const brightnessService = BrightnessService.getInstance();
|
const brightnessService = BrightnessService.getInstance();
|
||||||
const hyprlandService = AstalHyprland.get_default();
|
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;
|
const osdController = OsdRevealerController.getInstance();
|
||||||
|
|
||||||
/*
|
|
||||||
* So the OSD doesn't show on startup for no reason
|
|
||||||
*/
|
|
||||||
let isStartingUp = true;
|
|
||||||
timeout(3000, () => {
|
|
||||||
isStartingUp = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* @returns Variable containing the GDK monitor index where OSD should be displayed (defaults to 0 if no valid monitor)
|
||||||
*
|
|
||||||
* @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<number> representing the monitor index for the OSD.
|
|
||||||
*/
|
*/
|
||||||
export const getOsdMonitor = (): Variable<number> => {
|
export const getOsdMonitor = (): Variable<number> => {
|
||||||
const gdkMonitorMapper = new GdkMonitorService();
|
const gdkMonitorMapper = GdkMonitorService.getInstance();
|
||||||
|
|
||||||
return Variable.derive(
|
return Variable.derive(
|
||||||
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
|
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
|
||||||
(currentMonitor, defaultMonitor, followMonitor) => {
|
(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);
|
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(currentMonitor.id);
|
||||||
return gdkMonitor;
|
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<number> => {
|
|||||||
* @param self The Widget.Revealer instance to set up.
|
* @param self The Widget.Revealer instance to set up.
|
||||||
*/
|
*/
|
||||||
export const revealerSetup = (self: Widget.Revealer): void => {
|
export const revealerSetup = (self: Widget.Revealer): void => {
|
||||||
self.hook(enable, () => {
|
osdController.setRevealer(self);
|
||||||
handleReveal(self);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.hook(brightnessService, 'notify::screen', () => {
|
const handleReveal = (): void => {
|
||||||
handleReveal(self);
|
osdController.show();
|
||||||
});
|
};
|
||||||
|
|
||||||
self.hook(brightnessService, 'notify::kbd', () => {
|
self.hook(enable, handleReveal);
|
||||||
handleReveal(self);
|
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')],
|
[bind(audioService.defaultMicrophone, 'volume'), bind(audioService.defaultMicrophone, 'mute')],
|
||||||
() => {
|
handleReveal,
|
||||||
handleReveal(self);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Variable.derive(
|
const speakerBinding = Variable.derive(
|
||||||
[bind(audioService.defaultSpeaker, 'volume'), bind(audioService.defaultSpeaker, 'mute')],
|
[bind(audioService.defaultSpeaker, 'volume'), bind(audioService.defaultSpeaker, 'mute')],
|
||||||
() => {
|
handleReveal,
|
||||||
handleReveal(self);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.connect('destroy', () => {
|
||||||
|
microphoneBinding.drop();
|
||||||
|
speakerBinding.drop();
|
||||||
|
osdController.onRevealerDestroy(self);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ const wireplumber = AstalWp.get_default() as AstalWp.Wp;
|
|||||||
const audioService = wireplumber.audio;
|
const audioService = wireplumber.audio;
|
||||||
const brightnessService = BrightnessService.getInstance();
|
const brightnessService = BrightnessService.getInstance();
|
||||||
|
|
||||||
type OSDIcon = {
|
|
||||||
micVariable: Variable<unknown>;
|
|
||||||
speakerVariable: Variable<unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the OSD icon for a given widget.
|
* 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.
|
* @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.hook(brightnessService, 'notify::screen', () => {
|
||||||
self.label = '';
|
self.label = '';
|
||||||
});
|
});
|
||||||
@@ -45,8 +40,8 @@ export const setupOsdIcon = (self: Widget.Label): OSDIcon => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
self.connect('destroy', () => {
|
||||||
micVariable,
|
micVariable.drop();
|
||||||
speakerVariable,
|
speakerVariable.drop();
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import options from 'src/configuration';
|
import options from 'src/configuration';
|
||||||
import { bind } from 'astal';
|
import { bind } from 'astal';
|
||||||
import { Astal } from 'astal/gtk3';
|
import { App, Astal } from 'astal/gtk3';
|
||||||
import { getOsdMonitor } from './helpers';
|
import { getOsdMonitor } from './helpers';
|
||||||
import { getPosition } from 'src/lib/window/positioning';
|
import { getPosition } from 'src/lib/window/positioning';
|
||||||
import { OsdRevealer } from './revealer';
|
import { OsdRevealer } from './revealer';
|
||||||
@@ -8,20 +8,26 @@ import { OsdRevealer } from './revealer';
|
|||||||
const { location } = options.theme.osd;
|
const { location } = options.theme.osd;
|
||||||
|
|
||||||
export default (): JSX.Element => {
|
export default (): JSX.Element => {
|
||||||
|
const osdMonitorBinding = getOsdMonitor();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<window
|
<window
|
||||||
monitor={getOsdMonitor()()}
|
monitor={osdMonitorBinding()}
|
||||||
name={'indicator'}
|
name={'indicator'}
|
||||||
|
application={App}
|
||||||
namespace={'indicator'}
|
namespace={'indicator'}
|
||||||
className={'indicator'}
|
className={'indicator'}
|
||||||
visible={true}
|
visible={true}
|
||||||
layer={bind(options.tear).as((tear) => (tear ? Astal.Layer.TOP : Astal.Layer.OVERLAY))}
|
layer={bind(options.tear).as((tear) => (tear ? Astal.Layer.TOP : Astal.Layer.OVERLAY))}
|
||||||
anchor={bind(location).as((anchorPoint) => getPosition(anchorPoint))}
|
anchor={bind(location).as((anchorPoint) => getPosition(anchorPoint))}
|
||||||
setup={(self) => {
|
setup={(self) => {
|
||||||
getOsdMonitor().subscribe(() => {
|
osdMonitorBinding().subscribe(() => {
|
||||||
self.set_click_through(true);
|
self.set_click_through(true);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onDestroy={() => {
|
||||||
|
osdMonitorBinding.drop();
|
||||||
|
}}
|
||||||
clickThrough
|
clickThrough
|
||||||
>
|
>
|
||||||
<OsdRevealer />
|
<OsdRevealer />
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ export const setupOsdLabel = (self: Widget.Label): void => {
|
|||||||
self.label = `${Math.round(brightnessService.kbd * 100)}`;
|
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.toggleClassName('overflow', audioService.defaultMicrophone.volume > 1);
|
||||||
self.label = `${Math.round(audioService.defaultMicrophone.volume * 100)}`;
|
self.label = `${Math.round(audioService.defaultMicrophone.volume * 100)}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
Variable.derive([bind(audioService.defaultMicrophone, 'mute')], () => {
|
const micMuteBinding = Variable.derive([bind(audioService.defaultMicrophone, 'mute')], () => {
|
||||||
self.toggleClassName(
|
self.toggleClassName(
|
||||||
'overflow',
|
'overflow',
|
||||||
audioService.defaultMicrophone.volume > 1 &&
|
audioService.defaultMicrophone.volume > 1 &&
|
||||||
@@ -45,12 +45,12 @@ export const setupOsdLabel = (self: Widget.Label): void => {
|
|||||||
self.label = `${inputVolume}`;
|
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.toggleClassName('overflow', audioService.defaultSpeaker.volume > 1);
|
||||||
self.label = `${Math.round(audioService.defaultSpeaker.volume * 100)}`;
|
self.label = `${Math.round(audioService.defaultSpeaker.volume * 100)}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
Variable.derive([bind(audioService.defaultSpeaker, 'mute')], () => {
|
const speakerMuteBinding = Variable.derive([bind(audioService.defaultSpeaker, 'mute')], () => {
|
||||||
self.toggleClassName(
|
self.toggleClassName(
|
||||||
'overflow',
|
'overflow',
|
||||||
audioService.defaultSpeaker.volume > 1 &&
|
audioService.defaultSpeaker.volume > 1 &&
|
||||||
@@ -62,4 +62,11 @@ export const setupOsdLabel = (self: Widget.Label): void => {
|
|||||||
: Math.round(audioService.defaultSpeaker.volume * 100);
|
: Math.round(audioService.defaultSpeaker.volume * 100);
|
||||||
self.label = `${speakerVolume}`;
|
self.label = `${speakerVolume}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.connect('destroy', () => {
|
||||||
|
micVolumeBinding.drop();
|
||||||
|
micMuteBinding.drop();
|
||||||
|
speakerVolumeBinding.drop();
|
||||||
|
speakerMuteBinding.drop();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export const OsdRevealer = (): JSX.Element => {
|
|||||||
<revealer
|
<revealer
|
||||||
transitionType={Gtk.RevealerTransitionType.CROSSFADE}
|
transitionType={Gtk.RevealerTransitionType.CROSSFADE}
|
||||||
revealChild={false}
|
revealChild={false}
|
||||||
setup={revealerSetup}
|
setup={(self) => {
|
||||||
|
revealerSetup(self);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<box className={'osd-container'} vertical={osdOrientation}>
|
<box className={'osd-container'} vertical={osdOrientation}>
|
||||||
{bind(orientation).as((currentOrientation) => {
|
{bind(orientation).as((currentOrientation) => {
|
||||||
|
|||||||
117
src/components/osd/revealer/revealerController.ts
Normal file
117
src/components/osd/revealer/revealerController.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/components/osd/types.ts
Normal file
3
src/components/osd/types.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface RevealerSetupBindings {
|
||||||
|
cleanup: () => void;
|
||||||
|
}
|
||||||
99
src/core/initialization/index.ts
Normal file
99
src/core/initialization/index.ts
Normal file
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/lib/performance/timer.ts
Normal file
70
src/lib/performance/timer.ts
Normal file
@@ -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<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
||||||
|
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<T>(label: string, fn: () => T): T {
|
||||||
|
const timer = new Timer(label);
|
||||||
|
try {
|
||||||
|
const result = fn();
|
||||||
|
timer.end();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
timer.end();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,7 +56,7 @@ export class BarAutoHideService {
|
|||||||
if (hideMode === 'never') {
|
if (hideMode === 'never') {
|
||||||
this._showAllBars();
|
this._showAllBars();
|
||||||
} else if (hideMode === 'single-window') {
|
} else if (hideMode === 'single-window') {
|
||||||
this._updateBarVisibilityByWindowCount();
|
this._handleSingleWindowAutoHide();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -64,7 +64,7 @@ export class BarAutoHideService {
|
|||||||
this._subscriptions.client = Variable.derive(
|
this._subscriptions.client = Variable.derive(
|
||||||
[bind(this._hyprlandService, 'focusedClient')],
|
[bind(this._hyprlandService, 'focusedClient')],
|
||||||
(currentClient) => {
|
(currentClient) => {
|
||||||
this._handleFullscreenClientVisibility(currentClient);
|
this._handleFullscreenAutoHide(currentClient);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -91,8 +91,17 @@ export class BarAutoHideService {
|
|||||||
private _setBarVisibility(monitorId: number, isVisible: boolean): void {
|
private _setBarVisibility(monitorId: number, isVisible: boolean): void {
|
||||||
const barName = `bar-${monitorId}`;
|
const barName = `bar-${monitorId}`;
|
||||||
|
|
||||||
if (BarVisibility.get(barName)) {
|
if (!BarVisibility.get(barName)) {
|
||||||
App.get_window(barName)?.set_visible(isVisible);
|
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
|
* @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) {
|
if (client === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -109,12 +118,25 @@ export class BarAutoHideService {
|
|||||||
const fullscreenBinding = bind(client, 'fullscreen');
|
const fullscreenBinding = bind(client, 'fullscreen');
|
||||||
|
|
||||||
Variable.derive([bind(fullscreenBinding)], (isFullScreen) => {
|
Variable.derive([bind(fullscreenBinding)], (isFullScreen) => {
|
||||||
if (this._autoHide.get() === 'fullscreen') {
|
if (this._autoHide.get() === 'fullscreen' && client.monitor?.id !== undefined) {
|
||||||
this._setBarVisibility(client.monitor.id, !Boolean(isFullScreen));
|
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
|
* 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
|
* Updates bar visibility based on workspace fullscreen state
|
||||||
*/
|
*/
|
||||||
|
|||||||
117
src/services/display/bar/refreshManager.ts
Normal file
117
src/services/display/bar/refreshManager.ts
Normal file
@@ -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<typeof setTimeout> | 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<void> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,24 +4,37 @@ import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
|||||||
const hyprlandService = AstalHyprland.get_default();
|
const hyprlandService = AstalHyprland.get_default();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The MonitorMapper class encapsulates the conversion logic between GDK and Hyprland monitor IDs.
|
* Singleton service that manages the conversion between GDK and Hyprland monitor IDs.
|
||||||
* It maintains internal state for monitors that have already been used so that duplicate assignments are avoided.
|
* Maintains persistent state to ensure consistent monitor mappings across the application lifecycle.
|
||||||
*/
|
*/
|
||||||
export class GdkMonitorService {
|
export class GdkMonitorService {
|
||||||
private _usedGdkMonitors: Set<number>;
|
private static _instance: GdkMonitorService;
|
||||||
private _usedHyprlandMonitors: Set<number>;
|
private _usedHyprlandIds: Set<number>;
|
||||||
|
|
||||||
constructor() {
|
private constructor() {
|
||||||
this._usedGdkMonitors = new Set();
|
this._usedHyprlandIds = new Set();
|
||||||
this._usedHyprlandMonitors = 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 {
|
public reset(): void {
|
||||||
this._usedGdkMonitors.clear();
|
this._usedHyprlandIds.clear();
|
||||||
this._usedHyprlandMonitors.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,16 +51,25 @@ export class GdkMonitorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gdkMonitor = gdkMonitors[monitor];
|
const gdkMonitor = gdkMonitors[monitor];
|
||||||
const hyprlandMonitors = hyprlandService.get_monitors();
|
if (!gdkMonitor) {
|
||||||
|
return monitor;
|
||||||
|
}
|
||||||
|
|
||||||
return this._matchMonitor(
|
const hyprlandMonitors = hyprlandService.get_monitors();
|
||||||
hyprlandMonitors,
|
const validMonitors = hyprlandMonitors.filter((m) => m.model && m.model !== 'null');
|
||||||
|
const tempUsedIds = new Set<number>();
|
||||||
|
const monitorsToUse = validMonitors.length > 0 ? validMonitors : hyprlandMonitors;
|
||||||
|
|
||||||
|
const result = this._matchMonitor(
|
||||||
|
monitorsToUse,
|
||||||
gdkMonitor,
|
gdkMonitor,
|
||||||
monitor,
|
monitor,
|
||||||
this._usedHyprlandMonitors,
|
|
||||||
(mon) => mon.id,
|
(mon) => mon.id,
|
||||||
(mon, gdkMon) => this._matchMonitorKey(mon, gdkMon),
|
(mon, gdkMon) => this._matchMonitorKey(mon, gdkMon),
|
||||||
|
tempUsedIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,79 +93,66 @@ export class GdkMonitorService {
|
|||||||
const foundHyprlandMonitor =
|
const foundHyprlandMonitor =
|
||||||
hyprlandMonitors.find((mon) => mon.id === monitor) || hyprlandMonitors[0];
|
hyprlandMonitors.find((mon) => mon.id === monitor) || hyprlandMonitors[0];
|
||||||
|
|
||||||
|
const tempUsedIds = new Set<number>();
|
||||||
|
|
||||||
return this._matchMonitor(
|
return this._matchMonitor(
|
||||||
gdkCandidates,
|
gdkCandidates,
|
||||||
foundHyprlandMonitor,
|
foundHyprlandMonitor,
|
||||||
monitor,
|
monitor,
|
||||||
this._usedGdkMonitors,
|
|
||||||
(candidate) => candidate.id,
|
(candidate) => candidate.id,
|
||||||
(candidate, hyprlandMonitor) => this._matchMonitorKey(hyprlandMonitor, candidate.monitor),
|
(candidate, hyprlandMonitor) => this._matchMonitorKey(hyprlandMonitor, candidate.monitor),
|
||||||
|
tempUsedIds,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic helper that finds the best matching candidate monitor based on:
|
* 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).
|
* 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).
|
* 2. A relaxed match (candidate matches the source, regardless of id, and hasn't been used).
|
||||||
* 3. A fallback match (first candidate that hasn’t been used).
|
* 3. No fallback - return target to preserve intended mapping.
|
||||||
*
|
*
|
||||||
* @param candidates - Array of candidate monitors.
|
* @param candidates - Array of candidate monitors.
|
||||||
* @param source - The source monitor object to match against.
|
* @param source - The source monitor object to match against.
|
||||||
* @param target - The desired monitor id.
|
* @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 getId - Function to extract the id from a candidate.
|
||||||
* @param compare - Function that determines if a candidate matches the source.
|
* @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.
|
* @returns The chosen monitor id.
|
||||||
*/
|
*/
|
||||||
private _matchMonitor<T, U>(
|
private _matchMonitor<T, U>(
|
||||||
candidates: T[],
|
candidates: T[],
|
||||||
source: U,
|
source: U,
|
||||||
target: number,
|
target: number,
|
||||||
usedMonitors: Set<number>,
|
|
||||||
getId: (candidate: T) => number,
|
getId: (candidate: T) => number,
|
||||||
compare: (candidate: T, source: U) => boolean,
|
compare: (candidate: T, source: U) => boolean,
|
||||||
|
usedIds: Set<number>,
|
||||||
): number {
|
): number {
|
||||||
// Direct match: candidate matches the source and has the same id as the target.
|
const directMatch = candidates.find((candidate) => {
|
||||||
const directMatch = candidates.find(
|
const matches = compare(candidate, source);
|
||||||
(candidate) =>
|
const id = getId(candidate);
|
||||||
compare(candidate, source) &&
|
const isUsed = usedIds.has(id);
|
||||||
!usedMonitors.has(getId(candidate)) &&
|
return matches && id === target && !isUsed;
|
||||||
getId(candidate) === target,
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (directMatch !== undefined) {
|
if (directMatch !== undefined) {
|
||||||
usedMonitors.add(getId(directMatch));
|
const result = getId(directMatch);
|
||||||
return getId(directMatch);
|
usedIds.add(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relaxed match: candidate matches the source regardless of id.
|
const relaxedMatch = candidates.find((candidate) => {
|
||||||
const relaxedMatch = candidates.find(
|
const matches = compare(candidate, source);
|
||||||
(candidate) => compare(candidate, source) && !usedMonitors.has(getId(candidate)),
|
const id = getId(candidate);
|
||||||
);
|
const isUsed = usedIds.has(id);
|
||||||
|
return matches && !isUsed;
|
||||||
|
});
|
||||||
|
|
||||||
if (relaxedMatch !== undefined) {
|
if (relaxedMatch !== undefined) {
|
||||||
usedMonitors.add(getId(relaxedMatch));
|
const result = getId(relaxedMatch);
|
||||||
return 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;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +164,10 @@ export class GdkMonitorService {
|
|||||||
* @returns boolean indicating if the monitors match
|
* @returns boolean indicating if the monitors match
|
||||||
*/
|
*/
|
||||||
private _matchMonitorKey(hyprlandMonitor: AstalHyprland.Monitor, gdkMonitor: GdkMonitor): boolean {
|
private _matchMonitorKey(hyprlandMonitor: AstalHyprland.Monitor, gdkMonitor: GdkMonitor): boolean {
|
||||||
|
if (!hyprlandMonitor.model || hyprlandMonitor.model === 'null') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const isRotated90 = hyprlandMonitor.transform % 2 !== 0;
|
const isRotated90 = hyprlandMonitor.transform % 2 !== 0;
|
||||||
const gdkScaleFactor = Math.ceil(hyprlandMonitor.scale);
|
const gdkScaleFactor = Math.ceil(hyprlandMonitor.scale);
|
||||||
|
|
||||||
@@ -170,16 +183,6 @@ export class GdkMonitorService {
|
|||||||
|
|
||||||
const keyMatch = gdkMonitor.key === gdkScaleFactorKey || gdkMonitor.key === hyprlandScaleFactorKey;
|
const keyMatch = gdkMonitor.key === gdkScaleFactorKey || gdkMonitor.key === hyprlandScaleFactorKey;
|
||||||
|
|
||||||
this._logMonitorInfo(
|
|
||||||
gdkMonitor,
|
|
||||||
hyprlandMonitor,
|
|
||||||
isRotated90,
|
|
||||||
gdkScaleFactor,
|
|
||||||
gdkScaleFactorKey,
|
|
||||||
hyprlandScaleFactorKey,
|
|
||||||
keyMatch,
|
|
||||||
);
|
|
||||||
|
|
||||||
return keyMatch;
|
return keyMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,56 +208,21 @@ export class GdkMonitorService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = curMonitor.get_model() ?? '';
|
try {
|
||||||
const geometry = curMonitor.get_geometry();
|
const model = curMonitor.get_model() ?? '';
|
||||||
const scaleFactor = curMonitor.get_scale_factor();
|
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}`;
|
||||||
const key = `${model}_${geometry.width}x${geometry.height}_${scaleFactor}`;
|
gdkMonitors[i] = { key, model, used: false };
|
||||||
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;
|
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 = {
|
type GdkMonitor = {
|
||||||
|
|||||||
@@ -35,19 +35,6 @@ export class NetworkService {
|
|||||||
return this._instance;
|
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.
|
* Retrieves the appropriate WiFi icon based on the provided icon name.
|
||||||
*
|
*
|
||||||
@@ -67,4 +54,17 @@ export class NetworkService {
|
|||||||
|
|
||||||
return '';
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user