feat!: add W Engine plugin (#81)

* feat!: add W Engine plugin

* feat!: add en translation

* fix: rm useless line

* feat!: add preview window in a pannel

* fix: rm no dependencies tools

* feat!: update thumbnail with new image

* feat: add screen choose with selector and add id check for warn string command folder

* docs: add dependencies on toml and read me

* fix: rm pkill and use kill

* feat!: add multiple path check, default output, select where set wallpaper

* feat!: rm debug notif

* docs: add new action documentation

* feat!: add new service for good restart (pkill all instance linux-wallpaper and rm all tmp file created by w engine)

* feat!: add check if pid is a linux-wallpaper instance

* docs: add depedencie and new entries

* feat!: support personnal path with json load

* docs: add docs for custom path

* feat!: use new api function for show all wallpaper

* fix: rm useless notify
This commit is contained in:
Tadomika-Ari
2026-07-25 20:22:52 -04:00
committed by GitHub
parent 38aa2d212d
commit cd80c56409
7 changed files with 400 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
# W Engine
A [Noctalia](https://github.com/noctalia-dev/noctalia) v5 plugin for applying Wallpaper Engine wallpapers from Steam Workshop.
## Dependencies
Before installing this plugin, make sure you already have Wallpaper Engine installed from Steam. Without it, the plugin will not work.
You need Steam itself to download wallpapers from the Workshop, plus the `linux-wallpaperengine` tool.
You also need `kill` to stop duplicate wallpaper processes when changing wallpapers, and `setsid` to start the wallpaper process correctly.
For more details, see the install section below.
## Plugin
| Field | Value |
| --- | --- |
| ID | `tadomika_ari/w-engine` |
| Entries | Bar widget: `w-engine-widget`; panel: `w-engine-panel`; Service: `start`|
## Usage
Add the `W Engine` widget from Noctalia's widget picker, then click it to open the panel. You can also open the panel directly or bind it in your compositor:
```sh
noctalia msg panel-toggle tadomika_ari/w-engine:w-engine-panel
```
| Action | Effect |
| --- | --- |
| Left click on the bar glyph | Open or close the W Engine panel |
| Click a wallpaper entry in the panel | Apply that wallpaper |
| Select an output | Change the monitor where the wallpaper is displayed |
## Settings
Custom path:
To define a custom path for Steam or the Workshop, you can add a specific path for W Engine.
You can create a `data.json` file in `$NOCTALIA_STATE_HOME` based on this example:
```json
{
"personnalPath": ["path1"]
}
```
Replace `path1` with your personal path or add other paths. The path is loaded after a reload or if you reopen the widget.
## Requirements
- noctalia ≥ 5.0.0
- `linux-wallpaperengine`
- `kill`
- `pkill`
- `setsid`
- Wallpaper Engine
- Steam for Workshop access
## Install
Install **W Engine** from Noctalia's plugin store (*Settings → Plugins*), then add the widget to a bar from *Settings → Bar*. Plugin options live in *Settings → Plugins*.
Next, install Wallpaper Engine from Steam: https://store.steampowered.com/app/431960/Wallpaper_Engine/
Then install `linux-wallpaperengine` from its project page: https://github.com/Almamu/linux-wallpaperengine
Note that some Wallpaper Engine wallpapers may not work correctly on Linux, especially ones with advanced visual effects.
If you use NixOS, be aware that the package version in nixpkgs may lag behind upstream.
For local development, add your working copy as a path source instead
(`.luau` edits hot-reload):
```sh
noctalia msg plugins source add dev path /path/to/plugins
noctalia msg plugins enable tadomika_ari/w-engine
```
## License
MIT.
+24
View File
@@ -0,0 +1,24 @@
id = "tadomika_ari/w-engine"
name = "W Engine"
version = "1.0.0"
plugin_api = 3
author = "TadomiKa-Ari"
license = "MIT"
dependencies = ["linux-wallpaperengine", "kill", "setsid"]
icon = "movie"
description = "A Noctalia widget for applying Wallpaper Engine wallpapers from Steam Workshop with one click"
tags = ["wallpaper", "utility"]
[[widget]]
id = "w-engine-widget"
entry = "w-engine.luau"
[[panel]]
id = "w-engine-panel"
entry = "w-engine-panel.luau"
width = 800
height = 600
[[service]]
id = "start"
entry = "start.luau"
+11
View File
@@ -0,0 +1,11 @@
-- reset section
noctalia.runAsync("pkill linux-wallpaper")
local entries = noctalia.listDir("/tmp")
for _, entrieName in entries do
if entrieName:match("^w%-engine") then -- match for w-engine tmp file
noctalia.removeFile("/tmp/" .. entrieName)
end
end
-- realod section
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

+11
View File
@@ -0,0 +1,11 @@
{
"plugin_name": "W Engine",
"plugin_description": "A widget for Wallpaper Engine. Set wallpaper from Wallpaper Engine with simple click.",
"panel": {
"apply": "Apply wallpaper {id}",
"list_failed": "Could not list Wallpaper Engine wallpapers",
"missing_project_json": "No project.json for {name}",
"no_wallpapers": "No Wallpaper Engine wallpapers found",
"title": "W Engine"
}
}
+263
View File
@@ -0,0 +1,263 @@
--!nonstrict
-- W-Engine panel — picks a Wallpaper Engine workshop item and applies it via
-- linux-wallpaperengine.
-- Local variable
wallpaperPath = {"~/.steam/steam/steamapps/workshop/content/431960/",
"~/.local/share/Steam/steamapps/workshop/content/431960/",
"~/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960/",
"~/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960/",
"~/snap/steam/common/.local/share/steam/steamapps/workshop/content/431960/"}
wallpaperPathSelected = nil
local pickSlots = {}
local columns = 4
local previewWidth = 180
local previewHeight = 100
local outputFinalList = {}
local outputSelectedName = nil
local lastOutputUse = nil
local pidList: { killSystem } = {}
-- load personnal path
local dir = noctalia.pluginDataDir()
local path = dir .. "/data.json"
local raw = noctalia.readFile(path)
if raw then
local data = noctalia.json.decode(raw)
if data and data.personnalPath then
for _, p in ipairs(data.personnalPath) do
table.insert(wallpaperPath, p)
end
end
end
--
type Wallpaper = {
nb: string,
name: string,
preview: string,
}
type killSystem = {
pid: number,
output: string,
}
function tr(key, subst)
if subst then
return noctalia.tr(key, subst)
end
return noctalia.tr(key)
end
function takeWallpaper()
local wallpaper: { Wallpaper } = {}
for count = 1, #wallpaperPath, 1 do
if noctalia.listDir(wallpaperPath[count]) then
wallpaperPathSelected = wallpaperPath[count]
end
end
local entries, err = noctalia.listDir(wallpaperPathSelected)
if not entries then
warn(err or "impossible de lister")
noctalia.notify(tr("panel.title"), tr("panel.list_failed"))
return wallpaper
end
for _, entryName in entries do
if not tonumber(entryName) then -- test if is a id
continue
end
local content, readErr = noctalia.readFile(wallpaperPathSelected .. entryName .. "/project.json")
if content then
local value = noctalia.json.decode(content)
local name = value.title or entryName
local preview = value.preview or "no preview"
table.insert(wallpaper, {
nb = entryName,
name = name,
preview = preview,
})
else
noctalia.notify(tr("panel.title"), tr("panel.missing_project_json", { name = entryName }))
end
end
return wallpaper
end
function trim(s) -- personnal trim
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
function isValidPid(screen, pid)
local path = "/tmp/w-engine-" .. screen .. ".pid"
if noctalia.fileExists(path) then
local fileTmp = noctalia.readFile(path)
if not fileTmp then
return false
end
local file = trim(fileTmp)
if file == pid then
local check = "/proc/" .. pid .. "/cmdline"
local checkContentTmp = noctalia.readFile(check)
if not checkContentTmp then
return false
end
local checkContent = trim(checkContentTmp)
local match = "^linux%-wallpaperengine"
if checkContent:match(match) then
return true
end
end
end
return false
end
function getStoredPid(screen)
for _, entry in ipairs(pidList) do
if entry.output == screen then
if isValidPid(screen, entry.pid) then
return entry.pid
end
end
end
return nil
end
function setStoredPid(screen, pid)
for _, entry in ipairs(pidList) do
if entry.output == screen then
entry.pid = pid
return
end
end
table.insert(pidList, { pid = pid, output = screen })
end
function launchWallpaper(screen, id, pidFile)
local cmd = "setsid linux-wallpaperengine --screen-root " .. screen .. " " .. id
.. " > /dev/null 2>&1 & echo $! > " .. pidFile
noctalia.runAsync(cmd, function(result)
local pidContent = noctalia.readFile(pidFile)
if pidContent then
local pid = trim(pidContent)
setStoredPid(screen, pid)
end
end, 5000)
end
function loadWallpaper(id)
lastOutputUse = outputSelectedName or takeOutput()
local pidFile = "/tmp/w-engine-" .. lastOutputUse .. ".pid"
local oldPid = getStoredPid(lastOutputUse) -- here
if oldPid then
noctalia.runAsync("kill " .. oldPid, function() -- here
launchWallpaper(lastOutputUse, id, pidFile)
end, 3000)
else
launchWallpaper(lastOutputUse, id, pidFile)
end
end
function onPickAt(slot)
local id = pickSlots[slot]
if id ~= nil then
loadWallpaper(id)
noctalia.notify(tr("panel.title"), tr("panel.apply", { id = id }))
end
end
local function render()
pickSlots = {}
local tiles = {}
local list = takeWallpaper()
for count = 1, #list, 1 do
local data = list[count]
local slotIndex = count - 1
pickSlots[slotIndex] = data.nb
table.insert(tiles, ui.column({ gap = 4, key = data.nb, align = "center" }, {
ui.image({
path = wallpaperPathSelected .. data.nb .. "/" .. data.preview,
width = previewWidth,
height = previewHeight,
fit = "cover",
radius = 8,
onClick = function()
loadWallpaper(pickSlots[slotIndex])
noctalia.notify(tr("panel.title"), tr("panel.apply", { id = pickSlots[slotIndex] }))
end,
}),
ui.label({
text = data.name,
fontSize = 11,
maxLines = 1,
maxWidth = 120,
textAlign = "center",
}),
}))
end
local rows = {}
local row = {}
for i, tile in ipairs(tiles) do
table.insert(row, tile)
if #row == columns or i == #tiles then
table.insert(rows, ui.row({ gap = 12, align = "start" }, row))
row = {}
end
end
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
ui.label({ text = tr("panel.title"), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.select({ options = getOutput(), onChange = "outputSelected" }),
ui.button({ glyph = "close", onClick = "onCloseClicked" }),
}),
ui.scroll({ gap = 12, align = "stretch", flexGrow = 1 }, rows),
}))
end
function takeOutput()
local output = noctalia.outputs()
for _, outputF in ipairs(output) do
return outputF.name
end
end
function outputSelected(index, label)
if type(label) == "string" and label ~= "" then
outputSelectedName = label
noctalia.notify("Output change :", outputSelectedName)
end
end
function getOutput()
outputFinalList = {}
local outputsList = noctalia.outputs()
if not outputsList then
return
end
for _, output in ipairs(outputsList) do
noctalia.log(output.name)
table.insert(outputFinalList, output.name)
end
return outputFinalList
end
function onOpen(_context)
outputSelectedName = takeOutput()
render()
end
function onCloseClicked()
panel.close()
end
+8
View File
@@ -0,0 +1,8 @@
function update()
noctalia.setUpdateInterval(1000)
barWidget.setGlyph("movie")
end
function onClick()
noctalia.togglePanel("tadomika_ari/w-engine:w-engine-panel")
end