Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bin/omarchy-monitor-state
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ printf '%s\n' "$monitors_json" | jq -r '
printf '%s\n' "$focused_monitor"
omarchy-hyprland-monitor-scaling 2>/dev/null || echo

# scale and position ride along so the panel can put an output back where it
# was after switching it off; a disabled output no longer reports them dependably.
printf '%s\n' "$monitors_json" | jq -c \
'[.[] | {name, enabled:(.disabled != true), focused:(.focused == true), width, height}]'
'[.[] | {name, enabled:(.disabled != true), focused:(.focused == true), width, height, scale, x, y}]'
44 changes: 43 additions & 1 deletion shell/plugins/panels/monitor/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,46 @@ function parseDisplays(raw) {
}
}

// An output's scale and position can only be read dependably while it is on,
// and bringing one back with "auto" for both re-places the display and drops a
// scaled one to 1. Carry the last values seen while an output was on, so
// switching it back on restores the layout it had.
function rememberLayouts(previous, displays) {
var layouts = {}
for (var name in previous) layouts[name] = previous[name]
if (!Array.isArray(displays)) return layouts

for (var i = 0; i < displays.length; i++) {
var display = displays[i]
if (!display || !display.name || !display.enabled) continue

var scale = Number(display.scale)
var x = Number(display.x)
var y = Number(display.y)
if (!isFinite(scale) || scale <= 0 || !isFinite(x) || !isFinite(y)) continue

layouts[display.name] = { scale: scale, position: x + "x" + y }
}

return layouts
}

// Omarchy configures Hyprland through the Lua parser, which rejects
// `hyprctl keyword` outright ("keyword can't work with non-legacy parsers. Use
// eval.") while still exiting 0 — so a keyword-based toggle fails silently.
// `disabled = false` has to be spelled out as well: restating a mode alone
// leaves an already-disabled output off.
function monitorRule(name, disable, layout) {
if (disable) return 'hl.monitor({ output = "' + name + '", disabled = true })'

var position = layout && layout.position ? layout.position : "auto"
var scale = layout && layout.scale ? String(layout.scale) : '"auto"'

return 'hl.monitor({ output = "' + name + '", disabled = false, mode = "preferred"' +
', position = "' + position + '"' +
', scale = ' + scale + ' })'
}

if (typeof module !== "undefined") {
module.exports = {
clampBrightness: clampBrightness,
Expand All @@ -119,6 +159,8 @@ if (typeof module !== "undefined") {
matchingScaleIndex: matchingScaleIndex,
availableScales: availableScales,
brightnessName: brightnessName,
parseDisplays: parseDisplays
parseDisplays: parseDisplays,
rememberLayouts: rememberLayouts,
monitorRule: monitorRule
}
}
5 changes: 4 additions & 1 deletion shell/plugins/panels/monitor/Panel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Panel {
property string monitorScale: ""
property var displays: []
property int enabledDisplayCount: 0
// name -> { scale, position } last seen while that output was on
property var displayLayouts: ({})

// Carry sub-notch touchpad deltas between wheel events.
property real wheelAccumulator: 0
Expand Down Expand Up @@ -294,13 +296,14 @@ Panel {
var parsed = Model.parseDisplays(displaysJson)
root.displays = parsed.displays
root.enabledDisplayCount = parsed.enabledDisplayCount
root.displayLayouts = Model.rememberLayouts(root.displayLayouts, parsed.displays)
}

function toggleDisplay(name, enabled) {
if (!name) return
if (enabled && root.enabledDisplayCount <= 1) return

actionProc.command = ["hyprctl", "keyword", "monitor", name + (enabled ? ",disable" : ",preferred,auto,auto")]
actionProc.command = ["hyprctl", "eval", Model.monitorRule(name, enabled, root.displayLayouts[name])]
if (!actionProc.running) actionProc.running = true
}

Expand Down
22 changes: 11 additions & 11 deletions test/shell.d/monitor-state-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,26 +57,26 @@ assert_line_count() {
}

extended='[
{ "name": "eDP-1", "mirrorOf": "none", "disabled": false, "focused": false, "width": 1920, "height": 1080 },
{ "name": "DP-1", "mirrorOf": "none", "disabled": false, "focused": true, "width": 2560, "height": 1440 }
{ "name": "eDP-1", "mirrorOf": "none", "disabled": false, "focused": false, "width": 1920, "height": 1080, "scale": 2, "x": 0, "y": 0 },
{ "name": "DP-1", "mirrorOf": "none", "disabled": false, "focused": true, "width": 2560, "height": 1440, "scale": 1.5, "x": 960, "y": 0 }
]'

# Omarchy mirrors by pointing the external at the internal, so `mirrorOf` lands
# on the external and the internal keeps saying "none".
mirrored='[
{ "name": "eDP-1", "mirrorOf": "none", "disabled": false, "focused": true, "width": 1920, "height": 1080 },
{ "name": "DP-1", "mirrorOf": "eDP-1", "disabled": false, "focused": false, "width": 1920, "height": 1080 }
{ "name": "eDP-1", "mirrorOf": "none", "disabled": false, "focused": true, "width": 1920, "height": 1080, "scale": 2, "x": 0, "y": 0 },
{ "name": "DP-1", "mirrorOf": "eDP-1", "disabled": false, "focused": false, "width": 1920, "height": 1080, "scale": 2, "x": 0, "y": 0 }
]'

