Skip to content

Commit ab16afc

Browse files
gegnepItsLemmy
andauthored
feat(plugins): add noctalia.appIconPath icon resolution binding (#3356)
* feat(plugins): add noctalia.appIconPath icon resolution binding Resolves an app id (or raw icon name) to an icon file path using the same components the native taskbar uses: the shared desktop-entry index via app_identity::findDesktopEntry, then IconResolver with the caller's target size. The desktop-entry cache gains a mutex-guarded non-refreshing snapshot accessor so script worker threads read it race-free; the resolver instance is thread_local. Assisted with AI tooling. * fix(plugins): make appIconPath icon-theme state thread-safe and snapshots copy-free --------- Co-authored-by: Lemmy <studio@quadbyte.net>
1 parent fdc2448 commit ab16afc

5 files changed

Lines changed: 81 additions & 8 deletions

File tree

src/scripting/luau_host.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
#include "scripting/plugin_bindings.h"
1414
#include "scripting/plugin_state_store.h"
1515
#include "scripting/script_api_context.h"
16+
#include "system/app_identity.h"
17+
#include "system/desktop_entry.h"
18+
#include "system/icon_resolver.h"
1619
#include "system/terminal_launch.h"
1720
#include "time/time_format.h"
1821
#include "util/file_utils.h"
@@ -321,6 +324,37 @@ namespace {
321324
return 1;
322325
}
323326

327+
// appIconPath(appIdOrIconName, sizePx?) -> absolute icon file path or nil.
328+
// Same resolution the native taskbar uses: desktop-entry lookup (id /
329+
// StartupWMClass) for the icon name, then the XDG icon-theme resolver.
330+
// Unmatched inputs are treated as raw icon names so plugins can also
331+
// resolve themed icons directly.
332+
int luau_appIconPath(lua_State* L) {
333+
size_t len = 0;
334+
const char* appId = luaL_checklstring(L, 1, &len);
335+
const int targetSize = luaL_optinteger(L, 2, 0);
336+
337+
std::string iconName;
338+
const auto entries = desktopEntriesSnapshot();
339+
if (const auto entry = app_identity::findDesktopEntry(std::string_view(appId, len), *entries);
340+
entry.has_value() && !entry->icon.empty()) {
341+
iconName = entry->icon;
342+
} else {
343+
iconName.assign(appId, len);
344+
}
345+
346+
// One resolver (and icon-path cache) per script worker thread; the theme
347+
// plan it reads is shared across threads and mutex-guarded in IconResolver.
348+
static thread_local IconResolver resolver;
349+
const std::string& path = resolver.resolve(iconName, targetSize);
350+
if (path.empty()) {
351+
lua_pushnil(L);
352+
return 1;
353+
}
354+
lua_pushlstring(L, path.data(), path.size());
355+
return 1;
356+
}
357+
324358
int luau_setWallpaperEnabled(lua_State* L) {
325359
size_t len = 0;
326360
const char* connector = luaL_checklstring(L, 1, &len);
@@ -1137,6 +1171,7 @@ namespace {
11371171
{"portalAvailable", luau_portalAvailable},
11381172
{"focusedOutputName", luau_focusedOutputName},
11391173
{"outputs", luau_outputs},
1174+
{"appIconPath", luau_appIconPath},
11401175
{"setWallpaperEnabled", luau_setWallpaperEnabled},
11411176
{"setWallpaper", luau_setWallpaper},
11421177
{"togglePanel", luau_togglePanel},

src/system/desktop_entry.cpp

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
#include <cstdlib>
88
#include <filesystem>
99
#include <fstream>
10+
#include <memory>
11+
#include <mutex>
1012
#include <ranges>
1113
#include <string_view>
1214
#include <sys/inotify.h>
@@ -389,6 +391,14 @@ namespace {
389391

390392
const std::vector<DesktopEntry>& entries() {
391393
refreshIfNeeded();
394+
return *m_entries;
395+
}
396+
397+
// Worker-thread-safe shared snapshot. Deliberately non-refreshing:
398+
// freshness stays driven by the main thread's poll/reload path; this only
399+
// synchronizes against the reload swap.
400+
std::shared_ptr<const std::vector<DesktopEntry>> entriesSnapshot() const {
401+
std::scoped_lock lock(m_entriesMutex);
392402
return m_entries;
393403
}
394404

@@ -448,7 +458,11 @@ namespace {
448458
return;
449459
}
450460

451-
m_entries = scanDesktopEntries();
461+
auto scanned = std::make_shared<const std::vector<DesktopEntry>>(scanDesktopEntries());
462+
{
463+
std::scoped_lock lock(m_entriesMutex);
464+
m_entries = std::move(scanned);
465+
}
452466
rebuildWatches();
453467
m_sourceSignature = computeSourceSignature();
454468
m_dirty = false;
@@ -554,7 +568,8 @@ namespace {
554568
m_watches[wd] = key;
555569
}
556570

557-
std::vector<DesktopEntry> m_entries;
571+
std::shared_ptr<const std::vector<DesktopEntry>> m_entries = std::make_shared<std::vector<DesktopEntry>>();
572+
mutable std::mutex m_entriesMutex; // guards the m_entries swap against entriesSnapshot() readers
558573
std::uint64_t m_version = 0;
559574
int m_inotifyFd = -1;
560575
bool m_dirty = true;
@@ -610,6 +625,8 @@ std::vector<DesktopEntry> scanDesktopEntries() {
610625

611626
const std::vector<DesktopEntry>& desktopEntries() { return cache().entries(); }
612627

628+
std::shared_ptr<const std::vector<DesktopEntry>> desktopEntriesSnapshot() { return cache().entriesSnapshot(); }
629+
613630
std::uint64_t desktopEntriesVersion() { return cache().version(); }
614631

615632
int desktopEntryWatchFd() noexcept { return cache().watchFd(); }

src/system/desktop_entry.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include <cstdint>
4+
#include <memory>
45
#include <string>
56
#include <vector>
67

@@ -42,6 +43,12 @@ struct DesktopEntry {
4243
std::vector<DesktopEntry> scanDesktopEntries();
4344

4445
const std::vector<DesktopEntry>& desktopEntries();
46+
47+
// Shared snapshot of the current entry list, safe to call from non-main
48+
// threads (e.g. plugin script workers). Does not trigger a refresh —
49+
// freshness is owned by the main thread's reload path.
50+
std::shared_ptr<const std::vector<DesktopEntry>> desktopEntriesSnapshot();
51+
4552
std::uint64_t desktopEntriesVersion();
4653
int desktopEntryWatchFd() noexcept;
4754
void checkDesktopEntryReload();

src/system/desktop_entry_poll_source.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@
55

66
class DesktopEntryPollSource final : public PollSource {
77
public:
8+
// Prime the cache on the main thread and refresh it eagerly on change, so
9+
// worker-thread desktopEntriesSnapshot() readers see a populated, current
10+
// list even when no main-thread widget consumes desktopEntries().
11+
DesktopEntryPollSource() { desktopEntries(); }
12+
813
void dispatch(const std::vector<pollfd>& fds, std::size_t startIdx) override {
914
if (desktopEntryWatchFd() >= 0 && (fds[startIdx].revents & POLLIN) != 0) {
1015
checkDesktopEntryReload();
16+
desktopEntries();
1117
}
1218
}
1319

src/system/icon_resolver.cpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <fstream>
99
#include <gio/gio.h>
1010
#include <memory>
11+
#include <mutex>
1112
#include <optional>
1213
#include <set>
1314
#include <string_view>
@@ -50,7 +51,10 @@ namespace {
5051
return size;
5152
}
5253

54+
// Shared across all IconResolver instances, including thread_local ones on
55+
// script worker threads; every access goes through `mutex`.
5356
struct IconThemeState {
57+
std::mutex mutex;
5458
bool initialized = false;
5559
std::uint64_t generation = 1;
5660
IconThemePlan plan;
@@ -409,8 +413,8 @@ namespace {
409413
return plan;
410414
}
411415

412-
void ensureThemeState() {
413-
auto& state = iconThemeState();
416+
// Requires state.mutex to be held.
417+
void ensureThemeStateLocked(IconThemeState& state) {
414418
if (!state.initialized) {
415419
state.plan = buildThemePlan();
416420
state.initialized = true;
@@ -423,6 +427,7 @@ IconResolver::IconResolver() { rebuild(); }
423427

424428
bool IconResolver::checkThemeChanged() {
425429
auto& state = iconThemeState();
430+
std::scoped_lock lock(state.mutex);
426431
IconThemePlan next = buildThemePlan();
427432
if (!state.initialized) {
428433
state.plan = std::move(next);
@@ -439,13 +444,16 @@ bool IconResolver::checkThemeChanged() {
439444
}
440445

441446
std::uint64_t IconResolver::themeGeneration() {
442-
ensureThemeState();
443-
return iconThemeState().generation;
447+
auto& state = iconThemeState();
448+
std::scoped_lock lock(state.mutex);
449+
ensureThemeStateLocked(state);
450+
return state.generation;
444451
}
445452

446453
void IconResolver::rebuild() {
447-
ensureThemeState();
448-
const auto& state = iconThemeState();
454+
auto& state = iconThemeState();
455+
std::scoped_lock lock(state.mutex);
456+
ensureThemeStateLocked(state);
449457
m_baseDirs = state.plan.baseDirs;
450458
m_searchDirs = state.plan.searchDirs;
451459
m_pixmapDirs = state.plan.pixmapDirs;

0 commit comments

Comments
 (0)