From 0141f0ff9cf27c5404a7f372a5a295fc5a38cdc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladislav=20Jane=C4=8Dek?= <38503981+schotek@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:34:16 +0200 Subject: [PATCH 1/2] appstamp: stamp freedesktop metadata onto app binaries for Deskbar Debian binaries carry no BeOS resources, so Deskbar and Tracker show a generic icon and the raw file name. appstamp reads an application's .desktop entry and icon theme and writes the matching BeOS attributes (BEOS:L/M:STD_ICON, BEOS:APP_SIG, SYS:NAME) onto the executable, where Deskbar and Tracker resolve them. Toolkit-agnostic: any installed .deb application is stamped from its freedesktop metadata. Runs from a plain root session -- no BApplication, no BBitmap, no app_server. Icons come from the hicolor PNG theme, with rsvg-convert as a fallback for scalable-only themes. --- build/deps.cmake | 7 + build/profiles/bin.cmake | 1 + src/bin/CMakeLists.txt | 1 + src/bin/appstamp.cpp | 728 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 737 insertions(+) create mode 100644 src/bin/appstamp.cpp diff --git a/build/deps.cmake b/build/deps.cmake index 7c6b8571..ab838c32 100644 --- a/build/deps.cmake +++ b/build/deps.cmake @@ -172,6 +172,13 @@ DeclareDependency( INCLUDES "${HEADERS_PATH_BASE}/libpng/" ) +# Runtime only: appstamp shells out to rsvg-convert for SVG-only icon themes +# (most modern apps ship scalable/ only). Not linked, so no LIBRARIES/PACKAGES. +DeclareDependency( + RSVG + RUNTIMES "librsvg2-bin" +) + DeclareDependency( TIFF LIBRARIES "tiff" diff --git a/build/profiles/bin.cmake b/build/profiles/bin.cmake index 1cb529bc..50f56282 100644 --- a/build/profiles/bin.cmake +++ b/build/profiles/bin.cmake @@ -1,5 +1,6 @@ set(BIN_DIRECTORY addattr + appstamp keystore linkcatkeys dumpcatalog diff --git a/src/bin/CMakeLists.txt b/src/bin/CMakeLists.txt index de21d3a3..481b7ce1 100644 --- a/src/bin/CMakeLists.txt +++ b/src/bin/CMakeLists.txt @@ -58,6 +58,7 @@ Application(dstcheck SOURCES dstcheck.cpp LIBS localestub RDEF dstc Application(hey SOURCES hey.cpp RDEF hey.rdef) Application(reindex SOURCES reindex.cpp) Application(resattr SOURCES resattr.cpp) +Application(appstamp SOURCES appstamp.cpp LIBS ${PNG_LIBRARIES} INCLUDES ${PNG_INCLUDES}) Application(screeninfo SOURCES screeninfo.cpp) Application(setcontrollook SOURCES setcontrollook.cpp) Application(setdecor SOURCES setdecor.cpp) diff --git a/src/bin/appstamp.cpp b/src/bin/appstamp.cpp new file mode 100644 index 00000000..547427ee --- /dev/null +++ b/src/bin/appstamp.cpp @@ -0,0 +1,728 @@ +/* + * Author: Vláďa Janeček + * + * appstamp — stamp BeOS application attributes onto a Linux binary from its + * freedesktop metadata, so Deskbar and Tracker show the application's real + * icon instead of the generic one. + * + * Debian binaries carry no BeOS resources or attributes; their name and icon + * live in /usr/share/applications/.desktop and the icon theme. Deskbar + * resolves both purely from the executable's attributes (BAppFileInfo in + * attributes-only mode for a plain ELF), so bridging is a matter of writing + * the right xattrs: + * + * BEOS:M:STD_ICON 16x16 B_CMAP8 ('MICN') + * BEOS:L:STD_ICON 32x32 B_CMAP8 ('ICON') + * BEOS:ICON HVIF vector ('VICN', only with --hvif) + * BEOS:APP_SIG MIME signature + * SYS:NAME catalog entry (untranslated label; no catalog needed) + * + * dpkg does not track xattrs, so the package content stays pristine. + * + * Deliberately no BApplication and no BBitmap: both need a live app_server, + * and this runs from a root ssh session. PNG via libpng's simplified API; + * CMAP8 by nearest-colour over the static system palette (Palette.h). + */ +#include + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include + + +static const char* kMiniAttr = "BEOS:M:STD_ICON"; +static const char* kLargeAttr = "BEOS:L:STD_ICON"; +static const char* kVectorAttr = "BEOS:ICON"; + +// The ext4 xattr value ceiling is one block minus overhead; the nexus layer +// adds a 4-byte type prefix and does no chunking. Anything near 4K is refused +// by the filesystem anyway — check early for a friendlier message. +static const size_t kMaxAttrPayload = 4000; + + +struct DesktopEntry { + std::string path; + std::string name; + std::string icon; + std::string exec; // raw Exec= line, for --all to resolve the binary + // Display gating, honored by --all (not in single-binary mode, where the + // user has chosen the target deliberately). + bool noDisplay = false; + bool terminal = false; + bool isApplication = true; // Type= defaults to Application when absent + bool consoleOnly = false; // Categories contains ConsoleOnly +}; + + +static std::string +leafName(const std::string& path) +{ + size_t slash = path.find_last_of('/'); + return slash == std::string::npos ? path : path.substr(slash + 1); +} + + +// First word of an Exec= line, stripped of its directory part. Quoting and +// field codes (%f, %u) never appear in the first word of well-formed entries. +static std::string +execBasename(const std::string& execLine) +{ + size_t space = execLine.find(' '); + return leafName(execLine.substr(0, space)); +} + + +// With an empty wantedLeaf the Exec/TryExec match is not required — used for +// an explicitly given .desktop file, which the caller has already chosen. +static bool +parseDesktopFile(const std::string& path, DesktopEntry& entry, + const std::string& wantedLeaf) +{ + FILE* f = fopen(path.c_str(), "r"); + if (f == NULL) + return false; + + bool inDesktopEntry = false; + bool matches = wantedLeaf.empty(); + std::string name, icon, exec; + bool noDisplay = false, terminal = false, isApplication = true; + bool consoleOnly = false; + + char line[1024]; + while (fgets(line, sizeof(line), f) != NULL) { + std::string s(line); + while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) + s.pop_back(); + + if (!s.empty() && s[0] == '[') { + // Only the main group holds the keys we want; Desktop Actions + // repeat Exec= lines that must not cause false matches. + inDesktopEntry = (s == "[Desktop Entry]"); + continue; + } + if (!inDesktopEntry) + continue; + + size_t eq = s.find('='); + if (eq == std::string::npos) + continue; + std::string key = s.substr(0, eq); + std::string value = s.substr(eq + 1); + + if (key == "Name") + name = value; + else if (key == "Icon") + icon = value; + else if (key == "Type") + isApplication = (value == "Application"); + else if (key == "NoDisplay") + noDisplay = (value == "true"); + else if (key == "Terminal") + terminal = (value == "true"); + else if (key == "Categories") + consoleOnly = value.find("ConsoleOnly") != std::string::npos; + else if (key == "TryExec" || key == "Exec") { + // Exec is the one to run; TryExec only gates availability. + if (key == "Exec" && exec.empty()) + exec = value; + if (execBasename(value) == wantedLeaf) + matches = true; + } + } + fclose(f); + + if (!matches) + return false; + entry.path = path; + entry.name = name; + entry.icon = icon; + entry.exec = exec; + entry.noDisplay = noDisplay; + entry.terminal = terminal; + entry.isApplication = isApplication; + entry.consoleOnly = consoleOnly; + return true; +} + + +static bool +findDesktopFor(const std::string& binary, DesktopEntry& entry) +{ + const std::string leaf = leafName(binary); + static const char* kDirs[] = { + "/usr/share/applications", + "/usr/local/share/applications", + }; + + for (const char* dirPath : kDirs) { + DIR* dir = opendir(dirPath); + if (dir == NULL) + continue; + struct dirent* ent; + while ((ent = readdir(dir)) != NULL) { + std::string fname(ent->d_name); + if (fname.size() < 9 + || fname.compare(fname.size() - 8, 8, ".desktop") != 0) + continue; + if (parseDesktopFile(std::string(dirPath) + "/" + fname, entry, + leaf)) { + closedir(dir); + return true; + } + } + closedir(dir); + } + return false; +} + + +// ---- PNG loading and scaling ---------------------------------------------- + +struct Image { + uint32 width = 0; + uint32 height = 0; + std::vector rgba; // width * height * 4 +}; + + +static bool +loadPng(const std::string& path, Image& img) +{ + png_image png; + memset(&png, 0, sizeof(png)); + png.version = PNG_IMAGE_VERSION; + + if (!png_image_begin_read_from_file(&png, path.c_str())) + return false; + png.format = PNG_FORMAT_RGBA; + img.width = png.width; + img.height = png.height; + img.rgba.resize(PNG_IMAGE_SIZE(png)); + if (!png_image_finish_read(&png, NULL, img.rgba.data(), 0, NULL)) { + png_image_free(&png); + return false; + } + return true; +} + + +// Box filter, correct for both down- and upscaling at these tiny sizes. +// Icons are 16/32 px; nothing here is worth pulling a resampling library for. +static Image +scaleTo(const Image& src, uint32 size) +{ + Image dst; + dst.width = size; + dst.height = size; + dst.rgba.resize(size * size * 4); + + for (uint32 y = 0; y < size; y++) { + uint32 sy0 = y * src.height / size; + uint32 sy1 = (y + 1) * src.height / size; + if (sy1 <= sy0) + sy1 = sy0 + 1; + for (uint32 x = 0; x < size; x++) { + uint32 sx0 = x * src.width / size; + uint32 sx1 = (x + 1) * src.width / size; + if (sx1 <= sx0) + sx1 = sx0 + 1; + + uint32 sum[4] = {0, 0, 0, 0}; + uint32 count = 0; + for (uint32 sy = sy0; sy < sy1 && sy < src.height; sy++) { + const uint8* p = &src.rgba[(sy * src.width + sx0) * 4]; + for (uint32 sx = sx0; sx < sx1 && sx < src.width; sx++) { + sum[0] += p[0]; + sum[1] += p[1]; + sum[2] += p[2]; + sum[3] += p[3]; + p += 4; + count++; + } + } + uint8* d = &dst.rgba[(y * size + x) * 4]; + for (int c = 0; c < 4; c++) + d[c] = count > 0 ? sum[c] / count : 0; + } + } + return dst; +} + + +// Icon theme lookup: prefer the exact size, then the smallest larger art +// (downscales cleanly), then the largest smaller one, then pixmaps. +static bool +findThemePng(const std::string& iconName, uint32 size, std::string& path) +{ + if (!iconName.empty() && iconName[0] == '/') { + path = iconName; + FILE* f = fopen(path.c_str(), "r"); + if (f != NULL) { + fclose(f); + return true; + } + return false; + } + + static const uint32 kSizes[] = {16, 22, 24, 32, 48, 64, 128, 256, 512}; + std::vector order; + order.push_back(size); + for (uint32 s : kSizes) + if (s > size) + order.push_back(s); + for (int i = sizeof(kSizes) / sizeof(kSizes[0]) - 1; i >= 0; i--) + if (kSizes[i] < size) + order.push_back(kSizes[i]); + + char buf[512]; + for (uint32 s : order) { + snprintf(buf, sizeof(buf), + "/usr/share/icons/hicolor/%ux%u/apps/%s.png", s, s, + iconName.c_str()); + FILE* f = fopen(buf, "r"); + if (f != NULL) { + fclose(f); + path = buf; + return true; + } + } + snprintf(buf, sizeof(buf), "/usr/share/pixmaps/%s.png", iconName.c_str()); + FILE* f = fopen(buf, "r"); + if (f != NULL) { + fclose(f); + path = buf; + return true; + } + return false; +} + + +// Nearest colour in the static system palette. PaletteConverter would do +// this, but its lookup methods are inline-defined inside libbe and not +// exported; for an icon's ~1K pixels a direct search is instant anyway. +static uint8 +nearestPaletteIndex(uint8 r, uint8 g, uint8 b) +{ + uint32 best = UINT32_MAX; + uint8 bestIndex = 0; + for (int i = 0; i < 256; i++) { + const rgb_color& c = kSystemPalette[i]; + // Weighted euclidean; the eye is most sensitive to green. + int32 dr = (int32)c.red - r; + int32 dg = (int32)c.green - g; + int32 db = (int32)c.blue - b; + uint32 d = 3 * dr * dr + 6 * dg * dg + 2 * db * db; + if (d < best) { + best = d; + bestIndex = (uint8)i; + } + } + return bestIndex; +} + + +// SVG-only themes (featherpad ships nothing but scalable/): rasterize via +// rsvg-convert when it is installed. Kept external on purpose — pulling an +// SVG renderer into the tree for a stamping tool would be out of proportion. +static bool +rasterizeSvg(const std::string& iconName, uint32 size, Image& img) +{ + char svgPath[512]; + snprintf(svgPath, sizeof(svgPath), + "/usr/share/icons/hicolor/scalable/apps/%s.svg", iconName.c_str()); + FILE* probe = fopen(svgPath, "r"); + if (probe == NULL) + return false; + fclose(probe); + + char tmpPath[64]; + snprintf(tmpPath, sizeof(tmpPath), "/tmp/appstamp-%d-%u.png", + (int)getpid(), size); + char cmd[1200]; + snprintf(cmd, sizeof(cmd), + "rsvg-convert -w %u -h %u -o %s '%s' 2>/dev/null", size, size, + tmpPath, svgPath); + int rc = system(cmd); + if (rc != 0) { + fprintf(stderr, " found %s but rsvg-convert is unavailable —" + " install librsvg2-bin\n", svgPath); + return false; + } + bool ok = loadPng(tmpPath, img); + unlink(tmpPath); + return ok; +} + + +static std::vector +quantizeToCMAP8(const Image& img) +{ + std::vector out(img.width * img.height); + const uint8* p = img.rgba.data(); + for (size_t i = 0; i < out.size(); i++, p += 4) { + out[i] = p[3] < 128 + ? B_TRANSPARENT_MAGIC_CMAP8 + : nearestPaletteIndex(p[0], p[1], p[2]); + } + return out; +} + + +// ---- stamping -------------------------------------------------------------- + +static status_t +writeIconAttr(BNode& node, const char* attr, type_code type, uint32 size, + const Image& source, bool dryRun) +{ + Image scaled = (source.width == size && source.height == size) + ? source : scaleTo(source, size); + std::vector cmap = quantizeToCMAP8(scaled); + + if (dryRun) { + printf(" would write %s (%lu bytes)\n", attr, (unsigned long)cmap.size()); + return B_OK; + } + ssize_t written = node.WriteAttr(attr, type, 0, cmap.data(), cmap.size()); + if (written < 0) + return (status_t)written; + printf(" %s: %ux%u B_CMAP8\n", attr, size, size); + return B_OK; +} + + +struct Options { + std::string signature; // override, else derived from the leaf name + std::string name; // override, else the .desktop Name= + std::string hvifPath; // optional BEOS:ICON source + bool dryRun = false; + bool force = false; // in --all: restamp binaries already stamped +}; + + +// Stamp one binary from an already-resolved .desktop entry. Returns 0 on +// success. Never fatal on a single missing piece: a binary with no icon is +// still worth its name and signature. +static int +stampBinary(const std::string& binary, const DesktopEntry& entry, + const Options& opts) +{ + BNode node(binary.c_str()); + if (node.InitCheck() != B_OK) { + fprintf(stderr, "appstamp: cannot open %s\n", binary.c_str()); + return 1; + } + + const std::string leaf = leafName(binary); + std::string signature = opts.signature.empty() + ? "application/x-vnd.vos-" + leaf : opts.signature; + std::string name = !opts.name.empty() ? opts.name + : (entry.name.empty() ? leaf : entry.name); + + // Raster icons: theme PNGs first, SVG rasterization as fallback. + if (!entry.icon.empty()) { + std::string png32, png16; + Image img32, img16; + bool have32 = false, have16 = false; + + if (findThemePng(entry.icon, 32, png32) && loadPng(png32, img32)) { + have32 = true; + printf(" icon source: %s (%ux%u)\n", png32.c_str(), + img32.width, img32.height); + // A separate 16px file usually exists and looks better than a + // downscale of the large art; fall back to scaling img32. + have16 = findThemePng(entry.icon, 16, png16) && png16 != png32 + && loadPng(png16, img16); + } else if (rasterizeSvg(entry.icon, 32, img32)) { + have32 = true; + printf(" icon source: scalable SVG via rsvg-convert\n"); + have16 = rasterizeSvg(entry.icon, 16, img16); + } + + if (have32) { + writeIconAttr(node, kLargeAttr, B_LARGE_ICON_TYPE, 32, img32, + opts.dryRun); + writeIconAttr(node, kMiniAttr, B_MINI_ICON_TYPE, 16, + have16 ? img16 : img32, opts.dryRun); + } else { + fprintf(stderr, " no usable icon for '%s'\n", + entry.icon.c_str()); + } + } + + // Optional crisp vector icon. + if (!opts.hvifPath.empty()) { + FILE* f = fopen(opts.hvifPath.c_str(), "rb"); + if (f == NULL) { + fprintf(stderr, "appstamp: cannot read %s\n", + opts.hvifPath.c_str()); + return 1; + } + std::vector hvif; + uint8 buf[4096]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), f)) > 0) + hvif.insert(hvif.end(), buf, buf + n); + fclose(f); + if (hvif.size() > kMaxAttrPayload) { + fprintf(stderr, "appstamp: %s is %zu bytes; attribute values are" + " capped near 4K (single ext4 xattr, no chunking)\n", + opts.hvifPath.c_str(), hvif.size()); + return 1; + } + if (opts.dryRun) + printf(" would write %s (%zu bytes)\n", kVectorAttr, hvif.size()); + else if (node.WriteAttr(kVectorAttr, B_VECTOR_ICON_TYPE, 0, + hvif.data(), hvif.size()) >= 0) + printf(" %s: HVIF, %zu bytes\n", kVectorAttr, hvif.size()); + } + + // Via BNode, not BAppFileInfo: the latter needs a B_READ_WRITE BFile, + // which fails with ETXTBSY while the app runs. Wire format matches + // BAppFileInfo (trailing NUL included). SYS:NAME is + // ::