From 19c9d8e045a1ce2946fd09254aa90deaee37a823 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sun, 10 Aug 2025 22:29:19 -0400 Subject: [PATCH] Extension working! --- extension.ts | 8 +- monitorDBusUtils.ts | 310 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 312 insertions(+), 6 deletions(-) create mode 100644 monitorDBusUtils.ts diff --git a/extension.ts b/extension.ts index 9d515ea..f4ac73f 100644 --- a/extension.ts +++ b/extension.ts @@ -1,5 +1,5 @@ import { Extension } from "resource:///org/gnome/shell/extensions/extension.js" -import Gio from "gi://Gio" +import { setMonitorTransform } from "./monitorDBusUtils.js" export default class AutoRotate extends Extension { _listenerId?: number @@ -13,11 +13,7 @@ export default class AutoRotate extends Extension { global.display.get_monitor_in_fullscreen(index) ) != -1 - const { Meta } = imports.gi; - const monitorManager = Meta.MonitorManager.; - if (isFullscreen) { - } else { - } + setMonitorTransform(isFullscreen ? 1 : 0) } ) } diff --git a/monitorDBusUtils.ts b/monitorDBusUtils.ts new file mode 100644 index 0000000..00d4b62 --- /dev/null +++ b/monitorDBusUtils.ts @@ -0,0 +1,310 @@ +// All credit goes to mityax, code taken from here, thank you: https://github.com/mityax/gnome-extension-touchup/blob/main/src/features/screenRotateUtils/floatingScreenRotateButtonFeature.ts + +import Gio from "gi://Gio" +import GLib from "gi://GLib" + +export const Methods = Object.freeze({ + verify: 0, + temporary: 1, + persistent: 2, +}) + +export function callDbusMethod( + method: string, + handler: Gio.AsyncReadyCallback | null, + params: GLib.Variant | null = null +): void { + if (handler !== null && handler !== undefined) { + Gio.DBus.session.call( + "org.gnome.Mutter.DisplayConfig", + "/org/gnome/Mutter/DisplayConfig", + "org.gnome.Mutter.DisplayConfig", + method, + params, + null, + Gio.DBusCallFlags.NONE, + -1, + null, + handler + ) + } else { + Gio.DBus.session.call( + "org.gnome.Mutter.DisplayConfig", + "/org/gnome/Mutter/DisplayConfig", + "org.gnome.Mutter.DisplayConfig", + method, + params, + null, + Gio.DBusCallFlags.NONE, + -1, + null + ) + } +} + +export function setMonitorTransform( + transform: LogicalMonitorTransform, + targetMonitor?: Monitor +): void { + DisplayConfigState.getCurrent().then((state) => { + targetMonitor ??= state.builtinMonitor ?? state.monitors[0] + const logicalMonitor = state.getLogicalMonitorFor( + targetMonitor.connector + ) + if (logicalMonitor) { + logicalMonitor.transform = transform as any + callDbusMethod( + "ApplyMonitorsConfig", + null, + state.packToApply(Methods.temporary) + ) + } + }) +} + +/** + * Possible transform values: + * - 0: normal + * - 1: 90° + * - 2: 180° + * - 3: 270° + * - 4: flipped + * - 5: 90° flipped + * - 6: 180° flipped + * - 7: 270° flipped + */ +export type LogicalMonitorTransform = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 + +export class LogicalMonitor { + x: number + y: number + scale: number + transform: LogicalMonitorTransform + primary: boolean + monitors: [string, string, string, string][] + properties: Record + + constructor(variant: GLib.Variant) { + const unpacked = variant.unpack() as any[] + this.x = unpacked[0].unpack() + this.y = unpacked[1].unpack() + this.scale = unpacked[2].unpack() + this.transform = unpacked[3].unpack() + this.primary = unpacked[4].unpack() + this.monitors = unpacked[5].deep_unpack() + this.properties = unpacked[6].unpack() + + for (const key in this.properties) { + this.properties[key] = this.properties[key].unpack().unpack() + } + } +} + +export class Monitor { + connector: string + vendorName: string + productName: string + productSerial: string + currentModeId: number | null = null + isUnderscanning: boolean = false + isBuiltin: boolean = false + + constructor(variant: GLib.Variant) { + // variant.deepUnpack() yields (in Gnome 48): + // (see for docs: https://gitlab.gnome.org/GNOME/mutter/-/blob/main/data/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml#L385) + // [ + // [ // - [0] - meta information + // "LVDS1", // - [0][0] - connector + // "MetaProducts Inc.", // vendor name + // "MetaMonitor", // product name + // "0xC0FFEE-1" // product serial + // ], + // [ // - [1] - "modes" + // [ + // "1400x1000@60.000", // - [1][0] + // 1400, + // 1000, + // 60, + // 1, + // [ // - [1][1] + // 1, // - [1][1][0] + // 1.25, + // 1.5037593841552734, + // 1.7543859481811523 + // ], + // { + // "is-current": {}, + // "is-preferred": {} + // } + // ] + // ], + // { // - [2] - "props" + // "is-builtin": {}, + // "display-name": {}, + // "is-for-lease": {}, + // "color-mode": {}, + // "supported-color-modes": {} + // } + // ] + + const unpacked = variant.deepUnpack() as any[] + + this.connector = unpacked[0][0] + this.vendorName = unpacked[0][1] + this.productName = unpacked[0][2] + this.productSerial = unpacked[0][3] + + const modes = unpacked[1] + for (const modeVariant of modes) { + const mode = modeVariant + const id = mode[0] + const modeProps = mode[6] + if ("is-current" in modeProps) { + const isCurrent = modeProps["is-current"].get_boolean() + if (isCurrent) { + this.currentModeId = id + break + } + } + } + + const props = unpacked[2] + if ("is-underscanning" in props) { + this.isUnderscanning = props["is-underscanning"].get_boolean() + } + if ("is-builtin" in props) { + this.isBuiltin = props["is-builtin"].get_boolean() + } + } + + /** + * This method makes no guarantees about the returned string, except that it will uniquely + * identify this physical monitor, even after disconnecting and reconnecting it. + * + * This is at the moment done by monitor metadata by constructing a tuple of (vendor name, + * product name, serial). + */ + constructMonitorId(): string { + return `${this.vendorName}::${this.productName}::${this.productSerial}` + } +} + +export class DisplayConfigState { + serial: number + monitors: Monitor[] = [] + logicalMonitors: LogicalMonitor[] = [] + properties: Record + + private constructor(result: GLib.Variant) { + const unpacked = result.unpack() as any[] + this.serial = unpacked[0].unpack() + + const monitorVariants = unpacked[1].unpack() + for (const monitorPacked of monitorVariants) { + this.monitors.push(new Monitor(monitorPacked)) + } + + const logicalMonitorVariants = unpacked[2].unpack() + for (const logicalMonitorPacked of logicalMonitorVariants) { + this.logicalMonitors.push(new LogicalMonitor(logicalMonitorPacked)) + } + + this.properties = unpacked[3].unpack() + for (const key in this.properties) { + this.properties[key] = this.properties[key].unpack().unpack() + } + } + + static async getCurrent(): Promise { + return new Promise((resolve, reject) => { + callDbusMethod("GetCurrentState", (conn, res) => { + try { + const reply = conn?.call_finish(res)! + const configState = new DisplayConfigState(reply) + resolve(configState) + } catch (err) { + reject(err) + } + }) + }) + } + + get builtinMonitor(): Monitor { + return ( + this.monitors.find((monitor) => monitor.isBuiltin) ?? + this.monitors[0] + ) + } + + getMonitor(connector: string): Monitor | null { + return ( + this.monitors.find((monitor) => monitor.connector === connector) || + null + ) + } + + getLogicalMonitorFor(connector: string): LogicalMonitor | null { + return ( + this.logicalMonitors.find((logMonitor) => + logMonitor.monitors.some( + (lmMonitor) => connector === lmMonitor[0] + ) + ) || null + ) + } + + setPrimaryMonitor(monitor: LogicalMonitor) { + this.logicalMonitors.forEach((m) => (m.primary = false)) + monitor.primary = true + + callDbusMethod( + "ApplyMonitorsConfig", + null, + this.packToApply(Methods.temporary) + ) + } + + packToApply(method: number): GLib.Variant { + const packing = [this.serial, method, [], {}] + const logicalMonitors = packing[2] as any[] + const properties: Record = packing[3] + + this.logicalMonitors.forEach((logicalMonitor) => { + const lmonitorPack = [ + logicalMonitor.x, + logicalMonitor.y, + logicalMonitor.scale, + logicalMonitor.transform, + logicalMonitor.primary, + [], + ] + const monitors = lmonitorPack[5] as any[] + for (const logMonitor of logicalMonitor.monitors) { + const connector = logMonitor[0] + const monitor = this.getMonitor(connector) + if (monitor) { + monitors.push([ + connector, + monitor.currentModeId, + { + enable_underscanning: new GLib.Variant( + "b", + monitor.isUnderscanning + ), + }, + ]) + } + } + logicalMonitors.push(lmonitorPack) + }) + + if ("layout-mode" in this.properties) { + properties["layout-mode"] = new GLib.Variant( + "b", + this.properties["layout-mode"] + ) + } + + return new GLib.Variant("(uua(iiduba(ssa{sv}))a{sv})", packing) + } +}