From 0898c98f9c21b17b01af37f7b58eca7fac367d2d Mon Sep 17 00:00:00 2001 From: Jas Singh Date: Sat, 10 Aug 2024 18:34:55 -0700 Subject: [PATCH] Monitor identification update. (#107) * Add proper gtk to hyprland monitor mapping * Updated the monitor identification logic to sync GDK monitor IDs properly with hyprland monitor IDs. * Remove console statement. * Revert this to how it was before since its exactly the same thing. --- lib/utils.ts | 8 +- modules/bar/Bar.ts | 229 +++++++++++++++++++++++++++------- modules/menus/DropdownMenu.ts | 27 ++-- 3 files changed, 205 insertions(+), 59 deletions(-) diff --git a/lib/utils.ts b/lib/utils.ts index f63f3f3..10446b9 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -54,8 +54,8 @@ export async function sh(cmd: string | string[]) { } export function forMonitors(widget: (monitor: number) => Gtk.Window) { - const n = Gdk.Display.get_default()?.get_n_monitors() || 1 - return range(n, 0).flatMap(widget) + const n = Gdk.Display.get_default()?.get_n_monitors() || 1; + return range(n, 0).flatMap(widget); } /** @@ -148,7 +148,7 @@ export const Notify = (notifPayload: NotificationArgs): void => { Utils.execAsync(command) } -export function getPosition (pos: NotificationAnchor | OSDAnchor): ("top" | "bottom" | "left" | "right")[] { +export function getPosition(pos: NotificationAnchor | OSDAnchor): ("top" | "bottom" | "left" | "right")[] { const positionMap: { [key: string]: ("top" | "bottom" | "left" | "right")[] } = { "top": ["top"], "top right": ["top", "right"], @@ -161,4 +161,4 @@ export function getPosition (pos: NotificationAnchor | OSDAnchor): ("top" | "bot }; return positionMap[pos] || ["top"]; -} \ No newline at end of file +} diff --git a/modules/bar/Bar.ts b/modules/bar/Bar.ts index 775b99b..65f06e8 100644 --- a/modules/bar/Bar.ts +++ b/modules/bar/Bar.ts @@ -13,6 +13,7 @@ const hyprland = await Service.import("hyprland"); import { BarItemBox as WidgetContainer } from "../shared/barItemBox.js"; import options from "options"; +import Gdk from "gi://Gdk?version=3.0"; const { layouts } = options.bar; @@ -85,50 +86,186 @@ const widget = { systray: () => WidgetContainer(SysTray()), }; -export const Bar = (monitor: number) => { - return Widget.Window({ - name: `bar-${monitor}`, - class_name: "bar", - monitor, - visible: true, - anchor: ["top", "left", "right"], - exclusivity: "exclusive", - child: Widget.Box({ - class_name: 'bar-panel-container', - child: Widget.CenterBox({ - class_name: 'bar-panel', - css: 'padding: 1px', - startWidget: Widget.Box({ - class_name: "box-left", - hexpand: true, - setup: self => { - self.hook(layouts, (self) => { - const foundLayout = getModulesForMonitor(monitor, layouts.value as BarLayout) - self.children = foundLayout.left.filter(mod => Object.keys(widget).includes(mod)).map(w => widget[w](monitor)); - }) - }, - }), - centerWidget: Widget.Box({ - class_name: "box-center", - hpack: "center", - setup: self => { - self.hook(layouts, (self) => { - const foundLayout = getModulesForMonitor(monitor, layouts.value as BarLayout) - self.children = foundLayout.middle.filter(mod => Object.keys(widget).includes(mod)).map(w => widget[w](monitor)); - }) - }, - }), - endWidget: Widget.Box({ - class_name: "box-right", - hpack: "end", - setup: self => { - self.hook(layouts, (self) => { - const foundLayout = getModulesForMonitor(monitor, layouts.value as BarLayout) - self.children = foundLayout.right.filter(mod => Object.keys(widget).includes(mod)).map(w => widget[w](monitor)); - }) - }, - }), - }) - }) - }); +type GdkMonitors = { + [key: string]: { + key: string, + model: string, + used: boolean + } }; + +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 = {}; + + 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(); + + const key = `${model}_${geometry.width}x${geometry.height}_${scaleFactor}`; + gdkMonitors[i] = { key, model, used: false }; + } + + return gdkMonitors; +} + +/** + * 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: + */ + +const gdkMonitorIdToHyprlandId = (monitor: number, usedHyprlandMonitors: Set): number => { + const gdkMonitors = getGdkMonitors(); + + if (Object.keys(gdkMonitors).length === 0) { + console.error("No GDK monitors were found."); + 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 = hyprland.monitors.find(hypMon => { + const hyprlandKey = `${hypMon.model}_${hypMon.width}x${hypMon.height}_${hypMon.scale}`; + return gdkMonitor.key.startsWith(hyprlandKey) && !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 = hyprland.monitors.find(hypMon => { + const hyprlandKey = `${hypMon.model}_${hypMon.width}x${hypMon.height}_${hypMon.scale}`; + return gdkMonitor.key.startsWith(hyprlandKey) && !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 = hyprland.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 < hyprland.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; +}; + +export const Bar = (() => { + const usedHyprlandMonitors = new Set(); + + return (monitor: number) => { + const hyprlandMonitor = gdkMonitorIdToHyprlandId(monitor, usedHyprlandMonitors); + + return Widget.Window({ + name: `bar-${hyprlandMonitor}`, + class_name: "bar", + monitor, + visible: true, + anchor: ["top", "left", "right"], + exclusivity: "exclusive", + child: Widget.Box({ + class_name: 'bar-panel-container', + child: Widget.CenterBox({ + class_name: 'bar-panel', + css: 'padding: 1px', + startWidget: Widget.Box({ + class_name: "box-left", + hexpand: true, + setup: self => { + self.hook(layouts, (self) => { + const foundLayout = getModulesForMonitor(hyprlandMonitor, layouts.value as BarLayout); + self.children = foundLayout.left.filter(mod => Object.keys(widget).includes(mod)).map(w => widget[w](hyprlandMonitor)); + }); + }, + }), + centerWidget: Widget.Box({ + class_name: "box-center", + hpack: "center", + setup: self => { + self.hook(layouts, (self) => { + const foundLayout = getModulesForMonitor(hyprlandMonitor, layouts.value as BarLayout); + self.children = foundLayout.middle.filter(mod => Object.keys(widget).includes(mod)).map(w => widget[w](hyprlandMonitor)); + }); + }, + }), + endWidget: Widget.Box({ + class_name: "box-right", + hpack: "end", + setup: self => { + self.hook(layouts, (self) => { + const foundLayout = getModulesForMonitor(hyprlandMonitor, layouts.value as BarLayout); + self.children = foundLayout.right.filter(mod => Object.keys(widget).includes(mod)).map(w => widget[w](hyprlandMonitor)); + }); + }, + }), + }), + }), + }); + }; +})(); diff --git a/modules/menus/DropdownMenu.ts b/modules/menus/DropdownMenu.ts index 0526b15..ef3481b 100644 --- a/modules/menus/DropdownMenu.ts +++ b/modules/menus/DropdownMenu.ts @@ -17,11 +17,16 @@ const moveBoxToCursor = (self: any, fixed: boolean) => { } globalMousePos.connect("changed", ({ value }) => { - const hyprScaling = hyprland.monitors[hyprland.active.monitor.id].scale; - const currentWidth = self.child.get_allocation().width; + const curHyprlandMonitor = hyprland.monitors.find(m => m.id === hyprland.active.monitor.id); + const dropdownWidth = self.child.get_allocation().width; - let monWidth = hyprland.monitors[hyprland.active.monitor.id].width; - let monHeight = hyprland.monitors[hyprland.active.monitor.id].height; + const hyprScaling = curHyprlandMonitor?.scale; + let monWidth = curHyprlandMonitor?.width; + let monHeight = curHyprlandMonitor?.height; + + if (monWidth === undefined || monHeight === undefined || hyprScaling === undefined) { + return; + } // If GDK Scaling is applied, then get divide width by scaling // to get the proper coordinates. @@ -39,24 +44,28 @@ const moveBoxToCursor = (self: any, fixed: boolean) => { } // If monitor is vertical (transform = 1 || 3) swap height and width - if (hyprland.monitors[hyprland.active.monitor.id].transform % 2 !== 0) { + const isVertical = curHyprlandMonitor?.transform !== undefined + ? curHyprlandMonitor.transform % 2 !== 0 + : false; + + if (isVertical) { [monWidth, monHeight] = [monHeight, monWidth]; } - let marginRight = monWidth - currentWidth / 2; + let marginRight = monWidth - dropdownWidth / 2; marginRight = fixed ? marginRight - monWidth / 2 : marginRight - value[0]; - let marginLeft = monWidth - currentWidth - marginRight; + let marginLeft = monWidth - dropdownWidth - marginRight; const minimumMargin = 0; if (marginRight < minimumMargin) { marginRight = minimumMargin; - marginLeft = monWidth - currentWidth - minimumMargin; + marginLeft = monWidth - dropdownWidth - minimumMargin; } if (marginLeft < minimumMargin) { marginLeft = minimumMargin; - marginRight = monWidth - currentWidth - minimumMargin; + marginRight = monWidth - dropdownWidth - minimumMargin; } const marginTop = 45;