Refactoring

This commit is contained in:
Javier Feliz 2025-08-28 14:50:26 -04:00
parent 24ac5dc0e6
commit 1355eafd48
9 changed files with 250 additions and 113 deletions

View File

@ -1,18 +1,15 @@
#include "AppListModel.hpp"
#include "DesktopAppListItem.hpp"
#include <QDebug>
#include <QProcess>
#include <QRegularExpression>
#include <QIcon>
#include <QPixmap>
#include <QUrl>
#include <QFile>
#include <QStandardPaths>
#include <QDir>
#undef signals
#include "../dmenu.hpp"
#define signals public
AppListModel::AppListModel(QObject *parent)
: QAbstractListModel(parent)
{
loadApps();
loadItems();
}
int AppListModel::rowCount(const QModelIndex &parent) const
@ -23,21 +20,24 @@ int AppListModel::rowCount(const QModelIndex &parent) const
QVariant AppListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || !m_apps || index.row() >= static_cast<int>(m_filteredIndexes.size()))
if (!index.isValid() || index.row() >= static_cast<int>(m_filteredIndexes.size()))
return QVariant();
int appIndex = m_filteredIndexes[index.row()];
const dmenu::DesktopEntry &app = (*m_apps)[appIndex];
int itemIndex = m_filteredIndexes[index.row()];
if (itemIndex >= static_cast<int>(m_items.size()))
return QVariant();
const ListItemPtr &item = m_items[itemIndex];
switch (role) {
case NameRole:
return QString::fromStdString(app.name);
case ExecRole:
return QString::fromStdString(app.exec);
case IdRole:
return QString::fromStdString(app.id);
return item->name();
case DescriptionRole:
return item->description();
case IconRole:
return getIconUrl(app);
return item->iconUrl();
case ItemTypeRole:
return item->itemType();
default:
return QVariant();
}
@ -47,38 +47,34 @@ QHash<int, QByteArray> AppListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[ExecRole] = "exec";
roles[IdRole] = "id";
roles[DescriptionRole] = "description";
roles[IconRole] = "icon";
roles[ItemTypeRole] = "itemType";
return roles;
}
void AppListModel::loadApps()
void AppListModel::loadItems()
{
beginResetModel();
m_apps = dmenu::get_dmenu_app_data();
updateFilteredApps();
m_items.clear();
// Add desktop applications
addDesktopApps();
updateFilteredItems();
endResetModel();
}
void AppListModel::launchApp(int index)
void AppListModel::executeItem(int index)
{
if (!m_apps || index < 0 || index >= static_cast<int>(m_filteredIndexes.size()))
if (index < 0 || index >= static_cast<int>(m_filteredIndexes.size()))
return;
int appIndex = m_filteredIndexes[index];
const dmenu::DesktopEntry &app = (*m_apps)[appIndex];
// Parse exec command (remove %f, %u, %F, %U field codes if present)
QString command = QString::fromStdString(app.exec);
QRegularExpression fieldCodes("%[fuFU]");
command = command.replace(fieldCodes, "").trimmed();
qDebug() << "Launching:" << command;
// Use nohup and redirect output to /dev/null for proper detachment
QString detachedCommand = QString("nohup %1 >/dev/null 2>&1 &").arg(command);
QProcess::startDetached("/bin/sh", QStringList() << "-c" << detachedCommand);
int itemIndex = m_filteredIndexes[index];
if (itemIndex >= static_cast<int>(m_items.size()))
return;
m_items[itemIndex]->execute();
}
QString AppListModel::searchText() const
@ -95,75 +91,38 @@ void AppListModel::setSearchText(const QString &searchText)
emit searchTextChanged();
beginResetModel();
updateFilteredApps();
updateFilteredItems();
endResetModel();
}
void AppListModel::updateFilteredApps()
void AppListModel::addDesktopApps()
{
m_filteredIndexes.clear();
if (!m_apps)
dmenu::DEVec apps = dmenu::get_dmenu_app_data();
if (!apps)
return;
for (size_t i = 0; i < m_apps->size(); ++i) {
const dmenu::DesktopEntry &app = (*m_apps)[i];
if (m_searchText.isEmpty() ||
QString::fromStdString(app.name).contains(m_searchText, Qt::CaseInsensitive)) {
m_filteredIndexes.push_back(static_cast<int>(i));
}
for (const auto &entry : *apps) {
auto item = std::make_shared<DesktopAppListItem>(entry);
m_items.push_back(item);
}
}
QUrl AppListModel::getIconUrl(const dmenu::DesktopEntry &app) const
void AppListModel::addItems(const std::vector<ListItemPtr> &items)
{
QString iconName = QString::fromStdString(app.icon_path);
if (iconName.isEmpty())
return QUrl();
// If it's already a full path, use it directly
if (iconName.startsWith('/')) {
if (QFile::exists(iconName)) {
return QUrl::fromLocalFile(iconName);
}
return QUrl();
for (const auto &item : items) {
m_items.push_back(item);
}
}
void AppListModel::updateFilteredItems()
{
m_filteredIndexes.clear();
// Use Qt's proper icon theme search which follows XDG spec
QIcon icon = QIcon::fromTheme(iconName);
bool themeFound = !icon.isNull();
// Qt doesn't expose the resolved file path directly, so let's use QStandardPaths
// to search in the proper system directories
QStringList dataDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
// Common icon subdirectories and sizes (prioritized)
QStringList iconSubDirs = {
"icons/hicolor/scalable/apps",
"icons/hicolor/48x48/apps",
"icons/hicolor/64x64/apps",
"icons/hicolor/32x32/apps",
"icons/hicolor/128x128/apps",
"icons/Adwaita/scalable/apps",
"icons/Adwaita/48x48/apps",
"pixmaps" // This should search /path/to/share/pixmaps/
};
QStringList extensions = {"", ".png", ".svg", ".xpm"};
for (const QString &dataDir : dataDirs) {
for (const QString &iconSubDir : iconSubDirs) {
QString basePath = dataDir + "/" + iconSubDir + "/";
for (const QString &ext : extensions) {
QString fullPath = basePath + iconName + ext;
if (QFile::exists(fullPath)) {
return QUrl::fromLocalFile(fullPath);
}
}
for (size_t i = 0; i < m_items.size(); ++i) {
const ListItemPtr &item = m_items[i];
if (item->matches(m_searchText)) {
m_filteredIndexes.push_back(static_cast<int>(i));
}
}
return QUrl();
}

View File

@ -2,10 +2,8 @@
#include <QAbstractListModel>
#include <QQmlEngine>
#undef signals
#include "../dmenu.hpp"
#define signals public
#include "ListItem.hpp"
#include <vector>
class AppListModel : public QAbstractListModel
{
@ -13,11 +11,11 @@ class AppListModel : public QAbstractListModel
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
public:
enum AppRoles {
enum ItemRoles {
NameRole = Qt::UserRole + 1,
ExecRole,
IdRole,
IconRole
DescriptionRole,
IconRole,
ItemTypeRole
};
explicit AppListModel(QObject *parent = nullptr);
@ -26,8 +24,12 @@ public:
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void loadApps();
Q_INVOKABLE void launchApp(int index);
Q_INVOKABLE void loadItems();
Q_INVOKABLE void executeItem(int index);
// Add items from different sources
void addDesktopApps();
void addItems(const std::vector<ListItemPtr> &items);
QString searchText() const;
void setSearchText(const QString &searchText);
@ -36,10 +38,9 @@ signals:
void searchTextChanged();
private:
void updateFilteredApps();
QUrl getIconUrl(const dmenu::DesktopEntry &app) const;
void updateFilteredItems();
dmenu::DEVec m_apps;
std::vector<ListItemPtr> m_items;
std::vector<int> m_filteredIndexes;
QString m_searchText;
};

View File

@ -0,0 +1,97 @@
#include "DesktopAppListItem.hpp"
#include <QProcess>
#include <QRegularExpression>
#include <QIcon>
#include <QStandardPaths>
#include <QFile>
#include <QDebug>
DesktopAppListItem::DesktopAppListItem(const dmenu::DesktopEntry &entry)
: m_entry(entry)
{
}
QString DesktopAppListItem::name() const
{
return QString::fromStdString(m_entry.name);
}
QString DesktopAppListItem::description() const
{
// Could add support for Comment field from desktop entry later
return QString::fromStdString(m_entry.exec);
}
QUrl DesktopAppListItem::iconUrl() const
{
return resolveIconUrl(m_entry.icon_path);
}
void DesktopAppListItem::execute()
{
// Parse exec command (remove %f, %u, %F, %U field codes if present)
QString command = QString::fromStdString(m_entry.exec);
QRegularExpression fieldCodes("%[fuFU]");
command = command.replace(fieldCodes, "").trimmed();
// Use nohup and redirect output to /dev/null for proper detachment
QString detachedCommand = QString("nohup %1 >/dev/null 2>&1 &").arg(command);
QProcess::startDetached("/bin/sh", QStringList() << "-c" << detachedCommand);
}
QString DesktopAppListItem::itemType() const
{
return "app";
}
QUrl DesktopAppListItem::resolveIconUrl(const std::string &iconPath) const
{
QString iconName = QString::fromStdString(iconPath);
if (iconName.isEmpty())
return QUrl();
// If it's already a full path, use it directly
if (iconName.startsWith('/')) {
if (QFile::exists(iconName)) {
return QUrl::fromLocalFile(iconName);
}
return QUrl();
}
// Use Qt's proper icon theme search which follows XDG spec
QIcon icon = QIcon::fromTheme(iconName);
bool themeFound = !icon.isNull();
// Qt doesn't expose the resolved file path directly, so let's use QStandardPaths
// to search in the proper system directories
QStringList dataDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
// Common icon subdirectories and sizes (prioritized)
QStringList iconSubDirs = {
"icons/hicolor/scalable/apps",
"icons/hicolor/48x48/apps",
"icons/hicolor/64x64/apps",
"icons/hicolor/32x32/apps",
"icons/hicolor/128x128/apps",
"icons/Adwaita/scalable/apps",
"icons/Adwaita/48x48/apps",
"pixmaps" // This should search /path/to/share/pixmaps/
};
QStringList extensions = {"", ".png", ".svg", ".xpm"};
for (const QString &dataDir : dataDirs) {
for (const QString &iconSubDir : iconSubDirs) {
QString basePath = dataDir + "/" + iconSubDir + "/";
for (const QString &ext : extensions) {
QString fullPath = basePath + iconName + ext;
if (QFile::exists(fullPath)) {
return QUrl::fromLocalFile(fullPath);
}
}
}
}
return QUrl();
}

View File

@ -0,0 +1,25 @@
#pragma once
#include "ListItem.hpp"
#undef signals
#include "../dmenu.hpp"
#define signals public
class DesktopAppListItem : public ListItem
{
public:
explicit DesktopAppListItem(const dmenu::DesktopEntry &entry);
// ListItem interface
QString name() const override;
QString description() const override;
QUrl iconUrl() const override;
void execute() override;
QString itemType() const override;
private:
QUrl resolveIconUrl(const std::string &iconPath) const;
dmenu::DesktopEntry m_entry;
};

15
lib/ui/ListItem.cpp Normal file
View File

@ -0,0 +1,15 @@
#include "ListItem.hpp"
bool ListItem::matches(const QString &query) const
{
return defaultMatches(query);
}
bool ListItem::defaultMatches(const QString &query) const
{
if (query.isEmpty())
return true;
return name().contains(query, Qt::CaseInsensitive) ||
description().contains(query, Qt::CaseInsensitive);
}

31
lib/ui/ListItem.hpp Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include <QString>
#include <QUrl>
#include <memory>
class ListItem
{
public:
virtual ~ListItem() = default;
// Display properties for the UI
virtual QString name() const = 0;
virtual QString description() const = 0; // Optional subtitle/description
virtual QUrl iconUrl() const = 0;
// Action when item is selected
virtual void execute() = 0;
// Search matching - return true if this item matches the search query
virtual bool matches(const QString &query) const;
// Item type for extensibility (e.g., "app", "file", "bookmark")
virtual QString itemType() const = 0;
protected:
// Helper for default search matching (case-insensitive name search)
bool defaultMatches(const QString &query) const;
};
using ListItemPtr = std::shared_ptr<ListItem>;

View File

@ -20,14 +20,19 @@ pkgs.mkShell {
pkgs.kdePackages.layer-shell-qt # <- provides layer shell support
pkgs.kdePackages.layer-shell-qt.dev # <- provides headers
pkgs.pkg-config
# qt.qttools
# qt.qtshadertools
# THEMING
pkgs.qt6ct
pkgs.kdePackages.qqc2-desktop-style
pkgs.kdePackages.breeze-icons
pkgs.hicolor-icon-theme
];
shellHook = ''
export QT_PLUGIN_PATH='${qt.full}'
export QML2_IMPORT_PATH='${qt.full}:${pkgs.kdePackages.layer-shell-qt}/lib/qt-6/qml'
export QT_QPA_PLATFORM=wayland
export QT_QPA_PLATFORMTHEME=qt6ct
echo "--------------------------------------------"
echo "QT_PLUGIN_PATH: $QT_PLUGIN_PATH"
echo "QML2_IMPORT_PATH: $QML2_IMPORT_PATH"

View File

@ -3,9 +3,13 @@
#include <QIcon>
#include <QWindow>
#include <LayerShellQt/window.h>
#include "ui/AppListModel.hpp"
#undef signals
#include "dmenu.hpp"
#include "files.hpp"
#define signals public
#include "ui/AppListModel.hpp"
#include <cstdlib>
#include <string>
#include <iostream>

View File

@ -50,7 +50,7 @@ ApplicationWindow {
Keys.onUpPressed: listView.decrementCurrentIndex()
Keys.onReturnPressed: {
if (listView.currentItem) {
appModel.launchApp(listView.currentIndex)
appModel.executeItem(listView.currentIndex)
Qt.quit()
}
}
@ -119,7 +119,7 @@ ApplicationWindow {
onClicked: {
listView.currentIndex = index
appModel.launchApp(index)
appModel.executeItem(index)
Qt.quit()
}
}