Stacer: An Abandoned Goldmine

Stacer, a Linux system optimizer, received its last commit in August 2023. With 678 commits and a README stating "no further releases," it's a finished project frozen in time. That makes it perfect for dissection: no moving parts, no "we refactored that last week." At ~50 headers, 48 sources, and 26 .ui forms, you can read it all in an evening.

The Architecture Decision Everything Hangs On

The top-level CMakeLists.txt reveals the key split:

add_subdirectory(stacer-core)
add_subdirectory(stacer)

stacer-core is a static library with zero GUI dependencies. Its CMakeLists.txt:

find_package(Qt5 COMPONENTS Core Network REQUIRED)
add_library(${PROJECT_NAME} STATIC ${${PROJECT_NAME}_srcs})
target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network)

No Widgets, no Charts, nothing that can draw a pixel. It's the entire "talk to the OS" layer and would happily compile into a headless CLI. The main app links this plus Widgets, Charts, Svg, and Concurrent. Most codebases claim this separation, then someone sneaks a QMessageBox into the data layer. Stacer can't — the linker would slap it.

The core splits three ways by verb:

  • Info/ — reads system state
  • Tools/ — changes system state
  • Utils/ — primitives both are built on

Read, write, plumbing. I never had to grep for where anything lived.

/proc: Reading Text Files for System Monitoring

How does a system monitor work? It reads text files.

#define PROC_CPUINFO "/proc/cpuinfo"
#define PROC_LOADAVG "/proc/loadavg"
#define PROC_STAT    "/proc/stat"

Core count: read /proc/cpuinfo, filter ^processor, count lines. Load average: read /proc/loadavg, split on whitespace, take first three. Clock speed: filter ^cpu MHz, split on colon. The CPU section of a system monitor is a regex over a text file.

The CPU Percentage Algorithm: Stateful Getters

CPU usage isn't a value you can read. /proc/stat gives cumulative tick counters since boot — user, system, idle, iowait, etc. They only go up. CPU usage exists only as a relationship between two samples.

From cpu_info.cpp:

int CpuInfo::getCpuPercent(const QList &cpuTimes, const int &processor) const
{
    const int N = getCpuCoreCount()+1;
    static QVector l_idles(N);
    static QVector l_totals(N);
    int utilisation = 0;
    if (cpuTimes.count() > 0) {
        double idle = cpuTimes.at(3) + cpuTimes.at(4); // idle + iowait
        double total = 0.0;
        for (const double &t : cpuTimes) total += t;
        double idle_delta  = idle  - l_idles[processor];
        double total_delta = total - l_totals[processor];
        if (total_delta)
            utilisation = 100 * ((total_delta - idle_delta) / total_delta);
        l_idles[processor] = idle;
        l_totals[processor] = total;
    }
    return utilisation;
}

Those static locals make an innocent-looking getter stateful. Call it twice in a row from two places and the second caller gets garbage. The first call after launch is meaningless by definition. It's the correct algorithm (same as top), but it's a landmine.

The Refresh Loop: QTimer-Based Polling

The UI pulls on a clock. From dashboard_page.cpp:

connect(mTimer, &QTimer::timeout, this, &DashboardPage::updateCpuBar);
connect(mTimer, &QTimer::timeout, this, &DashboardPage::updateMemoryBar);
connect(mTimer, &QTimer::timeout, this, &DashboardPage::updateNetworkBar);
QTimer *timerDisk = new QTimer(this);
connect(timerDisk, &QTimer::timeout, this, &DashboardPage::updateDiskBar);
timerDisk->start(5 * 1000);
mTimer->start(1 * 1000);

One timer at 1s fanned out to three slots, another at 5s for disk (expensive). Nothing pushes; the UI pulls on a clock. That's the whole update architecture.

Privilege Escalation: pkexec Per Command

Stacer kills processes, purges caches, toggles systemd services, uninstalls packages. All of it needs root. Stacer does not run as root. From command_util.cpp:

QString CommandUtil::sudoExec(const QString &cmd, QStringList args, QByteArray data)
{
    args.push_front(cmd);
    QString result("");
    try {
        result = CommandUtil::exec("pkexec", args, data);
    } catch (QString &ex) {
        qCritical() << ex;
    }
    return result;
}

Shove the command onto the front of args, run pkexec instead. polkit pops the auth dialog for each command. The app process is unprivileged for its entire life — a bug in theming can't rm -rf your home directory. Sixteen sudoExec call sites, sixteen times the user gets asked. The alternative (launching the whole GUI under sudo) means your event loop and stylesheet parser run as root forever.

Twelve Screens: Dynamic Page Construction

Each page is a directory with .h, .cpp, .ui. App news them up into a SlidingStackedWidget (a QStackedWidget subclass with animated transitions). Best part: two pages don't always exist:

if (ToolManager::ins()->checkSourceRepository()) {
    aptSourceManagerPage = new APTSourceManagerPage(mSlidingStacked);
    mListPages.insert(7, aptSourceManagerPage);
    mListSidebarButtons.insert(7, ui->btnAptSourceManager);
} else {
    ui->btnAptSourceManager->hide();
}

An APT source manager on Arch is nonsense, so it checks for APT config and doesn't build the page. One binary reshaping itself around the machine it woke up on.

Theming: Hand-Rolled CSS Variables

QSS lacks variables. Stacer built them. Each theme is a style.qss plus a values.ini. AppManager::updateStylesheet does:

QString appThemePath = QString(":/static/themes/%1/style").arg(mSettingManager->getThemeName());
mStyleValues = new QSettings(QString("%1/values.ini").arg(appThemePath), QSettings::IniFormat);
mStylesheetFileContent = FileUtil::readStringFromFile(QString("%1/style.qss").arg(appThemePath));
for (const QString &key : mStyleValues->allKeys()) {
    mStylesheetFileContent.replace(key, mStyleValues->value(key).toString());
}
qApp->setStyleSheet(mStylesheetFileContent);
emit SignalMapper::ins()->sigChangedAppTheme();

A preprocessor in nine lines. Widgets opt into styling with setAccessibleName("danger") — an accessibility field as a CSS class.

SignalMapper: A Global Signal Bus

SignalMapper is a global QObject with three signals:

signals:
    void sigChangedAppTheme();
    void sigUninstallStarted();
    void sigUninstallFinished();

Widgets that paint themselves (circle bars, charts) can't get colors from stylesheets, so they need to know when the theme changed. Without the bus, AppManager needs a pointer to every widget that cares. With it, it shouts into the void and whoever cares listens.

What to Steal

  • Split the OS layer into a target that can't link Widgets — the linker enforces the boundary better than discipline.
  • Split that layer by verb.
  • Never run the whole app as root; pkexec per command is four lines and separates a bug from a catastrophe.
  • Poll with QTimer, two rates for two costs.
  • QSS plus a token file gives you variables.
  • A signal bus beats N pointers when a global thing changes and unknown widgets care.

Trace one thing end to end: the CPU percentage. Timer fires in dashboard_page.cpp, through InfoManager::getCpuPercents, into CpuInfo::getCpuPercents, which reads /proc/stat as text, diffs it against static state from a second ago, and hands a number to a hand-painted CircleBar. Every layer of the app in one number, once a second, on a project nobody maintains anymore.