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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
682 changes: 682 additions & 0 deletions wallpaper-carousel/Carousel.qml

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions wallpaper-carousel/Main.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import QtQuick
import qs.Commons
import qs.Services.UI
import qs.Services.Compositor

// Noctalia entry point
Item {
id: root

property var pluginApi: null

Carousel {
id: carousel
anchors.fill: parent

wlrNamespace: "noctalia:plugins:wallpaperCarousel"
cfg: pluginApi?.pluginSettings ?? {}
getFocusedScreen: () => CompositorService.getFocusedScreen()

defaultWallpaperFolder: {
const screenName = carousel.overlayScreen?.name ?? "";
const monDir = screenName ? WallpaperService.getMonitorDirectory(screenName) : "";
if (monDir) return monDir;
const globalDir = Settings.data.wallpaper.directory;
if (globalDir) return Settings.preprocessPath(globalDir);
return Settings.defaultWallpapersDirectory;
}

currentWallpaperPath: {
const screenName = carousel.overlayScreen?.name ?? "";
return WallpaperService.getWallpaper(screenName) ?? "";
}

extraDirectories: {
if (!Settings.data.wallpaper.enableMultiMonitorDirectories) return [];
var dirs = [];
var monDirs = Settings.data.wallpaper.monitorDirectories;
for (var i = 0; i < (monDirs ? monDirs.length : 0); i++) {
var d = monDirs[i].directory;
if (d) {
var resolved = Settings.preprocessPath(d);
if (resolved !== carousel.wallpaperFolder && dirs.indexOf(resolved) < 0)
dirs.push(resolved);
}
}
return dirs;
}

hasWallpaperConfigured: {
const dir = Settings.data.wallpaper.directory;
return !(!dir || Settings.data.wallpaper.useSolidColor);
}

shellSettingsHint: "Open Noctalia Settings → Wallpaper,\nand select a wallpaper directory."

onWallpaperPicked: (fullPath, screenName) => {
if (Settings.data.wallpaper.enableMultiMonitorDirectories && screenName)
WallpaperService.changeWallpaper(fullPath, screenName);
else
WallpaperService.changeWallpaper(fullPath);
}
}

Component.onCompleted: {
console.info("WallpaperCarousel: plugin loaded — use 'qs -c noctalia-shell ipc call wallpaperCarousel toggle' to open");
}
}
98 changes: 98 additions & 0 deletions wallpaper-carousel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Wallpaper Carousel

