add basic QT tree

This commit is contained in:
LordGrimmauld 2024-03-02 22:33:27 +01:00
commit f4a81cfa32
13 changed files with 740 additions and 0 deletions

78
.gitignore vendored Normal file
View File

@ -0,0 +1,78 @@
# This file is used to ignore files which are generated
# ----------------------------------------------------------------------------
*~
*.autosave
*.a
*.core
*.moc
*.o
*.obj
*.orig
*.rej
*.so
*.so.*
*_pch.h.cpp
*_resource.rc
*.qm
.#*
*.*#
core
!core/
tags
.DS_Store
.directory
*.debug
Makefile*
*.prl
*.app
moc_*.cpp
ui_*.h
qrc_*.cpp
Thumbs.db
*.res
*.rc
/.qmake.cache
/.qmake.stash
# qtcreator generated files
*.pro.user*
CMakeLists.txt.user*
# xemacs temporary files
*.flc
# Vim temporary files
.*.swp
# Visual Studio generated files
*.ib_pdb_index
*.idb
*.ilk
*.pdb
*.sln
*.suo
*.vcproj
*vcproj.*.*.user
*.ncb
*.sdf
*.opensdf
*.vcxproj
*vcxproj.*
# idea
cmake-build-debug
.idea
# MinGW generated files
*.Debug
*.Release
# Python byte code
*.pyc
# Binaries
# --------
*.dll
*.exe

79
CMakeLists.txt Normal file
View File

@ -0,0 +1,79 @@
cmake_minimum_required(VERSION 3.5)
project(swaymux VERSION 0.1 LANGUAGES CXX)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets LinguistTools)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets LinguistTools)
set(TS_FILES swaymux_en_US.ts)
set(PROJECT_SOURCES
main.cpp
mainwindow.cpp
mainwindow.h
mainwindow.ui
${TS_FILES}
tree/pstree.cpp
tree/PsTreeModel.cpp
)
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
qt_add_executable(swaymux
MANUAL_FINALIZATION
${PROJECT_SOURCES}
)
# Define target properties for Android with Qt 6 as:
# set_property(TARGET swaymux APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
# ${CMAKE_CURRENT_SOURCE_DIR}/android)
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES})
else()
if(ANDROID)
add_library(swaymux SHARED
${PROJECT_SOURCES}
)
# Define properties for Android with Qt 5 after find_package() calls as:
# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
else()
add_executable(swaymux
${PROJECT_SOURCES}
)
endif()
qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES})
endif()
target_link_libraries(swaymux PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though.
if(${QT_VERSION} VERSION_LESS 6.1.0)
set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.swaymux)
endif()
set_target_properties(swaymux PROPERTIES
${BUNDLE_ID_OPTION}
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)
include(GNUInstallDirs)
install(TARGETS swaymux
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
if(QT_VERSION_MAJOR EQUAL 6)
qt_finalize_executable(swaymux)
endif()

33
main.cpp Normal file
View File

@ -0,0 +1,33 @@
#include "mainwindow.h"
#include "tree/pstree.h"
#include "tree/PsTreeModel.h"
#include <QApplication>
#include <QLocale>
#include <QTranslator>
#include <QFile>
#include <QStringList>
#include <QScreen>
#include <QTreeView>
using namespace Qt::StringLiterals;
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QCoreApplication::setOrganizationName("Grimmauld");
QCoreApplication::setApplicationName("SwayMux");
QTranslator translator;
const QStringList uiLanguages = QLocale::system().uiLanguages();
for (const QString &locale : uiLanguages) {
const QString baseName = "swaymux_" + QLocale(locale).name();
if (translator.load(":/i18n/" + baseName)) {
a.installTranslator(&translator);
break;
}
}
MainWindow w;
w.showMaximized();
return a.exec();
}

32
mainwindow.cpp Normal file
View File

@ -0,0 +1,32 @@
#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include "tree/PsTreeModel.h"
#include <QScreen>
#include <QTimer>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), ui(new Ui::MainWindow) {
ui->setupUi(this);
model = new PsTreeModel(ui->treeView);
modelUpdateTimer = new QTimer(this);
connect(modelUpdateTimer, &QTimer::timeout, this, QOverload<>::of(&MainWindow::update));
modelUpdateTimer->start(1000);
ui->statusbar->showMessage(QString::fromStdString("test test 123"));
ui->treeView->setModel(model);
ui->treeView->expandAll();
for (int c = 0; c < model->columnCount(); ++c)
ui->treeView->resizeColumnToContents(c);
}
MainWindow::~MainWindow() {
delete ui;
delete model;
delete modelUpdateTimer;
}
void MainWindow::update() {
// model->update();
}

