Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Commit Lint

on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]

jobs:
lint-commits:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Gather commits range
id: range
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "pull_request" ]; then
base_ref=${{ github.event.pull_request.base.ref }}
# fetch base ref and compute range
git fetch origin "$base_ref":refs/remotes/origin/$base_ref || true
RANGE="origin/$base_ref..HEAD"
else
# push event: use before..after
before=${{ github.event.before }}
after=${{ github.sha }}
if [ "$before" = "0000000000000000000000000000000000000000" ]; then
# initial commit range: just check latest
RANGE="$after"
else
RANGE="$before..$after"
fi
fi
echo "range=$RANGE" >> $GITHUB_OUTPUT

- name: Check commit subjects for Conventional Commits
env:
RANGE: ${{ steps.range.outputs.range }}
run: |
set -euo pipefail
echo "Checking commits in range: $RANGE"
# gather commits; if RANGE is a single sha, handle that
if git rev-parse --verify "$RANGE" >/dev/null 2>&1; then
commits=$(git rev-list --no-merges $RANGE)
else
# fallback: recent 100 commits
commits=$(git rev-list --no-merges HEAD -n 100)
fi

if [ -z "$commits" ]; then
echo "No commits to check."
exit 0
fi

# Conventional commit regex (allowing bugfix and type aliases)
re='^(feat|fix|bugfix|docs|style|refactor|perf|test|chore|type)(\([^)]*\))?: .+'

failed=0
for c in $commits; do
subj=$(git log --format=%s -n 1 $c)
if ! echo "$subj" | grep -Eq "$re"; then
echo "Invalid commit message subject for $c: '$subj'"
failed=1
fi
done

if [ $failed -ne 0 ]; then
echo "One or more commit messages do not follow Conventional Commits." >&2
exit 1
fi

echo "All commit subjects look good."
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ Makefile
# Misc
tags
compile_commands.json

