diff --git a/Makefile b/Makefile index 651f077..e3321ac 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -UUID = "forge@jmmaranan.com" +UUID = forge@jmmaranan.com INSTALL_PATH = $(HOME)/.local/share/gnome-shell/extensions/$(UUID) MSGSRC = $(wildcard po/*.po) @@ -26,7 +26,9 @@ patchcss: metadata: echo "export const developers = Object.entries([" > lib/prefs/metadata.js - git shortlog -sne || echo "" >> lib/prefs/metadata.js + if git rev-parse HEAD >/dev/null 2>&1; then \ + git --no-pager shortlog -sne HEAD > lib/prefs/metadata.js; \ + fi awk -i inplace '!/dependabot|noreply/' lib/prefs/metadata.js sed -i 's/^[[:space:]]*[0-9]*[[:space:]]*\(.*\) <\(.*\)>/ {name:"\1", email:"\2"},/g' lib/prefs/metadata.js echo "].reduce((acc, x) => ({ ...acc, [x.email]: acc[x.email] ?? x.name }), {})).map(([email, name]) => name + ' <' + email + '>')" >> lib/prefs/metadata.js @@ -70,15 +72,22 @@ compilemsgs: potfile $(MSGSRC:.po=.mo) clean: rm -f lib/prefs/metadata.js - rm "$(UUID).zip" || echo "Nothing to delete" + rm -f "$(UUID).zip" rm -rf temp schemas/gschemas.compiled enable: - gnome-extensions enable "$(UUID)" + if command -v gnome-extensions >/dev/null 2>&1; then \ + gnome-extensions enable "$(UUID)" || echo "Extension "$(UUID)" could not be enabled"; \ + else \ + echo "gnome-extensions CLI is not available; skipping enable"; \ + fi disable: - gnome-extensions disable "$(UUID)" - + if command -v gnome-extensions >/dev/null 2>&1; then \ + gnome-extensions disable "$(UUID)" || echo "Extension "$(UUID)" is not installed"; \ + else \ + echo "gnome-extensions CLI is not available; skipping disable"; \ + fi install: mkdir -p $(INSTALL_PATH) cp -r temp/* $(INSTALL_PATH) @@ -93,14 +102,24 @@ dist: build zip -qr "../${UUID}.zip" . restart: - if bash -c 'xprop -root &> /dev/null'; then \ - killall -HUP gnome-shell; \ - else \ + if command -v xprop >/dev/null 2>&1 && bash -c 'xprop -root &> /dev/null'; then \ + if command -v killall >/dev/null 2>&1; then \ + killall -HUP gnome-shell || echo "Failed to signal gnome-shell"; \ + else \ + echo "killall not available; skipping gnome-shell restart"; \ + fi; \ + elif command -v gnome-session-quit >/dev/null 2>&1; then \ gnome-session-quit --logout; \ + else \ + echo "No GNOME restart command available; skipping restart"; \ fi log: - journalctl -o cat -n 0 -f "$$(which gnome-shell)" | grep -v -E 'warning|g_variant' + if command -v gnome-shell >/dev/null 2>&1; then \ + journalctl -o cat -n 0 -f "$$(command -v gnome-shell)" | grep -v -E 'warning|g_variant'; \ + else \ + echo "gnome-shell is not available; skipping journal log"; \ + fi journal: journalctl -b 0 -r --since "1 hour ago" diff --git a/lib/extension/window.js b/lib/extension/window.js index 24356f5..ec5430a 100644 --- a/lib/extension/window.js +++ b/lib/extension/window.js @@ -1172,16 +1172,117 @@ export class WindowManager extends GObject.Object { } processFloats() { + const minimumTiledWindows = Math.max(1, this.ext.settings.get_uint("minimum-tiled-windows")); + const ultrawideLimits = this._getUltrawideLimits(); + const workspaceCounts = this._countWorkspaceTileCandidates(); + this.allNodeWindows.forEach((nodeWindow) => { - let metaWindow = nodeWindow.nodeValue; - if (this.isFloatingExempt(metaWindow) || !this.isActiveWindowWorkspaceTiled(metaWindow)) { + const metaWindow = nodeWindow.nodeValue; + if (!metaWindow) return; + + const workspace = metaWindow.get_workspace(); + const workspaceIndex = workspace ? workspace.index() : null; + const eligibleCount = + workspaceIndex !== null && workspaceCounts.has(workspaceIndex) + ? workspaceCounts.get(workspaceIndex) + : 0; + const workspaceTiled = this.isActiveWindowWorkspaceTiled(metaWindow); + const floatingExempt = this.isFloatingExempt(metaWindow); + const meetsMinimum = eligibleCount >= minimumTiledWindows; + + if (floatingExempt || !workspaceTiled || !meetsMinimum) { + const enforceLimits = !floatingExempt && workspaceTiled && !meetsMinimum; nodeWindow.float = true; + if (enforceLimits) { + this._enforceUltrawideSize(metaWindow, ultrawideLimits); + } } else { nodeWindow.float = false; } }); } + _countWorkspaceTileCandidates() { + const counts = new Map(); + + this.allNodeWindows.forEach((nodeWindow) => { + const metaWindow = nodeWindow.nodeValue; + if (!this._shouldCountForTiling(metaWindow)) return; + + const workspace = metaWindow.get_workspace(); + if (!workspace) return; + + const workspaceIndex = workspace.index(); + const currentCount = counts.has(workspaceIndex) ? counts.get(workspaceIndex) : 0; + counts.set(workspaceIndex, currentCount + 1); + }); + + return counts; + } + + _shouldCountForTiling(metaWindow) { + if (!metaWindow) return false; + if (metaWindow.minimized) return false; + if (!this.isActiveWindowWorkspaceTiled(metaWindow)) return false; + if (this.isFloatingExempt(metaWindow)) return false; + return true; + } + + _getUltrawideLimits() { + return { + monitorWidth: this.ext.settings.get_uint("ultrawide-monitor-width"), + maxWidth: this.ext.settings.get_uint("ultrawide-max-window-width"), + maxHeight: this.ext.settings.get_uint("ultrawide-max-window-height"), + }; + } + + _enforceUltrawideSize(metaWindow, limits) { + if (!limits) return; + const { monitorWidth, maxWidth, maxHeight } = limits; + + if (!monitorWidth || !maxWidth || !maxHeight) return; + + const monitorIndex = metaWindow.get_monitor(); + if (monitorIndex < 0) return; + const monitorRect = global.display.get_monitor_geometry(monitorIndex); + if (!monitorRect || monitorRect.width < monitorWidth) return; + + const workArea = metaWindow.get_work_area_current_monitor(); + if (!workArea) return; + + const targetWidth = Math.min(maxWidth, workArea.width); + const targetHeight = Math.min(maxHeight, workArea.height); + if (targetWidth <= 0 || targetHeight <= 0) return; + + const frameRect = metaWindow.get_frame_rect(); + const width = Math.min(frameRect.width, targetWidth); + const height = Math.min(frameRect.height, targetHeight); + + let x = frameRect.x; + let y = frameRect.y; + + const maxX = workArea.x + workArea.width - width; + const maxY = workArea.y + workArea.height - height; + x = Math.min(Math.max(x, workArea.x), maxX); + y = Math.min(Math.max(y, workArea.y), maxY); + + if ( + frameRect.width === width && + frameRect.height === height && + frameRect.x === x && + frameRect.y === y + ) { + return; + } + + this.move(metaWindow, { + x: Math.round(x), + y: Math.round(y), + width: Math.round(width), + height: Math.round(height), + }); + } + get allNodeWindows() { return this.tree.getNodeByType(NODE_TYPES.WINDOW); } diff --git a/lib/prefs/settings.js b/lib/prefs/settings.js index a178a1d..906766d 100644 --- a/lib/prefs/settings.js +++ b/lib/prefs/settings.js @@ -8,7 +8,7 @@ import { Logger } from "../shared/logger.js"; import { production } from "../shared/settings.js"; // Prefs UI -import { DropDownRow, SwitchRow, PreferencesPage, EntryRow } from "./widgets.js"; +import { DropDownRow, SwitchRow, PreferencesPage, EntryRow, SpinButtonRow } from "./widgets.js"; // Extension imports import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js"; @@ -118,6 +118,42 @@ export class SettingsPage extends PreferencesPage { settings, bind: "float-always-on-top-enabled", }), + new SpinButtonRow({ + title: _("Minimum windows before tiling"), + subtitle: _("Keep windows floating until this number is reached"), + settings, + bind: "minimum-tiled-windows", + range: [1, 10, 1], + }), + ], + }); + this.add_group({ + title: _("Ultrawide window limits"), + description: _("Restrict floating window dimensions on large monitors"), + children: [ + new SpinButtonRow({ + title: _("Ultrawide monitor width"), + subtitle: _( + "Apply limits when the monitor width meets or exceeds this value (0 disables)" + ), + settings, + bind: "ultrawide-monitor-width", + range: [0, 10000, 10], + }), + new SpinButtonRow({ + title: _("Max window width"), + subtitle: _("Maximum floating window width on ultrawide monitors"), + settings, + bind: "ultrawide-max-window-width", + range: [0, 10000, 10], + }), + new SpinButtonRow({ + title: _("Max window height"), + subtitle: _("Maximum floating window height on ultrawide monitors"), + settings, + bind: "ultrawide-max-window-height", + range: [0, 10000, 10], + }), ], }); this.add_group({ diff --git a/schemas/org.gnome.shell.extensions.forge.gschema.xml b/schemas/org.gnome.shell.extensions.forge.gschema.xml index 241cc89..b205a4c 100644 --- a/schemas/org.gnome.shell.extensions.forge.gschema.xml +++ b/schemas/org.gnome.shell.extensions.forge.gschema.xml @@ -148,6 +148,26 @@ Whether to show the tab decoration or not + + 3 + Minimum number of windows before tiling is enabled + + + + 5160 + Monitor width threshold for applying ultrawide window limits + + + + 1720 + Maximum width for windows on ultrawide monitors + + + + 1440 + Maximum height for windows on ultrawide monitors + +