28
mainwindow.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "tree/PsTreeModel.h"
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
void update() ;
private:
Ui::MainWindow *ui;
PsTreeModel* model;
QTimer* modelUpdateTimer;
};
#endif // MAINWINDOW_H

51
mainwindow.ui Normal file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1080</width>
<height>740</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>SwayMux</string>
</property>
<widget class="QWidget" name="centralwidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTreeView" name="treeView">
<property name="font">
<font>
<pointsize>24</pointsize>
</font>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>

4
swaymux_en_US.ts Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US"></TS>

71
tree/AbstractTreeNode.h Normal file
View File

@ -0,0 +1,71 @@
//
// Created by grimmauld on 27.02.24.
//
#ifndef SWAYMUX_ABSTRACTTREENODE_H
#define SWAYMUX_ABSTRACTTREENODE_H
#include <filesystem>
#include <set>
#include <utility>
#include <map>
#include <vector>
#include <QVariant>
template<class T>
class AbstractTreeNode {
// static_assert(std::is_base_of<AbstractTreeNode, T>::value, "Derived not derived from BaseClass");
public:
std::vector<std::unique_ptr<T>> children{};
explicit AbstractTreeNode(T *parent = nullptr) : parent(parent) {}
[[nodiscard]] T *parentItem() { return parent; };
[[nodiscard]] size_t childCount() const { return children.size(); };
[[nodiscard]] T *child(int row) const { return row >= 0 && row < childCount() ? children.at(row).get() : nullptr; };
[[nodiscard]] virtual QVariant data(int column) const = 0;
[[nodiscard]] virtual QVariant headerData(int column) const = 0;
[[nodiscard]] virtual int columnCount() const = 0;
[[nodiscard]] int row() {
if (parent == nullptr)
return 0;
const auto it = std::find_if(parent->children.cbegin(), parent->children.cend(),
[this](const std::unique_ptr<T> &treeItem) {
return treeItem.get() == this;
});
if (it != parent->children.cend())
return std::distance(parent->children.cbegin(), it);
Q_ASSERT(false); // should not happen
return -1;
}
void appendChild(std::unique_ptr<T> &child) { children.push_back(std::move(child)); }
[[nodiscard]] T* findChild(std::function<bool(const T &)> test) {
for (int i = 0; i < childCount(); ++i) {
auto* c = child(i);
if (test(*c))
return c;
}
return nullptr;
};
void removeChild(int i) {
if (i >= 0 && i < childCount()) {
children.erase(std::next(children.begin(), i));
}
}
protected:
T *parent;
};
#endif //SWAYMUX_ABSTRACTTREENODE_H

79
tree/PsTreeModel.cpp Normal file
View File

@ -0,0 +1,79 @@
//
// Created by grimmauld on 25.02.24.
//
#include "PsTreeModel.h"
PsTreeModel::PsTreeModel(QObject *parent) : QAbstractItemModel(parent), rootItem(get_process_records()) {}
QModelIndex PsTreeModel::index(int row, int column, const QModelIndex &parent) const {
if (!hasIndex(row, column, parent))
return {};
const ProcessTreeNode *parentItem = parent.isValid()
? static_cast<ProcessTreeNode *>(parent.internalPointer())
: &rootItem;
if (auto *childItem = parentItem->child(row))
return createIndex(row, column, childItem);
return {};
}
QModelIndex PsTreeModel::parent(const QModelIndex &index) const {
if (!index.isValid())
return {};
auto *childItem = static_cast<ProcessTreeNode *>(index.internalPointer());
ProcessTreeNode *parentItem = childItem->parentItem();
return parentItem != &rootItem
? createIndex(parentItem->row(), 0, parentItem) : QModelIndex{};
}
int PsTreeModel::rowCount(const QModelIndex &parent) const {
if (parent.column() > 0)
return 0;
const ProcessTreeNode *parentItem = parent.isValid()
? static_cast<const ProcessTreeNode *>(parent.internalPointer())
: &rootItem;
return parentItem->childCount();
}
int PsTreeModel::columnCount(const QModelIndex &parent) const {
if (parent.isValid())
return static_cast<ProcessTreeNode *>(parent.internalPointer())->columnCount();
return ProcessTreeNode{}.columnCount();
}
QVariant PsTreeModel::data(const QModelIndex &index, int role) const {
if (!index.isValid() || role != Qt::DisplayRole)
return {};
const auto *item = static_cast<const ProcessTreeNode *>(index.internalPointer());
return item->data(index.column());
}
Qt::ItemFlags PsTreeModel::flags(const QModelIndex &index) const {
return index.isValid()
? QAbstractItemModel::flags(index) : Qt::ItemFlags(Qt::NoItemFlags);
}
QVariant PsTreeModel::headerData(int section, Qt::Orientation orientation,
int role) const {
if (orientation != Qt::Horizontal || role != Qt::DisplayRole)
return QVariant{};
return ProcessTreeNode{}.headerData(section);
}
void PsTreeModel::update() {
auto changes = update_process_records(&rootItem);
QList<QPersistentModelIndex> changedParents{};
for(auto* node: changes) {
changedParents.push_back(createIndex(node->row(), 0, node));
}
layoutAboutToBeChanged(changedParents);
}

