Fix: Improved GDK to Hyprland monitor mapping logic. (#867)
* Feat: Improved GDK<->Hyprland monitor mapping logic. * Update src/components/bar/utils/GdkMonitorMapper.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix type issue. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -34,7 +34,8 @@ import { App, Gtk } from 'astal/gtk3';
|
||||
|
||||
import Astal from 'gi://Astal?version=3.0';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { gdkMonitorIdToHyprlandId, getLayoutForMonitor, isLayoutEmpty } from './utils/monitors';
|
||||
import { getLayoutForMonitor, isLayoutEmpty } from './utils/monitors';
|
||||
import { GdkMonitorMapper } from './utils/GdkMonitorMapper';
|
||||
|
||||
const { layouts } = options.bar;
|
||||
const { location } = options.theme.bar;
|
||||
@@ -68,11 +69,10 @@ const widget = {
|
||||
cava: (): JSX.Element => WidgetContainer(Cava()),
|
||||
};
|
||||
|
||||
export const Bar = (() => {
|
||||
const usedHyprlandMonitors = new Set<number>();
|
||||
const gdkMonitorMapper = new GdkMonitorMapper();
|
||||
|
||||
return (monitor: number): JSX.Element => {
|
||||
const hyprlandMonitor = gdkMonitorIdToHyprlandId(monitor, usedHyprlandMonitors);
|
||||
export const Bar = (monitor: number): JSX.Element => {
|
||||
const hyprlandMonitor = gdkMonitorMapper.mapGdkToHyprland(monitor);
|
||||
|
||||
const computeVisibility = bind(layouts).as(() => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get());
|
||||
@@ -176,4 +176,3 @@ export const Bar = (() => {
|
||||
</window>
|
||||
);
|
||||
};
|
||||
})();
|
||||
|
||||
265
src/components/bar/utils/GdkMonitorMapper.ts
Normal file
265
src/components/bar/utils/GdkMonitorMapper.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import { Gdk } from 'astal/gtk3';
|
||||
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.
|
||||
*/
|
||||
export class GdkMonitorMapper {
|
||||
private usedGdkMonitors: Set<number>;
|
||||
private usedHyprlandMonitors: Set<number>;
|
||||
|
||||
constructor() {
|
||||
this.usedGdkMonitors = new Set();
|
||||
this.usedHyprlandMonitors = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the internal state for both GDK and Hyprland monitor mappings.
|
||||
*/
|
||||
public reset(): void {
|
||||
this.usedGdkMonitors.clear();
|
||||
this.usedHyprlandMonitors.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a GDK monitor id to the corresponding Hyprland monitor id.
|
||||
*
|
||||
* @param monitor The GDK monitor id.
|
||||
* @returns The corresponding Hyprland monitor id.
|
||||
*/
|
||||
public mapGdkToHyprland(monitor: number): number {
|
||||
const gdkMonitors = this._getGdkMonitors();
|
||||
|
||||
if (Object.keys(gdkMonitors).length === 0) {
|
||||
return monitor;
|
||||
}
|
||||
|
||||
const gdkMonitor = gdkMonitors[monitor];
|
||||
const hyprlandMonitors = hyprlandService.get_monitors();
|
||||
|
||||
return this._matchMonitor(
|
||||
hyprlandMonitors,
|
||||
gdkMonitor,
|
||||
monitor,
|
||||
this.usedHyprlandMonitors,
|
||||
(mon) => mon.id,
|
||||
(mon, gdkMon) => this._matchMonitorKey(mon, gdkMon),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Hyprland monitor id to the corresponding GDK monitor id.
|
||||
*
|
||||
* @param monitor The Hyprland monitor id.
|
||||
* @returns The corresponding GDK monitor id.
|
||||
*/
|
||||
public mapHyprlandToGdk(monitor: number): number {
|
||||
const gdkMonitors = this._getGdkMonitors();
|
||||
const gdkCandidates = Object.entries(gdkMonitors).map(([monitorId, monitorMetadata]) => ({
|
||||
id: Number(monitorId),
|
||||
monitor: monitorMetadata,
|
||||
}));
|
||||
|
||||
if (gdkCandidates.length === 0) {
|
||||
return monitor;
|
||||
}
|
||||
|
||||
const hyprlandMonitors = hyprlandService.get_monitors();
|
||||
const foundHyprlandMonitor = hyprlandMonitors.find((mon) => mon.id === monitor) || hyprlandMonitors[0];
|
||||
|
||||
return this._matchMonitor(
|
||||
gdkCandidates,
|
||||
foundHyprlandMonitor,
|
||||
monitor,
|
||||
this.usedGdkMonitors,
|
||||
(candidate) => candidate.id,
|
||||
(candidate, hyprlandMonitor) => this._matchMonitorKey(hyprlandMonitor, candidate.monitor),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*
|
||||
* @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.
|
||||
* @returns The chosen monitor id.
|
||||
*/
|
||||
private _matchMonitor<T, U>(
|
||||
candidates: T[],
|
||||
source: U,
|
||||
target: number,
|
||||
usedMonitors: Set<number>,
|
||||
getId: (candidate: T) => number,
|
||||
compare: (candidate: T, source: U) => boolean,
|
||||
): 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,
|
||||
);
|
||||
|
||||
if (directMatch !== undefined) {
|
||||
usedMonitors.add(getId(directMatch));
|
||||
return getId(directMatch);
|
||||
}
|
||||
|
||||
// Relaxed match: candidate matches the source regardless of id.
|
||||
const relaxedMatch = candidates.find(
|
||||
(candidate) => compare(candidate, source) && !usedMonitors.has(getId(candidate)),
|
||||
);
|
||||
|
||||
if (relaxedMatch !== undefined) {
|
||||
usedMonitors.add(getId(relaxedMatch));
|
||||
return getId(relaxedMatch);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a Hyprland monitor matches a GDK monitor by comparing their keys
|
||||
*
|
||||
* @param hyprlandMonitor - Hyprland monitor object
|
||||
* @param gdkMonitor - GDK monitor object
|
||||
* @returns boolean indicating if the monitors match
|
||||
*/
|
||||
private _matchMonitorKey(hyprlandMonitor: AstalHyprland.Monitor, gdkMonitor: GdkMonitor): boolean {
|
||||
const isRotated90 = hyprlandMonitor.transform % 2 !== 0;
|
||||
const gdkScaleFactor = Math.ceil(hyprlandMonitor.scale);
|
||||
|
||||
const scaleFactorWidth = Math.trunc(hyprlandMonitor.width / gdkScaleFactor);
|
||||
const scaleFactorHeight = Math.trunc(hyprlandMonitor.height / gdkScaleFactor);
|
||||
const gdkScaleFactorKey = `${hyprlandMonitor.model}_${scaleFactorWidth}x${scaleFactorHeight}_${gdkScaleFactor}`;
|
||||
|
||||
const transWidth = isRotated90 ? hyprlandMonitor.height : hyprlandMonitor.width;
|
||||
const transHeight = isRotated90 ? hyprlandMonitor.width : hyprlandMonitor.height;
|
||||
const scaleWidth = Math.trunc(transWidth / hyprlandMonitor.scale);
|
||||
const scaleHeight = Math.trunc(transHeight / hyprlandMonitor.scale);
|
||||
const hyprlandScaleFactorKey = `${hyprlandMonitor.model}_${scaleWidth}x${scaleHeight}_${gdkScaleFactor}`;
|
||||
|
||||
const keyMatch = gdkMonitor.key === gdkScaleFactorKey || gdkMonitor.key === hyprlandScaleFactorKey;
|
||||
|
||||
this._logMonitorInfo(
|
||||
gdkMonitor,
|
||||
hyprlandMonitor,
|
||||
isRotated90,
|
||||
gdkScaleFactor,
|
||||
gdkScaleFactorKey,
|
||||
hyprlandScaleFactorKey,
|
||||
keyMatch,
|
||||
);
|
||||
|
||||
return keyMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all GDK monitors from the default display
|
||||
*
|
||||
* @returns Object containing GDK monitor information indexed by monitor ID
|
||||
*/
|
||||
private _getGdkMonitors(): GdkMonitors {
|
||||
const display = Gdk.Display.get_default();
|
||||
if (display === null) {
|
||||
console.error('Failed to get Gdk display.');
|
||||
return {};
|
||||
}
|
||||
|
||||
const numGdkMonitors = display.get_n_monitors();
|
||||
const gdkMonitors: GdkMonitors = {};
|
||||
|
||||
for (let i = 0; i < numGdkMonitors; i++) {
|
||||
const curMonitor = display.get_monitor(i);
|
||||
if (curMonitor === null) {
|
||||
console.warn(`Monitor at index ${i} is null.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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 = {
|
||||
key: string;
|
||||
model: string;
|
||||
used: boolean;
|
||||
};
|
||||
|
||||
type GdkMonitors = {
|
||||
[key: string]: GdkMonitor;
|
||||
};
|
||||
@@ -1,19 +1,12 @@
|
||||
import { Gdk } from 'astal/gtk3';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { BarLayout, BarLayouts } from 'src/lib/types/options';
|
||||
|
||||
const hyprlandService = AstalHyprland.get_default();
|
||||
|
||||
type GdkMonitor = {
|
||||
key: string;
|
||||
model: string;
|
||||
used: boolean;
|
||||
};
|
||||
|
||||
type GdkMonitors = {
|
||||
[key: string]: GdkMonitor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the bar layout configuration for a specific monitor
|
||||
*
|
||||
* @param monitor - Monitor ID number
|
||||
* @param layouts - Object containing layout configurations for different monitors
|
||||
* @returns BarLayout configuration for the specified monitor, falling back to default if not found
|
||||
*/
|
||||
export const getLayoutForMonitor = (monitor: number, layouts: BarLayouts): BarLayout => {
|
||||
const matchingKey = Object.keys(layouts).find((key) => key === monitor.toString());
|
||||
const wildcard = Object.keys(layouts).find((key) => key === '*');
|
||||
@@ -33,6 +26,12 @@ export const getLayoutForMonitor = (monitor: number, layouts: BarLayouts): BarLa
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a bar layout configuration is empty
|
||||
*
|
||||
* @param layout - Bar layout configuration to check
|
||||
* @returns boolean indicating if all sections of the layout are empty
|
||||
*/
|
||||
export const isLayoutEmpty = (layout: BarLayout): boolean => {
|
||||
const isLeftSectionEmpty = !Array.isArray(layout.left) || layout.left.length === 0;
|
||||
const isRightSectionEmpty = !Array.isArray(layout.right) || layout.right.length === 0;
|
||||
@@ -40,169 +39,3 @@ export const isLayoutEmpty = (layout: BarLayout): boolean => {
|
||||
|
||||
return isLeftSectionEmpty && isRightSectionEmpty && isMiddleSectionEmpty;
|
||||
};
|
||||
|
||||
export function getGdkMonitors(): GdkMonitors {
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
if (display === null) {
|
||||
console.error('Failed to get Gdk display.');
|
||||
return {};
|
||||
}
|
||||
|
||||
const numGdkMonitors = display.get_n_monitors();
|
||||
const gdkMonitors: GdkMonitors = {};
|
||||
|
||||
for (let i = 0; i < numGdkMonitors; i++) {
|
||||
const curMonitor = display.get_monitor(i);
|
||||
|
||||
if (curMonitor === null) {
|
||||
console.warn(`Monitor at index ${i} is null.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const model = curMonitor.get_model() || '';
|
||||
const geometry = curMonitor.get_geometry();
|
||||
const scaleFactor = curMonitor.get_scale_factor();
|
||||
|
||||
// We can only use the scaleFactor for a scale variable in the key
|
||||
// GDK3 doesn't support the fractional "scale" attribute (available in GDK4)
|
||||
const key = `${model}_${geometry.width}x${geometry.height}_${scaleFactor}`;
|
||||
gdkMonitors[i] = { key, model, used: false };
|
||||
}
|
||||
|
||||
return gdkMonitors;
|
||||
}
|
||||
|
||||
export function matchMonitorKey(hypMon: AstalHyprland.Monitor, gdkMonitor: GdkMonitor): boolean {
|
||||
const isRotated90 = hypMon.transform % 2 !== 0;
|
||||
|
||||
// Needed for the key regardless of scaling below because GDK3 only has the scale factor for the key
|
||||
const gdkScaleFactor = Math.ceil(hypMon.scale);
|
||||
|
||||
// When gdk is scaled with the scale factor, the hyprland width/height will be the same as the base monitor resolution
|
||||
// The GDK width/height will NOT flip regardless of transformation (e.g. 90 degrees will NOT swap the GDK width/height)
|
||||
const scaleFactorWidth = Math.trunc(hypMon.width / gdkScaleFactor);
|
||||
const scaleFactorHeight = Math.trunc(hypMon.height / gdkScaleFactor);
|
||||
const scaleFactorKey = `${hypMon.model}_${scaleFactorWidth}x${scaleFactorHeight}_${gdkScaleFactor}`;
|
||||
|
||||
// When gdk geometry is scaled with the fractional scale, we need to scale the hyprland geometry to match it
|
||||
// However a 90 degree transformation WILL flip the GDK width/height
|
||||
const transWidth = isRotated90 ? hypMon.height : hypMon.width;
|
||||
const transHeight = isRotated90 ? hypMon.width : hypMon.height;
|
||||
const scaleWidth = Math.trunc(transWidth / hypMon.scale);
|
||||
const scaleHeight = Math.trunc(transHeight / hypMon.scale);
|
||||
const scaleKey = `${hypMon.model}_${scaleWidth}x${scaleHeight}_${gdkScaleFactor}`;
|
||||
|
||||
// In GDK3 the GdkMonitor geometry can change depending on how the compositor handles scaling surface framebuffers
|
||||
// We try to match against two different possibilities:
|
||||
// 1) The geometry is scaled by the correct fractional scale
|
||||
// 2) The geometry is scaled by the scaleFactor (the fractional scale rounded up)
|
||||
const keyMatch = gdkMonitor.key === scaleFactorKey || gdkMonitor.key === scaleKey;
|
||||
|
||||
// Monitor matching debug logging, use if your workspaces are appearing on the wrong screen
|
||||
// To use, kill any running HyprPanel instances and then start a terminal, then run:
|
||||
// G_MESSAGES_DEBUG=all hyprpanel | grep "hyprpanel-DEBUG"
|
||||
// Create an issue in HyprPanel github and post these logs
|
||||
console.debug('Attempting gdk key match');
|
||||
console.debug(`GDK key: ${gdkMonitor.key}`);
|
||||
console.debug(`HypMon.width: ${hypMon.width}`);
|
||||
console.debug(`HypMon.height: ${hypMon.height}`);
|
||||
console.debug(`HypMon.scale: ${hypMon.scale}`);
|
||||
console.debug(`HypMon.transform: ${hypMon.transform}`);
|
||||
console.debug(`isRotated90: ${isRotated90}`);
|
||||
console.debug(`scaleFactor: ${gdkScaleFactor}`);
|
||||
console.debug(`scaleFactorKey: ${scaleFactorKey}`);
|
||||
console.debug(`scaleKey: ${scaleKey}`);
|
||||
console.debug(`match?: ${keyMatch}`);
|
||||
|
||||
return keyMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: Some more funky stuff being done by GDK.
|
||||
* We render windows/bar based on the monitor ID. So if you have 3 monitors, then your
|
||||
* monitor IDs will be [0, 1, 2]. Hyprland will NEVER change what ID belongs to what monitor.
|
||||
*
|
||||
* So if hyprland determines id 0 = DP-1, even after you unplug, shut off or restart your monitor,
|
||||
* the id 0 will ALWAYS be DP-1.
|
||||
*
|
||||
* However, GDK (the righteous genius that it is) will change the order of ID anytime your monitor
|
||||
* setup is changed. So if you unplug your monitor and plug it back it, it now becomes the last id.
|
||||
* So if DP-1 was id 0 and you unplugged it, it will reconfigure to id 2. This sucks because now
|
||||
* there's a mismtach between what GDK determines the monitor is at id 2 and what Hyprland determines
|
||||
* is at id 2.
|
||||
*
|
||||
* So for that reason, we need to redirect the input `monitor` that the Bar module takes in, to the
|
||||
* proper Hyprland monitor. So when monitor id 0 comes in, we need to find what the id of that monitor
|
||||
* is being determined as by Hyprland so the bars show up on the right monitors.
|
||||
*
|
||||
* Since GTK3 doesn't contain connection names and only monitor models, we have to make the best guess
|
||||
* in the case that there are multiple models in the same resolution with the same scale. We find the
|
||||
* 'right' monitor by checking if the model matches along with the resolution and scale. If monitor at
|
||||
* ID 0 for GDK is being reported as 'MSI MAG271CQR' we find the same model in the Hyprland monitor list
|
||||
* and check if the resolution and scaling is the same... if it is then we determine it's a match.
|
||||
*
|
||||
* The edge-case that we just can't handle is if you have the same monitors in the same resolution at the same
|
||||
* scale. So if you've got 2 'MSI MAG271CQR' monitors at 2560x1440 at scale 1, then we just match the first
|
||||
* monitor in the list as the first match and then the second 'MSI MAG271CQR' as a match in the 2nd iteration.
|
||||
* You may have the bar showing up on the wrong one in this case because we don't know what the connector id
|
||||
* is of either of these monitors (DP-1, DP-2) which are unique values - as these are only in GTK4.
|
||||
*
|
||||
* Keep in mind though, this is ONLY an issue if you change your monitor setup by plugging in a new one, restarting
|
||||
* an existing one or shutting it off.
|
||||
*
|
||||
* If your monitors aren't changed in the current session you're in then none of this safeguarding is relevant.
|
||||
*
|
||||
* Fun stuff really... :facepalm:
|
||||
*/
|
||||
|
||||
export const gdkMonitorIdToHyprlandId = (monitor: number, usedHyprlandMonitors: Set<number>): number => {
|
||||
const gdkMonitors = getGdkMonitors();
|
||||
|
||||
if (Object.keys(gdkMonitors).length === 0) {
|
||||
return monitor;
|
||||
}
|
||||
|
||||
// Get the GDK monitor for the given monitor index
|
||||
const gdkMonitor = gdkMonitors[monitor];
|
||||
|
||||
// First pass: Strict matching including the monitor index (i.e., hypMon.id === monitor + resolution+scale criteria)
|
||||
const directMatch = hyprlandService.get_monitors().find((hypMon) => {
|
||||
return matchMonitorKey(hypMon, gdkMonitor) && !usedHyprlandMonitors.has(hypMon.id) && hypMon.id === monitor;
|
||||
});
|
||||
|
||||
if (directMatch) {
|
||||
usedHyprlandMonitors.add(directMatch.id);
|
||||
return directMatch.id;
|
||||
}
|
||||
|
||||
// Second pass: Relaxed matching without considering the monitor index
|
||||
const hyprlandMonitor = hyprlandService.get_monitors().find((hypMon) => {
|
||||
return matchMonitorKey(hypMon, gdkMonitor) && !usedHyprlandMonitors.has(hypMon.id);
|
||||
});
|
||||
|
||||
if (hyprlandMonitor) {
|
||||
usedHyprlandMonitors.add(hyprlandMonitor.id);
|
||||
return hyprlandMonitor.id;
|
||||
}
|
||||
|
||||
// Fallback: Find the first available monitor ID that hasn't been used
|
||||
const fallbackMonitor = hyprlandService.get_monitors().find((hypMon) => !usedHyprlandMonitors.has(hypMon.id));
|
||||
|
||||
if (fallbackMonitor) {
|
||||
usedHyprlandMonitors.add(fallbackMonitor.id);
|
||||
return fallbackMonitor.id;
|
||||
}
|
||||
|
||||
// Ensure we return a valid monitor ID that actually exists
|
||||
for (let i = 0; i < hyprlandService.get_monitors().length; i++) {
|
||||
if (!usedHyprlandMonitors.has(i)) {
|
||||
usedHyprlandMonitors.add(i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// As a last resort, return the original monitor index if no unique monitor can be found
|
||||
console.warn(`Returning original monitor index as a last resort: ${monitor}`);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Astal } from 'astal/gtk3';
|
||||
import { NotificationCard } from './Notification.js';
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { GdkMonitorMapper } from '../bar/utils/GdkMonitorMapper';
|
||||
|
||||
const hyprlandService = AstalHyprland.get_default();
|
||||
const { position, monitor, active_monitor, showActionsOnHover, displayedTotal } = options.notifications;
|
||||
@@ -19,15 +20,22 @@ trackPopupNotifications(popupNotifications);
|
||||
trackAutoTimeout();
|
||||
|
||||
export default (): JSX.Element => {
|
||||
const gdkMonitorMapper = new GdkMonitorMapper();
|
||||
|
||||
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) {
|
||||
return focusedMonitor.id;
|
||||
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(focusedMonitor.id);
|
||||
return gdkMonitor;
|
||||
}
|
||||
return monitor;
|
||||
|
||||
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(monitor);
|
||||
return gdkMonitor;
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||
import options from 'src/options';
|
||||
import Brightness from 'src/services/Brightness';
|
||||
import { GdkMonitorMapper } from '../bar/utils/GdkMonitorMapper';
|
||||
|
||||
const wireplumber = AstalWp.get_default() as AstalWp.Wp;
|
||||
const audioService = wireplumber.audio;
|
||||
@@ -59,14 +60,20 @@ export const handleReveal = (self: Widget.Revealer): void => {
|
||||
* @returns A Variable<number> representing the monitor index for the OSD.
|
||||
*/
|
||||
export const getOsdMonitor = (): Variable<number> => {
|
||||
const gdkMonitorMapper = new GdkMonitorMapper();
|
||||
|
||||
return Variable.derive(
|
||||
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
|
||||
(currentMonitor, defaultMonitor, followMonitor) => {
|
||||
gdkMonitorMapper.reset();
|
||||
|
||||
if (followMonitor === true) {
|
||||
return currentMonitor.id;
|
||||
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(currentMonitor.id);
|
||||
return gdkMonitor;
|
||||
}
|
||||
|
||||
return defaultMonitor;
|
||||
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(defaultMonitor);
|
||||
return gdkMonitor;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import FontButton from 'src/components/shared/FontButton';
|
||||
import { Opt } from 'src/lib/option';
|
||||
import { styleToString } from './utils';
|
||||
import { FontStyle, styleToString } from './utils';
|
||||
|
||||
export const FontInputter = <T extends string | number | boolean | object>({
|
||||
fontFamily,
|
||||
@@ -38,6 +38,6 @@ export const FontInputter = <T extends string | number | boolean | object>({
|
||||
|
||||
interface FontInputterProps<T> {
|
||||
fontFamily: Opt<T>;
|
||||
fontStyle?: Opt<string>;
|
||||
fontStyle?: Opt<FontStyle>;
|
||||
fontLabel?: Opt<string>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user