Skip to content

feat(processes): show application icons in process table and tree - #74

Merged
benapetr merged 10 commits into
benapetr:masterfrom
olob0:feature/process-icons
Sep 27, 2026
Merged

benapetr merged 10 commits into
benapetr:masterfrom
olob0:feature/process-icons

Conversation

@olob0

@olob0 olob0 commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Based on the icon resolution systems used by Mission Center and KDE System Monitor, I added icons to the process list and process tree

Closes #60


I originally started this for personal use. I came across TuxManager while looking for a task manager I liked. I really liked it, but the lack of process icons bothered me, so I implemented them just for myself. When I checked whether there was already something about this in the issues, I decided to investigate how other programs resolve process icons and bring the same approach to TuxManager.

Everything was implemented using only Qt and Linux APIs, with no dependency on KDE, GNOME, or any other DE. All credit for the approach goes to Mission Center (the app-rummage crate, which in turn credits Resources) and libksysguard/KDE System Monitor!

Screenshots

CachyOS (KDE Plasma). Process tree, Mission Center, and TuxManager: Mission Center and TuxManager process tree comparison

CachyOS (KDE Plasma). Process icons, KDE System Monitor, Mission Center, and TuxManager: KDE System Monitor, Mission Center, and TuxManager process icon comparison

CachyOS (KDE Plasma). char-white icon theme: TuxManager with icons from the char-white icon pack

Fedora Workstation 44 (GNOME). Compared with Resources: Fedora Workstation 44 with GNOME, comparing TuxManager and Resources

Alpine Linux (XFCE, OpenRC, no systemd). Process tree, Mission Center, and TuxManager: Alpine Linux Mission Center and TuxManager process tree icon comparison

How it works

Details

In very broad terms, this is a mix of what Mission Center and KDE System Monitor do. I brought the approach to TuxManager and made a few additional modifications. All credit goes to Mission Center and KDE System Monitor!

1. Per-process data collection

When LoadOptions::CollectAppInfo is enabled, Process::LoadAll() reads two additional pieces of information:

  • CGroup: the 0:: line from /proc/<pid>/cgroup (the unified hierarchy, cgroup v2). It is empty on cgroup v1, so this does not work there.
  • ExePath: the target of /proc/<pid>/exe, for example /usr/lib/firefox/firefox

