panescape 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. panescape-0.1.0/LICENSE +21 -0
  2. panescape-0.1.0/PKG-INFO +619 -0
  3. panescape-0.1.0/README.md +589 -0
  4. panescape-0.1.0/pyproject.toml +57 -0
  5. panescape-0.1.0/setup.cfg +4 -0
  6. panescape-0.1.0/src/panescape/__init__.py +3 -0
  7. panescape-0.1.0/src/panescape/__main__.py +5 -0
  8. panescape-0.1.0/src/panescape/backends/__init__.py +27 -0
  9. panescape-0.1.0/src/panescape/backends/base.py +221 -0
  10. panescape-0.1.0/src/panescape/backends/gnome.py +246 -0
  11. panescape-0.1.0/src/panescape/backends/kde.py +191 -0
  12. panescape-0.1.0/src/panescape/backends/wlroots.py +221 -0
  13. panescape-0.1.0/src/panescape/calibrate.py +184 -0
  14. panescape-0.1.0/src/panescape/cli.py +512 -0
  15. panescape-0.1.0/src/panescape/config.py +322 -0
  16. panescape-0.1.0/src/panescape/gui/__init__.py +76 -0
  17. panescape-0.1.0/src/panescape/gui/apply_tab.py +819 -0
  18. panescape-0.1.0/src/panescape/gui/common.py +75 -0
  19. panescape-0.1.0/src/panescape/gui/desk_view.py +326 -0
  20. panescape-0.1.0/src/panescape/gui/layout_editor.py +1188 -0
  21. panescape-0.1.0/src/panescape/gui/main_window.py +227 -0
  22. panescape-0.1.0/src/panescape/gui/pictures_tab.py +654 -0
  23. panescape-0.1.0/src/panescape/gui/unsplash_tab.py +510 -0
  24. panescape-0.1.0/src/panescape/gui/widgets.py +154 -0
  25. panescape-0.1.0/src/panescape/layout.py +140 -0
  26. panescape-0.1.0/src/panescape/models.py +60 -0
  27. panescape-0.1.0/src/panescape/project.py +1065 -0
  28. panescape-0.1.0/src/panescape/py.typed +0 -0
  29. panescape-0.1.0/src/panescape/render.py +181 -0
  30. panescape-0.1.0/src/panescape/skeleton.py +118 -0
  31. panescape-0.1.0/src/panescape/tags.py +223 -0
  32. panescape-0.1.0/src/panescape/units.py +98 -0
  33. panescape-0.1.0/src/panescape/unsplash.py +528 -0
  34. panescape-0.1.0/src/panescape/yamledit.py +86 -0
  35. panescape-0.1.0/src/panescape.egg-info/PKG-INFO +619 -0
  36. panescape-0.1.0/src/panescape.egg-info/SOURCES.txt +54 -0
  37. panescape-0.1.0/src/panescape.egg-info/dependency_links.txt +1 -0
  38. panescape-0.1.0/src/panescape.egg-info/entry_points.txt +6 -0
  39. panescape-0.1.0/src/panescape.egg-info/requires.txt +14 -0
  40. panescape-0.1.0/src/panescape.egg-info/top_level.txt +1 -0
  41. panescape-0.1.0/tests/test_apply_tab.py +148 -0
  42. panescape-0.1.0/tests/test_calibrate.py +34 -0
  43. panescape-0.1.0/tests/test_conflicts.py +42 -0
  44. panescape-0.1.0/tests/test_geometry.py +62 -0
  45. panescape-0.1.0/tests/test_gnome.py +48 -0
  46. panescape-0.1.0/tests/test_grid.py +110 -0
  47. panescape-0.1.0/tests/test_gui.py +178 -0
  48. panescape-0.1.0/tests/test_kde.py +98 -0
  49. panescape-0.1.0/tests/test_pictures.py +206 -0
  50. panescape-0.1.0/tests/test_project.py +155 -0
  51. panescape-0.1.0/tests/test_render.py +42 -0
  52. panescape-0.1.0/tests/test_setups.py +103 -0
  53. panescape-0.1.0/tests/test_tags.py +31 -0
  54. panescape-0.1.0/tests/test_unsplash.py +272 -0
  55. panescape-0.1.0/tests/test_unsplash_tab.py +160 -0
  56. panescape-0.1.0/tests/test_wlroots.py +176 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kieran Ralston
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,619 @@
1
+ Metadata-Version: 2.4
2
+ Name: panescape
3
+ Version: 0.1.0
4
+ Summary: True-to-scale wallpapers across monitors of different sizes, resolutions and orientations (Linux: GNOME, KDE Plasma, sway, Hyprland)
5
+ Author: Kieran Ralston
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/keo99/panescape
8
+ Project-URL: Issues, https://github.com/keo99/panescape/issues
9
+ Keywords: wallpaper,multi-monitor,gnome,slideshow,bezel
10
+ Classifier: Environment :: Console
11
+ Classifier: Environment :: X11 Applications :: Qt
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Desktop Environment
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: Pillow>=10.1
19
+ Requires-Dist: PyYAML>=6
20
+ Requires-Dist: ruamel.yaml>=0.18
21
+ Provides-Extra: gui
22
+ Requires-Dist: PySide6-Essentials>=6.5; extra == "gui"
23
+ Requires-Dist: keyring>=23; extra == "gui"
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest>=7; extra == "test"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7; extra == "dev"
28
+ Requires-Dist: ruff>=0.6; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # panescape
32
+
33
+ [![CI](https://github.com/keo99/panescape/actions/workflows/ci.yml/badge.svg)](https://github.com/keo99/panescape/actions/workflows/ci.yml)
34
+
35
+ **True-to-scale wallpapers for multi-monitor setups on Linux.** Your monitors
36
+ become panes of one window onto a single picture, even when they differ in size,
37
+ resolution, orientation and height.
38
+
39
+ ![The same photo spanned by pixels (top) and by panescape (bottom) across two portrait 28" 4K screens and a lower landscape 24" 1080p screen](https://raw.githubusercontent.com/keo99/panescape/main/docs/images/comparison.jpg)
40
+
41
+ Most spanning tools work in **pixels**. A 28" 4K panel packs about 1.7× more
42
+ pixels per millimetre than a 24" 1080p panel, so with pixel spanning the picture
43
+ changes size at every screen edge and the horizon breaks. panescape works in
44
+ **millimetres**. It uses each panel's real lit area, the bezels, the gaps and the
45
+ vertical offsets you measure. Then it resamples each monitor's slice to that
46
+ monitor's native resolution.
47
+
48
+ ## Quick start
49
+
50
+ ```bash
51
+ pipx install panescape
52
+ mkdir ~/panescape && cd ~/panescape
53
+ panescape init # pictures/, configurations/, backgrounds/, setup.yml
54
+ panescape detect --write my_desk # config skeleton from your current displays
55
+ $EDITOR configurations/my_desk.yml # set gaps, offsets, bezels (mm)
56
+ panescape calibrate --watch # test pattern; re-applies on every save
57
+ cp ~/Pictures/some-wide-photo.jpg pictures/
58
+ panescape make # check backgrounds/my_desk/*/preview.jpg
59
+ panescape apply
60
+ ```
61
+
62
+ Prefer a window? `pipx install 'panescape[gui]'`,
63
+ then run `panescape-gui` (see [The window](#the-window)).
64
+
65
+ `preview.jpg` draws your desk to scale. The yellow outline is the part of the photo that was used:
66
+
67
+ ![Preview of the example desk](https://raw.githubusercontent.com/keo99/panescape/main/docs/images/preview.jpg)
68
+
69
+ **Status:** applies wallpapers and slideshows on **GNOME** (tested on GNOME 50 /
70
+ Ubuntu 26.04, Wayland, with fractional scaling), **KDE Plasma 6** (tested with Plasma 6.6; see
71
+ [KDE Plasma](#kde-plasma)), and **sway** and **Hyprland** (tested with sway
72
+ 1.11, and Hyprland 0.53 with hyprpaper 0.8; see [sway and Hyprland](#sway-and-hyprland)). On other desktops panescape makes
73
+ native-resolution images per monitor for you to set with your own tool (see
74
+ [Other desktops](#other-desktops)). Python ≥ 3.10; the dependencies are
75
+ Pillow, PyYAML and ruamel.yaml, plus PySide6 for the optional window.
76
+
77
+ ### Why not HydraPaper or a "spanned" wallpaper?
78
+
79
+ Those work too. You give them one image per monitor, or one image stretched over
80
+ the desktop, and they place it in pixel space. panescape decides *what each
81
+ monitor should show* from the physical layout, and builds the images those
82
+ tools need. On GNOME it applies them directly, slideshows included, with
83
+ nothing running in the background.
84
+
85
+ If you used HydraPaper before, switch off its daemon in HydraPaper's
86
+ preferences. Otherwise it re-applies its own wallpaper whenever the displays
87
+ change, for example when the monitors wake up. panescape warns when it sees
88
+ HydraPaper's daemon or Variety running.
89
+
90
+ ## The window
91
+
92
+ ```bash
93
+ pipx install 'panescape[gui]'
94
+ panescape-gui # or: panescape gui
95
+ ```
96
+
97
+ ![The layout editor](https://raw.githubusercontent.com/keo99/panescape/main/docs/images/gui-layout.png)
98
+
99
+ It edits the same files as the command line, keeping your comments, so you
100
+ can switch between the two whenever you like.
101
+
102
+ - **Setup** (top bar): choose which [setup](#setupyml) is active, or create,
103
+ rename and delete setups.
104
+ - **Layout:** pick the configuration the active setup uses, or make a new one
105
+ from the monitors connected now, as a copy, or from scratch. Each monitor has
106
+ a form for its model or size, resolution, rotation, bezels, position and gap.
107
+ Mistakes show up as you type, not when you apply.
108
+ - **Drag monitors** on the to-scale desk, or select one and use the arrow keys
109
+ (0.5 mm steps; Shift ×10, Alt ×0.2). This changes the `gap` and the
110
+ `v_pos`/`h_pos` offsets.
111
+ - **Show** a picture or the calibration pattern across the desk.
112
+ - **Calibrate on my screens** puts the [test pattern](#calibrating) on your
113
+ real monitors and redraws it as you drag or type. Your wallpaper comes back
114
+ when you switch it off or close the window.
115
+
116
+ ![The pictures and tags tab](https://raw.githubusercontent.com/keo99/panescape/main/docs/images/gui-pictures.png)
117
+
118
+ The **Pictures & tags** tab:
119
+
120
+ - **Add pictures** with the button, or drop files from your file manager onto
121
+ the grid. They're copied into `pictures/`.
122
+ - **Tag** the selected pictures: type tags and press Enter. Click a tag's ✕ to
123
+ remove it. With several pictures selected, a tag that only some of them have
124
+ shows as e.g. `sea 2/5`; click it to give it to all of them.
125
+ - **Manage tags** in the sidebar, which shows how many pictures have each one.
126
+ Click a tag to filter by it, or Ctrl+click to add it to the filter. Rename a
127
+ tag to an existing one to merge them.
128
+ - **Filter** with the same syntax as `setup.yml` (`landscape + water +
129
+ !mountains`). **Use for setup** saves the filter to the active setup.
130
+ - **Frame** a picture: drag it across the desk preview and scroll to zoom. The
131
+ parts that will be cut off show faintly, and a note warns when a screen would
132
+ show the picture enlarged (soft). The framing is saved in
133
+ `pictures/crops.yml` and used by every setup.
134
+
135
+ If a panescape wallpaper is on the desktop, it updates a couple of seconds
136
+ after a change.
137
+
138
+ ![The apply tab](https://raw.githubusercontent.com/keo99/panescape/main/docs/images/gui-apply.png)
139
+
140
+ The **Apply** tab:
141
+
142
+ - **Edit the active setup:** its layout, all pictures or a chosen list
143
+ (names or patterns like `city-*`), the tag filter, one picture or a
144
+ slideshow, how long each picture stays, the cross-fade and shuffling. Each
145
+ change is saved to `setup.yml` straight away, with your comments kept.
146
+ - **Check the selection:** the tab shows which pictures the setup picks and how
147
+ long a full round of the slideshow takes.
148
+ - **Apply to the desktop** puts it on your screens. **Details** shows the log.
149
+ - **On the desktop now:** the picture showing, what comes next and when, and
150
+ a note when the setup has changed since it was applied.
151
+ - **Slideshow runner** (sway, Hyprland, KDE): see whether it's running, start
152
+ or stop it, and have it start when you log in. On sway and Hyprland the tab
153
+ gives you the line to add to your config, with a Copy button, and notices
154
+ when your config already has it. It also lets you choose the wallpaper tool
155
+ (`setter`).
156
+
157
+ ![The Unsplash tab (sample results)](https://raw.githubusercontent.com/keo99/panescape/main/docs/images/gui-unsplash.png)
158
+
159
+ The **Unsplash** tab finds new pictures (the screenshot shows made-up sample
160
+ results):
161
+
162
+ - **Search** Unsplash (landscape photos by default, since they suit a wide
163
+ desk), or press **Suggest for me** for photos like the ones you have, based
164
+ on your most used tags. Photos you already have are left out of the
165
+ suggestions.
166
+ - **Choose a photo** to see it larger, with its photographer and a note on
167
+ whether it's sharp enough for your desk.
168
+ - **Download to my pictures** saves it into `pictures/` (named like
169
+ `ann-lee-Zx3abcDEF12-unsplash.jpg`). The photo's Unsplash tags are added,
170
+ minus generic ones like "hd wallpapers", and the credit goes in
171
+ `pictures/credits.yml`. The Pictures tab then shows "Photo by … on Unsplash".
172
+
173
+ ### Your Unsplash access key
174
+
175
+ Searching needs a free key of your own: create an application at
176
+ [unsplash.com/oauth/applications](https://unsplash.com/oauth/applications) and
177
+ paste its **Access Key** (not the Secret key) into the tab. A new key allows 50
178
+ requests an hour, and search results are cached for an hour to save them.
179
+ Showing pictures doesn't count, and neither does the image file itself.
180
+ A download uses one request for the download report Unsplash requires,
181
+ plus one more with **Add Unsplash's tags** ticked, because search results
182
+ don't include tags. Unticked, the picture is tagged from your search words
183
+ and the photo's description instead.
184
+
185
+ How panescape looks after the key:
186
+
187
+ - **Checked before it's saved:** panescape makes one request with the key
188
+ first, and stores it only if Unsplash accepts it.
189
+ - **Where it's stored:** in your system keyring (GNOME Keyring, KWallet, …)
190
+ if you have one; otherwise in `~/.config/panescape/unsplash-key`, which
191
+ only you can read. The environment variable `PANESCAPE_UNSPLASH_KEY`, if
192
+ set, takes precedence.
193
+ - **Where it never goes:** your project folder, `setup.yml`, the window's
194
+ settings or the cache. The window never shows it again after you save it.
195
+ - **Who receives it:** only `https://api.unsplash.com`, in a request header,
196
+ never in a web address. panescape refuses redirects to anywhere else and
197
+ removes the key from any error message.
198
+ - **Removing it:** **Remove key** deletes it from the keyring and from the
199
+ file.
200
+
201
+ Following Unsplash's API guidelines, pictures are shown straight from
202
+ Unsplash's own links (never copied into a cache), every download is reported
203
+ to Unsplash so the photographer gets credit, and photographers and Unsplash
204
+ are credited with links.
205
+
206
+ The Unsplash tab is for personal use with your own key: each person makes
207
+ their own application and keeps within its limits. panescape is not
208
+ affiliated with or endorsed by Unsplash.
209
+
210
+ ## How a project folder looks
211
+
212
+ ```
213
+ pictures/ your source images (+ tags.yml, crops.yml, credits.yml)
214
+ configurations/my_desk.yml physical description of a monitor setup
215
+ setup.yml what `panescape apply` puts on the desktop
216
+ backgrounds/<config>/<image>/
217
+ 1_left.png 2_centre.png … one native-resolution image per monitor
218
+ preview.jpg to-scale picture of the desk with bezels
219
+ meta.json geometry used (mm, source pixel boxes, upscale)
220
+ gnome-….png the combined image GNOME displays
221
+ ```
222
+
223
+ Commands run in the current folder. Point them elsewhere with `--root DIR` or
224
+ `PANESCAPE_ROOT`. Examples are in [`examples/`](https://github.com/keo99/panescape/tree/main/examples/).
225
+
226
+ ## Usage
227
+
228
+ ```bash
229
+ panescape make # slice every image in pictures/ (cached)
230
+ panescape make sunset.jpg --anchor top # one image, cut from the top
231
+ panescape make --apply # make, then apply (several images = slideshow)
232
+
233
+ panescape apply # apply what setup.yml describes
234
+ panescape apply sunset.jpg # just this image
235
+ panescape apply a.jpg b.jpg "city-*" --interval 10m --transition 2s --shuffle
236
+ panescape add new.jpg # pin an image into the current slideshow
237
+ panescape sync # re-check tags / pictures / setup.yml and update
238
+ panescape tag new.jpg -a landscape # tag an image (updates a running slideshow)
239
+ panescape tags 'landscape + !night' # list images matching a filter
240
+ panescape calibrate --watch # millimetre test pattern while you tune the config
241
+ panescape status # configs, what's been made, what's applied
242
+ panescape setup # list the setups in setup.yml (* = active)
243
+ panescape setup evening --apply # switch to another setup and apply it
244
+ panescape gui # open the window
245
+ panescape-apply … # same as `panescape apply …`
246
+ ```
247
+
248
+ Use `--dry-run` to build everything without changing the wallpaper. Use `-c NAME` to
249
+ pick a configuration, and `--force` to re-render. For the best result, use a
250
+ source image at least as many pixels wide as your desk needs. `make` warns
251
+ when a screen would be enlarged.
252
+
253
+ ## Configuration (`configurations/*.yml`)
254
+
255
+ Lengths are in mm unless you give a unit (`28in`, `2.5cm`). For a single row,
256
+ list the monitors **left to right**. Stacks, grids and other arrangements are
257
+ covered in [Layouts](#layouts-rows-stacks-and-grids).
258
+
259
+ ```yaml
260
+ name: desk_H
261
+ monitors:
262
+ - name: left
263
+ model: Samsung U28E590 # built-in specs (see src/panescape/models.py)
264
+ serial: ABC1234567 # pin to a physical screen (from `panescape detect`)
265
+ rotation: portrait-right # landscape | portrait-right | portrait-left | landscape-flipped
266
+ bezel: {left: 24, right: 16, top: 20, bottom: 20} # measured, as mounted
267
+
268
+ - name: centre
269
+ resolution: FHD # or 1920x1080, 4K, QHD, …; always the panel's native mode
270
+ size: 24in # diagonal; or active_area: [527.04, 296.46] (landscape, mm)
271
+ bezel: 6 # all sides; or [top, right, bottom, left]
272
+ gap: 0 # mm from the previous housing (negative = overlap)
273
+ v_pos: -20 # mm, + is up, relative to the first monitor
274
+ v_align: center # measure v_pos between centres | tops | bottoms
275
+
276
+ - name: right
277
+ model: Samsung U28E590
278
+ rotation: portrait-left
279
+ v_pos: 0
280
+
281
+ crop:
282
+ anchor: center # center, top, bottom-left, … or [x, y] in 0..1
283
+ zoom: 1.0 # > 1 crops tighter
284
+ fit: cover # cover (fill and crop) | contain (whole image, bars)
285
+ output:
286
+ format: png # png | jpg | webp
287
+ # png_compression: 1 # 0-9: higher = smaller files, much slower (same pixels)
288
+ ```
289
+
290
+ Every monitor key:
291
+
292
+ | key | meaning |
293
+ |---|---|
294
+ | `name` | Label, used in output file names |
295
+ | `model` | Looked up in the built-in database. Supplies resolution, exact active area and estimated bezels |
296
+ | `resolution` | Native mode, e.g. `3840x2160`, `4K` or `FHD` |
297
+ | `size` | Diagonal (default unit: inches). Used only if there's no `model` or `active_area` |
298
+ | `active_area` | `[w, h]` of the lit area in mm, landscape. This is the most precise option |
299
+ | `rotation` / `orientation` | GNOME Settings name, or `vertical` / `horizontal` |
300
+ | `bezel` | Number, `[t, r, b, l]`, or `{left, right, top, bottom, x, y}`, measured **as mounted** |
301
+ | `right_of` / `left_of` / `below` / `above` | Place this monitor next to another one, by name (see [Layouts](#layouts-rows-stacks-and-grids)) |
302
+ | `gap` | Distance between this housing and the one it's placed next to (negative = overlap) |
303
+ | `v_pos`, `v_align`, `v_ref` | Side-by-side monitors: vertical offset (+ is up), which edges it's measured between (`center`/`top`/`bottom`), and the reference monitor |
304
+ | `h_pos`, `h_align`, `h_ref` | Stacked monitors: horizontal offset (+ is right), which edges it's measured between (`center`/`left`/`right`), and the reference monitor |
305
+ | `position` | `[x, y]` of the housing's top-left in mm (y down), for arrangements the keys above can't describe |
306
+ | `serial` / `connector` / `product` | Pin to a specific screen. Without these, screens are matched to the desktop by their arrangement |
307
+
308
+ `defaults:` at the top level sets `gap`, `bezel`, `v_align` or `h_align` for every monitor.
309
+
310
+ **Precision tips**
311
+
312
+ 1. Use `model` or `active_area`. Nominal diagonals are rounded, and a "28 inch"
313
+ panel is not exactly 28 inches.
314
+ 2. Measure your bezels with a ruler. Spec-sheet bezels are estimates, because
315
+ manufacturers rarely split top and bottom.
316
+ 3. Measure `v_pos` / `h_pos` between the edges that are easiest to measure, and set `v_align` / `h_align` to match.
317
+ 4. Look at `preview.jpg`. It is drawn to scale.
318
+ 5. Finish with `panescape calibrate --watch` (see [Calibrating](#calibrating)).
319
+
320
+ ## Layouts: rows, stacks and grids
321
+
322
+ The first monitor is the anchor. Every other monitor is placed relative to
323
+ another one. The order in the file doesn't matter, as long as the references
324
+ don't go round in a circle.
325
+
326
+ | arrangement | how to write it |
327
+ |---|---|
328
+ | A row | List the monitors left to right with no placement key. Each sits right of the previous one, and `v_pos` is measured from the **first** monitor |
329
+ | Beside a monitor | `right_of: NAME` or `left_of: NAME`, plus `gap`. `v_pos` is measured from NAME, unless `v_ref` says otherwise |
330
+ | Above or below | `below: NAME` or `above: NAME`, plus `gap` (vertical) and `h_pos` / `h_align` (horizontal offset from NAME, or `h_ref`) |
331
+ | Anything else | `position: [x, y]`, the housing's top-left in mm |
332
+
333
+ A 2×2 grid ([full example](https://github.com/keo99/panescape/blob/main/examples/configurations/grid-2x2.yml)):
334
+
335
+ ```yaml
336
+ monitors:
337
+ - name: top-left
338
+ model: …
339
+ - name: top-right
340
+ right_of: top-left
341
+ gap: 1
342
+ v_align: top # tops level
343
+ - name: bottom-left
344
+ below: top-left
345
+ gap: 2
346
+ h_align: left # left edges level
347
+ - name: bottom-right
348
+ right_of: bottom-left
349
+ gap: 1
350
+ v_align: top
351
+ ```
352
+
353
+ Other examples: [a laptop under a monitor](https://github.com/keo99/panescape/blob/main/examples/configurations/laptop-under-monitor.yml),
354
+ [portrait-landscape-portrait](https://github.com/keo99/panescape/blob/main/examples/configurations/portrait-landscape-portrait.yml).
355
+
356
+ `panescape detect --write NAME` writes this structure for you from the desktop's
357
+ arrangement (rows, stacks and grids), with the measured numbers left at 0. When
358
+ applying, config monitors are matched to the desktop's screens by **arrangement**,
359
+ so a grid pairs up correctly. Use `serial:` to pin a monitor if two arrangements
360
+ could be confused.
361
+
362
+ ## Calibrating
363
+
364
+ In the window, tick **Calibrate on my screens** on the Layout tab and drag
365
+ the monitors until the lines meet. From the command line:
366
+
367
+ ```bash
368
+ panescape calibrate --watch # Ctrl+C puts your wallpaper back
369
+ panescape calibrate # apply once; undo with --restore (or panescape apply)
370
+ ```
371
+
372
+ This puts a test pattern across your screens, drawn in physical millimetres
373
+ from your configuration:
374
+
375
+ - **Cyan horizontal and yellow vertical lines** every 50 mm.
376
+ - **Orange and green 45° lines** every 100 mm.
377
+ - **A magenta circle.**
378
+ - **Rulers** along every screen edge, in desk millimetres. On screens dense
379
+ enough, they have 1 mm ticks.
380
+ - **Each screen's settings**, plus the configured distance to its neighbours' lit
381
+ areas ("lit gap").
382
+
383
+ With `--watch`, editing and saving the configuration re-applies the pattern in
384
+ about a second. When the config matches reality, every line runs straight
385
+ across the bezels. To fix it:
386
+
387
+ 1. **Spacing.** Measure the lit gaps with a ruler and adjust `gap` (or `bezel`)
388
+ until they match the numbers shown.
389
+ 2. **Side-by-side screens.** Read both edge rulers at the same physical height,
390
+ then: `v_pos += this screen's reading − the other screen's reading`.
391
+ 3. **Stacked screens.** Use the yellow lines and the top/bottom rulers, then:
392
+ `h_pos += the other screen's reading − this screen's reading`.
393
+ 4. **Check.** The diagonals and the circle should now be straight and continuous.
394
+
395
+ **Angled screens:** if your outer screens are turned or tilted towards you,
396
+ they don't lie in one plane, so the ruler readings don't line up exactly.
397
+ Instead, tune from your seat, matching lines near eye level. Lines far above or
398
+ below that height will drift slightly on the angled screens.
399
+
400
+ Without a supported desktop, the pattern is written as per-monitor images in
401
+ `backgrounds/<config>/calibration/`.
402
+
403
+ ## `setup.yml`
404
+
405
+ ```yaml
406
+ configuration: desk_H
407
+ mode: slideshow # single | slideshow
408
+ images: all # or a list of names / globs / {file, anchor, zoom}
409
+ interval: 15m
410
+ transition: 2s
411
+ shuffle: false
412
+ ```
413
+
414
+ Images that aren't made yet, or have changed, are rendered automatically.
415
+
416
+ ### Several setups
417
+
418
+ `setup.yml` can hold several named setups, for example a calm one for work
419
+ and a colourful one for the evening:
420
+
421
+ ```yaml
422
+ active: work # what apply / sync / the slideshow follow
423
+ setter: swww # keys out here are shared by every setup
424
+ setups:
425
+ work:
426
+ configuration: desk_H
427
+ filter: minimal + !people
428
+ interval: 1h
429
+ evening:
430
+ configuration: desk_H
431
+ filter: landscape + sunset
432
+ interval: 10m
433
+ ```
434
+
435
+ Switch with `panescape setup evening` (add `--apply` to apply straight away),
436
+ `panescape apply --setup evening`, or the Setup menu in the window. A
437
+ `setup.yml` without `setups:`, like the one above, is a single setup called
438
+ `default`. Creating a second setup in the window converts the file for you,
439
+ and comments are kept.
440
+
441
+ ## Keeping the slideshow up to date
442
+
443
+ `apply` saves the **selection rule** (`images`, `filter`, `mode`, …), not just the
444
+ list of files that matched it at the time:
445
+
446
+ - `panescape apply` with no images follows `setup.yml`. Each re-check reads
447
+ `setup.yml` again, so edits to it count too. Options you give on the command line
448
+ (e.g. `--filter`, `--interval`) override it and are remembered.
449
+ - `panescape apply a.jpg "city-*" --filter night` follows that rule instead. Globs
450
+ pick up new matching files; file names are fixed.
451
+
452
+ The rule is re-checked by:
453
+
454
+ | command | what happens |
455
+ |---|---|
456
+ | `panescape tag …` | after saving tags, updates the running wallpaper (skip with `--no-sync`) |
457
+ | `panescape sync` | after hand-editing `tags.yml` or `setup.yml`, or adding or deleting pictures |
458
+ | `panescape add IMAGES` / `--filter F` | **pins** images: they stay even if they later stop matching the rule. Adding to a single image turns it into a slideshow |
459
+
460
+ A re-check renders new matches, drops images that stopped matching or were
461
+ deleted, and prints `+`/`-` for each change. It does nothing if the result is
462
+ unchanged. The image that is on screen stays on screen, at the same point in
463
+ its interval, so an update doesn't restart the slideshow. With `shuffle`, existing
464
+ images keep their order and new ones are inserted at random places.
465
+
466
+ A fresh `panescape apply` replaces the rule, clears the pins and starts from
467
+ the first image.
468
+
469
+ ## Tags and filters
470
+
471
+ Tags live in `pictures/tags.yml`. You can edit the file by hand or use the CLI:
472
+
473
+ ```yaml
474
+ lake-dawn.jpg: [landscape, water]
475
+ alps.jpg: [landscape, mountains, snow]
476
+ "city-*": [urban, night] # globs tag every match
477
+ ```
478
+
479
+ ```bash
480
+ panescape tag lake-dawn.jpg "beach-*" -a landscape water # -r to remove, --clear to reset
481
+ panescape tags # list images, tags and counts
482
+ panescape tags 'landscape + water + !mountains' # preview a filter
483
+ panescape apply --filter 'landscape + water + !mountains' # one-off, overrides setup.yml
484
+ panescape add --filter beach # pin everything tagged beach right now
485
+ ```
486
+
487
+ In `setup.yml`, `filter:` narrows `images:`. Use `images: all` to filter the whole folder.
488
+
489
+ | syntax | meaning |
490
+ |---|---|
491
+ | `a + b`, `a & b`, `a and b`, `a b` | AND |
492
+ | `a \| b`, `a , b`, `a or b` | OR |
493
+ | `!a`, `not a` | NOT (binds tightest) |
494
+ | `( … )` | grouping |
495
+ | `city*` | glob on tag names |
496
+
497
+ Tags are case-insensitive, untagged images match `!x`, and `!*` matches
498
+ pictures with no tags at all. Comments you add to `tags.yml` are kept when the
499
+ command line or the window edits it.
500
+
501
+ ## Framing a picture
502
+
503
+ By default each picture is centred and scaled to fill the desk. To frame one
504
+ differently, drag it in the window's Pictures tab, or write it in
505
+ `pictures/crops.yml`:
506
+
507
+ ```yaml
508
+ tall-photo.jpg: {anchor: [0.5, 0.2], zoom: 1.2} # higher up, a little closer
509
+ panorama.jpg: {fit: contain} # whole picture, black bars
510
+ ```
511
+
512
+ Later settings win: the configuration's `crop:`, then `setup.yml`'s `crop:`,
513
+ then `crops.yml`, then an image entry in `setup.yml`'s `images:` list, then
514
+ `--anchor`/`--zoom`/`--fit` on the command line.
515
+
516
+ ## How applying works on GNOME
517
+
518
+ GNOME has one wallpaper for the whole desktop. In `spanned` mode, Mutter
519
+ stretches it over the bounding box of the *logical* monitor layout. Each monitor
520
+ then samples its own part at its own scale factor. panescape builds that
521
+ combined image at the largest scale factor, so HiDPI screens get native pixels,
522
+ and places each monitor's slice in its rectangle. It then sets
523
+ `org.gnome.desktop.background` with gsettings. Slideshows use GNOME's native
524
+ background XML, so nothing runs in the background. If you rearrange your
525
+ displays in Settings, run `panescape apply` again.
526
+
527
+ panescape always calls the system `/usr/bin/gsettings`. Conda and Anaconda put
528
+ their own `gsettings` first on `PATH`. That copy has no dconf backend, so it reports
529
+ success while changing nothing. After setting the wallpaper, panescape reads the
530
+ value back from dconf to check that it was stored.
531
+
532
+ The physical layout (config) and GNOME's arrangement (Settings > Displays) are
533
+ separate. Moving the mouse between screens uses GNOME's arrangement, and the
534
+ picture uses your measured layout.
535
+
536
+ ## sway and Hyprland
537
+
538
+ On these compositors every output gets its own native-resolution slice, so no
539
+ combined image is needed. panescape reads the layout from `swaymsg -t get_outputs`
540
+ or `hyprctl monitors -j`. It sets the wallpaper with the first of these tools
541
+ that works:
542
+
543
+ | setter | how |
544
+ |---|---|
545
+ | `sway` | sway's own `output NAME bg FILE fill` (sway runs swaybg itself) |
546
+ | `hyprpaper` | `hyprctl hyprpaper wallpaper` (start hyprpaper in your Hyprland autostart) |
547
+ | `swww` | `swww img --outputs NAME`, with a fade for slideshow transitions (needs `swww-daemon` running) |
548
+ | `swaybg` | a `swaybg` process that panescape starts and replaces |
549
+
550
+ To force one, set `setter: swww` in `setup.yml` or `PANESCAPE_SETTER=swww` in
551
+ the environment.
552
+
553
+ **Slideshows:** these desktops have no synced multi-monitor slideshow, so
554
+ `panescape slideshow` changes every output together on schedule. `apply`
555
+ starts it in the background when you apply a slideshow. Add it to your
556
+ compositor's autostart so it survives logging out:
557
+
558
+ ```
559
+ # sway (~/.config/sway/config)
560
+ exec panescape --root ~/panescape slideshow
561
+ # Hyprland
562
+ exec-once = panescape --root ~/panescape slideshow
563
+ ```
564
+
565
+ It picks up `apply`, `add`, `sync` and `tag` changes by itself and pauses while
566
+ `calibrate` is running. `panescape slideshow --stop` stops it, and
567
+ `panescape status` shows whether it's running. Everything else (`calibrate`,
568
+ `detect`, tags, rules) works the same as on GNOME.
569
+
570
+ ## KDE Plasma
571
+
572
+ Plasma has a wallpaper per screen, so each monitor gets its native slice.
573
+ panescape reads the layout with `kscreen-doctor -j` and sets the wallpapers
574
+ through Plasma's scripting interface (`org.kde.PlasmaShell.evaluateScript` on
575
+ D-Bus), for every activity. Plasma numbers screens instead of naming them, so
576
+ outputs are matched to Plasma's screens by position.
577
+
578
+ **Slideshows:** Plasma's own slideshow runs each screen separately, so it can't
579
+ keep them in step. As on sway and Hyprland, `panescape slideshow` switches all
580
+ screens together. `apply` starts it, and
581
+ `panescape slideshow --autostart` adds a login entry in `~/.config/autostart`
582
+ (`--no-autostart` removes it).
583
+
584
+ ## Other desktops
585
+
586
+ `backgrounds/<config>/<image>/N_name.png` are plain per-monitor images that you
587
+ can set with any tool, for example `feh`, Cinnamon or HydraPaper. Backends
588
+ for more desktops are welcome; see `src/panescape/backends/`.
589
+
590
+ ## Adding a monitor model
591
+
592
+ Add an entry to `src/panescape/models.py`, using the spec sheet's active area,
593
+ outer size without stand, and the bezels if you've measured them. Then open a PR.
594
+
595
+ ## Development
596
+
597
+ ```bash
598
+ pip install -e '.[dev,gui]'
599
+ pytest # GUI tests run headless (QT_QPA_PLATFORM=offscreen)
600
+ ruff check .
601
+ python docs/make_images.py # rebuild the README images and the window screenshots
602
+ ```
603
+
604
+ The latest unreleased version: `pipx install 'panescape[gui] @ git+https://github.com/keo99/panescape'`.
605
+
606
+ **Releasing:** set `__version__` in `src/panescape/__init__.py`, commit, then
607
+ `git tag v0.1.0 && git push origin main v0.1.0`. The release workflow checks
608
+ that the tag matches the version, runs the tests, and publishes to PyPI.
609
+
610
+ ## Credits
611
+
612
+ README images use "Panorama of the valley of the Triglav mountain massif" by
613
+ otrow_photography, released under
614
+ [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) via
615
+ [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Panorama_of_the_valley_of_the_Triglav_mountain_massif.jpg).
616
+
617
+ ## License
618
+
619
+ MIT © Kieran Ralston