diff --git a/w-engine/README.md b/w-engine/README.md new file mode 100644 index 0000000..d0e94a3 --- /dev/null +++ b/w-engine/README.md @@ -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. diff --git a/w-engine/plugin.toml b/w-engine/plugin.toml new file mode 100644 index 0000000..4141d0b --- /dev/null +++ b/w-engine/plugin.toml @@ -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" \ No newline at end of file diff --git a/w-engine/start.luau b/w-engine/start.luau new file mode 100644 index 0000000..d2153e5 --- /dev/null +++ b/w-engine/start.luau @@ -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 \ No newline at end of file diff --git a/w-engine/thumbnail.webp b/w-engine/thumbnail.webp new file mode 100644 index 0000000..cea1394 Binary files /dev/null and b/w-engine/thumbnail.webp differ diff --git a/w-engine/translations/en.json b/w-engine/translations/en.json new file mode 100644 index 0000000..0d3cdee --- /dev/null +++ b/w-engine/translations/en.json @@ -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" + } +} diff --git a/w-engine/w-engine-panel.luau b/w-engine/w-engine-panel.luau new file mode 100644 index 0000000..e9da910 --- /dev/null +++ b/w-engine/w-engine-panel.luau @@ -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 \ No newline at end of file diff --git a/w-engine/w-engine.luau b/w-engine/w-engine.luau new file mode 100644 index 0000000..d6f1b80 --- /dev/null +++ b/w-engine/w-engine.luau @@ -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 \ No newline at end of file