commit f4a81cfa322af724a264d002283b6d55d42906d1 Author: LordGrimmauld Date: Sat Mar 2 22:33:27 2024 +0100 add basic QT tree diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f531417 --- /dev/null +++ b/.gitignore @@ -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 + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3b90d39 --- /dev/null +++ b/CMakeLists.txt @@ -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() diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..c71a897 --- /dev/null +++ b/main.cpp @@ -0,0 +1,33 @@ +#include "mainwindow.h" +#include "tree/pstree.h" +#include "tree/PsTreeModel.h" + +#include +#include +#include +#include +#include +#include +#include + +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(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..5832d8c --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,32 @@ +#include "mainwindow.h" +#include "./ui_mainwindow.h" +#include "tree/PsTreeModel.h" +#include +#include + +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(); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..6877df6 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,28 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#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 diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..6991628 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,51 @@ + + + MainWindow + + + + 0 + 0 + 1080 + 740 + + + + + 0 + 0 + + + + SwayMux + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + + + + 24 + + + + + + + + + + + diff --git a/swaymux_en_US.ts b/swaymux_en_US.ts new file mode 100644 index 0000000..fd371f5 --- /dev/null +++ b/swaymux_en_US.ts @@ -0,0 +1,4 @@ + + + + diff --git a/tree/AbstractTreeNode.h b/tree/AbstractTreeNode.h new file mode 100644 index 0000000..dee6ee9 --- /dev/null +++ b/tree/AbstractTreeNode.h @@ -0,0 +1,71 @@ +// +// Created by grimmauld on 27.02.24. +// + +#ifndef SWAYMUX_ABSTRACTTREENODE_H +#define SWAYMUX_ABSTRACTTREENODE_H + +#include +#include +#include +#include +#include +#include + +template +class AbstractTreeNode { + // static_assert(std::is_base_of::value, "Derived not derived from BaseClass"); +public: + std::vector> 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 &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 &child) { children.push_back(std::move(child)); } + + [[nodiscard]] T* findChild(std::function 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 diff --git a/tree/PsTreeModel.cpp b/tree/PsTreeModel.cpp new file mode 100644 index 0000000..7b105fe --- /dev/null +++ b/tree/PsTreeModel.cpp @@ -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(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(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(parent.internalPointer()) + : &rootItem; + + return parentItem->childCount(); +} + +int PsTreeModel::columnCount(const QModelIndex &parent) const { + if (parent.isValid()) + return static_cast(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(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 changedParents{}; + for(auto* node: changes) { + changedParents.push_back(createIndex(node->row(), 0, node)); + } + layoutAboutToBeChanged(changedParents); +} diff --git a/tree/PsTreeModel.h b/tree/PsTreeModel.h new file mode 100644 index 0000000..16c83ac --- /dev/null +++ b/tree/PsTreeModel.h @@ -0,0 +1,45 @@ +// +// Created by grimmauld on 25.02.24. +// + +#ifndef SWAYMUX_PSTREEMODEL_H +#define SWAYMUX_PSTREEMODEL_H + + +#include +#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 diff --git a/tree/pstree.cpp b/tree/pstree.cpp new file mode 100644 index 0000000..0dec592 --- /dev/null +++ b/tree/pstree.cpp @@ -0,0 +1,151 @@ +// +// Created by grimmauld on 25.02.24. +// + +#include "pstree.h" +#include "../util/StringUtil.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +const char *procFileDelim = ":"; + +void populateTree(ProcessTreeNode *node, std::map> &ppid_map, std::set& 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 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(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 update_process_records(ProcessTreeNode *node) { + std::map pid_map; + std::map> ppid_map; + std::set 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 &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{}; + } +} \ No newline at end of file diff --git a/tree/pstree.h b/tree/pstree.h new file mode 100644 index 0000000..73d4844 --- /dev/null +++ b/tree/pstree.h @@ -0,0 +1,76 @@ +// +// Created by grimmauld on 25.02.24. +// + +#ifndef SWAYMUX_PSTREE_H +#define SWAYMUX_PSTREE_H + +#define PROCFS_ROOT "/proc" + + +#include +#include +#include +#include +#include +#include +#include +#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 { +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 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 &buff); + +std::set update_process_records(ProcessTreeNode *node); + +#endif //SWAYMUX_PSTREE_H diff --git a/util/StringUtil.h b/util/StringUtil.h new file mode 100644 index 0000000..f89bdb3 --- /dev/null +++ b/util/StringUtil.h @@ -0,0 +1,13 @@ +// +// Created by grimmauld on 25.02.24. +// + +#include +#include +#include +#include + +inline void trim(std::string & s) { + s.erase(std::remove_if(s.begin(), s.end(), ::isspace), + s.end()); +} \ No newline at end of file