# Qt auto-gen artifacts (created by build in repo root)
.qt/
49 changes: 37 additions & 12 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ project(DLauncher LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Make compile commands available for tools (clang-tidy, language servers)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Development options
option(BUILD_TESTS "Build unit tests" ON)
option(ENABLE_STRICT_WARNINGS "Enable -Wall -Wextra for developer builds" ON)

# Qt settings
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
Expand All @@ -14,10 +21,13 @@ set(CMAKE_AUTORCC ON)
find_package(Qt6 COMPONENTS Widgets REQUIRED)

# Collect sources and headers only from source tree
# Glob only implementation sources in src; add top-level main.cpp explicitly
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS
${CMAKE_SOURCE_DIR}/src/*.cpp
${CMAKE_SOURCE_DIR}/*.cpp
)
if(EXISTS ${CMAKE_SOURCE_DIR}/main.cpp)
list(APPEND SOURCES ${CMAKE_SOURCE_DIR}/main.cpp)
endif()

# Remove duplicate sources that may exist in multiple places
list(REMOVE_ITEM SOURCES "${CMAKE_SOURCE_DIR}/src/utils/readApps.cpp")
Expand Down Expand Up @@ -49,15 +59,30 @@ add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS})

target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets)

target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/ui
${CMAKE_SOURCE_DIR}/src/core
${CMAKE_SOURCE_DIR}/src/services
${CMAKE_SOURCE_DIR}/src/components
${CMAKE_SOURCE_DIR}/src/utils
)
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/ui
${CMAKE_SOURCE_DIR}/src/core
${CMAKE_SOURCE_DIR}/src/services
${CMAKE_SOURCE_DIR}/src/components
${CMAKE_SOURCE_DIR}/src/utils
${CMAKE_SOURCE_DIR}/src/core
)

if(BUILD_TESTS)
enable_testing()
# Test target for menu parser
add_executable(menu_parser_test test/menu_parser_test.cpp src/utils/menu_parser.cpp)
target_include_directories(menu_parser_test PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/test)
target_link_libraries(menu_parser_test PRIVATE Qt6::Widgets)
add_test(NAME menu_parser_test COMMAND menu_parser_test)

add_executable(discovery_test test/discovery_test.cpp src/core/apps/readApps.cpp src/core/intern.cpp src/utils/utils.cpp)
target_include_directories(discovery_test PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/test)
target_link_libraries(discovery_test PRIVATE Qt6::Widgets)
add_test(NAME discovery_test COMMAND discovery_test)
endif()

# Test target for menu parser
add_executable(menu_parser_test test/menu_parser_test.cpp src/utils/menu_parser.cpp)
target_include_directories(menu_parser_test PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/test)
if(ENABLE_STRICT_WARNINGS)
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
endif()
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ type(scope): short description
- test: Adding or correcting tests
- chore: Maintenance

We also accept these concise aliases/types and encourage their use in commit messages:

- `bugfix:` (alias for `fix:`) — use when clarifying a bug-fix type
- `type:` — small project-specific type to indicate the change relates to "type" metadata

Preferred top-level types to use: `feat:`, `fix:`/`bugfix:`, `chore:`, `type:`.

**Examples:**
- `feat(launcher): add frequency-based sorting`
- `fix(theme): correct color parsing`
Expand Down
113 changes: 68 additions & 45 deletions main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
#include <filesystem>

#include "LockedLineEdit.h"
#include "src/ui/components/list.h"
#include "src/core/listeners/kb.h"
#include "src/ui/components/appRow.h"
#include "src/core/apps/readApps.h"
#include "ui/components/list.h"
#include "core/listeners/kb.h"
#include "ui/components/appRow.h"
#include "core/apps/readApps.h"
#include "utils.h"
#include "src/theme/theme.h"
#include "src/utils/utils.h"
#include "src/utils/debug.h"
#include "src/utils/json.hpp"
#include "theme/theme.h"
#include "utils/utils.h"
#include "utils/debug.h"
#include "utils/json.hpp"
#include "core/xdg.h"
#include "core/frequency_store.h"
#include <QApplication>
#include <QIcon>
#include <QLabel>
Expand All @@ -31,14 +33,12 @@ Qt::WindowFlags devFlags()

int main(int argc, char *argv[])
{
std::string configDir = std::string(getenv("HOME")) + "/.config";
std::string freqPath = configDir + "/dlauncher_freq.txt";
std::filesystem::create_directories(configDir);
{
std::ofstream f(freqPath, std::ios::app);
f.close();
}
std::map<std::string, int> appFrequency = json_util::load_freq(freqPath);
// Use XDG helpers and FrequencyStore
std::string freqPath = xdg::configPath("dlauncher_freq.txt").string();
// ensure dir exists
xdg::ensureDir(xdg::configPath(""));
// frequency store
FrequencyStore freqStore(freqPath);

Debug::log("Debug run string: " + Debug::generateRandomString());
AppReader appReader;
Expand All @@ -47,13 +47,20 @@ int main(int argc, char *argv[])
const Theme &theme = themeManager.currentTheme();
GlobalEventListener globalKbListener(app);

// By default don't include NoDisplay/Hidden entries; allow override via CLI flag
// By default include all .desktop entries (don't skip NoDisplay/Hidden/OnlyShowIn)
bool includeHidden = false;
bool dumpMode = false;
bool menuMode = false;
bool showSystem = false;
std::vector<std::pair<std::string, std::string>> menuItems; // label, command
for (int i = 1; i < argc; ++i) {
std::string arg(argv[i]);
if (arg == "--include-hidden" || arg == "--show-hidden") includeHidden = true;
else if (arg == "--show-system") showSystem = true;
else if (arg == "--dump") {
// Diagnostic mode: print each .desktop path and whether it would be included
dumpMode = true;
}
else if (arg == "--menu-file" && i + 1 < argc) {
menuMode = true;
std::string path = argv[++i];
Expand Down Expand Up @@ -99,7 +106,13 @@ int main(int argc, char *argv[])
if (!label.empty() && !cmd.empty()) menuItems.emplace_back(label, cmd);
}
}
appReader.LoadApps(includeHidden);
appReader.LoadApps(includeHidden, showSystem);

if (dumpMode) {
// Print diagnostics and exit
appReader.DumpAndPrint(includeHidden, showSystem);
return 0;
}

std::string searchTerm = "";

Expand Down Expand Up @@ -153,21 +166,21 @@ int main(int argc, char *argv[])

layout->addWidget(input);

std::vector<DesktopApp> allApps;
if (menuMode) {
for (auto &p : menuItems) {
DesktopApp a;
a.name = p.first;
a.exec = p.second;
allApps.push_back(a);
// Create rows lazily from pointers into the canonical app list
if (menuMode) {
for (auto &p : menuItems) {
DesktopApp a;
a.name = p.first;
a.exec = p.second;
list->addRow(new AppRow(list, a));
}
} else {
auto &master = appReader.GetAllApps();
size_t count = master.size();
for (size_t i = 0; i < count && i < 64; ++i) {
list->addRow(new AppRow(list, master[i]));
}
}
} else {
allApps = appReader.ReadDesktopApps(64, "");
}

for (const auto &app : allApps) {
list->addRow(new AppRow(list, app));
}

QObject::connect(list->listWidget, &QListWidget::currentRowChanged, [&](int row)
{
Expand All @@ -187,9 +200,9 @@ int main(int argc, char *argv[])
list->listWidget->clear();
if (search.empty()) {
std::vector<DesktopApp> allApps = appReader.ReadDesktopApps(64, "");
std::sort(allApps.begin(), allApps.end(), [&](const DesktopApp &a, const DesktopApp &b) {
int fa = appFrequency[a.exec];
int fb = appFrequency[b.exec];
std::sort(allApps.begin(), allApps.end(), [&](const DesktopApp &a, const DesktopApp &b) {
int fa = freqStore.get(a.exec);
int fb = freqStore.get(b.exec);
if (fa == fb) {
return a.name < b.name;
}
Expand All @@ -203,25 +216,25 @@ int main(int argc, char *argv[])
return;
}

std::vector<DesktopApp> filteredApps;
std::vector<const DesktopApp*> filteredAppsPtrs;
for (const auto &app : appReader.GetAllApps()) {
if (similarity(search, app.name) > 0.5 || contains(app.name, search, false) != std::string::npos) {
filteredApps.push_back(app);
filteredAppsPtrs.push_back(&app);
}
}

std::sort(filteredApps.begin(), filteredApps.end(), [&](const DesktopApp &a, const DesktopApp &b) {
int fa = appFrequency[a.exec];
int fb = appFrequency[b.exec];
std::sort(filteredAppsPtrs.begin(), filteredAppsPtrs.end(), [&](const DesktopApp *a, const DesktopApp *b) {
int fa = freqStore.get(a->exec);
int fb = freqStore.get(b->exec);
if (fa == fb) {
return a.name < b.name;
return a->name < b->name;
}
if (fa == 0) return false;
if (fb == 0) return true;
return fa > fb;
});
for (const auto &app : filteredApps) {
list->addRow(new AppRow(list, app));
for (const auto *appPtr : filteredAppsPtrs) {
list->addRow(new AppRow(list, *appPtr));
} });

QObject::connect(input, &QLineEdit::returnPressed, [&]()
Expand All @@ -243,8 +256,8 @@ int main(int argc, char *argv[])
std::cout << "Launching: " << program.toStdString();
for (const auto &arg : args) std::cout << " " << arg.toStdString();
std::cout << std::endl;
appFrequency[appRow->app.exec]++;
json_util::save_freq(freqPath, appFrequency);
freqStore.inc(appRow->app.exec);
freqStore.save();
if (envAssignments.isEmpty()) {
QProcess::startDetached(program, args);
} else {
Expand Down Expand Up @@ -274,5 +287,15 @@ int main(int argc, char *argv[])

window.show();

// Safety: ensure initial rows are populated (some environments/layout timing
// issues can leave the list empty). If list is empty, populate from master.
if (!menuMode && list->listWidget->count() == 0) {
auto &master = appReader.GetAllApps();
size_t count = master.size();
for (size_t i = 0; i < count && i < 64; ++i) {
list->addRow(new AppRow(list, master[i]));
}
}

return app.exec();
}
Loading
Loading