diff --git a/daily-wallpaper/README.md b/daily-wallpaper/README.md
index d8097eda..53060306 100644
--- a/daily-wallpaper/README.md
+++ b/daily-wallpaper/README.md
@@ -1,14 +1,19 @@
# Daily Wallpaper
-Daily Wallpaper fetches Bing's image of the day or NASA's image of the day and
-applies it through Noctalia's wallpaper API.
+Daily Wallpaper fetches Bing's image of the day or NASA's image of the day,
+applies it through Noctalia's wallpaper API, and shows the caption/credit that
+came with it — on the bar, in a panel, and on the desktop.
## Plugin
| Field | Value |
| --- | --- |
| ID | `nzlov/daily-wallpaper` |
-| Entry | Service: `service` |
+| Entries | Service: `service`; bar widget: `widget`; panel: `panel`; desktop widget: `desktop` |
+
+## Requirements
+
+Install `xdg-utils` on `PATH` (for the panel's "Learn more" button, via `xdg-open`).
## Usage
@@ -19,12 +24,27 @@ per source and Bing locale each day.
Choose Bing or NASA under the plugin's settings. Bing accepts a market locale
such as `en-US`, `de-DE`, or `fr-FR`; NASA ignores the locale setting.
+Add the `widget` bar entry and/or the `desktop` desktop widget from Settings
+to see today's caption. Clicking the bar glyph (or running
+`noctalia msg panel-toggle nzlov/daily-wallpaper:panel`) opens the full panel
+with the caption, credit (Bing only), and a "Learn more" link — Bing's quiz
+page, or NASA's full article about the image.
+
## Settings
| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `source` | `select` | `bing` | Selects Bing or NASA as the daily image source. |
| `locale` | `string` | *(automatic)* | Bing market locale; an empty value uses the service default. |
+| `widget.glyph` | `glyph` | `photo` | Bar icon. |
+| `widget.show_text` | `bool` | `true` | Show a truncated caption next to the bar glyph. |
+| `widget.max_chars` | `int` | `28` | Max caption length shown on the bar before truncating with an ellipsis. |
+
+## IPC
+
+```sh
+noctalia msg plugin nzlov/daily-wallpaper:service all refresh
+```
## Notes
@@ -32,3 +52,11 @@ The service contacts the selected provider and downloads images into a
dedicated `daily-wallpaper` cache directory. It removes cached images older
than five days. Repeated failures are logged, but error notifications are
limited to once per day.
+
+Neither source gives a wallpaper-application step any reason to keep the
+caption text that comes with the image, so it is captured separately at the
+point each source resolves and published as plugin state for the bar widget,
+panel, and desktop widget to display. Bing's `copyright` field is one caption
+line, e.g. "Runners at the base of Victoria Falls, Zimbabwe (© Byron.../Getty
+Images)", which is split into a title and a credit; NASA's gallery-item
+caption is a single sentence with no separate credit line.
diff --git a/daily-wallpaper/daily_wallpaper.luau b/daily-wallpaper/daily_wallpaper.luau
index dce80ede..c0a3f411 100644
--- a/daily-wallpaper/daily_wallpaper.luau
+++ b/daily-wallpaper/daily_wallpaper.luau
@@ -1,7 +1,8 @@
--!nonstrict
-- Daily Wallpaper service for Noctalia 5.
-- Fetches one Bing or NASA image per day, caches it locally, and applies it via
--- the native Noctalia wallpaper API.
+-- the native Noctalia wallpaper API. Also captures each source's caption and
+-- publishes it as "today" state for widget.luau/panel.luau/desktop_widget.luau.
local CHECK_INTERVAL_MS = 10 * 60 * 1000
local RETENTION_SECONDS = 5 * 24 * 60 * 60
@@ -49,10 +50,40 @@ end
local function decodeHtml(value)
local decoded = value:gsub("&", "&")
decoded = decoded:gsub(""", '"')
- decoded = decoded:gsub("'", "'")
+ -- Numeric entities, e.g. ' or '.
+ decoded = decoded:gsub("(%d+);", function(n)
+ local code = tonumber(n)
+ return (code and code < 256) and string.char(code) or ""
+ end)
return decoded
end
+-- Bing's `copyright` field is "Caption text (© Photographer/Source)"; split
+-- off the trailing credit so title and credit can be shown separately.
+local function splitCopyright(copyright)
+ local body, credit = copyright:match("^(.-)%s*%(([^()]*)%)%s*$")
+ if body and credit then
+ return decodeHtml(body), decodeHtml(credit)
+ end
+ return decodeHtml(copyright), ""
+end
+
+-- Publishes today's caption/credit/link for the bar widget, panel, and
+-- desktop widget to read via noctalia.state.
+local function publishToday(selectedSource, date, info)
+ if type(info) ~= "table" then
+ return
+ end
+ noctalia.state.set("today", {
+ date = date,
+ source = selectedSource,
+ title = info.title or "",
+ credit = info.credit or "",
+ link = info.link or "",
+ })
+ noctalia.log(`Daily Wallpaper caption: {info.title or ""}`)
+end
+
local function absoluteNasaUrl(url)
if url == nil or url == "" then
return ""
@@ -170,7 +201,12 @@ local function resolveBing(locale, done)
return
end
- done(`bing-{locale}`, `https://www.bing.com{urlBase}_UHD.jpg`, `https://www.bing.com{urlBase}_1920x1080.jpg`)
+ local title, credit = splitCopyright(first.copyright or "")
+ local link = (type(first.copyrightlink) == "string" and first.copyrightlink ~= "")
+ and first.copyrightlink or ""
+
+ done(`bing-{locale}`, `https://www.bing.com{urlBase}_UHD.jpg`, `https://www.bing.com{urlBase}_1920x1080.jpg`,
+ { title = title, credit = credit, link = link })
end)
if not started then
@@ -178,6 +214,16 @@ local function resolveBing(locale, done)
end
end
+-- A small window of markup around the first gallery item, so later matches
+-- run against a bounded string instead of the whole page.
+local function firstNasaGalleryItem(page)
+ local s = page:find("hds-gallery-item-single hds-gallery-image", 1, true)
+ if not s then
+ return nil
+ end
+ return page:sub(s, s + 4000)
+end
+
local function resolveNasa(done)
local started = noctalia.http({ url = "https://www.nasa.gov/image-of-the-day/" }, function(res)
if not res.ok or res.status < 200 or res.status >= 300 then
@@ -186,8 +232,17 @@ local function resolveNasa(done)
end
local page = res.body
- local primary = page:match('class="[^"]-hds%-gallery%-image[^"]-"[^>]->.-
]-src="([^"]+)"')
- or page:match('
]-class="[^"]-hds%-gallery%-image[^"]-"[^>]-src="([^"]+)"')
+ local item = firstNasaGalleryItem(page)
+
+ local primary, caption, link
+ if item then
+ primary = item:match('
]-src="([^"]+)"')
+ -- NASA has no separate credit line (unlike Bing), so the whole
+ -- caption becomes the title and credit stays empty.
+ caption = item:match('class="hds%-gallery%-item%-caption[^"]*">([^<]*)')
+ link = item:match('class="hds%-gallery%-item%-link[^"]*"%s+href="([^"]+)"')
+ end
+
local fallback = page:match('/dev/null 2>&1")
+ end
+end
+
+noctalia.state.watch("today", render)
+noctalia.state.watch("status", render)
diff --git a/daily-wallpaper/plugin.toml b/daily-wallpaper/plugin.toml
index 56ddde89..4bbecf3e 100644
--- a/daily-wallpaper/plugin.toml
+++ b/daily-wallpaper/plugin.toml
@@ -1,13 +1,13 @@
id = "nzlov/daily-wallpaper"
name = "Daily Wallpaper"
-version = "1.0.0"
+version = "1.1.0"
plugin_api = 3
author = "nzlov"
license = "MIT"
-dependencies = []
-tags = ["wallpaper", "desktop"]
+dependencies = ["xdg-open"]
+tags = ["wallpaper", "desktop", "bar", "panel", "service"]
icon = "photo"
-description = "Fetches a new daily wallpaper from Bing or NASA."
+description = "Fetches a new daily wallpaper from Bing or NASA, and shows its caption on the bar, in a panel, and on the desktop."
[[setting]]
key = "source"
@@ -29,3 +29,39 @@ default = ""
[[service]]
id = "service"
entry = "daily_wallpaper.luau"
+
+[[widget]]
+id = "widget"
+entry = "widget.luau"
+
+ [[widget.setting]]
+ key = "glyph"
+ type = "glyph"
+ label_key = "settings.widget.glyph.label"
+ default = "photo"
+
+ [[widget.setting]]
+ key = "show_text"
+ type = "bool"
+ label_key = "settings.widget.show_text.label"
+ default = true
+
+ [[widget.setting]]
+ key = "max_chars"
+ type = "int"
+ label_key = "settings.widget.max_chars.label"
+ default = 28
+ min = 8
+ max = 80
+
+[[panel]]
+id = "panel"
+entry = "panel.luau"
+width = 420
+height = 250
+placement = "attached"
+open_near_click = true
+
+[[desktop_widget]]
+id = "desktop"
+entry = "desktop_widget.luau"
diff --git a/daily-wallpaper/translations/en.json b/daily-wallpaper/translations/en.json
index b6e215f2..92ea385e 100644
--- a/daily-wallpaper/translations/en.json
+++ b/daily-wallpaper/translations/en.json
@@ -14,6 +14,31 @@
"bing": "Bing",
"nasa": "NASA"
}
+ },
+ "widget": {
+ "glyph": {
+ "label": "Glyph"
+ },
+ "show_text": {
+ "label": "Show caption text next to glyph"
+ },
+ "max_chars": {
+ "label": "Max caption characters on the bar"
+ }
}
+ },
+ "widget": {
+ "title": "Daily Wallpaper",
+ "loading_short": "Loading…",
+ "tooltip_loading": "Fetching today's wallpaper caption…",
+ "tooltip_error": "Could not fetch today's wallpaper caption.",
+ "panel_launch_failed": "Could not open the Daily Wallpaper panel."
+ },
+ "panel": {
+ "title": "Daily Wallpaper",
+ "loading": "Fetching today's wallpaper caption…",
+ "error": "Could not fetch today's wallpaper caption.",
+ "date": "{source} · {date}",
+ "learn_more": "Learn more"
}
}
diff --git a/daily-wallpaper/widget.luau b/daily-wallpaper/widget.luau
new file mode 100644
index 00000000..c236dc02
--- /dev/null
+++ b/daily-wallpaper/widget.luau
@@ -0,0 +1,52 @@
+--!nonstrict
+-- Bar widget: a glyph (+ optional short caption text) reflecting today's
+-- wallpaper. Pure view over daily_wallpaper.luau's "today"/"status" state.
+-- Click opens the panel with the full caption, credit, and "learn more" link.
+
+local function tr(key, args) return noctalia.tr(key, args) end
+
+local glyph = noctalia.getConfig("glyph") or "photo"
+local showText = noctalia.getConfig("show_text")
+local maxChars = tonumber(noctalia.getConfig("max_chars")) or 28
+
+local today = noctalia.state.get("today")
+local status = noctalia.state.get("status")
+
+local function truncate(text)
+ if #text <= maxChars then
+ return text
+ end
+ return text:sub(1, maxChars - 1) .. "…"
+end
+
+local function render()
+ barWidget.setGlyph(glyph)
+
+ if type(today) == "table" and type(today.title) == "string" and today.title ~= "" then
+ barWidget.setText(showText and truncate(today.title) or "")
+ barWidget.setTooltip(today.title)
+ else
+ barWidget.setText(showText and tr("widget.loading_short") or "")
+ local msg = (type(status) == "table" and status.state == "error")
+ and (status.message or tr("widget.tooltip_error"))
+ or tr("widget.tooltip_loading")
+ barWidget.setTooltip(msg)
+ end
+end
+
+noctalia.state.watch("today", function(value)
+ today = value
+ render()
+end)
+noctalia.state.watch("status", function(value)
+ status = value
+ render()
+end)
+
+function onClick()
+ if not noctalia.runAsync("noctalia msg panel-toggle 'nzlov/daily-wallpaper:panel'") then
+ noctalia.notifyError(tr("widget.title"), tr("widget.panel_launch_failed"))
+ end
+end
+
+render()