The cgroup is useful because desktops following the systemd desktop environment specification (https://systemd.io/DESKTOP_ENVIRONMENTS/) place each launched application in its own unit, such as app-org.kde.konsole-1234.scope, with all child processes inside it. The unit name identifies the application even when the process itself would not reveal much, such as a zsh shell running inside Konsole or a Chrome renderer

The Users tab requests snapshots without CollectAppInfo, so none of this applies there

ExePath is the path to the executable that is actually running. For example, /proc/1234/exe -> /usr/lib/firefox/firefox

2. Installed application index

AppRegistry scans *.desktop files in the following order:

  • $XDG_DATA_HOME/applications
  • each $XDG_DATA_DIRS/applications
  • ~/.local/share/flatpak/exports/share/applications
  • /var/lib/flatpak/exports/share/applications
  • /var/lib/snapd/desktop/applications

When two .desktop files have the same ID, the first one found wins

Only the [Desktop Entry] group is read. The entry must have Type=Application and Icon=, while Hidden=true entries are ignored. NoDisplay=true entries are intentionally kept (unlike Mission Center). Background helpers can still have useful icons in a process monitor

The parser is custom and intentionally small because QSettings does not handle Name[pt_BR]=, semicolon-separated lists, or the % field codes used by Exec= correctly for this use case

3. Resolving Exec= to the real executable

The goal is to determine which binary the .desktop file actually launches. For example, Exec=firefox %U becomes /usr/bin/firefox, which is then canonicalized to /usr/lib/firefox/firefox (following symlinks) so it can be matched against /proc/<pid>/exe

The following cases are handled, identically to Mission Center:

  • flags before the executable are skipped
  • transparent launchers (env, distrobox, toolbox) hand off to the next token
  • opaque launchers (flatpak, snap) stop without resolving an executable, because the target is not part of the host filesystem
  • interpreters and core utilities (bash, python3, java, node, ...) stop without resolving an executable. Without this, a single Exec=python3 foo.py entry would claim every Python process on the system

4. Ambiguous executables

Multiple .desktop files may launch the same executable (kcm_*.desktop entries that run systemsettings, Chrome PWAs, etc.). If their icons differ, the executable is marked ambiguous and executable-based matching is disabled. If they all agree on the same icon (google-chrome.desktop and com.google.Chrome.desktop), the executable association is kept

5. Updating the index

The directories are monitored with QFileSystemWatcher. Installing or removing an application rebuilds the index and clears the caches, with a 2-second debounce. The index is created only on the first refresh of the Processes tab

6. Resolving the application for each process

There are six methods, in this order. The first one that produces a valid result is used:

  1. cgroup. Path components are tested from the inside out against the systemd unit naming pattern (regex ported from libksysguard). For example, app-org.kde.okular@12345.service becomes org.kde.okular, which is then looked up in the index. Nested units are supported (app-org.kde.konsole-1234.scope/tab(5678).scope). The app-, apps-, flatpak-, and dbus- prefixes are accepted, and systemd's \xZZ escapes are decoded. Snap does not follow this specification, so it is handled differently: snap.firefox.firefox-<uuid>.scope is converted to firefox_firefox, which is the name of the .desktop file installed by snapd

  2. Flatpak. If the executable is under /app/ or /usr/, /proc/<pid>/root/.flatpak-info is read. Inside the sandbox, this file contains the application ID. This is necessary on systems without systemd, where app-flatpak-* units do not exist

  3. Executable. ExePath is looked up in the index. If /proc/<pid>/exe cannot be read (for example, for a process belonging to another user), the process name is looked up in PATH as a fallback. There are also exceptions for binaries whose names do not match the .desktop file (as in Mission Center): firefox-bin, soffice.bin, chrome, gnome-terminal-server. Binaries under /snap/<snap>/ or /var/lib/snapd/snap/<snap>/ are mapped to the snap's .desktop file. This covers snaps launched without a scope, which snapd does not always create

  4. Icon theme. The first token of the process name, lowercased, is checked against the icon theme using QIcon::hasThemeIcon. For example, process firefox -> icon firefox. This is essentially the strategy used by KDE System Monitor's process table

  5. Parent process. The icon of the nearest ancestor that has one is inherited via PPID, up to 64 levels

  6. Generic. application-x-executable. Every row ends up with an icon, even when it is only the generic one

Design decision: cgroup > executable. For example, sh -c 'exec kate' running inside Konsole shows the Konsole icon, not the Kate icon, because it is still part of the terminal's unit. This matches the behavior of KDE and Mission Center and answers "which application does this process belong to?" rather than "which executable is this?"

7. Converting the result to a QIcon

AppRegistry::IconFor() converts the resolved icon into a QIcon, with a name-based cache:

  1. absolute path: load directly
  2. icon theme
  3. <datadir>/pixmaps/<name>.{png,svg,xpm} (some applications, such as VS Code, install their icons here)
  4. generic icon, using the same lookup path
  5. if even the generic icon does not exist in the theme, use QStyle::SP_FileIcon to keep row alignment consistent

QIcon rasterizes on demand and handles HiDPI automatically

8. Cache

Methods 1 through 4 from section 6 are cached by PID + start time, so each process is resolved only once. PID alone would not be enough because PIDs can be reused. The cache is pruned to only live processes on each refresh. Method 5 (parent inheritance) is recalculated on every refresh based on the cache, at the cost of a few hash lookups

9. SVG support in the AppImage

Most modern icon themes use only SVG, and Qt needs the qsvgicon plugin to load them. Without it, SVG icons are silently ignored and only hicolor PNGs remain. The binary does not link against Qt Svg, so linuxdeploy-plugin-qt did not include the plugin. package-appimage.sh now passes EXTRA_QT_MODULES=svg. I discovered this while testing the AppImage on Fedora

Toggle

Added a "Show icons" item to the View submenu of the context menus, persisted as Processes/ShowIcons. It is enabled by default

Show icons checkbox configuration

Cost

  • Per refresh, per process: one /proc/<pid>/cgroup read and one readlink, inside the loop that already opens 4 to 5 files per PID
  • Index: built once, taking 8 to 20 ms for ~200 .desktop files, around 100 KB in size
  • Resolution: once per process. QIcon cache size is bounded by the number of distinct icon names
  • Toggle disabled: AppRegistry is not created and the additional reads do not happen

Limitations

  1. Processes belonging to other users: /proc/<pid>/exe and /proc/<pid>/root/.flatpak-info are unreadable without root. cgroup, process name, and inheritance are still available
  2. No systemd or cgroup v1: cgroup-based identification does not work with cgroup v1. Flatpak, Snap, executable, icon theme, and inheritance remain available. Tested on Alpine Linux; see screenshots above.
  3. Multiple applications using the same executable with different icons: executable matching alone cannot distinguish them, but cgroup-based matching normally resolves this
  4. Snap: Exec= goes through /snap/bin, so only cgroup and executable-path matching can resolve it

I also added Czech (cs_CZ) and Chinese (zh_CN) translations for the "Show icons" string (it's only two words, so native-speaker review is very welcome!)

I'm open to questions and feedback! More testing on different machines is very welcome!

@benapetr

Copy link
Copy Markdown
Owner

this look very nice I will review this and let you know, but the performance overhead is what I am most interested in, so you said there is some index? I am curious if you query all this icon information and data on each refresh or if there is some mechanism to speed this up. If there is a cache used, then that's great I would definitely recommend to cache these instead of loading on each refresh, but then - how large is the cache, how much does it increase the RAM usage?

If you don't have this data it's fine, I will measure it myself. If the difference is too large or overhead too dramatic, I will see if we can make the toggle not only hide the icons but also drop the overhead caused by displaying them.

@benapetr

Copy link
Copy Markdown
Owner

there is a problem with Qt5 integration and you need to rebase this to make it possible to merge.

I fixed both issues but I can't push commits into your branch:

remote: Permission to olob0/TuxManager.git denied to benapetr.
fatal: unable to access 'https://github.com/olob0/TuxManager.git/': The requested URL returned error: 403

@benapetr
benapetr force-pushed the feature/process-icons branch from 11d8b48 to 563ee0c Compare September 20, 2026 06:29
@benapetr

Copy link
Copy Markdown
Owner

ok I fixed the auth issue

@benapetr

Copy link
Copy Markdown
Owner

it would be nice if we could also update that tree view screenshot in readme file, it should probably have same resolution / scale as other screens so the table is uniform

@benapetr

Copy link
Copy Markdown
Owner

anyway, before you do it, I think we should also swap that PID and name columns, so that name is actually used for the tree nodes instead of PID, same way as in that other tool you show, it looks better than PIDs

@benapetr

benapetr commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

BTW just interesting obsevation - this adds around 10MB of additional RAM usage and about 150% additional CPU time, I will see if I can optimize it a bit more. It should be possible to at least disable this feature in a way to bring the resource usage back to old levels - it's already possible to disable it, but it doesn't seem to release all resources, some of that /proc scanning that was added is probably still happening now

olob0 and others added 4 commits September 24, 2026 12:07
it looks better with icons
QIcon::hasThemeIcon stats every directory of the theme chain on each
miss, costing 3-18 ms per process name on the GUI thread and ~440 ms
on the first refresh. Index the theme icon names once (hashes only)
so misses are rejected with a binary search; hits are still confirmed
by Qt. Resource themes (:/icons) are skipped because walking them
faults in the whole icon library; if a theme exists only as a
resource, fall back to QIcon::hasThemeIcon.

Read /proc/<pid>/cgroup and /proc/<pid>/exe lazily in resolveOwn,
once per process identity, instead of for every process on every
refresh. Process, LoadOptions and the refresh worker are back to
their master versions.

Compare IconName in processDataEquals and include DecorationRole in
dataChanged so table rows pick up icon changes, and repaint both views
when the icon toggle changes.
@olob0
olob0 force-pushed the feature/process-icons branch from 786cdca to a3bb28c Compare September 24, 2026 18:04
@olob0

olob0 commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

thanks for the measurements! I looked into it and measured more carefully

it looks like almost all of the cost was at startup, not on each refresh. On the first refresh, QIcon::hasThemeIcon took ~440 ms on the GUI thread. Each process name without an icon in the theme costs 3-18 ms, because Qt retries with dash-separated prefixes and stats every directory of every theme in the chain, and hicolor alone declares hundreds of directories.

and almost all of those lookups fail. On my machine, out of the 71 processes that reached the theme lookup, only 3 had an icon there. In total, the process used ~800 ms of CPU in the first few seconds (vs ~300 ms on master)

measuring a short window right after startup, that gives the ~+150% (I think that's where your measurement came from). On top of that, the UI froze for ~450 ms

after startup, the other waste was reading /proc/<pid>/cgroup and /proc/<pid>/exe for every process on every refresh, even though the result is only used once per process

about the toggle, the code doesn't do any extra /proc reads when it's off, and AppRegistry isn't even created. What doesn't come back after it was on is the already loaded Qt SVG library and the glibc heap, which doesn't return memory to the OS (I tried QPixmapCache::clear() + malloc_trim(0) when turning it off, but they only free ~0.3–0.6 MB, so I left them out)

some optimizations I made now:

I added an index of the theme's icon names. the program walks the current theme, the themes it inherits from, hicolor and the pixmaps directories once, and stores only the name hashes (~7k names in ~60 KB). If a name isn't in the index, it's rejected right away. If it is, Qt confirms it, which is cheap when the icon exists. The result is exactly the same as Qt's. Themes compiled in as resources (like the KDE platform theme registering Breeze under :/icons) are skipped, because walking them loads the whole libKF6BreezeIcons.so (~+22 MB of RSS). If a theme only exists as a resource, the code falls back to QIcon::hasThemeIcon

I also made the cgroup/exe reads on demand. they now happen inside resolveOwn, only the first time a process shows up. Process, LoadOptions and the refresh worker are back to the same as on master. Only the qHash for Qt 5 remains

I fixed a bug where the table view didn't update rows when only the icon changed (processDataEquals ignored IconName). Turning icons on while the app was running left idle processes without an icon

measurements

Measured on CachyOS with KDE Plasma (Wayland), ~425 processes, refreshing every 500 ms, showing kernel and other users' processes

with the window open, process table view (one 40s run):

CPU at startup CPU after RSS
master 360 ms 2.17% 106.8 MB
PR before, icons on 820 ms 2.33% 110.8 MB
PR now, icons on 450 ms 2.17% 108.4 MB
PR now, icons off 330 ms 2.23% 106.5 MB

without a window (QT_QPA_PLATFORM=offscreen), average of 3 runs of 60s:

CPU at startup CPU after RSS
master 300 ms 2.21% 96.8 MB
PR before, icons on 760 ms 2.36% 101.5 MB
PR now, icons on 413 ms 2.22% 98.9 MB
PR now, icons off 300 ms 2.23% 97.1 MB

"CPU at startup" is the CPU time spent in the first 10s. "CPU after" is the average usage with the app already open

I also compared, process by process, the icons picked before and after the changes, across 4 themes (breeze-dark, char-white, Adwaita and hicolor). Nothing changed in any of them, so it just got faster, at least as far as I could see

about the +10 MB you mentioned, I couldn't get to that number here. Before the changes I measured ~+4 MB. If you can tell me how you measured it, I can try to reproduce it

@olob0

olob0 commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

it would be nice if we could also update that tree view screenshot in readme file, it should probably have same resolution / scale as other screens so the table is uniform

Regarding this, I’ve updated the screenshots (screenshots/ubuntu/process.png and screenshots/ubuntu/process_tree.png)!

I took them in a VM running Ubuntu 26.04 with KDE Plasma 6.6 (Wayland), using the light Breeze theme, at the same size as the others (1075×880). Both are sorted by memory usage, and in the tree view I expanded everything in the user session section, where more icons are visible

Process View Prrocess Tree View
Processes view Process tree view

so that it's consistent with the tree view
the way it was before prevented users from setting PID as first column
you can change theme runtime, apps should be able to handle that
@benapetr
benapetr merged commit 5d9ca89 into benapetr:master Sep 27, 2026
3 checks passed
@olob0
olob0 deleted the feature/process-icons branch September 30, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: add icons to processes

2 participants