Split display layout into its own widget and fix installer early exit
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,88 +145,53 @@ 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)
|
|
||||||
|
|
||||||
Chip {
|
|
||||||
label: "Monitors"
|
|
||||||
active: root.activeTab === "monitors"
|
|
||||||
onActivated: root.activeTab = "monitors"
|
|
||||||
}
|
|
||||||
|
|
||||||
Chip {
|
|
||||||
label: "Wallpapers"
|
|
||||||
active: root.activeTab === "wallpapers"
|
|
||||||
onActivated: root.activeTab = "wallpapers"
|
|
||||||
}
|
|
||||||
|
|
||||||
CardIconButton {
|
|
||||||
icon: ""
|
icon: ""
|
||||||
onActivated: root.dismiss()
|
onActivated: root.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.spaceReal(4)
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
label: "All monitors"
|
||||||
|
active: root.globalMode
|
||||||
|
onActivated: root.selectedName = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
MonitorCanvas {
|
Repeater {
|
||||||
width: parent.width
|
model: root.outputs
|
||||||
height: Style.spaceReal(200)
|
|
||||||
monitors: root.monitors
|
Chip {
|
||||||
selectedName: root.selectedName
|
required property var modelData
|
||||||
onSelected: function(name) { root.selectedName = name }
|
label: root.assignments[modelData]
|
||||||
onMonitorMoved: function(name, x, y) { root.moveMonitor(name, x, y) }
|
? String(modelData) + " *" : String(modelData)
|
||||||
onLayoutSettled: root.applyMonitor(root.selectedName)
|
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 {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.activeTab === "monitors"
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.activeTab === "wallpapers"
|
|
||||||
spacing: Style.spaceReal(8)
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
implicitHeight: assignedLabel.implicitHeight
|
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: assignedLabel
|
id: status
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.selectedName.length > 0
|
text: root.globalMode
|
||||||
? (root.assignments[root.selectedName]
|
? "Setting the wallpaper for every monitor without its own"
|
||||||
? root.selectedName + ": " + String(root.assignments[root.selectedName]).replace(/^.*\//, "")
|
: (root.assignedPath.length > 0
|
||||||
|
? root.selectedName + ": " + root.assignedPath.replace(/^.*\//, "")
|
||||||
: root.selectedName + ": following the global wallpaper")
|
: root.selectedName + ": following the global wallpaper")
|
||||||
: "Select a monitor above"
|
|
||||||
textFormat: Text.PlainText
|
textFormat: Text.PlainText
|
||||||
color: Util.alpha(Color.foreground, 0.8)
|
color: Util.alpha(Color.foreground, 0.7)
|
||||||
font.family: Style.font.family
|
font.family: Style.font.family
|
||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
}
|
}
|
||||||
@@ -299,46 +200,18 @@ Item {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
label: "Clear"
|
label: "Clear"
|
||||||
enabled: root.selectedName.length > 0 && !!root.assignments[root.selectedName]
|
enabled: !root.globalMode && root.assignedPath.length > 0
|
||||||
onActivated: root.clearAssignment()
|
onActivated: root.clearAssignment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WallpaperGrid {
|
WallpaperGrid {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - assignedLabel.implicitHeight - Style.spaceReal(8)
|
height: Style.spaceReal(340)
|
||||||
polling: root.opened && root.activeTab === "wallpapers"
|
polling: root.opened
|
||||||
assignedPath: root.selectedName.length > 0
|
assignedPath: root.globalMode ? "" : root.assignedPath
|
||||||
? String(root.assignments[root.selectedName] || "") : ""
|
onChosen: function(path) { root.choose(path) }
|
||||||
onChosen: function(path) { root.assignWallpaper(path) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user