# A monitors.lua of the user's own can mirror the other way instead.
reverse_mirrored='[
{ "name": "eDP-1", "mirrorOf": "DP-1", "disabled": false, "focused": false, "width": 2560, "height": 1440 },
{ "name": "DP-1", "mirrorOf": "none", "disabled": false, "focused": true, "width": 2560, "height": 1440 }
{ "name": "eDP-1", "mirrorOf": "DP-1", "disabled": false, "focused": false, "width": 2560, "height": 1440, "scale": 1.5, "x": 0, "y": 0 },
{ "name": "DP-1", "mirrorOf": "none", "disabled": false, "focused": true, "width": 2560, "height": 1440, "scale": 1.5, "x": 0, "y": 0 }
]'

clamshell='[
{ "name": "eDP-1", "mirrorOf": "none", "disabled": true, "focused": false, "width": 0, "height": 0 },
{ "name": "DP-1", "mirrorOf": "none", "disabled": false, "focused": true, "width": 2560, "height": 1440 }
{ "name": "eDP-1", "mirrorOf": "none", "disabled": true, "focused": false, "width": 0, "height": 0, "scale": 1, "x": 0, "y": 0 },
{ "name": "DP-1", "mirrorOf": "none", "disabled": false, "focused": true, "width": 2560, "height": 1440, "scale": 1.5, "x": 0, "y": 0 }
]'

monitor_state "$extended"
Expand Down Expand Up @@ -109,9 +109,9 @@ assert_line 4 "" "monitor state reports no mirror while clamshelled"
pass "monitor state separates a disabled internal monitor from a missing one"

monitor_state "$extended"
[[ ${state_lines[7]-} == '[{"name":"eDP-1","enabled":true,"focused":false,"width":1920,"height":1080},{"name":"DP-1","enabled":true,"focused":true,"width":2560,"height":1440}]' ]] ||
[[ ${state_lines[7]-} == '[{"name":"eDP-1","enabled":true,"focused":false,"width":1920,"height":1080,"scale":2,"x":0,"y":0},{"name":"DP-1","enabled":true,"focused":true,"width":2560,"height":1440,"scale":1.5,"x":960,"y":0}]' ]] ||
fail "monitor state lists every display for the panel" "actual: ${state_lines[7]-<missing>}"
monitor_state "$clamshell"
[[ ${state_lines[7]-} == '[{"name":"eDP-1","enabled":false,"focused":false,"width":0,"height":0},{"name":"DP-1","enabled":true,"focused":true,"width":2560,"height":1440}]' ]] ||
[[ ${state_lines[7]-} == '[{"name":"eDP-1","enabled":false,"focused":false,"width":0,"height":0,"scale":1,"x":0,"y":0},{"name":"DP-1","enabled":true,"focused":true,"width":2560,"height":1440,"scale":1.5,"x":0,"y":0}]' ]] ||
fail "monitor state lists every display for the panel" "actual: ${state_lines[7]-<missing>}"
pass "monitor state lists every display with its enabled and focused state"
pass "monitor state lists every display with its enabled state, focus, scale and position"
45 changes: 45 additions & 0 deletions test/shell.d/monitor-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,49 @@ assertDeepEqual(
)

assertDeepEqual(monitor.parseDisplays('{'), { displays: [], enabledDisplayCount: 0 }, 'monitor handles invalid display JSON')

assertDeepEqual(
monitor.rememberLayouts({}, [
{ name: 'eDP-1', enabled: true, scale: 2, x: 0, y: 0 },
{ name: 'DP-1', enabled: true, scale: 1.5, x: 960, y: 0 }
]),
{
'eDP-1': { scale: 2, position: '0x0' },
'DP-1': { scale: 1.5, position: '960x0' }
},
'monitor remembers the layout of every enabled display'
)

assertDeepEqual(
monitor.rememberLayouts(
{ 'DP-1': { scale: 1.5, position: '960x0' } },
[
{ name: 'eDP-1', enabled: true, scale: 2, x: 0, y: 0 },
{ name: 'DP-1', enabled: false, scale: 1, x: 0, y: 0 }
]
),
{
'DP-1': { scale: 1.5, position: '960x0' },
'eDP-1': { scale: 2, position: '0x0' }
},
'monitor keeps the last known layout of a display that went off'
)

assertEqual(
monitor.monitorRule('DP-1', true, { scale: 1.5, position: '960x0' }),
'hl.monitor({ output = "DP-1", disabled = true })',
'monitor disables an output through the Lua parser'
)

assertEqual(
monitor.monitorRule('DP-1', false, { scale: 1.5, position: '960x0' }),
'hl.monitor({ output = "DP-1", disabled = false, mode = "preferred", position = "960x0", scale = 1.5 })',
'monitor restores the remembered scale and position when an output comes back'
)

assertEqual(
monitor.monitorRule('DP-1', false, undefined),
'hl.monitor({ output = "DP-1", disabled = false, mode = "preferred", position = "auto", scale = "auto" })',
'monitor falls back to auto placement for an output it never saw enabled'
)
JS