Split display layout into its own widget and fix installer early exit

This commit is contained in:
2026-09-20 04:12:56 -04:00
parent a32254c398
commit 9e34929e48
15 changed files with 378 additions and 242 deletions
+2 -2
View File
@@ -111,8 +111,8 @@
"system": {"icon":"","label":"System","aliases":["power-menu"]}, "system": {"icon":"","label":"System","aliases":["power-menu"]},
// Blob // Blob
"blob.wallpaper": {"icon":"󰸉","label":"Wallpaper","action":"blob-wallpaper-set --menu","description":"Pick a wallpaper from ~/wallpapers"}, "blob.wallpaper": {"icon":"󰸉","label":"Wallpaper","action":"blob-shell shell toggle blob.displays","description":"Pick a wallpaper, globally or per monitor"},
"blob.displays": {"icon":"󰍹","label":"Displays","action":"blob-shell shell toggle blob.displays","description":"Arrange monitors and assign wallpapers"}, "blob.display-layout": {"icon":"󰍹","label":"Display layout","action":"blob-shell shell toggle blob.display-layout","description":"Arrange monitors, set mode, scale, and mirroring"},
"blob.theme": {"icon":"󰢵","label":"Theme","action":"blob-theme-menu","description":"Pick a local or bundled color theme"}, "blob.theme": {"icon":"󰢵","label":"Theme","action":"blob-theme-menu","description":"Pick a local or bundled color theme"},
"blob.dynamic": {"icon":"󰁨","label":"Dynamic theme","action":"blob-theme-dynamic","description":"Recolor from the current wallpaper"}, "blob.dynamic": {"icon":"󰁨","label":"Dynamic theme","action":"blob-theme-dynamic","description":"Recolor from the current wallpaper"},
"blob.glass": {"icon":"󱡓","label":"Toggle glass","action":"blob-toggle-glass toggle","checked":"test -f $HOME/.local/state/blob/toggles/hypr/blob-glass.lua"}, "blob.glass": {"icon":"󱡓","label":"Toggle glass","action":"blob-toggle-glass toggle","checked":"test -f $HOME/.local/state/blob/toggles/hypr/blob-glass.lua"},
+2 -2
View File
@@ -11,10 +11,10 @@ Personal overrides in `hypr/bindings.lua` win over the defaults, and
| Keys | Action | | Keys | Action |
| --- | --- | | --- | --- |
| `SUPER + ALT + SPACE` | Blob menu | | `SUPER + ALT + SPACE` | Blob menu |
| `SUPER + ALT + W` | Wallpaper picker | | `SUPER + ALT + W` | Wallpapers |
| `SUPER + CTRL + M` | System monitor | | `SUPER + CTRL + M` | System monitor |
| `SUPER + CTRL + Q` | Quick settings | | `SUPER + CTRL + Q` | Quick settings |
| `SUPER + SHIFT + CTRL + D` | Displays | | `SUPER + SHIFT + CTRL + D` | Display layout |
| `SUPER + SPACE` | Apps menu | | `SUPER + SPACE` | Apps menu |
## Applications ## Applications
+25 -15
View File
@@ -53,18 +53,15 @@ runs while a panel is open, so the cost is bounded, but wiring these to the
services is the obvious next refinement and would remove the last per-tick services is the obvious next refinement and would remove the last per-tick
process spawns. process spawns.
## Displays ## Display layout
`blob.displays` arranges monitors and assigns wallpapers per monitor. Opened `blob.display-layout` arranges monitors. Opened with `Super + Shift + Ctrl + D`
with `Super + Shift + Ctrl + D` or the Blob menu's Displays row. or the Blob menu's Display layout row.
### Monitors tab
A scaled picture of the desktop, one draggable rectangle per monitor. Dropping A scaled picture of the desktop, one draggable rectangle per monitor. Dropping
one applies the new position immediately. Edges snap to a neighbour's edge one applies the new position immediately. Edges snap to a neighbour's edge
within 60 logical pixels, so monitors end up touching exactly. within 60 logical pixels, so monitors end up touching exactly. Per-monitor
controls: mode, scale, enable or disable, and mirror.
Per-monitor controls: mode, scale, enable or disable, and mirror.
**Scale options are filtered, not free.** Hyprland steps fractional scaling in **Scale options are filtered, not free.** Hyprland steps fractional scaling in
1/120 and rejects a scale that does not land on a whole number of physical 1/120 and rejects a scale that does not land on a whole number of physical
@@ -80,23 +77,36 @@ config authoritative again. To keep a layout, edit `monitors.lua` yourself -
`blob display arrange list` prints the current layout in the exact keyword form, `blob display arrange list` prints the current layout in the exact keyword form,
ready to copy. ready to copy.
### Wallpapers tab Rotation and primary-monitor selection are not implemented. `DisplayModel` reads
`transform` and round-trips it, so a rotated monitor is drawn and preserved
correctly, but nothing in the widget sets it.
A thumbnail grid of `~/wallpapers`. Clicking one assigns it to the selected ## Wallpapers
monitor through `blob-bg-monitor`, which symlinks it under
`~/.local/state/blob/backgrounds/<output>`. `blob.displays` picks wallpapers. Opened with `Super + Alt + W` or the Blob
menu's Wallpaper row.
A thumbnail grid of `~/wallpapers`, with a row of chips selecting the target:
"All monitors" sets the global wallpaper through `blob-bg-set`, and a specific
output assigns just that monitor through `blob-bg-monitor`, which symlinks the
image under `~/.local/state/blob/backgrounds/<output>`. An output with its own
wallpaper is marked with an asterisk.
`blob.background` already drew one window per screen but pointed them all at the `blob.background` already drew one window per screen but pointed them all at the
same image. It now resolves per screen: a monitor with an assignment shows it, same image. It now resolves per screen: a monitor with an assignment shows it,
and a monitor without one shows the global wallpaper from `blob-bg-set`. The and a monitor without one shows the global wallpaper. The crossfade machinery
crossfade machinery stays on the global path only, so an assigned monitor swaps stays on the global path only, so an assigned monitor swaps without the reveal
without the reveal animation rather than risking that code. animation rather than risking that code.
| Command | Does | | Command | Does |
| --- | --- | | --- | --- |
| `blob bg set <image>` | set the global wallpaper |
| `blob bg monitor <output> <image>` | assign a wallpaper to one monitor | | `blob bg monitor <output> <image>` | assign a wallpaper to one monitor |
| `blob bg monitor <output> --clear` | fall back to the global wallpaper | | `blob bg monitor <output> --clear` | fall back to the global wallpaper |
| `blob bg monitor --list` | show assignments | | `blob bg monitor --list` | show assignments |
| `blob display arrange list` | print the layout as monitor keywords | | `blob display arrange list` | print the layout as monitor keywords |
| `blob display arrange apply <kw>` | apply a keyword now | | `blob display arrange apply <kw>` | apply a keyword now |
| `blob display arrange reset` | reload, restoring monitors.lua | | `blob display arrange reset` | reload, restoring monitors.lua |
Both widgets share `Ui/Chip.qml`, `Ui/Card.qml` and `Ui/CardIconButton.qml`
rather than carrying their own copies.
+2 -2
View File
@@ -1,9 +1,9 @@
-- Personal keybinding overrides on top of the defaults. -- Personal keybinding overrides on top of the defaults.
-- See current bindings and descriptions: blob menu keybindings --print -- See current bindings and descriptions: blob menu keybindings --print
o.bind("SUPER + ALT + W", "Wallpaper picker", "blob-wallpaper-set --menu") o.bind("SUPER + ALT + W", "Wallpapers", "blob-shell shell toggle blob.displays")
o.bind("SUPER + CTRL + M", "System monitor", "blob-shell shell toggle blob.sysmon") o.bind("SUPER + CTRL + M", "System monitor", "blob-shell shell toggle blob.sysmon")
o.bind("SUPER + SHIFT + CTRL + D", "Displays", "blob-shell shell toggle blob.displays") o.bind("SUPER + SHIFT + CTRL + D", "Display layout", "blob-shell shell toggle blob.display-layout")
o.bind("SUPER + CTRL + Q", "Quick settings", "blob-shell shell toggle blob.quick-settings") o.bind("SUPER + CTRL + Q", "Quick settings", "blob-shell shell toggle blob.quick-settings")
-- Apps on SUPER + SPACE, root menu on ALT, swapping the shipped defaults back. -- Apps on SUPER + SPACE, root menu on ALT, swapping the shipped defaults back.
+16 -16
View File
@@ -74,14 +74,14 @@ create_directories() {
if (( ${#missing[@]} == 0 )); then if (( ${#missing[@]} == 0 )); then
say "ok directories" say "ok directories"
return return 0
fi fi
note_change note_change
if [[ $check == true ]]; then if [[ $check == true ]]; then
say "would create ${#missing[@]} directory(ies):" say "would create ${#missing[@]} directory(ies):"
printf ' %s\n' "${missing[@]}" printf ' %s\n' "${missing[@]}"
return return 0
fi fi
mkdir -p "${missing[@]}" mkdir -p "${missing[@]}"
@@ -110,7 +110,7 @@ missing_packages() {
install_packages() { install_packages() {
if [[ $skip_packages == true ]]; then if [[ $skip_packages == true ]]; then
say "skip packages (--skip-packages)" say "skip packages (--skip-packages)"
return return 0
fi fi
local -a repo_missing aur_missing local -a repo_missing aur_missing
@@ -119,14 +119,14 @@ install_packages() {
if (( ${#repo_missing[@]} == 0 && ${#aur_missing[@]} == 0 )); then if (( ${#repo_missing[@]} == 0 && ${#aur_missing[@]} == 0 )); then
say "ok packages" say "ok packages"
return return 0
fi fi
note_change note_change
if [[ $check == true ]]; then if [[ $check == true ]]; then
(( ${#repo_missing[@]} )) && say "would install ${#repo_missing[@]} repo package(s): ${repo_missing[*]}" (( ${#repo_missing[@]} )) && say "would install ${#repo_missing[@]} repo package(s): ${repo_missing[*]}"
(( ${#aur_missing[@]} )) && say "would install ${#aur_missing[@]} AUR package(s): ${aur_missing[*]}" (( ${#aur_missing[@]} )) && say "would install ${#aur_missing[@]} AUR package(s): ${aur_missing[*]}"
return return 0
fi fi
blob_sudo_keepalive blob_sudo_keepalive
@@ -139,7 +139,7 @@ install_packages() {
if (( ${#aur_missing[@]} )); then if (( ${#aur_missing[@]} )); then
if ! command -v yay &>/dev/null; then if ! command -v yay &>/dev/null; then
say "WARN yay is not installed; skipping ${#aur_missing[@]} AUR package(s)" say "WARN yay is not installed; skipping ${#aur_missing[@]} AUR package(s)"
return return 0
fi fi
say "install ${#aur_missing[@]} AUR package(s)" say "install ${#aur_missing[@]} AUR package(s)"
yay -S --needed --noconfirm "${aur_missing[@]}" yay -S --needed --noconfirm "${aur_missing[@]}"
@@ -159,19 +159,19 @@ link_blob_path() {
if [[ $current == "$repo_dir" ]]; then if [[ $current == "$repo_dir" ]]; then
say "ok BLOB_PATH -> $repo_dir" say "ok BLOB_PATH -> $repo_dir"
return return 0
fi fi
if [[ -e $blob_path && ! -L $blob_path ]]; then if [[ -e $blob_path && ! -L $blob_path ]]; then
say "WARN $blob_path exists and is not a symlink; move it aside first" say "WARN $blob_path exists and is not a symlink; move it aside first"
note_change note_change
return return 0
fi fi
note_change note_change
if [[ $check == true ]]; then if [[ $check == true ]]; then
say "would link BLOB_PATH -> $repo_dir" say "would link BLOB_PATH -> $repo_dir"
return return 0
fi fi
ln -sfn "$repo_dir" "$blob_path" ln -sfn "$repo_dir" "$blob_path"
@@ -186,24 +186,24 @@ install_file() {
if [[ ! -e $source ]]; then if [[ ! -e $source ]]; then
say "SKIP $label (missing in repo)" say "SKIP $label (missing in repo)"
return return 0
fi fi
if [[ -e $dest ]] && cmp -s "$source" "$dest"; then if [[ -e $dest ]] && cmp -s "$source" "$dest"; then
say "ok $label" say "ok $label"
return return 0
fi fi
note_change note_change
if [[ -e $dest ]] && [[ $force == false ]]; then if [[ -e $dest ]] && [[ $force == false ]]; then
say "DIFF $label (local changes kept; --force to overwrite)" say "DIFF $label (local changes kept; --force to overwrite)"
return return 0
fi fi
if [[ $check == true ]]; then if [[ $check == true ]]; then
say "would write $label" say "would write $label"
return return 0
fi fi
mkdir -p "$(dirname "$dest")" mkdir -p "$(dirname "$dest")"
@@ -217,7 +217,7 @@ install_tree() {
if [[ ! -d $source ]]; then if [[ ! -d $source ]]; then
say "SKIP $label (missing in repo)" say "SKIP $label (missing in repo)"
return return 0
fi fi
local relative local relative
@@ -232,13 +232,13 @@ install_session_entry() {
if [[ -e $dest ]] && cmp -s "$source" "$dest"; then if [[ -e $dest ]] && cmp -s "$source" "$dest"; then
say "ok wayland session entry" say "ok wayland session entry"
return return 0
fi fi
note_change note_change
if [[ $check == true ]]; then if [[ $check == true ]]; then
say "would install wayland session entry (needs sudo)" say "would install wayland session entry (needs sudo)"
return return 0
fi fi
sudo install -Dm644 "$source" "$dest" sudo install -Dm644 "$source" "$dest"
+1
View File
@@ -37,3 +37,4 @@ Card 1.0 Card.qml
CardHeader 1.0 CardHeader.qml CardHeader 1.0 CardHeader.qml
StatRow 1.0 StatRow.qml StatRow 1.0 StatRow.qml
CardIconButton 1.0 CardIconButton.qml CardIconButton 1.0 CardIconButton.qml
Chip 1.0 Chip.qml
@@ -0,0 +1,237 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Ui
import "DisplayModel.js" as DisplayModel
Item {
id: root
property var shell: null
property var manifest: null
property bool opened: false
readonly property string pluginId: (manifest && manifest.id) || "blob.display-layout"
readonly property int snapThreshold: 60
property var monitors: []
property string selectedName: ""
property bool layoutDirty: false
readonly property var selectedMonitor: {
for (var i = 0; i < root.monitors.length; i++)
if (root.monitors[i].name === root.selectedName) return root.monitors[i]
return null
}
function open(payloadJson) {
root.opened = true
root.refresh()
}
function close() {
root.opened = false
}
function dismiss() {
root.opened = false
if (root.shell && typeof root.shell.hide === "function")
root.shell.hide(root.pluginId)
}
function run(command) {
Quickshell.execDetached(["bash", "-c", command])
}
function refresh() {
if (!monitorProc.running) monitorProc.running = true
}
function loadMonitors(raw) {
var parsed = DisplayModel.parseMonitors(raw)
root.monitors = parsed
root.layoutDirty = false
if (root.selectedName.length > 0) {
for (var i = 0; i < parsed.length; i++)
if (parsed[i].name === root.selectedName) return
}
root.selectedName = parsed.length > 0 ? parsed[0].name : ""
}
// Mutating the array in place would not retrigger the bindings the canvas
// reads, so every edit rebuilds it.
function replaceMonitor(name, changes) {
var next = []
for (var i = 0; i < root.monitors.length; i++) {
var monitor = root.monitors[i]
if (monitor.name !== name) {
next.push(monitor)
continue
}
var copy = ({})
for (var key in monitor) copy[key] = monitor[key]
for (var change in changes) copy[change] = changes[change]
next.push(copy)
}
root.monitors = next
root.layoutDirty = true
}
function moveMonitor(name, x, y) {
var snapped = DisplayModel.snapPosition(root.monitors, name, x, y, root.snapThreshold)
root.replaceMonitor(name, { x: snapped.x, y: snapped.y })
}
function applyMonitor(name) {
for (var i = 0; i < root.monitors.length; i++) {
if (root.monitors[i].name !== name) continue
root.run("blob-display-arrange apply "
+ Util.shellQuote(DisplayModel.monitorKeyword(root.monitors[i])))
reloadTimer.restart()
return
}
}
function applyLayout() {
var keywords = DisplayModel.keywordsFor(root.monitors)
var quoted = []
for (var i = 0; i < keywords.length; i++) quoted.push(Util.shellQuote(keywords[i]))
root.run("blob-display-arrange apply " + quoted.join(" "))
root.layoutDirty = false
reloadTimer.restart()
}
function resetLayout() {
root.run("blob-display-arrange reset")
reloadTimer.restart()
}
Process {
id: monitorProc
command: ["hyprctl", "monitors", "all", "-j"]
stdout: StdioCollector { onStreamFinished: root.loadMonitors(text) }
}
Timer {
id: reloadTimer
interval: 400
onTriggered: root.refresh()
}
Timer {
interval: 4000
running: root.opened && !root.layoutDirty
repeat: true
onTriggered: root.refresh()
}
onOpenedChanged: if (root.opened) root.refresh()
PanelWindow {
visible: root.opened
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
WlrLayershell.namespace: "blob-display-layout"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
exclusionMode: ExclusionMode.Ignore
MouseArea {
anchors.fill: parent
onClicked: root.dismiss()
}
Card {
anchors.centerIn: parent
implicitWidth: Style.spaceReal(720)
implicitHeight: Style.spaceReal(540)
fillColor: Util.alpha(Color.background, Style.panelFillAlpha)
spacing: Style.spaceReal(10)
Item {
width: parent.width
implicitHeight: Math.max(title.implicitHeight, closeButton.implicitHeight)
Text {
id: title
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: "Display layout"
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.body
font.bold: true
}
CardIconButton {
id: closeButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
icon: ""
onActivated: root.dismiss()
}
}
Text {
width: parent.width
wrapMode: Text.WordWrap
text: "Drag a monitor to move it. Edges snap to a neighbour. Changes apply as you drop and last until the session ends."
textFormat: Text.PlainText
color: Util.alpha(Color.foreground, 0.6)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
MonitorCanvas {
width: parent.width
height: Style.spaceReal(220)
monitors: root.monitors
selectedName: root.selectedName
onSelected: function(name) { root.selectedName = name }
onMonitorMoved: function(name, x, y) { root.moveMonitor(name, x, y) }
onLayoutSettled: root.applyMonitor(root.selectedName)
}
MonitorControls {
width: parent.width
monitor: root.selectedMonitor
monitors: root.monitors
onScaleRequested: function(scale) {
root.replaceMonitor(root.selectedName, { scale: scale })
root.applyMonitor(root.selectedName)
}
onModeRequested: function(mode) {
root.replaceMonitor(root.selectedName, { mode: mode })
root.applyMonitor(root.selectedName)
}
onEnabledRequested: function(enabled) {
root.replaceMonitor(root.selectedName, { disabled: !enabled })
root.applyMonitor(root.selectedName)
}
onMirrorRequested: function(target) {
root.replaceMonitor(root.selectedName, { mirrorOf: target })
root.applyMonitor(root.selectedName)
}
}
Row {
width: parent.width
spacing: Style.spaceReal(6)
Chip {
label: root.layoutDirty ? "Apply all" : "Re-apply all"
active: root.layoutDirty
onActivated: root.applyLayout()
}
Chip {
label: "Reset to monitors.lua"
onActivated: root.resetLayout()
}
}
}
}
}
@@ -0,0 +1,15 @@
{
"schemaVersion": 1,
"name": "Display layout",
"version": "1.0.0",
"author": "Blob",
"description": "Arrange monitors by dragging, and set their mode, scale, and mirroring",
"id": "blob.display-layout",
"kinds": [
"overlay"
],
"keepLoaded": true,
"entryPoints": {
"overlay": "DisplayLayout.qml"
}
}
+76 -203
View File
@@ -4,7 +4,6 @@ import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import qs.Commons import qs.Commons
import qs.Ui import qs.Ui
import "DisplayModel.js" as DisplayModel
Item { Item {
id: root id: root
@@ -14,19 +13,14 @@ Item {
property bool opened: false property bool opened: false
readonly property string pluginId: (manifest && manifest.id) || "blob.displays" readonly property string pluginId: (manifest && manifest.id) || "blob.displays"
readonly property string home: Quickshell.env("HOME")
property var monitors: [] property var outputs: []
property string selectedName: "" property string selectedName: ""
property string activeTab: "monitors"
property var assignments: ({}) property var assignments: ({})
property bool layoutDirty: false
readonly property var selectedMonitor: { readonly property bool globalMode: root.selectedName === ""
for (var i = 0; i < root.monitors.length; i++) readonly property string assignedPath: root.globalMode
if (root.monitors[i].name === root.selectedName) return root.monitors[i] ? "" : String(root.assignments[root.selectedName] || "")
return null
}
function open(payloadJson) { function open(payloadJson) {
root.opened = true root.opened = true
@@ -48,19 +42,18 @@ Item {
} }
function refresh() { function refresh() {
if (!monitorProc.running) monitorProc.running = true if (!outputProc.running) outputProc.running = true
if (!assignmentProc.running) assignmentProc.running = true if (!assignmentProc.running) assignmentProc.running = true
} }
function loadMonitors(raw) { function loadOutputs(raw) {
var parsed = DisplayModel.parseMonitors(raw) var names = []
root.monitors = parsed var lines = String(raw || "").split("\n")
root.layoutDirty = false for (var i = 0; i < lines.length; i++) {
if (root.selectedName.length > 0) { var line = lines[i].trim()
for (var i = 0; i < parsed.length; i++) if (line.length > 0) names.push(line)
if (parsed[i].name === root.selectedName) return
} }
root.selectedName = parsed.length > 0 ? parsed[0].name : "" root.outputs = names
} }
function loadAssignments(raw) { function loadAssignments(raw) {
@@ -76,55 +69,13 @@ Item {
root.assignments = next root.assignments = next
} }
// Mutating the array in place would not retrigger the bindings the canvas function choose(path) {
// reads, so every edit rebuilds it. if (root.globalMode) {
function replaceMonitor(name, changes) { root.run("blob-bg-set " + Util.shellQuote(path))
var next = []
for (var i = 0; i < root.monitors.length; i++) {
var monitor = root.monitors[i]
if (monitor.name !== name) {
next.push(monitor)
continue
}
var copy = ({})
for (var key in monitor) copy[key] = monitor[key]
for (var change in changes) copy[change] = changes[change]
next.push(copy)
}
root.monitors = next
root.layoutDirty = true
}
function moveMonitor(name, x, y) {
var snapped = DisplayModel.snapPosition(root.monitors, name, x, y, 60)
root.replaceMonitor(name, { x: snapped.x, y: snapped.y })
}
function applyMonitor(name) {
for (var i = 0; i < root.monitors.length; i++) {
if (root.monitors[i].name !== name) continue
root.run("blob-display-arrange apply " + Util.shellQuote(DisplayModel.monitorKeyword(root.monitors[i])))
return return
} }
} root.run("blob-bg-monitor " + Util.shellQuote(root.selectedName)
+ " " + Util.shellQuote(path))
function applyLayout() {
var keywords = DisplayModel.keywordsFor(root.monitors)
var quoted = []
for (var i = 0; i < keywords.length; i++) quoted.push(Util.shellQuote(keywords[i]))
root.run("blob-display-arrange apply " + quoted.join(" "))
root.layoutDirty = false
reloadTimer.restart()
}
function resetLayout() {
root.run("blob-display-arrange reset")
reloadTimer.restart()
}
function assignWallpaper(path) {
if (root.selectedName.length === 0) return
root.run("blob-bg-monitor " + Util.shellQuote(root.selectedName) + " " + Util.shellQuote(path))
var next = ({}) var next = ({})
for (var key in root.assignments) next[key] = root.assignments[key] for (var key in root.assignments) next[key] = root.assignments[key]
next[root.selectedName] = path next[root.selectedName] = path
@@ -132,7 +83,7 @@ Item {
} }
function clearAssignment() { function clearAssignment() {
if (root.selectedName.length === 0) return if (root.globalMode) return
root.run("blob-bg-monitor " + Util.shellQuote(root.selectedName) + " --clear") root.run("blob-bg-monitor " + Util.shellQuote(root.selectedName) + " --clear")
var next = ({}) var next = ({})
for (var key in root.assignments) for (var key in root.assignments)
@@ -140,14 +91,10 @@ Item {
root.assignments = next root.assignments = next
} }
function setGlobalWallpaper(path) {
root.run("blob-bg-set " + Util.shellQuote(path))
}
Process { Process {
id: monitorProc id: outputProc
command: ["hyprctl", "monitors", "all", "-j"] command: ["bash", "-c", "hyprctl monitors -j | jq -r '.[].name'"]
stdout: StdioCollector { onStreamFinished: root.loadMonitors(text) } stdout: StdioCollector { onStreamFinished: root.loadOutputs(text) }
} }
Process { Process {
@@ -159,24 +106,13 @@ Item {
stdout: StdioCollector { onStreamFinished: root.loadAssignments(text) } stdout: StdioCollector { onStreamFinished: root.loadAssignments(text) }
} }
Timer { onOpenedChanged: if (root.opened) root.refresh()
id: reloadTimer
interval: 400
onTriggered: root.refresh()
}
Timer {
interval: 4000
running: root.opened && !root.layoutDirty
repeat: true
onTriggered: root.refresh()
}
PanelWindow { PanelWindow {
visible: root.opened visible: root.opened
anchors { top: true; bottom: true; left: true; right: true } anchors { top: true; bottom: true; left: true; right: true }
color: "transparent" color: "transparent"
WlrLayershell.namespace: "blob-displays" WlrLayershell.namespace: "blob-wallpapers"
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
@@ -188,20 +124,20 @@ Item {
Card { Card {
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: Style.spaceReal(760) implicitWidth: Style.spaceReal(720)
implicitHeight: Style.spaceReal(560) implicitHeight: Style.spaceReal(520)
fillColor: Util.alpha(Color.background, Style.panelFillAlpha) fillColor: Util.alpha(Color.background, Style.panelFillAlpha)
spacing: Style.spaceReal(10) spacing: Style.spaceReal(10)
Item { Item {
width: parent.width width: parent.width
implicitHeight: Math.max(title.implicitHeight, tabs.implicitHeight) implicitHeight: Math.max(title.implicitHeight, closeButton.implicitHeight)
Text { Text {
id: title id: title
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Displays" text: "Wallpapers"
textFormat: Text.PlainText textFormat: Text.PlainText
color: Color.foreground color: Color.foreground
font.family: Style.font.family font.family: Style.font.family
@@ -209,136 +145,73 @@ Item {
font.bold: true font.bold: true
} }
Row { CardIconButton {
id: tabs id: closeButton
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Style.spaceReal(6) icon: ""
onActivated: root.dismiss()
Chip {
label: "Monitors"
active: root.activeTab === "monitors"
onActivated: root.activeTab = "monitors"
}
Chip {
label: "Wallpapers"
active: root.activeTab === "wallpapers"
onActivated: root.activeTab = "wallpapers"
}
CardIconButton {
icon: ""
onActivated: root.dismiss()
}
} }
} }
MonitorCanvas { Flow {
width: parent.width width: parent.width
height: Style.spaceReal(200) spacing: Style.spaceReal(4)
monitors: root.monitors
selectedName: root.selectedName Chip {
onSelected: function(name) { root.selectedName = name } label: "All monitors"
onMonitorMoved: function(name, x, y) { root.moveMonitor(name, x, y) } active: root.globalMode
onLayoutSettled: root.applyMonitor(root.selectedName) onActivated: root.selectedName = ""
}
Repeater {
model: root.outputs
Chip {
required property var modelData
label: root.assignments[modelData]
? String(modelData) + " *" : String(modelData)
active: root.selectedName === modelData
onActivated: root.selectedName = String(modelData)
}
}
} }
Item { Item {
width: parent.width width: parent.width
height: Style.spaceReal(250) implicitHeight: status.implicitHeight
MonitorControls { Text {
anchors.fill: parent id: status
visible: root.activeTab === "monitors" anchors.left: parent.left
monitor: root.selectedMonitor anchors.verticalCenter: parent.verticalCenter
monitors: root.monitors text: root.globalMode
onScaleRequested: function(scale) { ? "Setting the wallpaper for every monitor without its own"
root.replaceMonitor(root.selectedName, { scale: scale }) : (root.assignedPath.length > 0
root.applyMonitor(root.selectedName) ? root.selectedName + ": " + root.assignedPath.replace(/^.*\//, "")
} : root.selectedName + ": following the global wallpaper")
onModeRequested: function(mode) { textFormat: Text.PlainText
root.replaceMonitor(root.selectedName, { mode: mode }) color: Util.alpha(Color.foreground, 0.7)
root.applyMonitor(root.selectedName) font.family: Style.font.family
} font.pixelSize: Style.font.bodySmall
onEnabledRequested: function(enabled) {
root.replaceMonitor(root.selectedName, { disabled: !enabled })
root.applyMonitor(root.selectedName)
}
onMirrorRequested: function(target) {
root.replaceMonitor(root.selectedName, { mirrorOf: target })
root.applyMonitor(root.selectedName)
}
} }
Column { Chip {
anchors.fill: parent anchors.right: parent.right
visible: root.activeTab === "wallpapers" anchors.verticalCenter: parent.verticalCenter
spacing: Style.spaceReal(8) label: "Clear"
enabled: !root.globalMode && root.assignedPath.length > 0
Item { onActivated: root.clearAssignment()
width: parent.width
implicitHeight: assignedLabel.implicitHeight
Text {
id: assignedLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.selectedName.length > 0
? (root.assignments[root.selectedName]
? root.selectedName + ": " + String(root.assignments[root.selectedName]).replace(/^.*\//, "")
: root.selectedName + ": following the global wallpaper")
: "Select a monitor above"
textFormat: Text.PlainText
color: Util.alpha(Color.foreground, 0.8)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
Chip {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
label: "Clear"
enabled: root.selectedName.length > 0 && !!root.assignments[root.selectedName]
onActivated: root.clearAssignment()
}
}
WallpaperGrid {
width: parent.width
height: parent.height - assignedLabel.implicitHeight - Style.spaceReal(8)
polling: root.opened && root.activeTab === "wallpapers"
assignedPath: root.selectedName.length > 0
? String(root.assignments[root.selectedName] || "") : ""
onChosen: function(path) { root.assignWallpaper(path) }
}
} }
} }
Row { WallpaperGrid {
width: parent.width width: parent.width
spacing: Style.spaceReal(6) height: Style.spaceReal(340)
polling: root.opened
Chip { assignedPath: root.globalMode ? "" : root.assignedPath
label: root.layoutDirty ? "Apply all" : "Re-apply all" onChosen: function(path) { root.choose(path) }
active: root.layoutDirty
onActivated: root.applyLayout()
}
Chip {
label: "Reset to monitors.lua"
onActivated: root.resetLayout()
}
Chip {
label: "Set globally"
enabled: root.activeTab === "wallpapers" && root.selectedName.length > 0
&& !!root.assignments[root.selectedName]
onActivated: root.setGlobalWallpaper(String(root.assignments[root.selectedName]))
}
} }
} }
} }
onOpenedChanged: if (root.opened) root.refresh()
} }
+2 -2
View File
@@ -1,9 +1,9 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"name": "Displays", "name": "Wallpapers",
"version": "1.0.0", "version": "1.0.0",
"author": "Blob", "author": "Blob",
"description": "Arrange monitors and assign wallpapers per monitor", "description": "Pick wallpapers, globally or per monitor",
"id": "blob.displays", "id": "blob.displays",
"kinds": [ "kinds": [
"overlay" "overlay"