45
tree/PsTreeModel.h Normal file
View File

@ -0,0 +1,45 @@
//
// Created by grimmauld on 25.02.24.
//
#ifndef SWAYMUX_PSTREEMODEL_H
#define SWAYMUX_PSTREEMODEL_H
#include <QAbstractItemModel>
#include "pstree.h"
class PsTreeModel : public QAbstractItemModel {
Q_OBJECT
public:
Q_DISABLE_COPY_MOVE(PsTreeModel)
explicit PsTreeModel(QObject *parent = nullptr);
~PsTreeModel() override = default;
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
[[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override;
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
[[nodiscard]] QModelIndex index(int row, int column,
const QModelIndex &parent = {}) const override;
[[nodiscard]] QModelIndex parent(const QModelIndex &index) const override;
[[nodiscard]] int rowCount(const QModelIndex &parent = {}) const override;
[[nodiscard]] int columnCount(const QModelIndex &parent = {}) const override;
void update();
private:
ProcessTreeNode rootItem;
};
#endif //SWAYMUX_PSTREEMODEL_H

151
tree/pstree.cpp Normal file
View File

@ -0,0 +1,151 @@
//
// Created by grimmauld on 25.02.24.
//
#include "pstree.h"
#include "../util/StringUtil.h"
#include <cstring>
#include <set>
#include <string>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <memory>
const char *procFileDelim = ":";
void populateTree(ProcessTreeNode *node, std::map<pid_t, std::set<ProcessRecord>> &ppid_map, std::set<ProcessTreeNode*>& changedParents) {
if (node == nullptr)
return;
for (int i = (int) node->childCount() - 1; i >= 0; i--) { // backwards to avoid changing stuff that might change later
std::set<ProcessRecord> children = ppid_map[node->proc.ppid];
auto* c = node->child(i);
if (c->proc.pid == 0)
continue;
if (children.find(c->proc) == children.end()) {
// node->removeChild(i); // fixme: segfaults
changedParents.insert(node);
}
}
for (const auto &child: ppid_map[node->proc.pid]) {
// check child exists, if so no need to update
auto* existingChildNode = node->findChild(ProcessTreeNode::matchingRecord(child));
if (existingChildNode != nullptr) {
populateTree(existingChildNode, ppid_map, changedParents);
continue;
}
// else add the new child node, keep track in notifier
changedParents.insert(node);
auto childNode = std::make_unique<ProcessTreeNode>(child, node);
populateTree(childNode.get(), ppid_map, changedParents);
node->appendChild(childNode);
}
}
ProcessTreeNode get_process_records() {
ProcessTreeNode root = ProcessTreeNode(); // rootPrc
update_process_records(&root);
return root;
}
std::set<ProcessTreeNode*> update_process_records(ProcessTreeNode *node) {
std::map<pid_t, ProcessRecord> pid_map;
std::map<pid_t, std::set<ProcessRecord>> ppid_map;
std::set<ProcessTreeNode*> changedParents{};
const auto proc_fs = fs::path(PROCFS_ROOT);
auto content = fs::directory_iterator(proc_fs);
for (const auto &proc_dir: content) {
if (fs::is_directory(proc_dir)) {
insert_process_record(proc_dir.path() / "status", pid_map);
}
}
pid_t rootPid = INT32_MAX;
for (auto &pair: pid_map) {
auto proc = pair.second;
if (proc.pid < rootPid) {
rootPid = proc.pid;
}
ppid_map[proc.ppid].insert(proc);
}
// auto rootPrc = pid_map[rootPid];
// ProcessTreeNode root = ProcessTreeNode(); // rootPrc
populateTree(node, ppid_map, changedParents);
return changedParents;
}
void insert_process_record(const std::filesystem::path &status_path, std::map<pid_t, ProcessRecord> &buff) {
if (!fs::exists(status_path) || !fs::is_regular_file(status_path))
return;
std::string proc_name;
pid_t pid, ppid;
bool foundPid, foundPPid, foundName;
auto reader = std::ifstream{status_path, std::ios::in};
std::string line;
while (std::getline(reader, line)) {
auto data = line.data();
auto name = strtok(data, procFileDelim);
auto val = strtok(nullptr, procFileDelim);
if (name == nullptr || val == nullptr)
continue;
if (strcmp(name, "Name") == 0) {
proc_name = std::string(val);
trim(proc_name);
foundName = true;
} else if (strcmp(name, "Pid") == 0) {
pid = std::stoi(val);
foundPid = true;
} else if (strcmp(name, "PPid") == 0) {
ppid = std::stoi(val);
foundPPid = true;
}
}
if (foundPPid && foundName && foundPid) {
buff.insert({pid, ProcessRecord(proc_name, pid, ppid)});
}
}
void printTree(const ProcessTreeNode &node, const std::string &prefix) {
std::cout << prefix << node.proc.name << ": " << node.proc.pid << "\n";
for (const auto &child: node.children) {
printTree(*child, prefix + "\t");
}
}
void printTree() {
printTree(get_process_records(), "");
}
QVariant ProcessTreeNode::data(int column) const {
switch (column) {
case 0:
return QString::fromStdString(std::to_string(proc.pid));
case 1:
return QString::fromStdString(proc.name);
default:
throw std::exception();
}
}
QVariant ProcessTreeNode::headerData(int column) const {
switch (column) {
case 0:
return "PID";
case 1:
return "Process Name";
default:
return QVariant{};
}
}

76
tree/pstree.h Normal file
View File

@ -0,0 +1,76 @@
//
// Created by grimmauld on 25.02.24.
//
#ifndef SWAYMUX_PSTREE_H
#define SWAYMUX_PSTREE_H
#define PROCFS_ROOT "/proc"
#include <filesystem>
#include <set>
#include <utility>
#include <map>
#include <functional>
#include <vector>
#include <QVariant>
#include "AbstractTreeNode.h"
void printTree();
namespace fs = std::filesystem;
class ProcessRecord {
public:
const std::string name;
const pid_t pid;
const pid_t ppid;
ProcessRecord(std::string name, pid_t pid, pid_t ppid) : name(std::move(name)), pid(pid), ppid(ppid) {}
ProcessRecord() : name("root"), pid(0), ppid(-1) {}
[[nodiscard]] bool operator<(const ProcessRecord &other) const {
return this->pid < other.pid;
}
[[nodiscard]] bool operator==(const ProcessRecord &other) const {
return this->pid == other.pid && this->ppid == other.ppid && this->name == other.name;
}
};
class ProcessTreeNode : public AbstractTreeNode<ProcessTreeNode> {
public:
explicit ProcessTreeNode(ProcessTreeNode *parent = nullptr) : proc(ProcessRecord()), AbstractTreeNode(parent) {}
const ProcessRecord proc;
explicit ProcessTreeNode(ProcessRecord proc, ProcessTreeNode *parent = nullptr) : proc(std::move(proc)),
AbstractTreeNode(parent) {}
[[nodiscard]] bool operator<(const ProcessTreeNode &other) const {
return this->proc < other.proc;
}
[[nodiscard]] QVariant data(int column) const override;
[[nodiscard]] QVariant headerData(int column) const override;
[[nodiscard]] int columnCount() const override { return 2; };
std::function<bool(const ProcessTreeNode &)> static matchingRecord(const ProcessRecord &other) {
return [&other](const ProcessTreeNode &node) {
return node.proc == other;
};
};
};
ProcessTreeNode get_process_records();
void insert_process_record(const std::filesystem::path &status_path, std::map<pid_t, ProcessRecord> &buff);
std::set<ProcessTreeNode*> update_process_records(ProcessTreeNode *node);
#endif //SWAYMUX_PSTREE_H

13
util/StringUtil.h Normal file
View File

@ -0,0 +1,13 @@
//
// Created by grimmauld on 25.02.24.
//
#include <iostream>
#include <algorithm>
#include <cctype>
#include <string>
inline void trim(std::string & s) {
s.erase(std::remove_if(s.begin(), s.end(), ::isspace),
s.end());
}