Based on the original wallpaper picker by [ilyamiro](https://github.com/ilyamiro/nixos-configuration).

A [DankMaterialShell](https://danklinux.com/) and [Noctalia](https://noctalia.dev/) plugin that lets you browse and pick wallpapers from a fullscreen skewed carousel overlay.

![screenshot](screenshot.png)


## About

Wallpaper Carousel scans your current wallpaper directory and displays all images in an animated 3D-skewed carousel. Navigate with keyboard or mouse, press Enter to apply. Thumbnails are pre-cached in memory at boot for instant opening.

This plugin integrates with all shell features — selecting a wallpaper updates the shell wallpaper, color scheme, and wallpaper animations configured in the shell.

https://github.com/user-attachments/assets/39bcde76-7d7b-40c0-a083-3b8961edf10b

## Credits

Original wallpaper picker by [ilyamiro](https://github.com/ilyamiro/nixos-configuration).

Wallpaper collection in the screenshot/video from [Andreas Rocha](https://www.andreasrocha.com/).


## Install

> **Note:** Your shell (Noctalia or DankMaterialShell) must be managing your wallpaper for this plugin to work. It does not work with external wallpaper engines (e.g. swww, swaybg, hyprpaper). Enable wallpaper management in DMS Settings → Wallpaper or Noctalia Settings → Wallpaper.

### Plugin manager

The plugin can be installed from the plugin browser in Noctalia and DankMaterialShell.

### Manual install

1. Download the latest archive from the [Releases](../../releases) page
2. a. Extract it into your DMS plugins directory:
```sh
tar xf wallpaperCarousel-*.tar.gz -C "${XDG_CONFIG_HOME:-$HOME/.config}/DankMaterialShell/plugins/"
```
b. _OR_ Extract it into your Noctalia plugins directory:
```sh
tar xf wallpaperCarousel-*.tar.gz -C "${XDG_CONFIG_HOME:-$HOME/.config}/noctalia/plugins/"
```
3. a. Open DankMaterialShell Settings → Plugins and enable **Wallpaper Carousel**
b. _OR_ Open Noctalia Settings → Plugins and enable **Wallpaper Carousel**
4. Bind keys in your compositor config (see below) or call the IPC commands from a script

## IPC Commands

### DMS

Control the carousel via DMS IPC:

| Command | Description |
|---------|-------------|
| `dms ipc wallpaperCarousel toggle` | Open or close the overlay |
| `dms ipc wallpaperCarousel open` | Open the overlay |
| `dms ipc wallpaperCarousel close` | Close the overlay |
| `dms ipc wallpaperCarousel cycleNext` | Open (if closed) and highlight next wallpaper |
| `dms ipc wallpaperCarousel cyclePrevious` | Open (if closed) and highlight previous wallpaper |

### Noctalia

Control the carousel via Noctalia IPC:

| Command | Description |
|---------|-------------|
| `qs -c noctalia-shell ipc call wallpaperCarousel toggle` | Open or close the overlay |
| `qs -c noctalia-shell ipc call wallpaperCarousel open` | Open the overlay |
| `qs -c noctalia-shell ipc call wallpaperCarousel close` | Close the overlay |
| `qs -c noctalia-shell ipc call wallpaperCarousel cycleNext` | Open (if closed) and highlight next wallpaper |
| `qs -c noctalia-shell ipc call wallpaperCarousel cyclePrevious` | Open (if closed) and highlight previous wallpaper |

**Keyboard shortcuts** (when open): `←` / `→` to navigate, `Enter` to apply, `Escape` to close.

## Example Compositor Keybindings

### Niri

In `~/.config/niri/config.kdl`:

```kdl
binds {
Mod+W { spawn "dms" "ipc" "wallpaperCarousel" "toggle"; }
Mod+Shift+Right { spawn "dms" "ipc" "wallpaperCarousel" "cycleNext"; }
Mod+Shift+Left { spawn "dms" "ipc" "wallpaperCarousel" "cyclePrevious"; }
}
```

### Hyprland

In `~/.config/hypr/hyprland.conf`:

```ini
bind = SUPER, W, exec, dms ipc wallpaperCarousel toggle
bind = SUPER SHIFT, Right, exec, dms ipc wallpaperCarousel cycleNext
bind = SUPER SHIFT, Left, exec, dms ipc wallpaperCarousel cyclePrevious
```
188 changes: 188 additions & 0 deletions wallpaper-carousel/Settings.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls

// Noctalia settings page
ColumnLayout {
id: root
spacing: 12

property var pluginApi: null
property var s: JSON.parse(
JSON.stringify(pluginApi?.pluginSettings ??
pluginApi?.manifest?.metadata?.defaultSettings ?? {})
)

function save() {
pluginApi.pluginSettings = s;
pluginApi.saveSettings();
}

// ── Reusable slider row ───────────────────────────────────────────────────
component SettingSlider: ColumnLayout {
id: sl
property string label: ""
property string description: ""
property string key: ""
property real min: 0
property real max: 100
property real defaultValue: 0
property string unit: ""

Layout.fillWidth: true
spacing: 2

RowLayout {
Layout.fillWidth: true
Label { text: sl.label; font.bold: true; Layout.fillWidth: true }
Label {
text: Math.round(root.s[sl.key] ?? sl.defaultValue) + sl.unit
font.pixelSize: 12; opacity: 0.7
}
}
Slider {
Layout.fillWidth: true
from: sl.min; to: sl.max; stepSize: 1
value: root.s[sl.key] ?? sl.defaultValue
onMoved: { root.s[sl.key] = Math.round(value); root.save(); }
}
Label {
text: sl.description
font.pixelSize: 12; opacity: 0.6; wrapMode: Text.WordWrap; Layout.fillWidth: true
}
}

// ── Reusable combo row ────────────────────────────────────────────────────
component SettingCombo: ColumnLayout {
id: cb
property string label: ""
property string description: ""
property string key: ""
property var options: []
property string defaultValue: ""

Layout.fillWidth: true
spacing: 2

Label { text: cb.label; font.bold: true }
ComboBox {
Layout.fillWidth: true
model: cb.options
textRole: "text"
valueRole: "value"
currentIndex: {
const v = root.s[cb.key] ?? cb.defaultValue;
for (let i = 0; i < cb.options.length; i++)
if (cb.options[i].value === v) return i;
return 0;
}
onActivated: { root.s[cb.key] = model[currentIndex].value; root.save(); }
}
Label {
text: cb.description
font.pixelSize: 12; opacity: 0.6; wrapMode: Text.WordWrap; Layout.fillWidth: true
}
}

// ── General ───────────────────────────────────────────────────────────────
Label { text: "General"; font.bold: true; font.pixelSize: 14 }

Label { text: "Wallpaper Directory"; font.bold: true }
TextField {
Layout.fillWidth: true
placeholderText: "/home/user/Pictures/Wallpapers"
text: root.s.wallpaperDirectory ?? ""
onTextChanged: { root.s.wallpaperDirectory = text; root.save(); }
}
Label {
text: "Override the wallpaper directory. Leave empty to follow the current wallpaper's directory."
font.pixelSize: 12; opacity: 0.6; wrapMode: Text.WordWrap; Layout.fillWidth: true
}

SettingCombo {
label: "Carousel Mode"
description: "Standard stops at the edges. Wrap loops the index. Infinite shows a seamless repeating view."
key: "carouselMode"; defaultValue: "wrap"
options: [
{ text: "Standard", value: "standard" },
{ text: "Wrap", value: "wrap" },
{ text: "Infinite", value: "infinite" }
]
}

// ── Visual ────────────────────────────────────────────────────────────────
Label { text: "Visual"; font.bold: true; font.pixelSize: 14; Layout.topMargin: 8 }

SettingSlider {
label: "Background Dimming"; description: "Opacity of the dark overlay behind the carousel."
key: "overlayOpacity"; min: 0; max: 100; defaultValue: 80; unit: "%"
}

SettingSlider {
label: "Border Width"; description: "Width of the skewed border around thumbnails."
key: "borderWidth"; min: 0; max: 20; defaultValue: 3; unit: "px"
}

SettingSlider {
label: "Corner Radius"; description: "Corner radius of thumbnails. Set to 0 to disable."
key: "cornerRadius"; min: 0; max: 60; defaultValue: 0; unit: "px"
}

// ── Size ──────────────────────────────────────────────────────────────────
Label { text: "Size"; font.bold: true; font.pixelSize: 14; Layout.topMargin: 8 }

SettingSlider {
label: "Item Width"; description: "Width of each wallpaper thumbnail."
key: "itemWidth"; min: 100; max: 1000; defaultValue: 300; unit: "px"
}

SettingSlider {
label: "Item Height"; description: "Height of each wallpaper thumbnail."
key: "itemHeight"; min: 100; max: 1440; defaultValue: 420; unit: "px"
}

SettingSlider {
label: "Center Tile Zoom"; description: "Size of the centered tile relative to the surrounding tiles."
key: "selectedScale"; min: 100; max: 150; defaultValue: 108; unit: "%"
}

// ── Expansion ─────────────────────────────────────────────────────────────
Label { text: "Expansion"; font.bold: true; font.pixelSize: 14; Layout.topMargin: 8 }

SettingCombo {
label: "Expand Selected"
description: "Expand the centered tile's width to reveal more of the image."
key: "expandSelected"; defaultValue: "false"
options: [
{ text: "Disabled", value: "false" },
{ text: "Enabled", value: "true" }
]
}

SettingSlider {
label: "Expansion Amount"; description: "Width multiplier applied when the centered tile is expanded."
key: "expandMultiplier"; min: 100; max: 300; defaultValue: 120; unit: "%"
}

SettingCombo {
label: "Hold to Expand"
description: "Dwell on a tile to trigger a large immersive preview."
key: "enableHoldExpand"; defaultValue: "false"
options: [
{ text: "Disabled", value: "false" },
{ text: "Enabled", value: "true" }
]
}

SettingSlider {
label: "Hold Coverage"; description: "Screen coverage for the hold preview."
key: "holdExpandRatio"; min: 30; max: 100; defaultValue: 35; unit: "%"
}

SettingSlider {
label: "Hold Delay"; description: "Time to dwell on a tile before the preview activates."
key: "holdDelay"; min: 200; max: 10000; defaultValue: 1500; unit: "ms"
}

Item { Layout.fillHeight: true }
}
Loading