From b6b58edf76b3f4c30bca96a403efbbc5c975e56e Mon Sep 17 00:00:00 2001 From: Jas Singh Date: Fri, 28 Mar 2025 01:52:25 -0700 Subject: [PATCH] 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> --- src/components/bar/index.tsx | 193 +++++++------ src/components/bar/utils/GdkMonitorMapper.ts | 265 ++++++++++++++++++ src/components/bar/utils/monitors.ts | 193 +------------ src/components/notifications/index.tsx | 12 +- src/components/osd/helpers.ts | 11 +- .../settings/shared/inputs/font/index.tsx | 4 +- 6 files changed, 395 insertions(+), 283 deletions(-) create mode 100644 src/components/bar/utils/GdkMonitorMapper.ts diff --git a/src/components/bar/index.tsx b/src/components/bar/index.tsx index eb9aea5..12f79b2 100644 --- a/src/components/bar/index.tsx +++ b/src/components/bar/index.tsx @@ -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,112 +69,110 @@ const widget = { cava: (): JSX.Element => WidgetContainer(Cava()), }; -export const Bar = (() => { - const usedHyprlandMonitors = new Set(); +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()); - return !isLayoutEmpty(foundLayout); - }); + const computeVisibility = bind(layouts).as(() => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get()); + return !isLayoutEmpty(foundLayout); + }); - const computeClassName = bind(layouts).as(() => { - const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get()); - return !isLayoutEmpty(foundLayout) ? `bar` : ''; - }); + const computeClassName = bind(layouts).as(() => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get()); + return !isLayoutEmpty(foundLayout) ? `bar` : ''; + }); - const computeAnchor = bind(location).as((loc) => { - if (loc === 'bottom') { - return Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT; - } + const computeAnchor = bind(location).as((loc) => { + if (loc === 'bottom') { + return Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT; + } - return Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT; - }); + return Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT; + }); - const computeLayer = Variable.derive([bind(options.theme.bar.layer), bind(options.tear)], (barLayer, tear) => { - if (tear && barLayer === 'overlay') { - return Astal.Layer.TOP; - } - const layerMap = { - overlay: Astal.Layer.OVERLAY, - top: Astal.Layer.TOP, - bottom: Astal.Layer.BOTTOM, - background: Astal.Layer.BACKGROUND, - }; + const computeLayer = Variable.derive([bind(options.theme.bar.layer), bind(options.tear)], (barLayer, tear) => { + if (tear && barLayer === 'overlay') { + return Astal.Layer.TOP; + } + const layerMap = { + overlay: Astal.Layer.OVERLAY, + top: Astal.Layer.TOP, + bottom: Astal.Layer.BOTTOM, + background: Astal.Layer.BACKGROUND, + }; - return layerMap[barLayer]; - }); + return layerMap[barLayer]; + }); - const computeBorderLocation = bind(borderLocation).as((brdrLcn) => - brdrLcn !== 'none' ? 'bar-panel withBorder' : 'bar-panel', - ); + const computeBorderLocation = bind(borderLocation).as((brdrLcn) => + brdrLcn !== 'none' ? 'bar-panel withBorder' : 'bar-panel', + ); - const leftBinding = Variable.derive([bind(layouts)], (currentLayouts) => { - const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); + const leftBinding = Variable.derive([bind(layouts)], (currentLayouts) => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); - return foundLayout.left - .filter((mod) => Object.keys(widget).includes(mod)) - .map((w) => widget[w](hyprlandMonitor)); - }); - const middleBinding = Variable.derive([bind(layouts)], (currentLayouts) => { - const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); + return foundLayout.left + .filter((mod) => Object.keys(widget).includes(mod)) + .map((w) => widget[w](hyprlandMonitor)); + }); + const middleBinding = Variable.derive([bind(layouts)], (currentLayouts) => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); - return foundLayout.middle - .filter((mod) => Object.keys(widget).includes(mod)) - .map((w) => widget[w](hyprlandMonitor)); - }); - const rightBinding = Variable.derive([bind(layouts)], (currentLayouts) => { - const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); + return foundLayout.middle + .filter((mod) => Object.keys(widget).includes(mod)) + .map((w) => widget[w](hyprlandMonitor)); + }); + const rightBinding = Variable.derive([bind(layouts)], (currentLayouts) => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); - return foundLayout.right - .filter((mod) => Object.keys(widget).includes(mod)) - .map((w) => widget[w](hyprlandMonitor)); - }); + return foundLayout.right + .filter((mod) => Object.keys(widget).includes(mod)) + .map((w) => widget[w](hyprlandMonitor)); + }); - return ( - { - computeLayer.drop(); - leftBinding.drop(); - middleBinding.drop(); - rightBinding.drop(); - }} - > - - - {leftBinding()} - - } - centerWidget={ - - {middleBinding()} - - } - endWidget={ - - {rightBinding()} - - } - /> - - - ); - }; -})(); + return ( + { + computeLayer.drop(); + leftBinding.drop(); + middleBinding.drop(); + rightBinding.drop(); + }} + > + + + {leftBinding()} + + } + centerWidget={ + + {middleBinding()} + + } + endWidget={ + + {rightBinding()} + + } + /> + + + ); +}; diff --git a/src/components/bar/utils/GdkMonitorMapper.ts b/src/components/bar/utils/GdkMonitorMapper.ts new file mode 100644 index 0000000..9d0485a --- /dev/null +++ b/src/components/bar/utils/GdkMonitorMapper.ts @@ -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; + private usedHyprlandMonitors: Set; + + 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( + candidates: T[], + source: U, + target: number, + usedMonitors: Set, + 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; +}; diff --git a/src/components/bar/utils/monitors.ts b/src/components/bar/utils/monitors.ts index c18e298..2ad2a60 100644 --- a/src/components/bar/utils/monitors.ts +++ b/src/components/bar/utils/monitors.ts @@ -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 => { - 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; -}; diff --git a/src/components/notifications/index.tsx b/src/components/notifications/index.tsx index 1fa129f..e335de0 100644 --- a/src/components/notifications/index.tsx +++ b/src/components/notifications/index.tsx @@ -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; }, ); diff --git a/src/components/osd/helpers.ts b/src/components/osd/helpers.ts index e62e486..77d88b8 100644 --- a/src/components/osd/helpers.ts +++ b/src/components/osd/helpers.ts @@ -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 representing the monitor index for the OSD. */ export const getOsdMonitor = (): Variable => { + 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; }, ); }; diff --git a/src/components/settings/shared/inputs/font/index.tsx b/src/components/settings/shared/inputs/font/index.tsx index 119fcd6..496aef4 100644 --- a/src/components/settings/shared/inputs/font/index.tsx +++ b/src/components/settings/shared/inputs/font/index.tsx @@ -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 = ({ fontFamily, @@ -38,6 +38,6 @@ export const FontInputter = ({ interface FontInputterProps { fontFamily: Opt; - fontStyle?: Opt; + fontStyle?: Opt; fontLabel?: Opt; }