diff --git a/.clang-format b/.clang-format index 36a3128..3c8da1a 100644 --- a/.clang-format +++ b/.clang-format @@ -7,7 +7,7 @@ AllowShortFunctionsOnASingleLine: Inline AllowShortIfStatementsOnASingleLine: "false" AllowShortLambdasOnASingleLine: All AllowShortLoopsOnASingleLine: "false" -AlwaysBreakAfterReturnType: TopLevelDefinitions +AlwaysBreakAfterReturnType: None AlwaysBreakTemplateDeclarations: Yes BinPackArguments: "false" BinPackParameters: "false" @@ -28,7 +28,7 @@ SortIncludes: "true" SpaceAfterCStyleCast: "false" SpaceInEmptyBlock: "false" SpacesBeforeTrailingComments: "2" -SpacesInAngles: "true" -SpacesInParentheses: "true" -SpacesInSquareBrackets: "true" -Standard: c++17 \ No newline at end of file +SpacesInAngles: "false" +SpacesInParentheses: "false" +SpacesInSquareBrackets: "false" +Standard: c++20 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ccbd91..bd67857 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Build and Release ALG App Store +name: Build and Release Explorer on: push: @@ -7,12 +7,34 @@ on: - '**.cpp' - '**.h' - 'CMakeLists.txt' + - 'VERSION' - '.github/workflows/**' pull_request: branches: [ "main", "devel", "qt6" ] workflow_dispatch: jobs: + format: + runs-on: ubuntu-latest + container: + image: archlinux:latest + + steps: + - name: Install clang-format + run: | + pacman -Syu --noconfirm + pacman -S --noconfirm --needed clang git + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Mark git directory as safe + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Check formatting + run: | + find src tests \( -name '*.cpp' -o -name '*.h' \) | xargs clang-format --dry-run --Werror + build: runs-on: ubuntu-latest container: @@ -35,7 +57,9 @@ jobs: pacman \ libarchive \ curl \ - pkgconf + pkgconf \ + spdlog \ + catch2 - name: Checkout code uses: actions/checkout@v4 @@ -45,25 +69,25 @@ jobs: - name: Mark git directory as safe run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: Extract version from CMakeLists.txt + - name: Extract version from VERSION file id: get_version run: | - VERSION=$(grep -oP 'project\(alg-app-store VERSION \K[0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt) + VERSION=$(cat VERSION) echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Detected version: $VERSION" - + - name: Check if this should trigger a release id: check_release run: | - # Only release on main branch when CMakeLists.txt was modified + # Only release on main branch when VERSION was modified if [[ "${{ github.ref }}" == "refs/heads/main" && "${{ github.event_name }}" == "push" ]]; then - # Check if CMakeLists.txt was changed in this push - if git diff --name-only HEAD~1 HEAD | grep -q "CMakeLists.txt"; then + # Check if VERSION was changed in this push + if git diff --name-only HEAD~1 HEAD | grep -q "^VERSION$"; then echo "should_release=true" >> $GITHUB_OUTPUT - echo "Release triggered: CMakeLists.txt changed on main branch" + echo "Release triggered: VERSION changed on main branch" else echo "should_release=false" >> $GITHUB_OUTPUT - echo "No release: CMakeLists.txt not changed" + echo "No release: VERSION not changed" fi else echo "should_release=false" >> $GITHUB_OUTPUT @@ -80,17 +104,22 @@ jobs: run: | cd build make -j$(nproc) - + + - name: Run tests + run: | + cd build + ctest --output-on-failure + - name: Check build artifacts run: | - ls -lh build/alg-app-store - file build/alg-app-store + ls -lh build/explorer + file build/explorer - name: Set artifact name id: artifact run: | # Replace forward slashes with dashes to handle PR refs like "12/merge" - ARTIFACT_NAME="alg-app-store-${{ github.ref_name }}-${{ steps.get_version.outputs.version }}" + ARTIFACT_NAME="explorer-${{ github.ref_name }}-${{ steps.get_version.outputs.version }}" ARTIFACT_NAME="${ARTIFACT_NAME//\//-}" echo "name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT echo "Artifact name: $ARTIFACT_NAME" @@ -99,7 +128,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ steps.artifact.outputs.name }} - path: build/alg-app-store + path: build/explorer retention-days: 30 release: @@ -121,23 +150,23 @@ jobs: - name: Prepare release assets run: | - chmod +x ./release/alg-app-store - tar -czvf alg-app-store-${{ needs.build.outputs.version }}-linux-x86_64.tar.gz -C ./release alg-app-store + chmod +x ./release/explorer + tar -czvf explorer-${{ needs.build.outputs.version }}-linux-x86_64.tar.gz -C ./release explorer - name: Create GitHub Release uses: softprops/action-gh-release@v1 with: tag_name: v${{ needs.build.outputs.version }} - name: ALG App Store v${{ needs.build.outputs.version }} + name: Explorer v${{ needs.build.outputs.version }} body: | - ## ALG App Store v${{ needs.build.outputs.version }} + ## Explorer v${{ needs.build.outputs.version }} A modern package manager GUI for Arch Linux. ### Installation ```bash - tar -xzvf alg-app-store-${{ needs.build.outputs.version }}-linux-x86_64.tar.gz - sudo mv alg-app-store /usr/local/bin/ + tar -xzvf explorer-${{ needs.build.outputs.version }}-linux-x86_64.tar.gz + sudo mv explorer /usr/local/bin/ ``` ### Requirements @@ -145,7 +174,7 @@ jobs: - libalpm (pacman library) - yay or paru (for AUR support) files: | - alg-app-store-${{ needs.build.outputs.version }}-linux-x86_64.tar.gz + explorer-${{ needs.build.outputs.version }}-linux-x86_64.tar.gz draft: false prerelease: false env: diff --git a/.gitignore b/.gitignore index b202a2f..d19caf4 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ Makefile *.pro.user.* # Executable -alg-app-store +explorer # IDE .vscode/ diff --git a/CMakeLists.txt b/CMakeLists.txt index f9f3860..62dc4fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,14 +1,15 @@ cmake_minimum_required(VERSION 3.16) -project(alg-app-store VERSION 0.2.30 LANGUAGES CXX) +file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" PROJECT_VERSION_STRING) -set(CMAKE_CXX_STANDARD 17) +project(explorer VERSION ${PROJECT_VERSION_STRING} LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) -# Find Qt6 packages find_package(Qt6 REQUIRED COMPONENTS Core Gui @@ -17,78 +18,32 @@ find_package(Qt6 REQUIRED COMPONENTS Concurrent ) -# Find libalpm find_package(PkgConfig REQUIRED) pkg_check_modules(ALPM REQUIRED libalpm) -# Include directories -include_directories( - ${CMAKE_SOURCE_DIR}/src - ${ALPM_INCLUDE_DIRS} -) +find_package(spdlog REQUIRED) -# Source files -set(SOURCES - src/main.cpp - - # Core - src/core/alpm_wrapper.cpp - src/core/aur_helper.cpp - src/core/package_manager.cpp - - # GUI - src/gui/mainwindow.cpp - src/gui/home_widget.cpp - src/gui/search_widget.cpp - src/gui/installed_widget.cpp - src/gui/updates_widget.cpp - src/gui/settings_widget.cpp - src/gui/package_card.cpp - src/gui/package_details_dialog.cpp -) +add_subdirectory(src/utils) +add_subdirectory(src/core) +add_subdirectory(src/gui) -# Header files -set(HEADERS - src/utils/logger.h - src/utils/types.h - - # Core - src/core/alpm_wrapper.h - src/core/aur_helper.h - src/core/package_manager.h - - # GUI - src/gui/mainwindow.h - src/gui/home_widget.h - src/gui/search_widget.h - src/gui/installed_widget.h - src/gui/updates_widget.h - src/gui/settings_widget.h - src/gui/package_card.h - src/gui/package_details_dialog.h -) +# Defines BUILD_TESTING (default ON) and calls enable_testing(). +include(CTest) +if(BUILD_TESTING) + find_package(Catch2 3 REQUIRED) + add_subdirectory(tests) +endif() -# Create executable add_executable(${PROJECT_NAME} - ${SOURCES} - ${HEADERS} - resources.qrc + src/main.cpp ) -# Link libraries target_link_libraries(${PROJECT_NAME} PRIVATE - Qt6::Core - Qt6::Gui - Qt6::Widgets - Qt6::Network - Qt6::Concurrent - ${ALPM_LIBRARIES} + gui + core + utils ) -# Link directories -link_directories(${ALPM_LIBRARY_DIRS}) - -# Compiler flags target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra @@ -101,6 +56,11 @@ install(TARGETS ${PROJECT_NAME} ) # Install desktop file -install(FILES assets/alg-app-store.desktop +install(FILES assets/explorer.desktop DESTINATION share/applications ) + +# Install icon +install(FILES assets/explorer.png + DESTINATION share/pixmaps +) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 48ad1ab..c768b8f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Contributions are welcome and appreciated! To contribute: -- Code follows C++17 standards +- Code follows C++20 standards - Proper error handling and logging - Thread safety for concurrent operations - Qt best practices for GUI code @@ -15,7 +15,7 @@ Contributions are welcome and appreciated! To contribute: git checkout -b feature/your-feature-name ``` 3. **Make Your Changes:** - - Follow modern C++17 best practices + - Follow modern C++20 best practices - Use Qt6 APIs and conventions - Ensure code compiles without warnings - Test on your desktop environment (KDE, GNOME, or Xfce) @@ -31,8 +31,8 @@ Contributions are welcome and appreciated! To contribute: ``` ├── assets -│ ├── alg-app-store.desktop -│ └── alg-app-store.png +│ ├── explorer.desktop +│ └── explorer.png ├── build.sh ├── CMakeLists.txt ├── CONTRIBUTING.md @@ -47,7 +47,12 @@ Contributions are welcome and appreciated! To contribute: │ │ ├── aur_helper.cpp │ │ ├── aur_helper.h │ │ ├── package_manager.cpp -│ │ └── package_manager.h +│ │ ├── package_manager.h +│ │ ├── pacman_conf.cpp +│ │ ├── pacman_conf.h +│ │ ├── progress_parser.cpp +│ │ ├── progress_parser.h +│ │ └── CMakeLists.txt │ ├── gui │ │ ├── home_widget.cpp │ │ ├── home_widget.h @@ -64,15 +69,24 @@ Contributions are welcome and appreciated! To contribute: │ │ ├── settings_widget.cpp │ │ ├── settings_widget.h │ │ ├── updates_widget.cpp -│ │ └── updates_widget.h +│ │ ├── updates_widget.h +│ │ └── CMakeLists.txt │ ├── main.cpp │ └── utils -│ ├── logger.h -│ └── types.h +│ ├── logging.cpp +│ ├── logging.h +│ ├── types.h +│ ├── version.h.in +│ └── CMakeLists.txt ├── stylesheet.qss +├── tests +│ ├── test_aur_helper.cpp +│ ├── test_pacman_conf.cpp +│ ├── test_progress_parser.cpp +│ └── CMakeLists.txt └── TODO.md -6 directories, 36 files +7 directories, 40 files ``` ## Understanding the code @@ -97,6 +111,16 @@ Manages package operations with proper privilege escalation: - Automatic helper detection (yay/paru/pacman) - Process management with signals +#### pacman_conf / progress_parser +Pure, dependency-free logic pulled out of GUI classes specifically so it can +be unit tested: `pacman_conf` scans `pacman.conf` text for `[multilib]`/ +`[chaotic-aur]` section state (`SettingsWidget` reads the file and calls +into it); `progress_parser` turns raw pacman/yay/paru output lines into a +`ProgressParseResult` (`PackageDetailsDialog` applies the result to its +widgets). When adding logic to a GUI class, prefer this pattern — a free +function in `core` taking plain data in and returning plain data out — over +burying it in a slot that touches widgets directly, so it stays testable. + ### GUI Components - **MainWindow**: Tabbed interface container - **HomeWidget**: Featured packages display @@ -131,17 +155,66 @@ Used for `AlpmWrapper` and `PackageManager` to ensure: ## Logging -The application includes a comprehensive logging system: +Logging is done with [spdlog](https://github.com/gabime/spdlog), called +directly at each call site: + +```cpp +spdlog::info("Informational message"); +spdlog::warn("Warning message"); +spdlog::error("Error message"); +spdlog::debug("Debug message"); +``` + +Dynamic (non-literal) messages must be passed as a format argument, not as +the format string itself, so content containing `{`/`}` (package output, +JSON, file contents, etc.) can't be misparsed as a format placeholder: ```cpp -Logger::info("Information message"); -Logger::warning("Warning message"); -Logger::error("Error message"); -Logger::debug("Debug message"); +spdlog::info("{}", someQString.toStdString()); ``` +`Log::init(argc, argv)` (in `src/utils/logging.h`) is called at the very +start of `main()`, before `QApplication` is constructed. It parses and +strips verbosity flags from argv and sets the logger's level: + +- No flags: `debug` in dev builds, `info` in Release builds (`NDEBUG`) +- `-v`: `debug` +- `-vv` (or more `v`s): `trace` +- `-D `: explicit `spdlog::level::level_enum` value (`0`=trace .. `6`=off), + overriding `-v` when both are given + Logs are output to standard output and can be redirected for persistent logging. +## Testing + +Unit tests use [Catch2](https://github.com/catchorg/Catch2) (v3, system +package `catch2`) driven through CTest, and live in `tests/`: + +```bash +cd build +ctest --output-on-failure +``` + +**Scope is pure logic only.** `AlpmWrapper` and `PackageManager` are hard +singletons that talk to real `libalpm`/`pkexec`/`/etc/pacman.conf` and +aren't mocked yet — a full ALPM mock backend is planned once the +architecture cleanup gives it a real seam to mock against. Everything +currently covered is a free function that takes plain data (a `QJsonObject`, +a `QString`) and returns plain data, with no Qt Widgets or I/O: + +- `AurHelper::parseAurPackage` — AUR JSON → `PackageInfo` +- `PacmanConf::isMultilibEnabled` / `isChaoticAurEnabled` — `pacman.conf` + section-state parsing +- `parseOperationProgress` — pacman/yay/paru output line → `ProgressParseResult` + +When you add a test-worthy piece of logic to a GUI class, pull it out the +same way (see [pacman_conf / progress_parser](#pacman_conf--progress_parser) +above) rather than writing it inline in a slot. + +`BUILD_TESTING` (from CMake's built-in `CTest` module, default `ON`) gates +the `tests/` subdirectory and the `find_package(Catch2 3 REQUIRED)` call — +pass `-DBUILD_TESTING=OFF` to skip both if you don't have `catch2` installed. + ## Package Helper Detection The application automatically detects available package helpers in this order: diff --git a/LICENSE b/LICENSE index 33100ba..2e74696 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 ALG Team +Copyright (c) 2024-2026 ALG Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..075f84b --- /dev/null +++ b/PLAN.md @@ -0,0 +1,280 @@ + + +# Explorer Development Plan (0.3.x → 0.6.x) + +This supersedes the ad-hoc items in `TODO.md` for anything covered below. `0.2.x` +was the Qt6/C++ rewrite baseline. Starting `0.3.x`, the project is renamed from +**ALG App Store** to **Explorer**, as part of ALG's project-wide push toward +single, memorable app names instead of `alg-`. + +**Repo:** renamed on GitHub → `https://github.com/arch-linux-gui/explorer.git` +(local folder and git remote already updated to match). + +**Language baseline:** C++20 (confirmed — not 23 for now). + +**Branch:** work for this release line happens on `0.3.x`. + +**Note on numbering:** the `0.3.0`/`0.3.1`/... labels below are milestone +*ordering* labels for this plan, not literal targets for the actual +`VERSION` file. `VERSION`'s patch component auto-bumps on every commit via +the pre-commit hook (manually bumped to the `0.3.x` minor line, currently +`0.3.3`), so it will not line up 1:1 with these milestone numbers — treat +the milestone numbers as "do this work in roughly this order," not as a +version to hit. + +--- + +## 0.3.x — Foundation & Rebrand + +Housekeeping release line. No user-facing feature work; the goal is a clean, +testable, properly branded base for the security work in 0.4.x. + +### 0.3.0 — Versioning infrastructure ✅ done +- [x] `VERSION` file at repo root as the single source of truth for the + project version (starts at `0.2.30`, matching the last hardcoded + CMake value) +- [x] `CMakeLists.txt` reads `PROJECT_VERSION_STRING` from `VERSION` via + `file(STRINGS ...)` instead of hardcoding the version — verified with + a real `cmake` configure (`CMAKE_PROJECT_VERSION` resolves to `0.2.30`) +- [x] `scripts/bump-version.sh` — pre-commit hook that auto-bumps the + **patch** component of `VERSION` on every commit; minor/major stay a + deliberate manual edit (hook no-ops if `VERSION` is already staged) +- [x] `scripts/install-hooks.sh` — one-time setup script, symlinks the hook + into `.git/hooks/pre-commit` +- [x] Hook installed locally and confirmed working + +### 0.3.1 — Rename `alg-app-store` → `Explorer` +- [x] Repo renamed on GitHub → `arch-linux-gui/explorer` +- [x] Local folder renamed to `explorer` +- [x] Local git remote (`origin`) updated to new URL +- [x] `CMakeLists.txt`: `project(explorer ...)`, binary target renamed + (verified with a clean configure + build — binary is `explorer`, + `CMAKE_PROJECT_VERSION` resolves to `0.3.3`); also added a missing + `install(FILES assets/explorer.png DESTINATION share/pixmaps)` rule + so the desktop file's `Icon=` actually resolves after install +- [x] `main.cpp`: `setApplicationName`, `setOrganizationName` strings updated +- [x] `mainwindow.cpp`/`mainwindow.h`: window title (`"ALG App Store (Beta)"` + → `"Explorer (Beta)"`), About dialog text, doc comment +- [x] `assets/`: renamed `alg-app-store.desktop` → `explorer.desktop` + (`Name=`, `GenericName=`, `Exec=`, `Icon=` updated); renamed + `alg-app-store.png` → `explorer.png` +- [x] `README.md`, `CONTRIBUTING.md`, `build.sh`: updated all references, + clone URLs, binary paths +- [x] `.github/workflows/release.yml`: artifact naming, job/release names, + release body text updated; also fixed two pre-existing bugs found + while touching this file — the version-extraction regex still + grepped for a literal `project(alg-app-store VERSION x.y.z` in + `CMakeLists.txt`, which broke when 0.3.0 switched to reading + `PROJECT_VERSION_STRING` from the `VERSION` file (now `cat VERSION` + directly); and the release-trigger check still watched + `CMakeLists.txt` for changes instead of `VERSION` (now watches + `VERSION`, and `VERSION` was added to the workflow's trigger `paths` + so a patch-bump-only push isn't silently skipped) +- [x] `.gitignore`: `alg-app-store` → `explorer` ignored-binary entry +- [ ] **External/coordination task (not in this repo):** PKGBUILD lives in + ALG's packaging repo — needs a coordinated rename PR there + (`pkgname=explorer`, with `provides=`/`conflicts=`/`replaces=` against + the old `alg-app-store` package so existing installs upgrade cleanly + instead of ending up with both installed side by side) + +### 0.3.2 — Build system modernization ✅ done +- [x] Bump `CMAKE_CXX_STANDARD` to `20` (verified: `-std=gnu++20` in the + generated compile flags) +- [x] Split into granular CMake, one `CMakeLists.txt` per source folder: + - `src/core/CMakeLists.txt` → builds `core` (static lib): alpm wrapper, + AUR helper, package manager. Pure Qt Core/Network, no Widgets — kept + that way deliberately so it stays a viable seam for the ALPM mock + backend in 0.5.x. Publicly exposes ALPM include/link dirs so `gui` + and the `explorer` target get them transitively. + - `src/gui/CMakeLists.txt` → builds `gui` (static lib), links `core` + + `utils`, owns `resources.qrc` + - `src/utils/CMakeLists.txt` → header-only `INTERFACE` target (logger, + types), links `Qt6::Core`, exposes `src/` as the include root + - `tests/CMakeLists.txt` → still to come in 0.3.5, links `core` only (no + Qt Widgets needed for pure-logic tests) +- [x] Root `CMakeLists.txt` shrunk to: version read, `project()`, C++ + standard + AUTOMOC/AUTORCC/AUTOUIC globals, `find_package` calls + (Qt6, libalpm), `add_subdirectory()` calls, the thin `explorer` + executable target (just `main.cpp`, linking `gui`/`core`/`utils`), + install rules +- [x] Verified with a full clean configure + build (`explorer` binary + links, runs libalpm at version 16.0.1 via `ldd`) +- [x] Fixed stale `C++17` mentions accompanying the bump: `README.md`, + `CONTRIBUTING.md` (contribution guidelines + project structure tree, + now showing the per-folder `CMakeLists.txt` files), and the About + dialog text in `mainwindow.cpp` +- [x] **Regression found post-merge (caught by manual testing, not the + build):** moving `resources.qrc` into the static `gui` library broke + QSS loading at runtime — all four stylesheet modules failed silently + (`"Could not load style module: ..."` warnings, app ran unstyled). + Root cause: a static library's Qt resource initializer only gets + linked into the final binary if something references it; nothing + did. Fixed with `Q_INIT_RESOURCE(resources);` in `main.cpp` right + after `QApplication` construction. Verified headless + (`QT_QPA_PLATFORM=offscreen`) that stylesheets now load successfully. + +### 0.3.3 — Logging overhaul ✅ done +- [x] Replaced `src/utils/logger.h` (qDebug wrapper) with spdlog + (`find_package(spdlog REQUIRED)`, linked into `utils`); `utils` is now + a real static library (`logging.cpp`/`logging.h`) instead of a + header-only `INTERFACE` target, since it needs compiled CLI-parsing + logic +- [x] `src/utils/version.h.in`'s generated header and the new logging setup + both live behind `utils`, keeping `main.cpp`/`gui` decoupled from the + details +- [x] `Log::init(argc, argv)` called at the top of `main()`, before + `QApplication` construction: parses and strips `-v`/`-vv`(+)/`-D ` + from argv so Qt never sees them + - No flags: `debug` in dev builds, `info` in Release builds (`NDEBUG`) + - `-v` → `debug`, `-vv` (or more `v`s) → `trace` + - `-D ` → explicit `spdlog::level::level_enum` (0=trace..6=off), + overrides `-v` when both given + - Verified with actual dev + `-DCMAKE_BUILD_TYPE=Release` builds: default + Release run shows only info+ lines, `-v` on that same Release binary + correctly surfaces `debug` lines +- [x] Retired the `Logger::` namespace entirely; all 109 call sites across + `core`+`gui`+`main.cpp` now call `spdlog::info/warn/error/debug` + directly. Plain string-literal messages pass through unchanged; + dynamic messages (anything built from a `QString`) are passed as a + format *argument* — `spdlog::info("{}", msg.toStdString())` — never + as the format string itself, so arbitrary content (pacman/AUR output, + JSON, file contents) containing literal `{`/`}` can't be misparsed as + a fmt placeholder and crash the logger +- [x] `CONTRIBUTING.md`: rewrote the "Logging" section and project + structure tree to match (spdlog usage + the `-v`/`-D` flags, + `logging.{h,cpp}` instead of `logger.h`) + +### 0.3.4 — clang-format enforcement +- [ ] Finalize `.clang-format` ruleset +- [ ] One isolated repo-wide reformat commit (formatting only, no logic + changes, easy to review/skip in blame) +- [ ] CI job: `clang-format --dry-run --Werror` across `src/`, gating PRs + +### 0.3.5 — Test infrastructure +- [ ] Catch2 as the test framework, driven through CTest + (`add_test()` + `ctest` in CI — Catch2 registers tests, CTest runs them) +- [ ] **Scope for this release: pure logic only.** `AlpmWrapper` and + `PackageManager` remain hard singletons touching real `libalpm` / + `pkexec` / `/etc/pacman.conf` — not mocked yet. Extract and cover what's + already (or easily made) pure: + - AUR JSON → `PackageInfo` parsing (`AurHelper::parseAurPackage`) + - `pacman.conf` section parsing (repo detection, multilib/chaotic-aur + enabled checks) — extract into free functions taking a `QString`/stream + so tests can feed fixture text instead of hitting the real file + - Update-diff / version-compare call sites + - Progress-output regex parsing (`PackageDetailsDialog::parseProgressOutput`) +- [ ] Full ALPM mock backend (so `AlpmWrapper`'s own logic is testable) is + **explicitly deferred to 0.5.x**, alongside the architecture cleanup + that gives it a real seam to mock against +- [ ] CI job running `ctest` + +### 0.3.6 — std::jthread / RAII pass +- [ ] Not a blanket rewrite — the codebase has no raw `std::thread` today + (it's `QtConcurrent::run` + Qt signals throughout). Target the two + spots that actually need cooperative cancellation: + - `PackageManager::cancelRunningOperation()` — currently a manual + `pkill`/`terminate`/`waitForFinished` sequence; candidate for + `jthread` + `stop_token` + - AUR update-polling loop (`AurHelper::checkAurUpdates`) — also touched + in 0.5.x when it's batched into a single RPC call, so sequence these + together + +--- + +## 0.4.x — Security & Trust hardening + +Everything here is either a real vulnerability in the current code or an +Arch-specific correctness risk. Concentrated in one release line per your +call, ahead of the architecture/feature work. + +### 0.4.0 — Fix `pkexec` command injection +- [ ] `PackageManager::installPackage/updatePackage/updateAllPackages` + currently build `sh -c "pkexec %1 -S %2 --noconfirm"` via + `QString::arg()` with package names sourced from AUR JSON / search + results — a shell-metacharacter-laced name is exploitable +- [ ] Replace with direct argv `QProcess::start("pkexec", {"pacman", "-S", + packageName, "--noconfirm"})` (no `sh -c` wrapper) everywhere + +### 0.4.1 — Fix pacman.conf tmpfile TOCTOU +- [ ] `SettingsWidget`'s multilib/chaotic-aur toggles write to a fixed, + predictable `/tmp/pacman.conf.tmp` before `pkexec cp` — symlink/race + risk. Replace with `QTemporaryFile` +- [ ] Move all `pacman.conf` parse/rewrite logic out of `SettingsWidget` and + into a new core `RepoConfigManager` (also unblocks proper unit testing + of this logic per 0.3.5/0.5.x) + +### 0.4.2 — Safer uninstall default +- [ ] Default single-package uninstall: `pacman -Rdd` → `pacman -Rs` + (dependency-aware removal) +- [ ] Keep `-Rdd` (skip dependency checks) available as an explicit, + clearly-labeled "force remove" advanced action, not the default path + +### 0.4.3 — Partial-upgrade guardrail +- [ ] Detect when the local package DB is stale relative to sync DBs before + allowing a single-package install/update +- [ ] Warn the user about partial-upgrade risk (a well-known way to break an + Arch system) and steer toward a full `-Syu` when appropriate + +### 0.4.4 — PKGBUILD visibility for AUR packages +- [ ] Fetch and display PKGBUILD / `.SRCINFO` in the package details dialog + before an AUR install, so users can review the build script they're + about to run + +### 0.4.5 — Real transaction preview (stretch) +- [ ] Use `alpm_trans_*` to compute and show actual dependency/conflict + resolution before confirming an install, instead of trusting raw + `pacman -S` output after the fact + +--- + +## 0.5.x — Architecture cleanup + +- [ ] Centralize ALPM release+reinitialize-after-mutation (currently + duplicated across `UpdatesWidget` and `PackageDetailsDialog`, both + success and error paths — 4 copies) +- [ ] Dedupe `formatSize()` (duplicated in `UpdatesWidget` and its nested + `UpdateItem`) +- [ ] Extract a shared `PackageGridView` widget — the row/col grid-packing + loop is triplicated across `HomeWidget`, `SearchWidget`, + `InstalledWidget` +- [ ] `std::optional` instead of returning an empty struct as a + failure sentinel +- [ ] Repository enum/normalization instead of scattered + `"aur"`/`"AUR"`/`"chaotic-aur"` string comparisons with inconsistent + casing +- [ ] Remove dead `PackageManager::Helper::Paru` code (currently commented + out and unreachable) or properly re-enable it +- [ ] Replace nested blocking `QEventLoop` calls (`HomeWidget`/`SearchWidget` + package-click → AUR detail fetch) with proper async signal + continuations +- [ ] Batch AUR update-checking into a single multi-`arg[]` RPC call instead + of N sequential blocking calls (pairs with the 0.3.6 jthread work) +- [ ] Introduce the full ALPM mock-backend seam deferred from 0.3.5, backfill + unit test coverage for `AlpmWrapper`/`PackageManager` logic + +--- + +## 0.6.x — Feature round + +- [ ] Single sudo prompt at startup (`startup_auth`, carried over from the + old `TODO.md`) +- [ ] Own lightweight AUR helper in core, reducing the hard dependency on + yay/paru +- [ ] Mirrorlist management tab +- [ ] Light/dark theme toggle, or follow system theme +- [ ] Settings page UX pass +- [ ] Package categories/tags +- [ ] Transaction history / persistent operation log +- [ ] Dependency visualization + +--- + +## Decisions log + +| # | Question | Decision | +|---|----------|----------| +| 1 | C++20 vs C++23 baseline | **C++20** | +| 2 | Rename scope | **Full**, including the GitHub repo itself — done, new URL is `https://github.com/arch-linux-gui/explorer.git` | +| 3 | 0.3.5 test scope | **Pure logic gets unit tests now; full ALPM mock backend lands in 0.5.x** | +| 4 | Versioning hook semantics | Confirmed: `bump-version.sh` bumps the **patch** component only, minor/major stay manual | diff --git a/README.md b/README.md index 3fe1751..1d7d13d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# ALG App Store - Qt6/C++ Version +# Explorer - ALG App Store and GUI package management tool -A modern, native package manager for Arch Linux built with Qt6 and C++17. This is a complete rewrite of the original Wails-based application. +A modern, native package manager for Arch Linux built with Qt6 and C++20. This is a complete rewrite of the original Wails-based application (formerly known as ALG App Store). It is a GUI frontend to libalpm. ## Features @@ -11,11 +11,11 @@ A modern, native package manager for Arch Linux built with Qt6 and C++17. This i - **Modern UI**: Clean, dark-themed interface with responsive design - **Smart Helper Detection**: Automatically detects and uses yay, paru, or falls back to pacman - **Thread-Safe**: Uses modern C++ threading features for safe concurrent operations -- **Comprehensive Logging**: Built-in logger for debugging and monitoring +- **Comprehensive Logging**: [spdlog](https://github.com/gabime/spdlog)-backed logging with configurable verbosity (see [Logging](#logging) below) ## Technology Stack -- **Language**: C++17 +- **Language**: C++20 - **GUI Framework**: Qt6 (Widgets) - **Package Management**: libalpm (Arch Linux Package Manager library) - **AUR Integration**: AUR RPC API via Qt Network + Chaotic AUR Support @@ -27,17 +27,19 @@ A modern, native package manager for Arch Linux built with Qt6 and C++17. This i ### Build Dependencies ```bash -sudo pacman -S base-devel cmake qt6-base qt6-svg alpm pkgconf +sudo pacman -S base-devel cmake qt6-base qt6-svg alpm pkgconf spdlog catch2 ``` You can optionally also have either either yay or paru if you would like to work with packages from the AUR. +`catch2` is only needed to build the test suite (see [Testing](#testing) below); pass `-DBUILD_TESTING=OFF` to `cmake` to skip it. + ## Building 1. Clone the repository: ```bash -git clone https://github.com/arch-linux-gui/alg-app-store.git -cd alg-app-store +git clone https://github.com/arch-linux-gui/explorer.git +cd explorer ``` 2. Run Build Script @@ -53,15 +55,58 @@ Binary will be in the build directory. ### From Build Directory ```bash -./build/alg-app-store +./build/explorer ``` ### From System Installation (if installed) ```bash -alg-app-store +explorer +``` + +## Logging + +Explorer logs via [spdlog](https://github.com/gabime/spdlog) to standard +output. By default it logs at `debug` level in a development build and +`info` level in a Release build (`-DCMAKE_BUILD_TYPE=Release`). + +Verbosity can be raised with `-v` flags or set explicitly with `-D `: + +| Flag | Level | +|------------|--------------| +| *(none)* | `debug` (dev build) / `info` (Release build) | +| `-v` | `debug` | +| `-vv` | `trace` | +| `-D ` | explicit level by number — `0`=trace, `1`=debug, `2`=info, `3`=warn, `4`=err, `5`=critical, `6`=off | + +`-D ` takes precedence over `-v` if both are given. Examples: + +```bash +# Default verbosity +./build/explorer + +# Debug-level logging +./build/explorer -v + +# Most verbose (trace) +./build/explorer -vv + +# Explicit level: warnings and above only +./build/explorer -D 3 ``` +## Testing + +Explorer has a [Catch2](https://github.com/catchorg/Catch2)-based unit test +suite, run through CTest. Coverage is currently pure logic only — AUR JSON +parsing, `pacman.conf` section parsing, and pacman/yay/paru progress-output +parsing. `AlpmWrapper`/`PackageManager` still talk to real `libalpm`/`pkexec` +and aren't covered yet. + +```bash +cd build +ctest --output-on-failure +``` ## License @@ -71,13 +116,13 @@ It is distributed under the MIT License. Check LICENSE. ## Credits - **Author**: DemonKiller -- **Original Project**: Wails-based ALG App Store +- **Original Project**: Wails-based ALG App Store (predecessor of Explorer) - **Rewrite**: Qt6/C++ implementation - **Community**: Arch Linux and Qt communities ## Contact For issues, questions, or contributions, please visit: -- GitHub: https://github.com/arch-linux-gui/alg-app-store +- GitHub: https://github.com/arch-linux-gui/explorer - Website: https://arkalinuxgui.org - Discord: https://discord.com/invite/NgAFEw9Tkf \ No newline at end of file diff --git a/TODO.md b/TODO.md index 6998488..ea7c440 100644 --- a/TODO.md +++ b/TODO.md @@ -9,7 +9,7 @@ - [] Clean up UI; make UI look more modern (check gnome's styling options) - style_and_theme - [] Set a light/dark theme toggle, or follow system's theme - style_and_theme - [] Look into spdlog for logging -- [] Look into CppUTest or Google Test (gtest) for test +- [] Look into Catch2 and Ctest ## Future Enhancements diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..e23fb32 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.3.26 diff --git a/assets/alg-app-store.desktop b/assets/explorer.desktop similarity index 66% rename from assets/alg-app-store.desktop rename to assets/explorer.desktop index a348a07..a7ffee1 100644 --- a/assets/alg-app-store.desktop +++ b/assets/explorer.desktop @@ -1,13 +1,13 @@ [Desktop Entry] Type=Application Version=1.0 -Name=App Store -GenericName=ALG App Store +Name=Explorer +GenericName=App Store Keywords=app;store;software;install;system; Encoding=UTF-8 Terminal=false -Exec=alg-app-store -Icon=/usr/share/pixmaps/alg-app-store.png +Exec=explorer +Icon=/usr/share/pixmaps/explorer.png Comment=Install all your favourite apps Categories=System;Apps; StartupNotify=true diff --git a/assets/alg-app-store.png b/assets/explorer.png similarity index 100% rename from assets/alg-app-store.png rename to assets/explorer.png diff --git a/build.sh b/build.sh index 34e8e84..51754cf 100755 --- a/build.sh +++ b/build.sh @@ -1,11 +1,11 @@ #!/bin/bash -# Build script for ALG App Store Qt6 version +# Build script for Explorer Qt6 version set -e echo "===================================" -echo "ALG App Store - Qt6 Build Script" +echo "Explorer - Qt6 Build Script" echo "===================================" echo "" @@ -57,7 +57,7 @@ echo "Build completed successfully!" echo "===================================" echo "" echo "To run the application:" -echo " ./build/alg-app-store" +echo " ./build/explorer" echo "" echo "To install system-wide:" echo " sudo make install (from build directory)" diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 0000000..aba37ca --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Pre-commit hook: auto-bump the patch component of VERSION on every commit. +# Minor/major bumps stay a deliberate manual edit — if VERSION is already +# staged (someone edited it themselves), this script leaves it alone. +set -euo pipefail + +repo_root="$(git rev-parse --show-toplevel)" +version_file="$repo_root/VERSION" + +if git diff --cached --name-only | grep -qx "VERSION"; then + exit 0 +fi + +current="$(tr -d '[:space:]' < "$version_file")" +IFS='.' read -r major minor patch <<< "$current" + +if [[ -z "${major:-}" || -z "${minor:-}" || -z "${patch:-}" ]]; then + echo "bump-version: could not parse VERSION file ('$current'), skipping auto-bump" >&2 + exit 0 +fi + +patch=$((patch + 1)) +new="${major}.${minor}.${patch}" + +echo "$new" > "$version_file" +git add "$version_file" + +echo "bump-version: $current -> $new" \ No newline at end of file diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh new file mode 100644 index 0000000..fab3834 --- /dev/null +++ b/scripts/install-hooks.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Run once after cloning to install the repo's git hooks (currently just the +# VERSION patch-bump pre-commit hook). +set -euo pipefail + +repo_root="$(git rev-parse --show-toplevel)" +hooks_dir="$repo_root/.git/hooks" + +chmod +x "$repo_root/scripts/bump-version.sh" +ln -sf "../../scripts/bump-version.sh" "$hooks_dir/pre-commit" + +echo "Installed pre-commit hook -> scripts/bump-version.sh" \ No newline at end of file diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 0000000..d9c843a --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1,32 @@ +# Core logic: libalpm wrapper, AUR client, package operations. No Qt Widgets +# dependency — kept pure Qt Core/Network so it stays usable headlessly and +# stays a viable seam for a future ALPM mock backend (see PLAN.md 0.5.x). +add_library(core STATIC + alpm_wrapper.cpp + alpm_wrapper.h + aur_helper.cpp + aur_helper.h + package_manager.cpp + package_manager.h + pacman_conf.cpp + pacman_conf.h + progress_parser.cpp + progress_parser.h +) + +target_include_directories(core PUBLIC ${ALPM_INCLUDE_DIRS}) +target_link_directories(core PUBLIC ${ALPM_LIBRARY_DIRS}) + +target_link_libraries(core + PUBLIC + Qt6::Core + Qt6::Network + ${ALPM_LIBRARIES} + utils +) + +target_compile_options(core PRIVATE + -Wall + -Wextra + -Wpedantic +) diff --git a/src/core/alpm_wrapper.cpp b/src/core/alpm_wrapper.cpp index 3fc92b7..6d2b491 100644 --- a/src/core/alpm_wrapper.cpp +++ b/src/core/alpm_wrapper.cpp @@ -1,102 +1,119 @@ #include "alpm_wrapper.h" -#include "../utils/logger.h" +#include "../utils/logging.h" #include #include #include #include -AlpmWrapper& AlpmWrapper::instance() { +AlpmWrapper& AlpmWrapper::instance() +{ static AlpmWrapper instance; return instance; } -AlpmWrapper::AlpmWrapper() { +AlpmWrapper::AlpmWrapper() +{ // Member initialization is done in header file } -AlpmWrapper::~AlpmWrapper() { +AlpmWrapper::~AlpmWrapper() +{ release(); } -bool AlpmWrapper::initialize() { +bool AlpmWrapper::initialize() +{ std::lock_guard lock(m_mutex); - - if (m_initialized) { + + if (m_initialized) + { return true; } - + alpm_errno_t err; m_handle = alpm_initialize("/", "/var/lib/pacman", &err); - - if (!m_handle) { - Logger::error(QString("Failed to initialize ALPM: %1") - .arg(alpm_strerror(err))); + + if (!m_handle) + { + spdlog::error("{}", (QString("Failed to initialize ALPM: %1").arg(alpm_strerror(err))).toStdString()); return false; } - + // Register sync databases - read from enabled repositories QStringList repos = getEnabledRepositories(); - for (const auto& repo : repos) { - alpm_db_t* db = alpm_register_syncdb(m_handle, - repo.toStdString().c_str(), - ALPM_SIG_USE_DEFAULT); - if (!db) { - Logger::warning(QString("Failed to register sync db: %1").arg(repo)); - } else { - Logger::info(QString("Registered sync db: %1").arg(repo)); + for (const auto& repo : repos) + { + alpm_db_t* db = alpm_register_syncdb(m_handle, repo.toStdString().c_str(), ALPM_SIG_USE_DEFAULT); + if (!db) + { + spdlog::warn("{}", (QString("Failed to register sync db: %1").arg(repo)).toStdString()); + } + else + { + spdlog::info("{}", (QString("Registered sync db: %1").arg(repo)).toStdString()); } } - + m_syncDbs = alpm_get_syncdbs(m_handle); m_initialized = true; - - Logger::info("ALPM initialized successfully"); + + spdlog::info("ALPM initialized successfully"); return true; } -void AlpmWrapper::release() { +void AlpmWrapper::release() +{ std::lock_guard lock(m_mutex); - - if (m_handle) { + + if (m_handle) + { alpm_release(m_handle); m_handle = nullptr; m_syncDbs = nullptr; m_initialized = false; - Logger::info("ALPM released"); + spdlog::info("ALPM released"); } } -QVector AlpmWrapper::searchPackages(const QString& query) { +QVector AlpmWrapper::searchPackages(const QString& query) +{ std::lock_guard lock(m_mutex); - - if (!m_initialized) { - Logger::error("ALPM not initialized"); - return {}; + + if (!m_initialized) + { + spdlog::error("ALPM not initialized"); + return { }; } - + QVector results; - + // Search in sync databases - for (alpm_list_t* i = m_syncDbs; i; i = i->next) { + for (alpm_list_t* i = m_syncDbs; i; i = i->next) + { auto* db = static_cast(i->data); searchInDatabase(db, query, results); } - + return results; } -void AlpmWrapper::searchInDatabase(alpm_db_t* db, const QString& query, - QVector& results) { - if (!db) return; - +void AlpmWrapper::searchInDatabase(alpm_db_t* db, const QString& query, QVector& results) +{ + if (!db) + { + return; + } + alpm_list_t* pkgs = alpm_db_get_pkgcache(db); QString lowerQuery = query.toLower(); - - for (alpm_list_t* i = pkgs; i; i = i->next) { + + for (alpm_list_t* i = pkgs; i; i = i->next) + { auto* pkg = static_cast(i->data); QString pkgName = QString::fromUtf8(alpm_pkg_get_name(pkg)); - - if (pkgName.toLower().contains(lowerQuery)) { + + if (pkgName.toLower().contains(lowerQuery)) + { PackageInfo info; info.name = pkgName; info.version = QString::fromUtf8(alpm_pkg_get_version(pkg)); @@ -105,36 +122,40 @@ void AlpmWrapper::searchInDatabase(alpm_db_t* db, const QString& query, info.maintainer = QString::fromUtf8(alpm_pkg_get_packager(pkg)); info.upstreamUrl = QString::fromUtf8(alpm_pkg_get_url(pkg)); info.dependList = convertDependList(alpm_pkg_get_depends(pkg)); - + alpm_time_t buildDate = alpm_pkg_get_builddate(pkg); info.lastUpdated = QDateTime::fromSecsSinceEpoch(buildDate); - + results.push_back(std::move(info)); } } } -QVector AlpmWrapper::getInstalledPackages() { +QVector AlpmWrapper::getInstalledPackages() +{ std::lock_guard lock(m_mutex); - - if (!m_initialized) { - Logger::error("ALPM not initialized"); - return {}; + + if (!m_initialized) + { + spdlog::error("ALPM not initialized"); + return { }; } - + QVector packages; alpm_db_t* localDb = alpm_get_localdb(m_handle); - - if (!localDb) { - Logger::error("Failed to get local database"); - return {}; + + if (!localDb) + { + spdlog::error("Failed to get local database"); + return { }; } - + alpm_list_t* pkgs = alpm_db_get_pkgcache(localDb); - - for (alpm_list_t* i = pkgs; i; i = i->next) { + + for (alpm_list_t* i = pkgs; i; i = i->next) + { auto* pkg = static_cast(i->data); - + PackageInfo info; info.name = QString::fromUtf8(alpm_pkg_get_name(pkg)); info.version = QString::fromUtf8(alpm_pkg_get_version(pkg)); @@ -143,46 +164,53 @@ QVector AlpmWrapper::getInstalledPackages() { info.maintainer = QString::fromUtf8(alpm_pkg_get_packager(pkg)); info.upstreamUrl = QString::fromUtf8(alpm_pkg_get_url(pkg)); info.dependList = convertDependList(alpm_pkg_get_depends(pkg)); - + alpm_time_t buildDate = alpm_pkg_get_builddate(pkg); info.lastUpdated = QDateTime::fromSecsSinceEpoch(buildDate); - + packages.push_back(std::move(info)); } - - Logger::info(QString("Found %1 installed packages").arg(packages.size())); + + spdlog::info("{}", (QString("Found %1 installed packages").arg(packages.size())).toStdString()); return packages; } -bool AlpmWrapper::isPackageInstalled(const QString& packageName) { +bool AlpmWrapper::isPackageInstalled(const QString& packageName) +{ std::lock_guard lock(m_mutex); - - if (!m_initialized) { + + if (!m_initialized) + { return false; } - + alpm_db_t* localDb = alpm_get_localdb(m_handle); - if (!localDb) { + if (!localDb) + { return false; } - + alpm_pkg_t* pkg = alpm_db_get_pkg(localDb, packageName.toStdString().c_str()); return pkg != nullptr; } -PackageInfo AlpmWrapper::getPackageInfo(const QString& packageName) { +PackageInfo AlpmWrapper::getPackageInfo(const QString& packageName) +{ std::lock_guard lock(m_mutex); - + PackageInfo info; - if (!m_initialized) { + if (!m_initialized) + { return info; } - + // First check local database alpm_db_t* localDb = alpm_get_localdb(m_handle); - if (localDb) { + if (localDb) + { alpm_pkg_t* pkg = alpm_db_get_pkg(localDb, packageName.toStdString().c_str()); - if (pkg) { + if (pkg) + { info.name = QString::fromUtf8(alpm_pkg_get_name(pkg)); info.version = QString::fromUtf8(alpm_pkg_get_version(pkg)); info.description = QString::fromUtf8(alpm_pkg_get_desc(pkg)); @@ -190,20 +218,22 @@ PackageInfo AlpmWrapper::getPackageInfo(const QString& packageName) { info.maintainer = QString::fromUtf8(alpm_pkg_get_packager(pkg)); info.upstreamUrl = QString::fromUtf8(alpm_pkg_get_url(pkg)); info.dependList = convertDependList(alpm_pkg_get_depends(pkg)); - + alpm_time_t buildDate = alpm_pkg_get_builddate(pkg); info.lastUpdated = QDateTime::fromSecsSinceEpoch(buildDate); - + return info; } } - + // Check sync databases - for (alpm_list_t* i = m_syncDbs; i; i = i->next) { + for (alpm_list_t* i = m_syncDbs; i; i = i->next) + { auto* db = static_cast(i->data); alpm_pkg_t* pkg = alpm_db_get_pkg(db, packageName.toStdString().c_str()); - - if (pkg) { + + if (pkg) + { info.name = QString::fromUtf8(alpm_pkg_get_name(pkg)); info.version = QString::fromUtf8(alpm_pkg_get_version(pkg)); info.description = QString::fromUtf8(alpm_pkg_get_desc(pkg)); @@ -211,138 +241,156 @@ PackageInfo AlpmWrapper::getPackageInfo(const QString& packageName) { info.maintainer = QString::fromUtf8(alpm_pkg_get_packager(pkg)); info.upstreamUrl = QString::fromUtf8(alpm_pkg_get_url(pkg)); info.dependList = convertDependList(alpm_pkg_get_depends(pkg)); - + alpm_time_t buildDate = alpm_pkg_get_builddate(pkg); info.lastUpdated = QDateTime::fromSecsSinceEpoch(buildDate); - + return info; } } - + return info; } -QVector AlpmWrapper::getAvailableUpdates() { +QVector AlpmWrapper::getAvailableUpdates() +{ std::lock_guard lock(m_mutex); - + QVector updates; - - if (!m_initialized) { - Logger::error("ALPM not initialized"); + + if (!m_initialized) + { + spdlog::error("ALPM not initialized"); return updates; } - + alpm_db_t* localDb = alpm_get_localdb(m_handle); - if (!localDb) { + if (!localDb) + { return updates; } - + alpm_list_t* pkgs = alpm_db_get_pkgcache(localDb); - - for (alpm_list_t* i = pkgs; i; i = i->next) { + + for (alpm_list_t* i = pkgs; i; i = i->next) + { auto* localPkg = static_cast(i->data); const char* pkgName = alpm_pkg_get_name(localPkg); - + // Check each sync database for newer version - for (alpm_list_t* j = m_syncDbs; j; j = j->next) { + for (alpm_list_t* j = m_syncDbs; j; j = j->next) + { auto* syncDb = static_cast(j->data); alpm_pkg_t* syncPkg = alpm_db_get_pkg(syncDb, pkgName); - - if (syncPkg) { - int cmp = alpm_pkg_vercmp(alpm_pkg_get_version(syncPkg), - alpm_pkg_get_version(localPkg)); - - if (cmp > 0) { + + if (syncPkg) + { + int cmp = alpm_pkg_vercmp(alpm_pkg_get_version(syncPkg), alpm_pkg_get_version(localPkg)); + + if (cmp > 0) + { UpdateInfo update; update.name = QString::fromUtf8(pkgName); update.oldVersion = QString::fromUtf8(alpm_pkg_get_version(localPkg)); update.newVersion = QString::fromUtf8(alpm_pkg_get_version(syncPkg)); update.repository = QString::fromUtf8(alpm_db_get_name(syncDb)); update.downloadSize = alpm_pkg_get_size(syncPkg); - + updates.push_back(std::move(update)); break; } } } } - - Logger::info(QString("Found %1 available updates").arg(updates.size())); + + spdlog::info("{}", (QString("Found %1 available updates").arg(updates.size())).toStdString()); return updates; } -QStringList AlpmWrapper::convertDependList(alpm_list_t* deps) { +QStringList AlpmWrapper::convertDependList(alpm_list_t* deps) +{ QStringList result; - - for (alpm_list_t* i = deps; i; i = i->next) { + + for (alpm_list_t* i = deps; i; i = i->next) + { auto* dep = static_cast(i->data); result.append(QString::fromUtf8(dep->name)); } - + return result; } -QStringList AlpmWrapper::getEnabledRepositories() const { +QStringList AlpmWrapper::getEnabledRepositories() const +{ QStringList repos; - + // Read /etc/pacman.conf to find enabled repositories QFile file("/etc/pacman.conf"); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - Logger::error("Failed to open /etc/pacman.conf"); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + spdlog::error("Failed to open /etc/pacman.conf"); // Return default repositories - return {"core", "extra"}; + return { "core", "extra" }; } - + QTextStream in(&file); - while (!in.atEnd()) { + while (!in.atEnd()) + { QString line = in.readLine().trimmed(); - + // Check for repository sections (not commented out) - if (line.startsWith("[") && line.endsWith("]") && !line.startsWith("#")) { + if (line.startsWith("[") && line.endsWith("]") && !line.startsWith("#")) + { QString repo = line.mid(1, line.length() - 2); - + // Filter out non-repository sections - if (repo != "options" && repo != "testing" && repo != "core-testing" && - repo != "extra-testing" && repo != "multilib-testing") { + if (repo != "options" && repo != "testing" && repo != "core-testing" && repo != "extra-testing" + && repo != "multilib-testing") + { repos.append(repo); } } } - + file.close(); - + // Ensure core and extra are always present - if (!repos.contains("core")) { + if (!repos.contains("core")) + { repos.prepend("core"); } - if (!repos.contains("extra")) { + if (!repos.contains("extra")) + { repos.insert(1, "extra"); } - - Logger::info(QString("Enabled repositories: %1").arg(repos.join(", "))); + + spdlog::info("{}", (QString("Enabled repositories: %1").arg(repos.join(", "))).toStdString()); return repos; } -void AlpmWrapper::refreshDatabases() { +void AlpmWrapper::refreshDatabases() +{ std::lock_guard lock(m_mutex); - - if (!m_initialized) { - Logger::error("ALPM not initialized"); + + if (!m_initialized) + { + spdlog::error("ALPM not initialized"); return; } - + // Release current handle - if (m_handle) { + if (m_handle) + { alpm_release(m_handle); m_handle = nullptr; m_syncDbs = nullptr; m_initialized = false; } - + // Re-initialize to pick up new repositories m_mutex.unlock(); initialize(); m_mutex.lock(); - - Logger::info("ALPM databases refreshed"); + + spdlog::info("ALPM databases refreshed"); } diff --git a/src/core/alpm_wrapper.h b/src/core/alpm_wrapper.h index b1f8906..15d2d9c 100644 --- a/src/core/alpm_wrapper.h +++ b/src/core/alpm_wrapper.h @@ -1,13 +1,13 @@ #ifndef ALPM_WRAPPER_H #define ALPM_WRAPPER_H -#include +#include "../utils/types.h" #include #include #include +#include #include #include -#include "../utils/types.h" /** * @brief Singleton wrapper for libalpm (Arch Linux Package Manager library). @@ -20,40 +20,40 @@ * Note: libalpm uses C-style memory management, so smart pointers are not * directly applicable to the alpm types. */ -class AlpmWrapper { +class AlpmWrapper +{ public: static AlpmWrapper& instance(); - + ~AlpmWrapper(); - + // Disable copy and move AlpmWrapper(const AlpmWrapper&) = delete; AlpmWrapper& operator=(const AlpmWrapper&) = delete; AlpmWrapper(AlpmWrapper&&) = delete; AlpmWrapper& operator=(AlpmWrapper&&) = delete; - + bool initialize(); void release(); void refreshDatabases(); - + QVector searchPackages(const QString& query); QVector getInstalledPackages(); bool isPackageInstalled(const QString& packageName); PackageInfo getPackageInfo(const QString& packageName); QVector getAvailableUpdates(); - + private: AlpmWrapper(); - + alpm_handle_t* m_handle = nullptr; alpm_list_t* m_syncDbs = nullptr; std::mutex m_mutex; bool m_initialized = false; - + QStringList convertDependList(alpm_list_t* deps); - void searchInDatabase(alpm_db_t* db, const QString& query, - QVector& results); + void searchInDatabase(alpm_db_t* db, const QString& query, QVector& results); QStringList getEnabledRepositories() const; }; -#endif // ALPM_WRAPPER_H +#endif // ALPM_WRAPPER_H diff --git a/src/core/aur_helper.cpp b/src/core/aur_helper.cpp index bdce2a3..507c87b 100644 --- a/src/core/aur_helper.cpp +++ b/src/core/aur_helper.cpp @@ -1,119 +1,137 @@ #include "aur_helper.h" -#include "../utils/logger.h" -#include -#include +#include "../utils/logging.h" +#include +#include #include +#include #include -#include +#include #include -#include -#include +#include AurHelper::AurHelper(QObject* parent) : QObject(parent) - , m_networkManager(std::make_unique(this)) { + , m_networkManager(std::make_unique(this)) +{ } AurHelper::~AurHelper() = default; -void AurHelper::searchPackages(const QString& query) { - Logger::debug(QString("Searching AUR for: %1").arg(query)); - +void AurHelper::searchPackages(const QString& query) +{ + spdlog::debug("{}", (QString("Searching AUR for: %1").arg(query)).toStdString()); + QUrl url("https://aur.archlinux.org/rpc/"); QUrlQuery urlQuery; urlQuery.addQueryItem("v", "5"); urlQuery.addQueryItem("type", "search"); urlQuery.addQueryItem("arg", query); url.setQuery(urlQuery); - + QNetworkRequest request(url); auto* reply = m_networkManager->get(request); - + connect(reply, &QNetworkReply::finished, this, &AurHelper::onSearchFinished); } -void AurHelper::onSearchFinished() { +void AurHelper::onSearchFinished() +{ auto* reply = qobject_cast(sender()); - if (!reply) return; - + if (!reply) + { + return; + } + reply->deleteLater(); - - if (reply->error() != QNetworkReply::NoError) { - Logger::error(QString("AUR search error: %1").arg(reply->errorString())); + + if (reply->error() != QNetworkReply::NoError) + { + spdlog::error("{}", (QString("AUR search error: %1").arg(reply->errorString())).toStdString()); emit error(reply->errorString()); return; } - + QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); - - if (!doc.isObject()) { - Logger::error("Invalid AUR response format"); + + if (!doc.isObject()) + { + spdlog::error("Invalid AUR response format"); emit error("Invalid response from AUR"); return; } - + QJsonObject root = doc.object(); QJsonArray results = root["results"].toArray(); - + QVector packages; - for (const auto& result : results) { + for (const auto& result : results) + { packages.push_back(parseAurPackage(result.toObject())); } - - Logger::info(QString("Found %1 AUR packages").arg(packages.size())); + + spdlog::info("{}", (QString("Found %1 AUR packages").arg(packages.size())).toStdString()); emit searchCompleted(packages); } -void AurHelper::getPackageInfo(const QString& packageName) { - Logger::debug(QString("Getting AUR package info for: %1").arg(packageName)); - +void AurHelper::getPackageInfo(const QString& packageName) +{ + spdlog::debug("{}", (QString("Getting AUR package info for: %1").arg(packageName)).toStdString()); + QUrl url("https://aur.archlinux.org/rpc/"); QUrlQuery urlQuery; urlQuery.addQueryItem("v", "5"); urlQuery.addQueryItem("type", "info"); urlQuery.addQueryItem("arg", packageName); url.setQuery(urlQuery); - + QNetworkRequest request(url); auto* reply = m_networkManager->get(request); - + connect(reply, &QNetworkReply::finished, this, &AurHelper::onPackageInfoFinished); } -void AurHelper::onPackageInfoFinished() { +void AurHelper::onPackageInfoFinished() +{ auto* reply = qobject_cast(sender()); - if (!reply) return; - + if (!reply) + { + return; + } + reply->deleteLater(); - - if (reply->error() != QNetworkReply::NoError) { - Logger::error(QString("AUR package info error: %1").arg(reply->errorString())); + + if (reply->error() != QNetworkReply::NoError) + { + spdlog::error("{}", (QString("AUR package info error: %1").arg(reply->errorString())).toStdString()); emit error(reply->errorString()); return; } - + QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); - - if (!doc.isObject()) { + + if (!doc.isObject()) + { emit error("Invalid response from AUR"); return; } - + QJsonObject root = doc.object(); QJsonArray results = root["results"].toArray(); - - if (results.isEmpty()) { + + if (results.isEmpty()) + { emit error("Package not found in AUR"); return; } - + PackageInfo info = parseAurPackage(results[0].toObject()); emit packageInfoReceived(info); } -PackageInfo AurHelper::parseAurPackage(const QJsonObject& obj) { +PackageInfo AurHelper::parseAurPackage(const QJsonObject& obj) +{ PackageInfo info; info.name = obj["Name"].toString(); info.version = obj["Version"].toString(); @@ -121,49 +139,63 @@ PackageInfo AurHelper::parseAurPackage(const QJsonObject& obj) { info.repository = "AUR"; info.maintainer = obj["Maintainer"].toString(); info.upstreamUrl = obj["URL"].toString(); - + qint64 lastModified = obj["LastModified"].toInteger(); info.lastUpdated = QDateTime::fromSecsSinceEpoch(lastModified); - + // Parse dependencies QJsonArray depends = obj["Depends"].toArray(); - for (const auto& dep : depends) { + for (const auto& dep : depends) + { info.dependList.append(dep.toString()); } - + // Also add make dependencies if available QJsonArray makeDepends = obj["MakeDepends"].toArray(); - for (const auto& dep : makeDepends) { + for (const auto& dep : makeDepends) + { QString depStr = dep.toString() + " (make)"; info.dependList.append(depStr); } - + return info; } -QVector AurHelper::checkAurUpdates() { +QVector AurHelper::checkAurUpdates(std::stop_token stopToken) +{ QVector updates; - + // Get list of foreign (AUR) packages QProcess process; process.start("pacman", QStringList() << "-Qm"); process.waitForFinished(); - - if (process.exitCode() != 0) { - Logger::warning("Failed to get list of foreign packages"); + + if (process.exitCode() != 0) + { + spdlog::warn("Failed to get list of foreign packages"); return updates; } - + QString output = process.readAllStandardOutput(); QStringList lines = output.split('\n', Qt::SkipEmptyParts); - - for (const auto& line : lines) { + + for (const auto& line : lines) + { + if (stopToken.stop_requested()) + { + spdlog::info("AUR update check cancelled"); + break; + } + QStringList parts = line.split(' ', Qt::SkipEmptyParts); - if (parts.size() < 2) continue; - + if (parts.size() < 2) + { + continue; + } + QString name = parts[0]; QString version = parts[1]; - + // Query AUR for latest version QUrl url("https://aur.archlinux.org/rpc/"); QUrlQuery urlQuery; @@ -171,23 +203,40 @@ QVector AurHelper::checkAurUpdates() { urlQuery.addQueryItem("type", "info"); urlQuery.addQueryItem("arg", name); url.setQuery(urlQuery); - + QNetworkRequest request(url); auto reply = m_networkManager->get(request); - + QEventLoop loop; connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + + // Interrupts a request that's already in flight (not just the gap + // between packages), so cancellation is prompt even if the AUR RPC + // is slow or unreachable. + std::stop_callback stopCallback(stopToken, [&loop]() { loop.quit(); }); + loop.exec(); - - if (reply->error() == QNetworkReply::NoError) { + + if (stopToken.stop_requested()) + { + reply->abort(); + reply->deleteLater(); + spdlog::info("AUR update check cancelled"); + break; + } + + if (reply->error() == QNetworkReply::NoError) + { QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); QJsonObject root = doc.object(); QJsonArray results = root["results"].toArray(); - - if (!results.isEmpty()) { + + if (!results.isEmpty()) + { QString newVersion = results[0].toObject()["Version"].toString(); - if (newVersion != version) { + if (newVersion != version) + { UpdateInfo update; update.name = name; update.oldVersion = version; @@ -198,9 +247,9 @@ QVector AurHelper::checkAurUpdates() { } } } - + reply->deleteLater(); } - + return updates; } diff --git a/src/core/aur_helper.h b/src/core/aur_helper.h index e29321f..a2b7a23 100644 --- a/src/core/aur_helper.h +++ b/src/core/aur_helper.h @@ -1,13 +1,14 @@ #ifndef AUR_HELPER_H #define AUR_HELPER_H -#include -#include +#include "../utils/types.h" #include #include #include +#include +#include #include -#include "../utils/types.h" +#include /** * @brief Helper class for interacting with the Arch User Repository (AUR). @@ -16,30 +17,39 @@ * - m_networkManager: Owned by std::unique_ptr for RAII-style cleanup * - Network replies are managed via Qt parent-child and deleteLater() */ -class AurHelper : public QObject { +class AurHelper : public QObject +{ Q_OBJECT - + public: explicit AurHelper(QObject* parent = nullptr); ~AurHelper() override; - + void searchPackages(const QString& query); void getPackageInfo(const QString& packageName); - QVector checkAurUpdates(); - + + // Sequentially queries the AUR RPC for each foreign package's latest + // version. stopToken allows a caller (see UpdatesWidget, which runs + // this on a std::jthread) to interrupt the loop between packages, and + // to abort a request that's already in flight rather than blocking + // until it completes or times out. + QVector checkAurUpdates(std::stop_token stopToken = { }); + + // Pure JSON -> PackageInfo mapping, exposed as a static so it can be unit + // tested without a QNetworkAccessManager or a live AUR request. + static PackageInfo parseAurPackage(const QJsonObject& obj); + signals: void searchCompleted(const QVector& results); void packageInfoReceived(const PackageInfo& info); void error(const QString& message); - + private slots: void onSearchFinished(); void onPackageInfoFinished(); - + private: std::unique_ptr m_networkManager; - - PackageInfo parseAurPackage(const QJsonObject& obj); }; -#endif // AUR_HELPER_H +#endif // AUR_HELPER_H diff --git a/src/core/package_manager.cpp b/src/core/package_manager.cpp index eeab245..b74637a 100644 --- a/src/core/package_manager.cpp +++ b/src/core/package_manager.cpp @@ -1,226 +1,288 @@ #include "package_manager.h" -#include "../utils/logger.h" -#include +#include "../utils/logging.h" #include +#include +#include -PackageManager& PackageManager::instance() { +PackageManager& PackageManager::instance() +{ static PackageManager instance; return instance; } PackageManager::PackageManager() : QObject(nullptr) - , m_process(std::make_unique()) { - + , m_process(std::make_unique()) +{ + detectHelper(); - - connect(m_process.get(), &QProcess::finished, - this, &PackageManager::onProcessFinished); - connect(m_process.get(), &QProcess::errorOccurred, - this, &PackageManager::onProcessError); - connect(m_process.get(), &QProcess::readyReadStandardOutput, - this, &PackageManager::onProcessOutput); - connect(m_process.get(), &QProcess::readyReadStandardError, - this, &PackageManager::onProcessOutput); -} - -PackageManager::~PackageManager() { - if (m_process && m_process->state() != QProcess::NotRunning) { + + connect(m_process.get(), &QProcess::finished, this, &PackageManager::onProcessFinished); + connect(m_process.get(), &QProcess::errorOccurred, this, &PackageManager::onProcessError); + connect(m_process.get(), &QProcess::readyReadStandardOutput, this, &PackageManager::onProcessOutput); + connect(m_process.get(), &QProcess::readyReadStandardError, this, &PackageManager::onProcessOutput); +} + +PackageManager::~PackageManager() +{ + if (m_process && m_process->state() != QProcess::NotRunning) + { m_process->terminate(); m_process->waitForFinished(3000); } } -void PackageManager::detectHelper() { +void PackageManager::detectHelper() +{ // Check for yay first QString yayPath = QStandardPaths::findExecutable("yay"); - if (!yayPath.isEmpty()) { + if (!yayPath.isEmpty()) + { m_helper = Helper::Yay; - Logger::info("Using yay as package helper"); + spdlog::info("Using yay as package helper"); return; } - + // Check for paru - deprecate because paru doesn't allow running with pkexec // QString paruPath = QStandardPaths::findExecutable("paru"); // if (!paruPath.isEmpty()) { // m_helper = Helper::Paru; - // Logger::info("Using paru as package helper"); + // spdlog::info("Using paru as package helper"); // return; // } - + // Default to pacman m_helper = Helper::Pacman; - Logger::info("Using pacman as package helper"); + spdlog::info("Using pacman as package helper"); } -QString PackageManager::getHelperName() const { - switch (m_helper) { - case Helper::Yay: return "yay"; - case Helper::Pacman: return "pacman"; - default: return "pacman"; +QString PackageManager::getHelperName() const +{ + switch (m_helper) + { + case Helper::Yay: + return "yay"; + case Helper::Pacman: + return "pacman"; + default: + return "pacman"; } } -void PackageManager::installPackage(const QString& packageName, const QString& repository) { +void PackageManager::installPackage(const QString& packageName, const QString& repository) +{ std::lock_guard lock(m_mutex); - - Logger::info(QString("Installing package: %1 from %2").arg(packageName, repository.isEmpty() ? "default" : repository)); + + spdlog::info( + "{}", + (QString("Installing package: %1 from %2").arg(packageName, repository.isEmpty() ? "default" : repository)) + .toStdString()); emit operationStarted(QString("Installing %1...").arg(packageName)); - + // Determine if this is an AUR package (not from official repos or chaotic-aur) QString repoLower = repository.toLower(); bool isAUR = repoLower == "aur"; QString helper = getHelperName(); - + QString command; - if (isAUR && (m_helper == Helper::Yay)) { + if (isAUR && (m_helper == Helper::Yay)) + { // AUR packages - use pkexec to get userpassword before hand // Paru has a problem here, so default to yay command = QString("pkexec %1 -S %2 --noconfirm").arg(helper, packageName); - } else { + } + else + { // Official repos and chaotic-aur need root access and use pacman command = QString("pkexec pacman -S %1 --noconfirm").arg(packageName); } - + executeCommand("sh", QStringList() << "-c" << command); } -void PackageManager::uninstallPackage(const QString& packageName, const QString& repository) { +void PackageManager::uninstallPackage(const QString& packageName, const QString& repository) +{ std::lock_guard lock(m_mutex); - - Logger::info(QString("Uninstalling package: %1 from %2").arg(packageName, repository.isEmpty() ? "default" : repository)); + + spdlog::info( + "{}", + (QString("Uninstalling package: %1 from %2").arg(packageName, repository.isEmpty() ? "default" : repository)) + .toStdString()); emit operationStarted(QString("Uninstalling %1...").arg(packageName)); - + // Uninstall always needs root (even for AUR packages, they're in the system db once installed) QString command = QString("pkexec pacman -Rdd %1 --noconfirm").arg(packageName); - + executeCommand("sh", QStringList() << "-c" << command); } -void PackageManager::updatePackage(const QString& packageName, const QString& repository) { +void PackageManager::updatePackage(const QString& packageName, const QString& repository) +{ std::lock_guard lock(m_mutex); - - Logger::info(QString("Updating package: %1 from %2").arg(packageName, repository.isEmpty() ? "default" : repository)); + + spdlog::info( + "{}", + (QString("Updating package: %1 from %2").arg(packageName, repository.isEmpty() ? "default" : repository)) + .toStdString()); emit operationStarted(QString("Updating %1...").arg(packageName)); - + // Determine if this is an AUR package (not from official repos or chaotic-aur) QString repoLower = repository.toLower(); bool isAUR = repoLower == "aur"; QString helper = getHelperName(); - + QString command; - if (isAUR && (m_helper == Helper::Yay)) { + if (isAUR && (m_helper == Helper::Yay)) + { // AUR packages - run helper as regular user (no pkexec) command = QString("%1 -S %2 --noconfirm").arg(helper, packageName); - } else { + } + else + { // Official repos and chaotic-aur need root access and use pacman command = QString("pkexec pacman -S %1 --noconfirm").arg(packageName); } - + executeCommand("sh", QStringList() << "-c" << command); } -void PackageManager::updateAllPackages() { +void PackageManager::updateAllPackages() +{ std::lock_guard lock(m_mutex); - - Logger::info("Updating all packages"); + + spdlog::info("Updating all packages"); emit operationStarted("Updating all packages..."); - - QString command = QString("pkexec %1 -Syu --noconfirm") - .arg(getHelperName()); - + + QString command = QString("pkexec %1 -Syu --noconfirm").arg(getHelperName()); + executeCommand("sh", QStringList() << "-c" << command); } -void PackageManager::executeCommand(const QString& command, const QStringList& args) { - if (m_process->state() != QProcess::NotRunning) { - Logger::warning("Another operation is already running"); +void PackageManager::executeCommand(const QString& command, const QStringList& args) +{ + if (m_process->state() != QProcess::NotRunning) + { + spdlog::warn("Another operation is already running"); emit operationError("Another operation is already in progress"); return; } - + // Merge stdout and stderr so we capture all output m_process->setProcessChannelMode(QProcess::MergedChannels); - - Logger::debug(QString("Executing: %1 %2").arg(command, args.join(" "))); - + + spdlog::debug("{}", (QString("Executing: %1 %2").arg(command, args.join(" "))).toStdString()); + // Emit the actual command being executed to the UI for visibility QString fullCommand = command + " " + args.join(" "); emit operationOutput(QString(">> Executing: %1\n").arg(fullCommand)); - + m_process->start(command, args); - + // Check if process started successfully - if (!m_process->waitForStarted(3000)) { + if (!m_process->waitForStarted(3000)) + { QString error = QString("Failed to start process: %1").arg(m_process->errorString()); - Logger::error(error); + spdlog::error("{}", (error).toStdString()); emit operationError(error); } } -void PackageManager::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { +void PackageManager::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ QString output = m_process->readAllStandardOutput(); QString error = m_process->readAllStandardError(); - - if (exitStatus == QProcess::NormalExit && exitCode == 0) { - Logger::info("Operation completed successfully"); + + if (exitStatus == QProcess::NormalExit && exitCode == 0) + { + spdlog::info("Operation completed successfully"); emit operationCompleted(true, "Operation completed successfully"); - } else { - Logger::error(QString("Operation failed with exit code %1").arg(exitCode)); - Logger::error(QString("Error output: %1").arg(error)); + } + else + { + spdlog::error("{}", (QString("Operation failed with exit code %1").arg(exitCode)).toStdString()); + spdlog::error("{}", (QString("Error output: %1").arg(error)).toStdString()); emit operationCompleted(false, QString("Operation failed: %1").arg(error)); } } -void PackageManager::onProcessError(QProcess::ProcessError /*error*/) { +void PackageManager::onProcessError(QProcess::ProcessError /*error*/) +{ QString errorString = m_process->errorString(); - Logger::error(QString("Process error: %1").arg(errorString)); + spdlog::error("{}", (QString("Process error: %1").arg(errorString)).toStdString()); emit operationError(errorString); } -void PackageManager::onProcessOutput() { +void PackageManager::onProcessOutput() +{ // Since we merged channels, only read stdout (which includes stderr) QString output = m_process->readAll(); - if (!output.isEmpty()) { - Logger::debug(QString("Process output: %1").arg(output.trimmed())); + if (!output.isEmpty()) + { + spdlog::debug("{}", (QString("Process output: %1").arg(output.trimmed())).toStdString()); emit operationOutput(output); } } -void PackageManager::cancelRunningOperation() { - if (m_process && m_process->state() != QProcess::NotRunning) { - Logger::warning("Killing running operation..."); - emit operationOutput("\n>>> Operation cancelled by user <<<\n"); - - // When using pkexec, we need to kill the actual pacman/yay/paru process - // not just the pkexec wrapper. Use pkill to terminate all package manager processes. - QProcess killProcess; - killProcess.start("pkexec", QStringList() << "bash" << "-c" - << "pkill -TERM pacman; pkill -TERM yay; pkill -TERM paru"); - killProcess.waitForFinished(2000); - - // Also terminate the QProcess wrapper - m_process->terminate(); - - // Wait up to 3 seconds for graceful termination - if (!m_process->waitForFinished(3000)) { - // Force kill if still running - Logger::warning("Process did not terminate gracefully, forcing kill..."); - killProcess.start("pkexec", QStringList() << "bash" << "-c" - << "pkill -KILL pacman; pkill -KILL yay; pkill -KILL paru"); +void PackageManager::spawnKillHelper(const QStringList& pkillArgs) +{ + // Reassigning m_killHelperThread auto-requests-stop and joins whatever + // was previously running here first (std::jthread destructor/move-assign + // semantics), so a second cancel click interrupts an in-flight wait + // instead of stacking up behind it. + m_killHelperThread = std::jthread( + [pkillArgs](std::stop_token stopToken) + { + QProcess killProcess; + killProcess.start("pkexec", pkillArgs); + + // Cuts the wait short if request_stop() is called (second cancel + // click, or PackageManager being destroyed) instead of always + // riding out the full 2s. + std::stop_callback stopCallback(stopToken, [&killProcess]() { killProcess.kill(); }); + killProcess.waitForFinished(2000); - - m_process->kill(); - m_process->waitForFinished(1000); - } - - emit operationCompleted(false, "Operation cancelled by user"); - Logger::info("Operation cancelled successfully"); - } else { - Logger::warning("No operation is currently running"); + }); +} + +void PackageManager::cancelRunningOperation() +{ + if (!m_process || m_process->state() == QProcess::NotRunning) + { + spdlog::warn("No operation is currently running"); + return; } + + spdlog::warn("Killing running operation..."); + emit operationOutput("\n>>> Operation cancelled by user <<<\n"); + + // When using pkexec, we need to kill the actual pacman/yay/paru process, + // not just the pkexec wrapper - terminate()/kill() below only reach the + // local wrapper. That pkexec call has to wait on a polkit prompt, which + // is exactly the kind of blocking work that shouldn't run on the GUI + // thread, so it runs on a std::jthread (see spawnKillHelper) instead of + // blocking here; it runs concurrently with the waitForFinished() below + // rather than before it. + spawnKillHelper(QStringList() << "bash" << "-c" << "pkill -TERM pacman; pkill -TERM yay; pkill -TERM paru"); + + // Also terminate the QProcess wrapper + m_process->terminate(); + + // Wait up to 3 seconds for graceful termination + if (!m_process->waitForFinished(3000)) + { + // Force kill if still running + spdlog::warn("Process did not terminate gracefully, forcing kill..."); + spawnKillHelper(QStringList() << "bash" << "-c" << "pkill -KILL pacman; pkill -KILL yay; pkill -KILL paru"); + + m_process->kill(); + m_process->waitForFinished(1000); + } + + emit operationCompleted(false, "Operation cancelled by user"); + spdlog::info("Operation cancelled successfully"); } -bool PackageManager::isOperationRunning() const { +bool PackageManager::isOperationRunning() const +{ return m_process && m_process->state() != QProcess::NotRunning; } diff --git a/src/core/package_manager.h b/src/core/package_manager.h index 0e86a9b..c6fc156 100644 --- a/src/core/package_manager.h +++ b/src/core/package_manager.h @@ -2,68 +2,77 @@ #define PACKAGE_MANAGER_H #include -#include #include +#include #include #include +#include /** * @brief Singleton class for managing package operations (install, uninstall, update). - * + * * Memory Management: * - m_process: Owned by std::unique_ptr for RAII-style cleanup and clear ownership * - Thread-safe via m_mutex for operation serialization + * - m_killHelperThread: a std::jthread that runs the `pkexec pkill` calls used + * by cancelRunningOperation() (see there for why). Its destructor + * auto-requests-stop and joins, so a still-waiting call is interrupted + * rather than left dangling if cancelled again or on shutdown. */ -class PackageManager : public QObject { +class PackageManager : public QObject +{ Q_OBJECT - + public: - enum class Helper { + enum class Helper + { Pacman, Yay, Paru }; - + static PackageManager& instance(); - + ~PackageManager() override; - + // Disable copy and move PackageManager(const PackageManager&) = delete; PackageManager& operator=(const PackageManager&) = delete; PackageManager(PackageManager&&) = delete; PackageManager& operator=(PackageManager&&) = delete; - + void installPackage(const QString& packageName, const QString& repository = QString()); void uninstallPackage(const QString& packageName, const QString& repository = QString()); void updatePackage(const QString& packageName, const QString& repository = QString()); void updateAllPackages(); void cancelRunningOperation(); bool isOperationRunning() const; - + Helper getHelper() const { return m_helper; } QString getHelperName() const; - + signals: void operationStarted(const QString& message); void operationOutput(const QString& output); void operationCompleted(bool success, const QString& message); void operationError(const QString& error); - + private: PackageManager(); - + void detectHelper(); void executeCommand(const QString& command, const QStringList& args); - + void spawnKillHelper(const QStringList& pkillArgs); + Helper m_helper = Helper::Pacman; std::unique_ptr m_process; mutable std::mutex m_mutex; - + std::jthread m_killHelperThread; + private slots: void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); void onProcessError(QProcess::ProcessError error); void onProcessOutput(); }; -#endif // PACKAGE_MANAGER_H +#endif // PACKAGE_MANAGER_H diff --git a/src/core/pacman_conf.cpp b/src/core/pacman_conf.cpp new file mode 100644 index 0000000..2c5b690 --- /dev/null +++ b/src/core/pacman_conf.cpp @@ -0,0 +1,78 @@ +#include "pacman_conf.h" + +namespace PacmanConf +{ + +bool isMultilibEnabled(const QString& contents) +{ + bool inMultilibSection = false; + + const QStringList lines = contents.split('\n'); + for (const QString& rawLine : lines) + { + const QString line = rawLine.trimmed(); + + // Check for [multilib] section header + if (line == "[multilib]") + { + inMultilibSection = true; + continue; + } + + // If we found [multilib] section, check if it's not commented + if (inMultilibSection && !line.isEmpty() && !line.startsWith("#")) + { + // If we find Include directive, multilib is enabled + if (line.startsWith("Include")) + { + return true; + } + } + + // If we hit another section, stop + if (inMultilibSection && line.startsWith("[") && line != "[multilib]") + { + break; + } + } + + return false; +} + +bool isChaoticAurEnabled(const QString& contents) +{ + bool inChaoticAurSection = false; + + const QStringList lines = contents.split('\n'); + for (const QString& rawLine : lines) + { + const QString line = rawLine.trimmed(); + + // Check for [chaotic-aur] section header + if (line == "[chaotic-aur]") + { + inChaoticAurSection = true; + continue; + } + + // If we found [chaotic-aur] section, check if it's not commented + if (inChaoticAurSection && !line.isEmpty() && !line.startsWith("#")) + { + // If we find Include or Server directive, chaotic-aur is enabled + if (line.startsWith("Include") || line.startsWith("Server")) + { + return true; + } + } + + // If we hit another section, stop + if (inChaoticAurSection && line.startsWith("[") && line != "[chaotic-aur]") + { + break; + } + } + + return false; +} + +} // namespace PacmanConf diff --git a/src/core/pacman_conf.h b/src/core/pacman_conf.h new file mode 100644 index 0000000..9c4c80a --- /dev/null +++ b/src/core/pacman_conf.h @@ -0,0 +1,18 @@ +#ifndef PACMAN_CONF_H +#define PACMAN_CONF_H + +#include +#include + +// Pure section-scanning helpers for pacman.conf content. Take the file +// contents as a QString rather than a path so callers (and tests) can feed +// in fixture text instead of hitting /etc/pacman.conf directly. +namespace PacmanConf +{ + +bool isMultilibEnabled(const QString& contents); +bool isChaoticAurEnabled(const QString& contents); + +} // namespace PacmanConf + +#endif // PACMAN_CONF_H diff --git a/src/core/progress_parser.cpp b/src/core/progress_parser.cpp new file mode 100644 index 0000000..0451826 --- /dev/null +++ b/src/core/progress_parser.cpp @@ -0,0 +1,56 @@ +#include "progress_parser.h" + +#include + +ProgressParseResult parseOperationProgress(const QString& output) +{ + ProgressParseResult result; + + // Pattern: "downloading..." or "installing..." + if (output.contains("downloading", Qt::CaseInsensitive)) + { + result.statusText = "Downloading packages..."; + } + else if (output.contains("installing", Qt::CaseInsensitive)) + { + result.statusText = "Installing packages..."; + } + else if (output.contains("building", Qt::CaseInsensitive)) + { + result.statusText = "Building packages..."; + } + else if (output.contains("checking", Qt::CaseInsensitive)) + { + result.statusText = "Checking dependencies..."; + } + else if (output.contains("resolving", Qt::CaseInsensitive)) + { + result.statusText = "Resolving dependencies..."; + } + + // Pattern: "(1/5)" or "( 1/5)" to track package progress + static const QRegularExpression packagePattern(R"(\(\s*(\d+)/(\d+)\))"); + auto match = packagePattern.match(output); + if (match.hasMatch()) + { + const int currentPackage = match.captured(1).toInt(); + const int totalPackages = match.captured(2).toInt(); + result.currentPackage = currentPackage; + result.totalPackages = totalPackages; + + if (totalPackages > 0) + { + result.progressPercent = (currentPackage * 100) / totalPackages; + } + } + + // Pattern: "[##########] 100%" for download progress + static const QRegularExpression percentPattern(R"(\s+(\d+)%\s*)"); + auto percentMatch = percentPattern.match(output); + if (percentMatch.hasMatch()) + { + result.progressPercent = percentMatch.captured(1).toInt(); + } + + return result; +} diff --git a/src/core/progress_parser.h b/src/core/progress_parser.h new file mode 100644 index 0000000..a70541c --- /dev/null +++ b/src/core/progress_parser.h @@ -0,0 +1,24 @@ +#ifndef PROGRESS_PARSER_H +#define PROGRESS_PARSER_H + +#include +#include + +// Pure parsing of pacman/yay/paru operation output, extracted so the +// progress-message and progress-bar logic can be unit tested without a +// QProgressBar/QLabel-backed dialog. +struct ProgressParseResult +{ + std::optional statusText; + std::optional currentPackage; + std::optional totalPackages; + + // Final percentage to apply to the progress bar, if any. When both the + // "(n/total)" and a bare "NN%" pattern match the same output, the "NN%" + // match takes precedence, mirroring the original inline parsing order. + std::optional progressPercent; +}; + +ProgressParseResult parseOperationProgress(const QString& output); + +#endif // PROGRESS_PARSER_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt new file mode 100644 index 0000000..523d462 --- /dev/null +++ b/src/gui/CMakeLists.txt @@ -0,0 +1,37 @@ +# Qt Widgets GUI layer. Links against `core` for package logic. +add_library(gui STATIC + mainwindow.cpp + mainwindow.h + home_widget.cpp + home_widget.h + search_widget.cpp + search_widget.h + installed_widget.cpp + installed_widget.h + updates_widget.cpp + updates_widget.h + settings_widget.cpp + settings_widget.h + package_card.cpp + package_card.h + package_details_dialog.cpp + package_details_dialog.h + ${CMAKE_SOURCE_DIR}/resources.qrc +) + +target_link_libraries(gui + PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Network + Qt6::Concurrent + core + utils +) + +target_compile_options(gui PRIVATE + -Wall + -Wextra + -Wpedantic +) diff --git a/src/gui/home_widget.cpp b/src/gui/home_widget.cpp index 645ef07..7f5590b 100644 --- a/src/gui/home_widget.cpp +++ b/src/gui/home_widget.cpp @@ -1,167 +1,197 @@ #include "home_widget.h" -#include "package_card.h" -#include "package_details_dialog.h" #include "../core/alpm_wrapper.h" #include "../core/aur_helper.h" -#include "../utils/logger.h" -#include -#include +#include "../utils/logging.h" +#include "package_card.h" +#include "package_details_dialog.h" #include +#include +#include HomeWidget::HomeWidget(QWidget* parent) : QWidget(parent) , m_scrollArea(new QScrollArea(this)) , m_contentWidget(new QWidget()) , m_gridLayout(new QGridLayout(m_contentWidget)) - , m_updateTimer(new QTimer(this)) { - + , m_updateTimer(new QTimer(this)) +{ + setupUi(); loadFeaturedPackages(); createPackageCards(); - + // Setup timer to periodically check installation status connect(m_updateTimer, &QTimer::timeout, this, &HomeWidget::onUpdateTimer); - m_updateTimer->start(3000); // Check every 3 seconds - + m_updateTimer->start(3000); // Check every 3 seconds + // Initial check checkInstalledPackages(); } -void HomeWidget::setupUi() { +void HomeWidget::setupUi() +{ auto* mainLayout = new QVBoxLayout(this); - + auto* titleLabel = new QLabel("Featured Packages", this); titleLabel->setObjectName("view-title"); mainLayout->addWidget(titleLabel); - + m_scrollArea->setWidget(m_contentWidget); m_scrollArea->setWidgetResizable(true); m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - + m_gridLayout->setSpacing(15); m_gridLayout->setContentsMargins(10, 10, 10, 10); - + mainLayout->addWidget(m_scrollArea); setLayout(mainLayout); } -void HomeWidget::loadFeaturedPackages() { +void HomeWidget::loadFeaturedPackages() +{ // Featured packages list with initial repositories - m_featuredPackages = { - {"firefox", "Latest", "Fast, Private & Safe Web Browser", "extra"}, - {"gimp", "Latest", "GNU Image Manipulation Program", "extra"}, - {"vlc", "Latest", "Multi-platform MPEG, VCD/DVD, and DivX player", "extra"}, - {"telegram-desktop", "Latest", "Official Telegram Desktop client", "extra"}, - {"obs-studio", "Latest", "Free, open source software for live streaming and recording", "extra"}, - {"blender", "Latest", "A fully integrated 3D graphics creation suite", "extra"}, - {"spotify", "Latest", "A proprietary music streaming service", "AUR"}, - {"discord", "Latest", "All-in-one voice and text chat for gamers", "extra"}, - {"google-chrome", "Latest", "The popular web browser by Google", "AUR"}, - {"visual-studio-code-bin", "Latest", "Visual Studio Code (official binary version)", "AUR"}, - {"libreoffice-still", "Latest", "Free and Open Source Office Suite", "extra"}, - {"zoom", "Latest", "Video Conferencing and Web Conferencing Service", "AUR"} - }; - + m_featuredPackages + = { { "firefox", "Latest", "Fast, Private & Safe Web Browser", "extra" }, + { "gimp", "Latest", "GNU Image Manipulation Program", "extra" }, + { "vlc", "Latest", "Multi-platform MPEG, VCD/DVD, and DivX player", "extra" }, + { "telegram-desktop", "Latest", "Official Telegram Desktop client", "extra" }, + { "obs-studio", "Latest", "Free, open source software for live streaming and recording", "extra" }, + { "blender", "Latest", "A fully integrated 3D graphics creation suite", "extra" }, + { "spotify", "Latest", "A proprietary music streaming service", "AUR" }, + { "discord", "Latest", "All-in-one voice and text chat for gamers", "extra" }, + { "google-chrome", "Latest", "The popular web browser by Google", "AUR" }, + { "visual-studio-code-bin", "Latest", "Visual Studio Code (official binary version)", "AUR" }, + { "libreoffice-still", "Latest", "Free and Open Source Office Suite", "extra" }, + { "zoom", "Latest", "Video Conferencing and Web Conferencing Service", "AUR" } }; + // Fetch actual package information from repositories - for (auto& pkg : m_featuredPackages) { + for (auto& pkg : m_featuredPackages) + { PackageInfo repoInfo = AlpmWrapper::instance().getPackageInfo(pkg.name); - - if (!repoInfo.name.isEmpty() && !repoInfo.repository.isEmpty()) { + + if (!repoInfo.name.isEmpty() && !repoInfo.repository.isEmpty()) + { // Package found in official repos (including chaotic-aur), update with actual information pkg.repository = repoInfo.repository; pkg.version = repoInfo.version; pkg.description = repoInfo.description; - - if (pkg.repository.toLower() != "aur") { - Logger::debug(QString("Package %1 found in %2 repository with version %3") - .arg(pkg.name, pkg.repository, pkg.version)); + + if (pkg.repository.toLower() != "aur") + { + spdlog::debug("{}", + (QString("Package %1 found in %2 repository with version %3") + .arg(pkg.name, pkg.repository, pkg.version)) + .toStdString()); } - } else if (pkg.repository.toLower() == "aur") { + } + else if (pkg.repository.toLower() == "aur") + { // Package not found in official repos (including chaotic-aur) // Default to AUR helper (yay/paru) since chaotic-aur is not enabled or doesn't have this package pkg.repository = "aur"; - Logger::debug(QString("Package %1 not found in enabled repositories, defaulting to AUR helper").arg(pkg.name)); + spdlog::debug( + "{}", + (QString("Package %1 not found in enabled repositories, defaulting to AUR helper").arg(pkg.name)) + .toStdString()); } } - - Logger::info(QString("Loaded %1 featured packages").arg(m_featuredPackages.size())); + + spdlog::info("{}", (QString("Loaded %1 featured packages").arg(m_featuredPackages.size())).toStdString()); } -void HomeWidget::createPackageCards() { +void HomeWidget::createPackageCards() +{ int row = 0; int col = 0; const int columns = 3; - - for (const auto& pkg : m_featuredPackages) { + + for (const auto& pkg : m_featuredPackages) + { auto* card = new PackageCard(pkg, m_contentWidget); connect(card, &PackageCard::clicked, this, &HomeWidget::onPackageClicked); - + m_gridLayout->addWidget(card, row, col); m_packageCards.append(card); - + col++; - if (col >= columns) { + if (col >= columns) + { col = 0; row++; } } - + // Add stretch to push cards to the top m_gridLayout->setRowStretch(row + 1, 1); } -void HomeWidget::checkInstalledPackages() { - for (auto* card : m_packageCards) { +void HomeWidget::checkInstalledPackages() +{ + for (auto* card : m_packageCards) + { card->checkInstallStatus(); } } -void HomeWidget::onUpdateTimer() { +void HomeWidget::onUpdateTimer() +{ checkInstalledPackages(); } -void HomeWidget::onPackageClicked(const PackageInfo& info) { - Logger::info(QString("Package clicked: %1").arg(info.name)); - +void HomeWidget::onPackageClicked(const PackageInfo& info) +{ + spdlog::info("{}", (QString("Package clicked: %1").arg(info.name)).toStdString()); + // Fetch full package details including dependencies PackageInfo fullInfo; - - if (info.repository.toLower() == "aur") { + + if (info.repository.toLower() == "aur") + { // For AUR packages, query AUR API for full details AurHelper aurHelper; QEventLoop loop; - - connect(&aurHelper, &AurHelper::packageInfoReceived, [&fullInfo, &loop](const PackageInfo& aurInfo) { - fullInfo = aurInfo; - loop.quit(); - }); - - connect(&aurHelper, &AurHelper::error, [&fullInfo, &info, &loop](const QString& error) { - Logger::warning(QString("Failed to fetch AUR package info: %1").arg(error)); - fullInfo = info; // Fallback to basic info - loop.quit(); - }); - + + connect(&aurHelper, + &AurHelper::packageInfoReceived, + [&fullInfo, &loop](const PackageInfo& aurInfo) + { + fullInfo = aurInfo; + loop.quit(); + }); + + connect(&aurHelper, + &AurHelper::error, + [&fullInfo, &info, &loop](const QString& error) + { + spdlog::warn("{}", (QString("Failed to fetch AUR package info: %1").arg(error)).toStdString()); + fullInfo = info; // Fallback to basic info + loop.quit(); + }); + aurHelper.getPackageInfo(info.name); - loop.exec(); // Wait for response - + loop.exec(); // Wait for response + // If we didn't get full info, use the basic info - if (fullInfo.name.isEmpty()) { + if (fullInfo.name.isEmpty()) + { fullInfo = info; } - } else { + } + else + { // For official repos, fetch full details from ALPM fullInfo = AlpmWrapper::instance().getPackageInfo(info.name); // If not found, use the basic info - if (fullInfo.name.isEmpty()) { + if (fullInfo.name.isEmpty()) + { fullInfo = info; } } - + auto* dialog = new PackageDetailsDialog(fullInfo, this); dialog->exec(); dialog->deleteLater(); - + // Update installation status after dialog closes checkInstalledPackages(); } diff --git a/src/gui/home_widget.h b/src/gui/home_widget.h index 08684a2..4c0f317 100644 --- a/src/gui/home_widget.h +++ b/src/gui/home_widget.h @@ -1,12 +1,12 @@ #ifndef HOME_WIDGET_H #define HOME_WIDGET_H -#include -#include -#include +#include "../utils/types.h" #include +#include #include -#include "../utils/types.h" +#include +#include class PackageCard; @@ -17,31 +17,32 @@ class PackageCard; * - All Qt widget members use Qt parent-child ownership (raw pointers are non-owning) * - m_packageCards contains non-owning pointers to cards owned by m_contentWidget */ -class HomeWidget : public QWidget { +class HomeWidget : public QWidget +{ Q_OBJECT - + public: explicit HomeWidget(QWidget* parent = nullptr); ~HomeWidget() override = default; - + private: void setupUi(); void loadFeaturedPackages(); void createPackageCards(); void checkInstalledPackages(); - + QVector m_featuredPackages; QVector m_packageCards; // Non-owning pointers, owned by m_contentWidget - + // Qt parent-child managed widgets (non-owning pointers) QScrollArea* m_scrollArea = nullptr; QWidget* m_contentWidget = nullptr; QGridLayout* m_gridLayout = nullptr; QTimer* m_updateTimer = nullptr; - + private slots: void onPackageClicked(const PackageInfo& info); void onUpdateTimer(); }; -#endif // HOME_WIDGET_H +#endif // HOME_WIDGET_H diff --git a/src/gui/installed_widget.cpp b/src/gui/installed_widget.cpp index aa45881..920a47a 100644 --- a/src/gui/installed_widget.cpp +++ b/src/gui/installed_widget.cpp @@ -1,13 +1,13 @@ #include "installed_widget.h" +#include "../core/alpm_wrapper.h" +#include "../utils/logging.h" #include "package_card.h" #include "package_details_dialog.h" -#include "../core/alpm_wrapper.h" -#include "../utils/logger.h" -#include #include #include -#include #include +#include +#include InstalledWidget::InstalledWidget(QWidget* parent) : QWidget(parent) @@ -16,167 +16,186 @@ InstalledWidget::InstalledWidget(QWidget* parent) , m_contentWidget(new QWidget()) , m_gridLayout(new QGridLayout(m_contentWidget)) , m_statusLabel(new QLabel(this)) - , m_countLabel(new QLabel(this)) - , m_filterTimer(new QTimer(this)) { + , m_countLabel(new QLabel(this)) + , m_filterTimer(new QTimer(this)) +{ // debounce timer (waits 300ms after last keystroke) m_filterTimer->setSingleShot(true); m_filterTimer->setInterval(300); - connect(m_filterTimer, &QTimer::timeout, this, [this]() { - filterPackages(m_filterInput->text()); - }); + connect(m_filterTimer, &QTimer::timeout, this, [this]() { filterPackages(m_filterInput->text()); }); setupUi(); loadInstalledPackages(); } -void InstalledWidget::setupUi() { +void InstalledWidget::setupUi() +{ auto* mainLayout = new QVBoxLayout(this); - + // Header auto* headerLayout = new QHBoxLayout(); - + auto* titleLabel = new QLabel("Installed Packages", this); - titleLabel->setObjectName("view-title"); - headerLayout->addWidget(titleLabel); + titleLabel->setObjectName("view-title"); + headerLayout->addWidget(titleLabel); headerLayout->addStretch(); - + // Counter Label - m_countLabel->setObjectName("package-count-label"); - headerLayout->addWidget(m_countLabel); + m_countLabel->setObjectName("package-count-label"); + headerLayout->addWidget(m_countLabel); auto* refreshButton = new QPushButton("Refresh", this); connect(refreshButton, &QPushButton::clicked, this, &InstalledWidget::refreshPackages); headerLayout->addWidget(refreshButton); - + mainLayout->addLayout(headerLayout); - + // Filter m_filterInput->setPlaceholderText("Filter installed packages..."); m_filterInput->setMinimumHeight(35); m_filterInput->setClearButtonEnabled(true); - connect(m_filterInput, &QLineEdit::textChanged, - this, &InstalledWidget::onFilterTextChanged); + connect(m_filterInput, &QLineEdit::textChanged, this, &InstalledWidget::onFilterTextChanged); mainLayout->addWidget(m_filterInput); - + // Status label m_statusLabel->setObjectName("status-message"); - m_statusLabel->setAlignment(Qt::AlignCenter); + m_statusLabel->setAlignment(Qt::AlignCenter); m_statusLabel->setText("Loading installed packages..."); mainLayout->addWidget(m_statusLabel); - + // Results area m_scrollArea->setWidget(m_contentWidget); m_scrollArea->setWidgetResizable(true); m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - + m_gridLayout->setSpacing(15); m_gridLayout->setContentsMargins(10, 10, 10, 10); - + mainLayout->addWidget(m_scrollArea); setLayout(mainLayout); } -void InstalledWidget::loadInstalledPackages() { +void InstalledWidget::loadInstalledPackages() +{ m_statusLabel->setText("Loading installed packages..."); m_statusLabel->show(); - - (void)QtConcurrent::run([this]() { - auto packages = AlpmWrapper::instance().getInstalledPackages(); - - QMetaObject::invokeMethod(this, [this, packages]() { - m_allPackages = packages; - m_filteredPackages = packages; - - m_statusLabel->hide(); - m_countLabel->setText(QString("%1 packages installed") - .arg(packages.size())); - - filterPackages(m_filterInput->text()); - - Logger::info(QString("Loaded %1 installed packages").arg(packages.size())); - }, Qt::QueuedConnection); - }); + + (void)QtConcurrent::run( + [this]() + { + auto packages = AlpmWrapper::instance().getInstalledPackages(); + + QMetaObject::invokeMethod( + this, + [this, packages]() + { + m_allPackages = packages; + m_filteredPackages = packages; + + m_statusLabel->hide(); + m_countLabel->setText(QString("%1 packages installed").arg(packages.size())); + + filterPackages(m_filterInput->text()); + + spdlog::info("{}", (QString("Loaded %1 installed packages").arg(packages.size())).toStdString()); + }, + Qt::QueuedConnection); + }); } -void InstalledWidget::refreshPackages() { +void InstalledWidget::refreshPackages() +{ clearResults(); loadInstalledPackages(); } -void InstalledWidget::displayPackages(const QVector& packages) { +void InstalledWidget::displayPackages(const QVector& packages) +{ clearResults(); - - if (packages.isEmpty()) { + + if (packages.isEmpty()) + { m_statusLabel->setText("No packages found"); m_statusLabel->show(); return; } - + int row = 0; int col = 0; const int columns = 3; - - for (const auto& pkg : packages) { + + for (const auto& pkg : packages) + { auto* card = new PackageCard(pkg, m_contentWidget); card->updateInstallStatus(true); connect(card, &PackageCard::clicked, this, &InstalledWidget::onPackageClicked); - + m_gridLayout->addWidget(card, row, col); - + col++; - if (col >= columns) { + if (col >= columns) + { col = 0; row++; } } - + m_gridLayout->setRowStretch(row + 1, 1); } -void InstalledWidget::clearResults() { - while (auto* item = m_gridLayout->takeAt(0)) { - if (auto* widget = item->widget()) { +void InstalledWidget::clearResults() +{ + while (auto* item = m_gridLayout->takeAt(0)) + { + if (auto* widget = item->widget()) + { widget->deleteLater(); } delete item; } } -void InstalledWidget::filterPackages(const QString& query) { - if (query.isEmpty()) { +void InstalledWidget::filterPackages(const QString& query) +{ + if (query.isEmpty()) + { m_filteredPackages = m_allPackages; - } else { + } + else + { m_filteredPackages.clear(); QString lowerQuery = query.toLower(); - - for (const auto& pkg : m_allPackages) { - if (pkg.name.toLower().contains(lowerQuery) || - pkg.description.toLower().contains(lowerQuery)) { + + for (const auto& pkg : m_allPackages) + { + if (pkg.name.toLower().contains(lowerQuery) || pkg.description.toLower().contains(lowerQuery)) + { m_filteredPackages.append(pkg); } } } - - m_countLabel->setText(QString("%1 of %2 packages") - .arg(m_filteredPackages.size()) - .arg(m_allPackages.size())); - + + m_countLabel->setText(QString("%1 of %2 packages").arg(m_filteredPackages.size()).arg(m_allPackages.size())); + displayPackages(m_filteredPackages); } -void InstalledWidget::onFilterTextChanged(const QString& text) { +void InstalledWidget::onFilterTextChanged(const QString& text) +{ Q_UNUSED(text); m_filterTimer->start(); } -void InstalledWidget::onPackageClicked(const PackageInfo& info) { - Logger::info(QString("Package clicked: %1").arg(info.name)); - +void InstalledWidget::onPackageClicked(const PackageInfo& info) +{ + spdlog::info("{}", (QString("Package clicked: %1").arg(info.name)).toStdString()); + auto* dialog = new PackageDetailsDialog(info, this); - if(dialog->exec() == QDialog::Accepted) { - // Refresh after dialog closes in case package was uninstalled - refreshPackages(); + if (dialog->exec() == QDialog::Accepted) + { + // Refresh after dialog closes in case package was uninstalled + refreshPackages(); } dialog->deleteLater(); diff --git a/src/gui/installed_widget.h b/src/gui/installed_widget.h index e5e03e4..215fed0 100644 --- a/src/gui/installed_widget.h +++ b/src/gui/installed_widget.h @@ -1,14 +1,14 @@ #ifndef INSTALLED_WIDGET_H #define INSTALLED_WIDGET_H -#include -#include +#include "../utils/types.h" #include -#include #include +#include +#include #include #include -#include "../utils/types.h" +#include /** * @brief Widget displaying installed packages. @@ -17,22 +17,23 @@ * - All Qt widget members use Qt parent-child ownership (raw pointers are non-owning) * - Package cards are dynamically created/destroyed in displayPackages/clearResults */ -class InstalledWidget : public QWidget { +class InstalledWidget : public QWidget +{ Q_OBJECT - + public: explicit InstalledWidget(QWidget* parent = nullptr); ~InstalledWidget() override = default; - + void refreshPackages(); - + private: void setupUi(); void loadInstalledPackages(); void displayPackages(const QVector& packages); void filterPackages(const QString& query); void clearResults(); - + // Qt parent-child managed widgets (non-owning pointers) QLineEdit* m_filterInput = nullptr; QScrollArea* m_scrollArea = nullptr; @@ -40,15 +41,15 @@ class InstalledWidget : public QWidget { QGridLayout* m_gridLayout = nullptr; QLabel* m_statusLabel = nullptr; QLabel* m_countLabel = nullptr; - + QVector m_allPackages; QVector m_filteredPackages; QTimer* m_filterTimer; - + private slots: void onPackageClicked(const PackageInfo& info); void onFilterTextChanged(const QString& text); }; -#endif // INSTALLED_WIDGET_H +#endif // INSTALLED_WIDGET_H diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 01caaa8..5d1e0df 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -1,158 +1,184 @@ #include "mainwindow.h" +#include "../core/alpm_wrapper.h" +#include "../utils/logging.h" #include "home_widget.h" -#include "search_widget.h" #include "installed_widget.h" -#include "updates_widget.h" +#include "search_widget.h" #include "settings_widget.h" -#include "../utils/logger.h" -#include "../core/alpm_wrapper.h" -#include -#include +#include "updates_widget.h" +#include "utils/version.h" #include -#include #include +#include +#include +#include #include MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) - , m_tabWidget(std::make_unique(this)) { - + , m_tabWidget(std::make_unique(this)) +{ + // Initialize ALPM before creating widgets that might need it - if (!AlpmWrapper::instance().initialize()) { - QMessageBox::critical(this, "Error", - "Failed to initialize package manager. Please check your system configuration."); - Logger::error("Failed to initialize ALPM in MainWindow"); + if (!AlpmWrapper::instance().initialize()) + { + QMessageBox::critical( + this, "Error", "Failed to initialize package manager. Please check your system configuration."); + spdlog::error("Failed to initialize ALPM in MainWindow"); } - + setupUi(); loadStyleSheet(); - - Logger::info("MainWindow created successfully"); + + spdlog::info("MainWindow created successfully"); } -MainWindow::~MainWindow() { +MainWindow::~MainWindow() +{ AlpmWrapper::instance().release(); - Logger::info("MainWindow destroyed"); + spdlog::info("MainWindow destroyed"); } -void MainWindow::setupUi() { - setWindowTitle("ALG App Store (Beta)"); +void MainWindow::setupUi() +{ + setWindowTitle("Explorer (Beta)"); setMinimumSize(1024, 768); resize(1124, 868); - + // Create widgets m_homeWidget = new HomeWidget(this); m_searchWidget = new SearchWidget(this); m_installedWidget = new InstalledWidget(this); m_updatesWidget = new UpdatesWidget(this); m_settingsWidget = new SettingsWidget(this); - + // Add tabs m_tabWidget->addTab(m_homeWidget, "Home"); m_tabWidget->addTab(m_searchWidget, "Search"); m_tabWidget->addTab(m_installedWidget, "Installed"); m_tabWidget->addTab(m_updatesWidget, "Updates"); m_tabWidget->addTab(m_settingsWidget, "Settings"); - + // Connect settings signals - connect(m_settingsWidget, &SettingsWidget::multilibStatusChanged, - this, [this]() { - m_searchWidget->updateRepositoryList( - m_settingsWidget->isMultilibEnabled(), - m_settingsWidget->isChaoticAurEnabled() - ); - }); - - connect(m_settingsWidget, &SettingsWidget::chaoticAurStatusChanged, - this, [this]() { - m_searchWidget->updateRepositoryList( - m_settingsWidget->isMultilibEnabled(), - m_settingsWidget->isChaoticAurEnabled() - ); - }); - + connect(m_settingsWidget, + &SettingsWidget::multilibStatusChanged, + this, + [this]() + { + m_searchWidget->updateRepositoryList(m_settingsWidget->isMultilibEnabled(), + m_settingsWidget->isChaoticAurEnabled()); + }); + + connect(m_settingsWidget, + &SettingsWidget::chaoticAurStatusChanged, + this, + [this]() + { + m_searchWidget->updateRepositoryList(m_settingsWidget->isMultilibEnabled(), + m_settingsWidget->isChaoticAurEnabled()); + }); + // Initialize search widget with current repository states - m_searchWidget->updateRepositoryList( - m_settingsWidget->isMultilibEnabled(), - m_settingsWidget->isChaoticAurEnabled() - ); - - m_tabWidget->setDocumentMode(true); - m_tabWidget->tabBar()->setExpanding(true); + m_searchWidget->updateRepositoryList(m_settingsWidget->isMultilibEnabled(), + m_settingsWidget->isChaoticAurEnabled()); + + m_tabWidget->setDocumentMode(true); + m_tabWidget->tabBar()->setExpanding(true); m_tabWidget->setTabPosition(QTabWidget::North); m_tabWidget->setMovable(false); - + setCentralWidget(m_tabWidget.get()); createMenuBar(); } -void MainWindow::createMenuBar() { +void MainWindow::createMenuBar() +{ auto* fileMenu = menuBar()->addMenu("&File"); - + auto* refreshAction = new QAction("&Refresh", this); refreshAction->setShortcut(QKeySequence::Refresh); - connect(refreshAction, &QAction::triggered, [this]() { - int currentIndex = m_tabWidget->currentIndex(); - if (currentIndex == 0) { - // Home widget refresh - } else if (currentIndex == 1) { - // Search widget refresh - } else if (currentIndex == 2) { - m_installedWidget->refreshPackages(); - } else if (currentIndex == 3) { - m_updatesWidget->checkForUpdates(); - } - }); + connect(refreshAction, + &QAction::triggered, + [this]() + { + int currentIndex = m_tabWidget->currentIndex(); + if (currentIndex == 0) + { + // Home widget refresh + } + else if (currentIndex == 1) + { + // Search widget refresh + } + else if (currentIndex == 2) + { + m_installedWidget->refreshPackages(); + } + else if (currentIndex == 3) + { + m_updatesWidget->checkForUpdates(); + } + }); fileMenu->addAction(refreshAction); - + fileMenu->addSeparator(); - + auto* quitAction = new QAction("&Quit", this); quitAction->setShortcut(QKeySequence::Quit); connect(quitAction, &QAction::triggered, this, &QMainWindow::close); fileMenu->addAction(quitAction); - + auto* helpMenu = menuBar()->addMenu("&Help"); - + auto* aboutAction = new QAction("&About", this); - connect(aboutAction, &QAction::triggered, [this]() { - QMessageBox::about(this, "About ALG App Store", - "ALG App Store (Beta)\n\n" - "A modern package manager for Arch Linux\n" - "Version: 0.2.30\n" - "Built with Qt6 and C++17\n\n" - "© 2025 Arka Linux GUI"); - }); + connect(aboutAction, + &QAction::triggered, + [this]() + { + QMessageBox::about(this, + "About Explorer", + "Explorer (Beta)\n\n" + "A modern package manager for Arch Linux\n" + "Version: " APP_VERSION "\n" + "Built with Qt6 and C++20\n\n" + "© 2025 Arka Linux GUI"); + }); helpMenu->addAction(aboutAction); } -void MainWindow::loadStyleSheet() { - QStringList styleFiles = { - ":/resource/styles/base.qss", - ":/resource/styles/navigation.qss", - ":/resource/styles/components.qss", - ":/resource/styles/containers.qss" - }; +void MainWindow::loadStyleSheet() +{ + QStringList styleFiles = { ":/resource/styles/base.qss", + ":/resource/styles/navigation.qss", + ":/resource/styles/components.qss", + ":/resource/styles/containers.qss" }; QString combinedStyleSheet; - bool anyLoaded = false; - - for (const QString &path : styleFiles) { + bool anyLoaded = false; + + for (const QString& path : styleFiles) + { QFile file(path); - if (file.open(QFile::ReadOnly | QFile::Text)) { + if (file.open(QFile::ReadOnly | QFile::Text)) + { combinedStyleSheet += QLatin1String(file.readAll()); file.close(); anyLoaded = true; - } else { - Logger::warning(QString("Could not load style module: %1").arg(path)); + } + else + { + spdlog::warn("{}", (QString("Could not load style module: %1").arg(path)).toStdString()); } } - if (anyLoaded) { + if (anyLoaded) + { qApp->setStyleSheet(combinedStyleSheet); - Logger::info("Modular stylesheets loaded and combined successfully from resources."); - } else { - Logger::error("Failed to load any stylesheet modules from resources!"); + spdlog::info("Modular stylesheets loaded and combined successfully from resources."); + } + else + { + spdlog::error("Failed to load any stylesheet modules from resources!"); } } diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h index cf6d531..f522981 100644 --- a/src/gui/mainwindow.h +++ b/src/gui/mainwindow.h @@ -13,27 +13,28 @@ class UpdatesWidget; class SettingsWidget; /** - * @brief Main application window for ALG App Store. + * @brief Main application window for Explorer. * * Memory Management: * - m_tabWidget: Owned by std::unique_ptr (central widget) * - Child widgets (m_homeWidget, etc.): Owned by Qt parent-child hierarchy * through m_tabWidget. Raw pointers are used as non-owning references. */ -class MainWindow : public QMainWindow { +class MainWindow : public QMainWindow +{ Q_OBJECT - + public: explicit MainWindow(QWidget* parent = nullptr); ~MainWindow() override; - + private: void setupUi(); void createMenuBar(); void loadStyleSheet(); - + std::unique_ptr m_tabWidget; - + // Non-owning pointers - owned by m_tabWidget via Qt parent-child hierarchy HomeWidget* m_homeWidget = nullptr; SearchWidget* m_searchWidget = nullptr; @@ -42,4 +43,4 @@ class MainWindow : public QMainWindow { SettingsWidget* m_settingsWidget = nullptr; }; -#endif // MAINWINDOW_H +#endif // MAINWINDOW_H diff --git a/src/gui/package_card.cpp b/src/gui/package_card.cpp index 0da8c7c..94f2c68 100644 --- a/src/gui/package_card.cpp +++ b/src/gui/package_card.cpp @@ -1,12 +1,12 @@ #include "package_card.h" #include "../core/alpm_wrapper.h" -#include #include +#include #include +#include #include #include -#include -#include +#include PackageCard::PackageCard(const PackageInfo& info, QWidget* parent) : QWidget(parent) @@ -15,100 +15,112 @@ PackageCard::PackageCard(const PackageInfo& info, QWidget* parent) , m_descriptionLabel(new QLabel(this)) , m_versionLabel(new QLabel(this)) , m_repositoryLabel(new QLabel(this)) - , m_statusLabel(new QLabel(this)) { - + , m_statusLabel(new QLabel(this)) +{ + setupUi(); checkInstallStatus(); } -void PackageCard::setupUi() { +void PackageCard::setupUi() +{ setMinimumHeight(180); setMaximumHeight(220); setMinimumWidth(280); setCursor(Qt::PointingHandCursor); - + setProperty("class", "package-card"); - + auto* mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(15, 15, 15, 15); mainLayout->setSpacing(10); - + // Header with name and status auto* headerLayout = new QHBoxLayout(); - + m_nameLabel->setText(m_info.name); m_nameLabel->setObjectName("card-name"); - m_nameLabel->setWordWrap(false); + m_nameLabel->setWordWrap(false); m_nameLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); headerLayout->addWidget(m_nameLabel, 1); - + m_statusLabel->setProperty("class", "status-badge"); m_statusLabel->hide(); headerLayout->addWidget(m_statusLabel, 0); - + mainLayout->addLayout(headerLayout); - + // Description - with word wrap and proper sizing m_descriptionLabel->setText(m_info.description); m_descriptionLabel->setObjectName("card-description"); - m_descriptionLabel->setWordWrap(true); + m_descriptionLabel->setWordWrap(true); m_descriptionLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_descriptionLabel->setMinimumHeight(40); m_descriptionLabel->setMaximumHeight(70); mainLayout->addWidget(m_descriptionLabel, 1); - + // Version m_versionLabel->setText(QString("Version: %1").arg(m_info.version)); m_versionLabel->setObjectName("card-version"); - mainLayout->addWidget(m_versionLabel, 0); + mainLayout->addWidget(m_versionLabel, 0); // Repository badge m_repositoryLabel->setText(m_info.repository); m_repositoryLabel->setProperty("class", "repo-badge"); m_repositoryLabel->setAlignment(Qt::AlignLeft); mainLayout->addWidget(m_repositoryLabel, 0); - + setLayout(mainLayout); } -void PackageCard::checkInstallStatus() { +void PackageCard::checkInstallStatus() +{ m_isInstalled = AlpmWrapper::instance().isPackageInstalled(m_info.name); updateInstallStatus(m_isInstalled); } -void PackageCard::updateInstallStatus(bool installed) { +void PackageCard::updateInstallStatus(bool installed) +{ m_isInstalled = installed; - - if (m_isInstalled) { + + if (m_isInstalled) + { m_statusLabel->setText("Installed"); m_statusLabel->show(); - } else { + } + else + { m_statusLabel->hide(); } } -void PackageCard::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { +void PackageCard::mousePressEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) + { emit clicked(m_info); } QWidget::mousePressEvent(event); } -void PackageCard::enterEvent(QEnterEvent* event) { +void PackageCard::enterEvent(QEnterEvent* event) +{ setProperty("hovered", true); style()->unpolish(this); style()->polish(this); QWidget::enterEvent(event); } -void PackageCard::leaveEvent(QEvent* event) { +void PackageCard::leaveEvent(QEvent* event) +{ setProperty("hovered", false); style()->unpolish(this); style()->polish(this); QWidget::leaveEvent(event); } -void PackageCard::paintEvent(QPaintEvent*) { +void PackageCard::paintEvent(QPaintEvent*) +{ QStyleOption opt; opt.initFrom(this); QPainter p(this); diff --git a/src/gui/package_card.h b/src/gui/package_card.h index ca6fc27..2087b0e 100644 --- a/src/gui/package_card.h +++ b/src/gui/package_card.h @@ -1,11 +1,11 @@ #ifndef PACKAGE_CARD_H #define PACKAGE_CARD_H -#include +#include "../utils/types.h" #include -#include #include -#include "../utils/types.h" +#include +#include /** * @brief A clickable card widget displaying package information. @@ -13,39 +13,40 @@ * Memory Management: * - All Qt widget members use Qt parent-child ownership (raw pointers are non-owning) */ -class PackageCard : public QWidget { +class PackageCard : public QWidget +{ Q_OBJECT - + public: explicit PackageCard(const PackageInfo& info, QWidget* parent = nullptr); ~PackageCard() override = default; - + const PackageInfo& packageInfo() const { return m_info; } void updateInstallStatus(bool installed); void checkInstallStatus(); - + signals: void clicked(const PackageInfo& info); - + protected: void mousePressEvent(QMouseEvent* event) override; void enterEvent(QEnterEvent* event) override; void leaveEvent(QEvent* event) override; - void paintEvent(QPaintEvent*) override; + void paintEvent(QPaintEvent*) override; private: void setupUi(); - + PackageInfo m_info; - + // Qt parent-child managed widgets (non-owning pointers) QLabel* m_nameLabel = nullptr; QLabel* m_descriptionLabel = nullptr; QLabel* m_versionLabel = nullptr; QLabel* m_repositoryLabel = nullptr; QLabel* m_statusLabel = nullptr; - + bool m_isInstalled = false; }; -#endif // PACKAGE_CARD_H +#endif // PACKAGE_CARD_H diff --git a/src/gui/package_details_dialog.cpp b/src/gui/package_details_dialog.cpp index a50c355..f67a370 100644 --- a/src/gui/package_details_dialog.cpp +++ b/src/gui/package_details_dialog.cpp @@ -1,27 +1,28 @@ #include "package_details_dialog.h" #include "../core/alpm_wrapper.h" #include "../core/package_manager.h" -#include "../utils/logger.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "../core/progress_parser.h" +#include "../utils/logging.h" #include -#include -#include +#include #include -#include #include +#include +#include +#include +#include +#include #include -#include +#include +#include #include +#include +#include +#include +#include #include +#include +#include PackageDetailsDialog::PackageDetailsDialog(const PackageInfo& info, QWidget* parent) : QDialog(parent) @@ -44,54 +45,60 @@ PackageDetailsDialog::PackageDetailsDialog(const PackageInfo& info, QWidget* par , m_progressWidget(new QWidget(this)) , m_logViewer(new QTextEdit(this)) , m_toggleLogButton(new QPushButton("Show Logs", this)) - , m_logWidget(new QWidget(this)) { - + , m_logWidget(new QWidget(this)) +{ + setupUi(); checkInstallStatus(); updateButtonStates(); - + // Connect to PackageManager signals - connect(&PackageManager::instance(), &PackageManager::operationStarted, - this, &PackageDetailsDialog::onOperationStarted); - connect(&PackageManager::instance(), &PackageManager::operationOutput, - this, &PackageDetailsDialog::onOperationOutput); - connect(&PackageManager::instance(), &PackageManager::operationCompleted, - this, &PackageDetailsDialog::onOperationCompleted); - connect(&PackageManager::instance(), &PackageManager::operationError, - this, &PackageDetailsDialog::onOperationError); + connect(&PackageManager::instance(), + &PackageManager::operationStarted, + this, + &PackageDetailsDialog::onOperationStarted); + connect( + &PackageManager::instance(), &PackageManager::operationOutput, this, &PackageDetailsDialog::onOperationOutput); + connect(&PackageManager::instance(), + &PackageManager::operationCompleted, + this, + &PackageDetailsDialog::onOperationCompleted); + connect( + &PackageManager::instance(), &PackageManager::operationError, this, &PackageDetailsDialog::onOperationError); } -void PackageDetailsDialog::setupUi() { +void PackageDetailsDialog::setupUi() +{ setWindowTitle("Package Details"); setMinimumSize(700, 600); setModal(true); - + // Remove window icon setWindowIcon(QIcon()); - + auto* dialogLayout = new QVBoxLayout(this); dialogLayout->setContentsMargins(0, 0, 0, 0); dialogLayout->setSpacing(0); - + // Create scroll area for content auto* scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setFrameShape(QFrame::NoFrame); - + // Content widget auto* contentWidget = new QWidget(); auto* mainLayout = new QVBoxLayout(contentWidget); mainLayout->setContentsMargins(20, 30, 20, 20); mainLayout->setSpacing(20); - + // Header with name and repository badge auto* headerLayout = new QHBoxLayout(); m_nameLabel->setText(m_info.name); - m_nameLabel->setObjectName("details-title"); + m_nameLabel->setObjectName("details-title"); headerLayout->addWidget(m_nameLabel); headerLayout->addStretch(); - + // Repository badge (member so we can update it later) m_repositoryLabel->setText(m_info.repository); m_repositoryLabel->setProperty("class", "repo-badge"); @@ -102,33 +109,33 @@ void PackageDetailsDialog::setupUi() { m_statusBadge->setProperty("class", "status-badge"); m_statusBadge->hide(); headerLayout->addWidget(m_statusBadge); - + mainLayout->addLayout(headerLayout); - + // Description right under the name auto* descLabel = new QLabel(m_info.description, this); - descLabel->setObjectName("details-desc"); + descLabel->setObjectName("details-desc"); descLabel->setWordWrap(true); mainLayout->addWidget(descLabel); - + // Separator line auto* line1 = new QFrame(this); line1->setFrameShape(QFrame::HLine); line1->setObjectName("details-separator"); mainLayout->addWidget(line1); - + // Package Details section - 2x2 grid layout auto* detailsTitle = new QLabel("Package Details", this); detailsTitle->setObjectName("section-header"); mainLayout->addWidget(detailsTitle); - + auto* infoWidget = new QWidget(this); auto* infoGrid = new QGridLayout(infoWidget); infoGrid->setSpacing(15); infoGrid->setContentsMargins(0, 10, 0, 10); infoGrid->setColumnStretch(1, 1); infoGrid->setColumnStretch(3, 1); - + // Version (top-left) auto* versionTitle = new QLabel("Version", this); versionTitle->setProperty("class", "detail-label"); @@ -136,9 +143,10 @@ void PackageDetailsDialog::setupUi() { m_versionLabel->setProperty("class", "detail-value"); infoGrid->addWidget(versionTitle, 0, 0, Qt::AlignTop); infoGrid->addWidget(m_versionLabel, 0, 1); - + // Maintainer (top-right) - if (!m_info.maintainer.isEmpty()) { + if (!m_info.maintainer.isEmpty()) + { auto* maintainerTitle = new QLabel("Maintainer", this); maintainerTitle->setProperty("class", "detail-label"); m_maintainerLabel->setText(m_info.maintainer); @@ -146,13 +154,13 @@ void PackageDetailsDialog::setupUi() { infoGrid->addWidget(maintainerTitle, 0, 2, Qt::AlignTop); infoGrid->addWidget(m_maintainerLabel, 0, 3); } - + // Upstream URL (bottom-left) - if (!m_info.upstreamUrl.isEmpty()) { + if (!m_info.upstreamUrl.isEmpty()) + { auto* urlTitle = new QLabel("Upstream URL", this); urlTitle->setProperty("class", "detail-label"); - m_urlLabel->setText(QString("%1") - .arg(m_info.upstreamUrl)); + m_urlLabel->setText(QString("%1").arg(m_info.upstreamUrl)); m_urlLabel->setOpenExternalLinks(true); m_urlLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); m_urlLabel->setWordWrap(true); @@ -160,9 +168,10 @@ void PackageDetailsDialog::setupUi() { infoGrid->addWidget(urlTitle, 1, 0, Qt::AlignTop); infoGrid->addWidget(m_urlLabel, 1, 1); } - + // Last Updated (bottom-right) - if (!m_info.lastUpdated.isNull()) { + if (!m_info.lastUpdated.isNull()) + { auto* updatedTitle = new QLabel("Last Updated", this); updatedTitle->setProperty("class", "detail-label"); m_lastUpdatedLabel->setText(m_info.lastUpdated.toString("MMM. d, yyyy, h a")); @@ -170,62 +179,64 @@ void PackageDetailsDialog::setupUi() { infoGrid->addWidget(updatedTitle, 1, 2, Qt::AlignTop); infoGrid->addWidget(m_lastUpdatedLabel, 1, 3); } - + mainLayout->addWidget(infoWidget); - + // Separator line auto* line2 = new QFrame(this); line2->setFrameShape(QFrame::HLine); line2->setObjectName("details-separator"); mainLayout->addWidget(line2); - + // Dependencies section - if (!m_info.dependList.isEmpty()) { + if (!m_info.dependList.isEmpty()) + { auto* depsTitle = new QLabel("Dependencies", this); depsTitle->setObjectName("section-header"); mainLayout->addWidget(depsTitle); - + m_dependenciesText->setPlainText(m_info.dependList.join("\n")); m_dependenciesText->setReadOnly(true); m_dependenciesText->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_dependenciesText->setObjectName("details-text-area"); - + m_dependenciesText->setObjectName("details-text-area"); + // Adjust height to fit all dependencies without scrolling QFontMetrics fm(m_dependenciesText->font()); int lineHeight = fm.lineSpacing(); int numLines = m_info.dependList.size(); - int contentHeight = (numLines * lineHeight) + 20; // +20 for padding + int contentHeight = (numLines * lineHeight) + 20; // +20 for padding m_dependenciesText->setMinimumHeight(contentHeight); m_dependenciesText->setMaximumHeight(contentHeight); - + mainLayout->addWidget(m_dependenciesText); - + // Separator line auto* line3 = new QFrame(this); line3->setFrameShape(QFrame::HLine); line3->setObjectName("details-separator"); - mainLayout->addWidget(line3); + mainLayout->addWidget(line3); } - + // Command section auto* commandTitle = new QLabel("Command", this); commandTitle->setObjectName("section-header"); mainLayout->addWidget(commandTitle); - + // Command buttons and text auto* commandWidget = new QWidget(this); auto* commandLayout = new QVBoxLayout(commandWidget); commandLayout->setContentsMargins(0, 0, 0, 0); commandLayout->setSpacing(10); - + // Determine which package managers to show bool isAUR = (m_info.repository.toLower() == "aur"); QString command; - - if (isAUR) { + + if (isAUR) + { // Show yay and paru for AUR auto* buttonLayout = new QHBoxLayout(); - + auto* yayButton = new QPushButton("yay", this); yayButton->setCheckable(true); yayButton->setChecked(true); @@ -238,192 +249,212 @@ void PackageDetailsDialog::setupUi() { buttonLayout->addWidget(yayButton); buttonLayout->addWidget(paruButton); buttonLayout->addStretch(); - + commandLayout->addLayout(buttonLayout); - + command = QString("yay -S %1").arg(m_info.name); auto* commandText = new QLabel(this); commandText->setText(command); - commandText->setObjectName("command-display"); - commandText->setTextInteractionFlags(Qt::TextSelectableByMouse); + commandText->setObjectName("command-display"); + commandText->setTextInteractionFlags(Qt::TextSelectableByMouse); commandLayout->addWidget(commandText); - + // Connect buttons to update command - connect(yayButton, &QPushButton::clicked, [yayButton, paruButton, commandText, this]() { - yayButton->setChecked(true); - paruButton->setChecked(false); - commandText->setText(QString("yay -S %1").arg(m_info.name)); - }); - - connect(paruButton, &QPushButton::clicked, [yayButton, paruButton, commandText, this]() { - paruButton->setChecked(true); - yayButton->setChecked(false); - commandText->setText(QString("paru -S %1").arg(m_info.name)); - }); - - } else { + connect(yayButton, + &QPushButton::clicked, + [yayButton, paruButton, commandText, this]() + { + yayButton->setChecked(true); + paruButton->setChecked(false); + commandText->setText(QString("yay -S %1").arg(m_info.name)); + }); + + connect(paruButton, + &QPushButton::clicked, + [yayButton, paruButton, commandText, this]() + { + paruButton->setChecked(true); + yayButton->setChecked(false); + commandText->setText(QString("paru -S %1").arg(m_info.name)); + }); + } + else + { // Show pacman for official repos - auto* buttonLayout = new QHBoxLayout(); + auto* buttonLayout = new QHBoxLayout(); auto* pacmanButton = new QPushButton("pacman", this); pacmanButton->setCheckable(true); pacmanButton->setChecked(true); - pacmanButton->setProperty("class", "command-selector-btn"); + pacmanButton->setProperty("class", "command-selector-btn"); - buttonLayout->addWidget(pacmanButton); - buttonLayout->addStretch(); + buttonLayout->addWidget(pacmanButton); + buttonLayout->addStretch(); - commandLayout->addLayout(buttonLayout); + commandLayout->addLayout(buttonLayout); command = QString("sudo pacman -S %1").arg(m_info.name); auto* commandText = new QLabel(command, this); commandText->setObjectName("command-display"); - commandText->setTextInteractionFlags(Qt::TextSelectableByMouse); + commandText->setTextInteractionFlags(Qt::TextSelectableByMouse); commandLayout->addWidget(commandText); } - + mainLayout->addWidget(commandWidget); - - auto* noteLabel = new QLabel("Please ensure your system meets the minimum requirements before installation.", contentWidget); + + auto* noteLabel + = new QLabel("Please ensure your system meets the minimum requirements before installation.", contentWidget); noteLabel->setProperty("class", "footer-note"); noteLabel->setWordWrap(true); mainLayout->addWidget(noteLabel); - + mainLayout->addStretch(); - + // Set content widget to scroll area contentWidget->setLayout(mainLayout); scrollArea->setWidget(contentWidget); dialogLayout->addWidget(scrollArea, 1); - + // Buttons at the bottom (not scrollable) auto* buttonWidget = new QWidget(this); auto* buttonLayout = new QHBoxLayout(buttonWidget); buttonLayout->setContentsMargins(20, 10, 20, 20); buttonLayout->addStretch(); - + m_installButton->setMinimumWidth(100); m_installButton->setMinimumHeight(35); m_installButton->setProperty("class", "primary-btn"); connect(m_installButton, &QPushButton::clicked, this, &PackageDetailsDialog::onInstall); buttonLayout->addWidget(m_installButton); - + m_uninstallButton->setMinimumWidth(100); m_uninstallButton->setMinimumHeight(35); m_uninstallButton->setProperty("class", "danger-btn"); connect(m_uninstallButton, &QPushButton::clicked, this, &PackageDetailsDialog::onUninstall); buttonLayout->addWidget(m_uninstallButton); - + m_launchButton->setMinimumWidth(100); m_launchButton->setMinimumHeight(35); connect(m_launchButton, &QPushButton::clicked, this, &PackageDetailsDialog::launchApplication); buttonLayout->addWidget(m_launchButton); - + m_closeButton->setMinimumWidth(100); m_closeButton->setMinimumHeight(35); connect(m_closeButton, &QPushButton::clicked, this, &QDialog::reject); buttonLayout->addWidget(m_closeButton); - + dialogLayout->addWidget(buttonWidget, 0); - + // Progress bar section (hidden by default) auto* progressLayout = new QVBoxLayout(m_progressWidget); progressLayout->setContentsMargins(20, 0, 20, 20); progressLayout->setSpacing(8); - + m_progressLabel->setProperty("class", "detail-label"); - m_progressLabel->setAlignment(Qt::AlignCenter); + m_progressLabel->setAlignment(Qt::AlignCenter); progressLayout->addWidget(m_progressLabel); - + m_progressBar->setMinimumHeight(20); m_progressBar->setMaximumHeight(20); m_progressBar->setTextVisible(true); m_progressBar->setFormat("%p%"); - m_progressBar->setObjectName("details-progress"); - progressLayout->addWidget(m_progressBar); - + m_progressBar->setObjectName("details-progress"); + progressLayout->addWidget(m_progressBar); + // Toggle log button m_toggleLogButton->setProperty("class", "link-button"); - connect(m_toggleLogButton, &QPushButton::clicked, this, &PackageDetailsDialog::toggleLogViewer); + connect(m_toggleLogButton, &QPushButton::clicked, this, &PackageDetailsDialog::toggleLogViewer); progressLayout->addWidget(m_toggleLogButton, 0, Qt::AlignCenter); - + m_progressWidget->hide(); dialogLayout->addWidget(m_progressWidget, 0); - + // Log viewer section (hidden by default) auto* logLayout = new QVBoxLayout(m_logWidget); logLayout->setContentsMargins(20, 0, 20, 20); logLayout->setSpacing(8); - + m_logViewer->setReadOnly(true); m_logViewer->setMaximumHeight(200); m_logViewer->setObjectName("log-viewer"); - logLayout->addWidget(m_logViewer); - + logLayout->addWidget(m_logViewer); + m_logWidget->hide(); dialogLayout->addWidget(m_logWidget, 0); - + setLayout(dialogLayout); } -void PackageDetailsDialog::checkInstallStatus() { +void PackageDetailsDialog::checkInstallStatus() +{ m_isInstalled = AlpmWrapper::instance().isPackageInstalled(m_info.name); // Update the installed badge in the header - if (m_isInstalled) { + if (m_isInstalled) + { m_statusBadge->show(); - } else { + } + else + { m_statusBadge->hide(); } } -void PackageDetailsDialog::updateButtonStates() { +void PackageDetailsDialog::updateButtonStates() +{ m_installButton->setEnabled(!m_isInstalled); m_uninstallButton->setEnabled(m_isInstalled); - + // Enable launch button only if installed and has a desktop file QString desktopFile = findDesktopFile(); m_launchButton->setEnabled(m_isInstalled && !desktopFile.isEmpty()); } -void PackageDetailsDialog::onInstall() { - auto reply = QMessageBox::question(this, "Install Package", - QString("Are you sure you want to install %1?").arg(m_info.name), - QMessageBox::Yes | QMessageBox::No); - - if (reply == QMessageBox::Yes) { - Logger::info(QString("Installing package: %1").arg(m_info.name)); - +void PackageDetailsDialog::onInstall() +{ + auto reply = QMessageBox::question(this, + "Install Package", + QString("Are you sure you want to install %1?").arg(m_info.name), + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) + { + spdlog::info("{}", (QString("Installing package: %1").arg(m_info.name)).toStdString()); + // Disable buttons during operation m_installButton->setEnabled(false); m_uninstallButton->setEnabled(false); m_launchButton->setEnabled(false); m_closeButton->setEnabled(false); - + PackageManager::instance().installPackage(m_info.name, m_info.repository); } } -void PackageDetailsDialog::onUninstall() { - auto reply = QMessageBox::question(this, "Uninstall Package", - QString("Are you sure you want to uninstall %1?\n\n" - "This will remove the package and skip dependency checks.") - .arg(m_info.name), - QMessageBox::Yes | QMessageBox::No); - - if (reply == QMessageBox::Yes) { - Logger::info(QString("Uninstalling package: %1").arg(m_info.name)); - +void PackageDetailsDialog::onUninstall() +{ + auto reply = QMessageBox::question(this, + "Uninstall Package", + QString("Are you sure you want to uninstall %1?\n\n" + "This will remove the package and skip dependency checks.") + .arg(m_info.name), + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) + { + spdlog::info("{}", (QString("Uninstalling package: %1").arg(m_info.name)).toStdString()); + // Disable buttons during operation m_installButton->setEnabled(false); m_uninstallButton->setEnabled(false); m_launchButton->setEnabled(false); m_closeButton->setEnabled(false); - + PackageManager::instance().uninstallPackage(m_info.name, m_info.repository); } } -void PackageDetailsDialog::showProgress(const QString& message) { +void PackageDetailsDialog::showProgress(const QString& message) +{ m_progressLabel->setText(message); m_progressBar->setRange(0, 100); m_progressBar->setValue(0); @@ -436,7 +467,8 @@ void PackageDetailsDialog::showProgress(const QString& message) { m_currentPackage = 0; } -void PackageDetailsDialog::hideProgress() { +void PackageDetailsDialog::hideProgress() +{ // Hide the progress bar and label, but keep the widget and toggle button visible m_progressBar->hide(); m_progressLabel->hide(); @@ -445,82 +477,78 @@ void PackageDetailsDialog::hideProgress() { // This allows users to review logs after operation completes } -void PackageDetailsDialog::toggleLogViewer() { +void PackageDetailsDialog::toggleLogViewer() +{ m_logVisible = !m_logVisible; - if (m_logVisible) { + if (m_logVisible) + { m_logWidget->show(); m_toggleLogButton->setText("Hide Logs"); - } else { + } + else + { m_logWidget->hide(); m_toggleLogButton->setText("Show Logs"); } } -void PackageDetailsDialog::parseProgressOutput(const QString& output) { - // Parse pacman/yay/paru output for progress information - - // Pattern: "downloading..." or "installing..." - if (output.contains("downloading", Qt::CaseInsensitive)) { - m_progressLabel->setText("Downloading packages..."); - } else if (output.contains("installing", Qt::CaseInsensitive)) { - m_progressLabel->setText("Installing packages..."); - } else if (output.contains("building", Qt::CaseInsensitive)) { - m_progressLabel->setText("Building packages..."); - } else if (output.contains("checking", Qt::CaseInsensitive)) { - m_progressLabel->setText("Checking dependencies..."); - } else if (output.contains("resolving", Qt::CaseInsensitive)) { - m_progressLabel->setText("Resolving dependencies..."); +void PackageDetailsDialog::parseProgressOutput(const QString& output) +{ + // Parsing itself lives in core/progress_parser.{h,cpp} (pure logic, unit + // tested there); this just applies the result to the dialog's widgets. + const ProgressParseResult result = parseOperationProgress(output); + + if (result.statusText) + { + m_progressLabel->setText(*result.statusText); } - - // Pattern: "(1/5)" or "( 1/5)" to track package progress - QRegularExpression packagePattern(R"(\(\s*(\d+)/(\d+)\))"); - auto match = packagePattern.match(output); - if (match.hasMatch()) { - m_currentPackage = match.captured(1).toInt(); - m_totalPackages = match.captured(2).toInt(); - - if (m_totalPackages > 0) { - int percentage = (m_currentPackage * 100) / m_totalPackages; - m_progressBar->setValue(percentage); - } + + if (result.currentPackage) + { + m_currentPackage = *result.currentPackage; } - - // Pattern: "[##########] 100%" for download progress - QRegularExpression percentPattern(R"(\s+(\d+)%\s*)"); - auto percentMatch = percentPattern.match(output); - if (percentMatch.hasMatch()) { - int percentage = percentMatch.captured(1).toInt(); - m_progressBar->setValue(percentage); + if (result.totalPackages) + { + m_totalPackages = *result.totalPackages; + } + + if (result.progressPercent) + { + m_progressBar->setValue(*result.progressPercent); } } -void PackageDetailsDialog::onOperationStarted(const QString& message) { +void PackageDetailsDialog::onOperationStarted(const QString& message) +{ showProgress(message); } -void PackageDetailsDialog::onOperationOutput(const QString& output) { - if (output.trimmed().isEmpty()) { +void PackageDetailsDialog::onOperationOutput(const QString& output) +{ + if (output.trimmed().isEmpty()) + { return; } - + // Add to log viewer m_logViewer->append(output.trimmed()); - + // Auto-scroll to bottom QTextCursor cursor = m_logViewer->textCursor(); cursor.movePosition(QTextCursor::End); m_logViewer->setTextCursor(cursor); - + // Parse output for progress information parseProgressOutput(output); - + // Force UI update m_progressLabel->repaint(); m_progressBar->repaint(); QCoreApplication::processEvents(); } -void PackageDetailsDialog::onOperationCompleted(bool success, const QString& message) { +void PackageDetailsDialog::onOperationCompleted(bool success, const QString& message) +{ // Refresh ALPM state so subsequent queries reflect the change AlpmWrapper::instance().release(); AlpmWrapper::instance().initialize(); @@ -534,14 +562,18 @@ void PackageDetailsDialog::onOperationCompleted(bool success, const QString& mes // Re-enable close button m_closeButton->setEnabled(true); - if (success) { + if (success) + { QMessageBox::information(this, "Success", message); - } else { + } + else + { QMessageBox::warning(this, "Operation Failed", message); } } -void PackageDetailsDialog::onOperationError(const QString& error) { +void PackageDetailsDialog::onOperationError(const QString& error) +{ // Refresh ALPM state (best-effort) AlpmWrapper::instance().release(); AlpmWrapper::instance().initialize(); @@ -558,222 +590,270 @@ void PackageDetailsDialog::onOperationError(const QString& error) { QMessageBox::critical(this, "Error", error); } -QString PackageDetailsDialog::findDesktopFile() const { - if (!m_isInstalled) { +QString PackageDetailsDialog::findDesktopFile() const +{ + if (!m_isInstalled) + { return QString(); } - + // Common locations for .desktop files - QStringList desktopDirs = { - "/usr/share/applications", - "/usr/local/share/applications", - QDir::homePath() + "/.local/share/applications" - }; - + QStringList desktopDirs = { "/usr/share/applications", + "/usr/local/share/applications", + QDir::homePath() + "/.local/share/applications" }; + // Try to find desktop file matching the package name // Common patterns: package.desktop, package-*.desktop - QStringList patterns = { - m_info.name + ".desktop", - m_info.name + "-*.desktop" - }; - - for (const QString& dir : desktopDirs) { + QStringList patterns = { m_info.name + ".desktop", m_info.name + "-*.desktop" }; + + for (const QString& dir : desktopDirs) + { QDir desktopDir(dir); - if (!desktopDir.exists()) { + if (!desktopDir.exists()) + { continue; } - + // First try exact patterns - for (const QString& pattern : patterns) { + for (const QString& pattern : patterns) + { QStringList matches = desktopDir.entryList(QStringList() << pattern, QDir::Files); - if (!matches.isEmpty()) { + if (!matches.isEmpty()) + { QString desktopFile = desktopDir.absoluteFilePath(matches.first()); - Logger::info(QString("Found desktop file for %1: %2").arg(m_info.name, desktopFile)); + spdlog::info("{}", + (QString("Found desktop file for %1: %2").arg(m_info.name, desktopFile)).toStdString()); return desktopFile; } } - + // If not found, try fuzzy matching with all .desktop files QStringList allDesktopFiles = desktopDir.entryList(QStringList() << "*.desktop", QDir::Files); - + // Create regex patterns for fuzzy matching // Handle reverse domain names: com.obsproject.Studio.desktop -> obs-studio // Handle simple names: code.desktop -> visual-studio-code-bin QString packageNameLower = m_info.name.toLower(); QStringList nameVariants; - + // Add the full package name nameVariants << packageNameLower; - + // Extract keywords from package name (split by dash and underscore) QStringList parts = packageNameLower.split(QRegularExpression("[-_]")); QStringList significantParts; - for (const QString& part : parts) { - if (part.length() > 3) { // Skip very short parts to avoid false matches + for (const QString& part : parts) + { + if (part.length() > 3) + { // Skip very short parts to avoid false matches significantParts << part; } } - + // Special handling for common patterns QStringList specialVariants; - if (packageNameLower.contains("visual-studio-code")) { + if (packageNameLower.contains("visual-studio-code")) + { specialVariants << "vscode" << "code"; - } else if (packageNameLower == "obs-studio") { + } + else if (packageNameLower == "obs-studio") + { // For obs-studio, look for obsproject specifically specialVariants << "obsproject"; } - + // Try special variants first (highest priority) - for (const QString& variant : specialVariants) { - for (const QString& desktopFileName : allDesktopFiles) { + for (const QString& variant : specialVariants) + { + for (const QString& desktopFileName : allDesktopFiles) + { QString fileNameLower = desktopFileName.toLower(); - - if (fileNameLower.contains(variant)) { + + if (fileNameLower.contains(variant)) + { QString desktopFile = desktopDir.absoluteFilePath(desktopFileName); - - if (verifyDesktopFile(desktopFile, nameVariants + specialVariants + significantParts)) { - Logger::info(QString("Found desktop file for %1 via special match: %2") - .arg(m_info.name, desktopFile)); + + if (verifyDesktopFile(desktopFile, nameVariants + specialVariants + significantParts)) + { + spdlog::info( + "{}", + (QString("Found desktop file for %1 via special match: %2").arg(m_info.name, desktopFile)) + .toStdString()); return desktopFile; } } } } - + // Try full package name match - for (const QString& desktopFileName : allDesktopFiles) { + for (const QString& desktopFileName : allDesktopFiles) + { QString fileNameLower = desktopFileName.toLower(); - - if (fileNameLower.contains(packageNameLower)) { + + if (fileNameLower.contains(packageNameLower)) + { QString desktopFile = desktopDir.absoluteFilePath(desktopFileName); - - if (verifyDesktopFile(desktopFile, nameVariants + specialVariants + significantParts)) { - Logger::info(QString("Found desktop file for %1 via full name match: %2") - .arg(m_info.name, desktopFile)); + + if (verifyDesktopFile(desktopFile, nameVariants + specialVariants + significantParts)) + { + spdlog::info( + "{}", + (QString("Found desktop file for %1 via full name match: %2").arg(m_info.name, desktopFile)) + .toStdString()); return desktopFile; } } } - + // Finally, try matching individual significant parts (but verify carefully) - for (const QString& part : significantParts) { - for (const QString& desktopFileName : allDesktopFiles) { + for (const QString& part : significantParts) + { + for (const QString& desktopFileName : allDesktopFiles) + { QString fileNameLower = desktopFileName.toLower(); - + // Use word boundary-like matching: ensure part is not in the middle of another word // Check if part appears as a separate component (after . or at start, before . or -) QRegularExpression wordBoundary(QString("(^|[._-])%1([._-]|$)").arg(QRegularExpression::escape(part))); - - if (wordBoundary.match(fileNameLower).hasMatch()) { + + if (wordBoundary.match(fileNameLower).hasMatch()) + { QString desktopFile = desktopDir.absoluteFilePath(desktopFileName); - - if (verifyDesktopFile(desktopFile, nameVariants + specialVariants + significantParts)) { - Logger::info(QString("Found desktop file for %1 via word match: %2") - .arg(m_info.name, desktopFile)); + + if (verifyDesktopFile(desktopFile, nameVariants + specialVariants + significantParts)) + { + spdlog::info( + "{}", + (QString("Found desktop file for %1 via word match: %2").arg(m_info.name, desktopFile)) + .toStdString()); return desktopFile; } } } } } - - Logger::debug(QString("No desktop file found for package: %1").arg(m_info.name)); + + spdlog::debug("{}", (QString("No desktop file found for package: %1").arg(m_info.name)).toStdString()); return QString(); } -bool PackageDetailsDialog::verifyDesktopFile(const QString& desktopFilePath, - const QStringList& nameVariants) const { +bool PackageDetailsDialog::verifyDesktopFile(const QString& desktopFilePath, const QStringList& nameVariants) const +{ QFile file(desktopFilePath); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { return false; } - + QTextStream in(&file); QString content = in.readAll(); file.close(); - + // Check if Exec line contains any of our name variants QRegularExpression execPattern(R"(^Exec=(.*)$)", QRegularExpression::MultilineOption); auto match = execPattern.match(content); - - if (match.hasMatch()) { + + if (match.hasMatch()) + { QString execLine = match.captured(1).toLower(); - + // Check if any variant appears in the Exec line - for (const QString& variant : nameVariants) { - if (execLine.contains(variant)) { + for (const QString& variant : nameVariants) + { + if (execLine.contains(variant)) + { return true; } } } - + // Also check Name field as a fallback QRegularExpression namePattern(R"(^Name=(.*)$)", QRegularExpression::MultilineOption); match = namePattern.match(content); - - if (match.hasMatch()) { + + if (match.hasMatch()) + { QString nameField = match.captured(1).toLower(); - - for (const QString& variant : nameVariants) { - if (nameField.contains(variant)) { + + for (const QString& variant : nameVariants) + { + if (nameField.contains(variant)) + { return true; } } } - + return false; } -void PackageDetailsDialog::launchApplication() { +void PackageDetailsDialog::launchApplication() +{ QString desktopFile = findDesktopFile(); - - if (desktopFile.isEmpty()) { - QMessageBox::warning(this, "Launch Failed", - QString("Could not find a desktop file for %1.\n" - "This application may not have a graphical interface or " - "may need to be launched from the terminal.").arg(m_info.name)); + + if (desktopFile.isEmpty()) + { + QMessageBox::warning(this, + "Launch Failed", + QString("Could not find a desktop file for %1.\n" + "This application may not have a graphical interface or " + "may need to be launched from the terminal.") + .arg(m_info.name)); return; } - + // Launch the application using gtk-launch or similar QProcess* process = new QProcess(this); - + // Try gtk-launch first (works on most desktop environments) QString baseName = QFileInfo(desktopFile).fileName(); - - connect(process, QOverload::of(&QProcess::finished), - this, [this, process, baseName](int exitCode, QProcess::ExitStatus exitStatus) { - process->deleteLater(); - - if (exitCode != 0 || exitStatus != QProcess::NormalExit) { - Logger::error(QString("Failed to launch application: %1").arg(baseName)); - QMessageBox::warning(this, "Launch Failed", - QString("Failed to launch %1.\n" - "Exit code: %2").arg(m_info.name).arg(exitCode)); - } else { - Logger::info(QString("Successfully launched: %1").arg(baseName)); - } - }); - + + connect(process, + QOverload::of(&QProcess::finished), + this, + [this, process, baseName](int exitCode, QProcess::ExitStatus exitStatus) + { + process->deleteLater(); + + if (exitCode != 0 || exitStatus != QProcess::NormalExit) + { + spdlog::error("{}", (QString("Failed to launch application: %1").arg(baseName)).toStdString()); + QMessageBox::warning(this, + "Launch Failed", + QString("Failed to launch %1.\n" + "Exit code: %2") + .arg(m_info.name) + .arg(exitCode)); + } + else + { + spdlog::info("{}", (QString("Successfully launched: %1").arg(baseName)).toStdString()); + } + }); + // Try gtk-launch first process->start("gtk-launch", QStringList() << baseName); - + // If gtk-launch doesn't start, try alternative methods - if (!process->waitForStarted(1000)) { + if (!process->waitForStarted(1000)) + { // Try dex (Desktop Entry Execution) process->start("dex", QStringList() << desktopFile); - - if (!process->waitForStarted(1000)) { + + if (!process->waitForStarted(1000)) + { // Try exo-open (XFCE) process->start("exo-open", QStringList() << desktopFile); - - if (!process->waitForStarted(1000)) { + + if (!process->waitForStarted(1000)) + { // Last resort: try to parse and execute the Exec line process->deleteLater(); - QMessageBox::warning(this, "Launch Failed", - "Could not find a suitable desktop file launcher.\n" - "Please install gtk-launch, dex, or exo-open."); - Logger::error("No desktop file launcher available"); + QMessageBox::warning(this, + "Launch Failed", + "Could not find a suitable desktop file launcher.\n" + "Please install gtk-launch, dex, or exo-open."); + spdlog::error("No desktop file launcher available"); } } } diff --git a/src/gui/package_details_dialog.h b/src/gui/package_details_dialog.h index 4a8a324..265f3f1 100644 --- a/src/gui/package_details_dialog.h +++ b/src/gui/package_details_dialog.h @@ -1,12 +1,12 @@ #ifndef PACKAGE_DETAILS_DIALOG_H #define PACKAGE_DETAILS_DIALOG_H +#include "../utils/types.h" #include #include +#include #include #include -#include -#include "../utils/types.h" /** * @brief Dialog showing detailed package information and actions. @@ -14,13 +14,14 @@ * Memory Management: * - All Qt widget members use Qt parent-child ownership (raw pointers are non-owning) */ -class PackageDetailsDialog : public QDialog { +class PackageDetailsDialog : public QDialog +{ Q_OBJECT - + public: explicit PackageDetailsDialog(const PackageInfo& info, QWidget* parent = nullptr); ~PackageDetailsDialog() override = default; - + private: void setupUi(); void updateButtonStates(); @@ -30,13 +31,12 @@ class PackageDetailsDialog : public QDialog { void toggleLogViewer(); void parseProgressOutput(const QString& output); QString findDesktopFile() const; - bool verifyDesktopFile(const QString& desktopFilePath, - const QStringList& nameVariants) const; + bool verifyDesktopFile(const QString& desktopFilePath, const QStringList& nameVariants) const; void launchApplication(); - + PackageInfo m_info; bool m_isInstalled = false; - + // Qt parent-child managed widgets (non-owning pointers) QLabel* m_nameLabel = nullptr; QLabel* m_versionLabel = nullptr; @@ -57,18 +57,18 @@ class PackageDetailsDialog : public QDialog { QProgressBar* m_progressBar = nullptr; QLabel* m_progressLabel = nullptr; QWidget* m_progressWidget = nullptr; - + // Log viewer (Qt parent-child managed) QTextEdit* m_logViewer = nullptr; QPushButton* m_toggleLogButton = nullptr; QWidget* m_logWidget = nullptr; bool m_logVisible = false; - + // Progress tracking QString m_currentOperation; int m_totalPackages = 0; int m_currentPackage = 0; - + private slots: void onInstall(); void onUninstall(); @@ -78,4 +78,4 @@ private slots: void onOperationError(const QString& error); }; -#endif // PACKAGE_DETAILS_DIALOG_H +#endif // PACKAGE_DETAILS_DIALOG_H diff --git a/src/gui/search_widget.cpp b/src/gui/search_widget.cpp index 667bc8b..cbdffe9 100644 --- a/src/gui/search_widget.cpp +++ b/src/gui/search_widget.cpp @@ -1,14 +1,14 @@ #include "search_widget.h" -#include "package_card.h" -#include "package_details_dialog.h" #include "../core/alpm_wrapper.h" #include "../core/aur_helper.h" -#include "../utils/logger.h" -#include +#include "../utils/logging.h" +#include "package_card.h" +#include "package_details_dialog.h" +#include #include #include +#include #include -#include SearchWidget::SearchWidget(QWidget* parent) : QWidget(parent) @@ -19,78 +19,80 @@ SearchWidget::SearchWidget(QWidget* parent) , m_contentWidget(new QWidget()) , m_gridLayout(new QGridLayout(m_contentWidget)) , m_statusLabel(new QLabel(this)) - , m_aurHelper(std::make_unique(this)) { - + , m_aurHelper(std::make_unique(this)) +{ + setupUi(); - - connect(m_aurHelper.get(), &AurHelper::searchCompleted, - this, &SearchWidget::onAurSearchCompleted); - connect(m_aurHelper.get(), &AurHelper::error, - this, &SearchWidget::onAurSearchError); + + connect(m_aurHelper.get(), &AurHelper::searchCompleted, this, &SearchWidget::onAurSearchCompleted); + connect(m_aurHelper.get(), &AurHelper::error, this, &SearchWidget::onAurSearchError); } -void SearchWidget::setupUi() { +void SearchWidget::setupUi() +{ auto* mainLayout = new QVBoxLayout(this); - + // Title auto* titleLabel = new QLabel("Search Packages", this); titleLabel->setObjectName("view-title"); mainLayout->addWidget(titleLabel); - + // Search bar auto* searchLayout = new QHBoxLayout(); - + m_searchInput->setPlaceholderText("Search for packages..."); m_searchInput->setMinimumHeight(35); m_searchInput->setClearButtonEnabled(true); connect(m_searchInput, &QLineEdit::returnPressed, this, &SearchWidget::onSearchClicked); searchLayout->addWidget(m_searchInput, 1); - + // Filter combo m_filterCombo->addItem("All", "all"); m_filterCombo->addItem("Core", "core"); m_filterCombo->addItem("Extra", "extra"); m_filterCombo->addItem("AUR", "AUR"); m_filterCombo->setMinimumHeight(35); - connect(m_filterCombo, QOverload::of(&QComboBox::currentIndexChanged), - this, &SearchWidget::onFilterChanged); + connect(m_filterCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &SearchWidget::onFilterChanged); searchLayout->addWidget(m_filterCombo); - + m_searchButton->setMinimumHeight(35); m_searchButton->setMinimumWidth(100); m_searchButton->setProperty("class", "primary-btn"); connect(m_searchButton, &QPushButton::clicked, this, &SearchWidget::onSearchClicked); searchLayout->addWidget(m_searchButton); - + mainLayout->addLayout(searchLayout); - + // Status label m_statusLabel->setAlignment(Qt::AlignCenter); m_statusLabel->hide(); mainLayout->addWidget(m_statusLabel); - + // Results area m_scrollArea->setWidget(m_contentWidget); m_scrollArea->setWidgetResizable(true); m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - + m_gridLayout->setSpacing(15); m_gridLayout->setContentsMargins(10, 10, 10, 10); - + mainLayout->addWidget(m_scrollArea); setLayout(mainLayout); } -void SearchWidget::onSearchClicked() { +void SearchWidget::onSearchClicked() +{ QString query = m_searchInput->text().trimmed(); - - if (query.isEmpty()) { + + if (query.isEmpty()) + { m_statusLabel->setText("Please enter a search term"); m_statusLabel->show(); return; } - - if (m_searchInProgress) { + + if (m_searchInProgress) + { return; } @@ -99,70 +101,87 @@ void SearchWidget::onSearchClicked() { m_searchButton->setText("Searching..."); m_statusLabel->setText("Searching..."); m_statusLabel->show(); - + clearResults(); performSearch(); } -void SearchWidget::performSearch() { +void SearchWidget::performSearch() +{ QString query = m_searchInput->text().trimmed().toLower().replace(' ', '-'); - + // Search in official repos using ALPM - (void)QtConcurrent::run([this, query]() { - auto results = AlpmWrapper::instance().searchPackages(query); - - QMetaObject::invokeMethod(this, [this, results]() { - m_allResults = results; - - // Also search AUR - m_aurHelper->searchPackages(m_searchInput->text().trimmed()); - }, Qt::QueuedConnection); - }); + (void)QtConcurrent::run( + [this, query]() + { + auto results = AlpmWrapper::instance().searchPackages(query); + + QMetaObject::invokeMethod( + this, + [this, results]() + { + m_allResults = results; + + // Also search AUR + m_aurHelper->searchPackages(m_searchInput->text().trimmed()); + }, + Qt::QueuedConnection); + }); } -void SearchWidget::onAurSearchCompleted(const QVector& results) { +void SearchWidget::onAurSearchCompleted(const QVector& results) +{ // Combine with official repo results m_allResults.append(results); - + m_searchButton->setEnabled(true); m_searchButton->setText("Search"); m_searchInProgress = false; - - if (m_allResults.isEmpty()) { + + if (m_allResults.isEmpty()) + { m_statusLabel->setText("No results found"); return; } - + m_statusLabel->hide(); - + // Apply filter onFilterChanged(m_filterCombo->currentIndex()); - - Logger::info(QString("Search completed: %1 results").arg(m_allResults.size())); + + spdlog::info("{}", (QString("Search completed: %1 results").arg(m_allResults.size())).toStdString()); } -void SearchWidget::onAurSearchError(const QString& errorMsg) { - Logger::warning(QString("AUR search failed: %1").arg(errorMsg)); +void SearchWidget::onAurSearchError(const QString& errorMsg) +{ + spdlog::warn("{}", (QString("AUR search failed: %1").arg(errorMsg)).toStdString()); m_searchButton->setEnabled(true); m_searchButton->setText("Search"); m_searchInProgress = false; - if (m_allResults.isEmpty()) { + if (m_allResults.isEmpty()) + { m_statusLabel->setText("No results found"); m_statusLabel->show(); } } -void SearchWidget::onFilterChanged(int index) { +void SearchWidget::onFilterChanged(int index) +{ QString filter = m_filterCombo->itemData(index).toString(); - - if (filter == "all") { + + if (filter == "all") + { displayResults(m_allResults); - } else { + } + else + { QVector filtered; - for (const auto& pkg : m_allResults) { - if (pkg.repository == filter) { + for (const auto& pkg : m_allResults) + { + if (pkg.repository == filter) + { filtered.append(pkg); } } @@ -170,142 +189,181 @@ void SearchWidget::onFilterChanged(int index) { } } -void SearchWidget::displayResults(const QVector& results) { +void SearchWidget::displayResults(const QVector& results) +{ clearResults(); m_currentResults = results; - - if (results.isEmpty()) { + + if (results.isEmpty()) + { m_statusLabel->setText("No results found for selected filter"); m_statusLabel->show(); return; } - + int row = 0; int col = 0; const int columns = 3; - - for (const auto& pkg : results) { + + for (const auto& pkg : results) + { auto* card = new PackageCard(pkg, m_contentWidget); connect(card, &PackageCard::clicked, this, &SearchWidget::onPackageClicked); - + m_gridLayout->addWidget(card, row, col); - + col++; - if (col >= columns) { + if (col >= columns) + { col = 0; row++; } } - + m_gridLayout->setRowStretch(row + 1, 1); } -void SearchWidget::clearResults() { - while (auto* item = m_gridLayout->takeAt(0)) { - if (auto* widget = item->widget()) { +void SearchWidget::clearResults() +{ + while (auto* item = m_gridLayout->takeAt(0)) + { + if (auto* widget = item->widget()) + { widget->deleteLater(); } delete item; } } -void SearchWidget::onPackageClicked(const PackageInfo& info) { - Logger::info(QString("Package clicked: %1").arg(info.name)); - +void SearchWidget::onPackageClicked(const PackageInfo& info) +{ + spdlog::info("{}", (QString("Package clicked: %1").arg(info.name)).toStdString()); + PackageInfo fullInfo = info; - + // For AUR packages, fetch complete details including maintainer, URL, dependencies - if (info.repository.toLower() == "aur") { + if (info.repository.toLower() == "aur") + { auto* aurHelper = new AurHelper(this); QEventLoop loop; - - connect(aurHelper, &AurHelper::packageInfoReceived, [&](const PackageInfo& detailedInfo) { - fullInfo = detailedInfo; - loop.quit(); - }); - - connect(aurHelper, &AurHelper::error, [&](const QString& errorMsg) { - Logger::warning(QString("Failed to fetch AUR details for %1: %2").arg(info.name, errorMsg)); - loop.quit(); - }); - + + connect(aurHelper, + &AurHelper::packageInfoReceived, + [&](const PackageInfo& detailedInfo) + { + fullInfo = detailedInfo; + loop.quit(); + }); + + connect(aurHelper, + &AurHelper::error, + [&](const QString& errorMsg) + { + spdlog::warn( + "{}", + (QString("Failed to fetch AUR details for %1: %2").arg(info.name, errorMsg)).toStdString()); + loop.quit(); + }); + aurHelper->getPackageInfo(info.name); loop.exec(); - + aurHelper->deleteLater(); } - + auto* dialog = new PackageDetailsDialog(fullInfo, this); dialog->exec(); dialog->deleteLater(); } -void SearchWidget::updateRepositoryList(bool multilibEnabled, bool chaoticAurEnabled) { +void SearchWidget::updateRepositoryList(bool multilibEnabled, bool chaoticAurEnabled) +{ // Save the current selection int currentIndex = m_filterCombo->currentIndex(); QString currentFilter = m_filterCombo->itemData(currentIndex).toString(); - + // Check if multilib already exists in the list bool multilibExists = false; - for (int i = 0; i < m_filterCombo->count(); ++i) { - if (m_filterCombo->itemData(i).toString() == "multilib") { + for (int i = 0; i < m_filterCombo->count(); ++i) + { + if (m_filterCombo->itemData(i).toString() == "multilib") + { multilibExists = true; break; } } - + // Check if chaotic-aur already exists in the list bool chaoticAurExists = false; - for (int i = 0; i < m_filterCombo->count(); ++i) { - if (m_filterCombo->itemData(i).toString() == "chaotic-aur") { + for (int i = 0; i < m_filterCombo->count(); ++i) + { + if (m_filterCombo->itemData(i).toString() == "chaotic-aur") + { chaoticAurExists = true; break; } } - + // Handle multilib - if (multilibEnabled && !multilibExists) { + if (multilibEnabled && !multilibExists) + { // Add multilib to the dropdown (insert before AUR) int aurIndex = m_filterCombo->findData("AUR"); - if (aurIndex != -1) { + if (aurIndex != -1) + { m_filterCombo->insertItem(aurIndex, "Multilib", "multilib"); - } else { + } + else + { m_filterCombo->addItem("Multilib", "multilib"); } - Logger::info("Added multilib repository to search filter"); - } else if (!multilibEnabled && multilibExists) { + spdlog::info("Added multilib repository to search filter"); + } + else if (!multilibEnabled && multilibExists) + { // Remove multilib from the dropdown int multilibIndex = m_filterCombo->findData("multilib"); - if (multilibIndex != -1) { + if (multilibIndex != -1) + { m_filterCombo->removeItem(multilibIndex); - Logger::info("Removed multilib repository from search filter"); + spdlog::info("Removed multilib repository from search filter"); } } - + // Handle chaotic-aur - if (chaoticAurEnabled && !chaoticAurExists) { + if (chaoticAurEnabled && !chaoticAurExists) + { // Add chaotic-aur to the dropdown (insert before AUR) int aurIndex = m_filterCombo->findData("AUR"); - if (aurIndex != -1) { + if (aurIndex != -1) + { m_filterCombo->insertItem(aurIndex, "Chaotic-AUR", "chaotic-aur"); - } else { + } + else + { m_filterCombo->addItem("Chaotic-AUR", "chaotic-aur"); } - Logger::info("Added chaotic-aur repository to search filter"); - } else if (!chaoticAurEnabled && chaoticAurExists) { + spdlog::info("Added chaotic-aur repository to search filter"); + } + else if (!chaoticAurEnabled && chaoticAurExists) + { // Remove chaotic-aur from the dropdown int chaoticAurIndex = m_filterCombo->findData("chaotic-aur"); - if (chaoticAurIndex != -1) { + if (chaoticAurIndex != -1) + { m_filterCombo->removeItem(chaoticAurIndex); - Logger::info("Removed chaotic-aur repository from search filter"); + spdlog::info("Removed chaotic-aur repository from search filter"); } } - + // Restore previous selection if it still exists int newIndex = m_filterCombo->findData(currentFilter); - if (newIndex != -1) { + if (newIndex != -1) + { m_filterCombo->setCurrentIndex(newIndex); - } else { + } + else + { // If previous selection was removed, select "All" m_filterCombo->setCurrentIndex(0); } diff --git a/src/gui/search_widget.h b/src/gui/search_widget.h index ffa059b..aaabc03 100644 --- a/src/gui/search_widget.h +++ b/src/gui/search_widget.h @@ -1,17 +1,17 @@ #ifndef SEARCH_WIDGET_H #define SEARCH_WIDGET_H -#include -#include -#include +#include "../core/aur_helper.h" +#include "../utils/types.h" #include -#include #include #include +#include +#include +#include #include +#include #include -#include "../utils/types.h" -#include "../core/aur_helper.h" /** * @brief Widget for searching packages across repositories. @@ -21,22 +21,23 @@ * - All Qt widget members use Qt parent-child ownership (raw pointers are non-owning) * - Package cards are dynamically created/destroyed in displayResults/clearResults */ -class SearchWidget : public QWidget { +class SearchWidget : public QWidget +{ Q_OBJECT - + public: explicit SearchWidget(QWidget* parent = nullptr); ~SearchWidget() override = default; - + public slots: void updateRepositoryList(bool multilibEnabled, bool chaoticAurEnabled = false); - + private: void setupUi(); void performSearch(); void displayResults(const QVector& results); void clearResults(); - + // Qt parent-child managed widgets (non-owning pointers) QLineEdit* m_searchInput = nullptr; QPushButton* m_searchButton = nullptr; @@ -45,14 +46,14 @@ public slots: QWidget* m_contentWidget = nullptr; QGridLayout* m_gridLayout = nullptr; QLabel* m_statusLabel = nullptr; - + // Owned resources std::unique_ptr m_aurHelper; - + QVector m_currentResults; QVector m_allResults; bool m_searchInProgress = false; - + private slots: void onSearchClicked(); void onAurSearchCompleted(const QVector& results); @@ -61,4 +62,4 @@ private slots: void onPackageClicked(const PackageInfo& info); }; -#endif // SEARCH_WIDGET_H +#endif // SEARCH_WIDGET_H diff --git a/src/gui/settings_widget.cpp b/src/gui/settings_widget.cpp index 5199240..a1e6c15 100644 --- a/src/gui/settings_widget.cpp +++ b/src/gui/settings_widget.cpp @@ -1,289 +1,278 @@ #include "settings_widget.h" -#include "../utils/logger.h" #include "../core/alpm_wrapper.h" #include "../core/package_manager.h" -#include +#include "../core/pacman_conf.h" +#include "../utils/logging.h" +#include #include #include -#include -#include #include #include #include +#include +#include #include SettingsWidget::SettingsWidget(QWidget* parent) - : QWidget(parent) { - + : QWidget(parent) +{ + setupUi(); loadCurrentSettings(); - - Logger::info("SettingsWidget created successfully"); + + spdlog::info("SettingsWidget created successfully"); } -void SettingsWidget::setupUi() { +void SettingsWidget::setupUi() +{ // Create main layout for the widget auto* outerLayout = new QVBoxLayout(this); outerLayout->setContentsMargins(0, 0, 0, 0); - + // Create scroll area auto* scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - + // Create content widget that will be scrollable auto* contentWidget = new QWidget(); auto* mainLayout = new QVBoxLayout(contentWidget); mainLayout->setSpacing(20); mainLayout->setContentsMargins(10, 10, 10, 10); - + // Title auto* titleLabel = new QLabel("Settings", contentWidget); - titleLabel->setObjectName("view-title"); + titleLabel->setObjectName("view-title"); mainLayout->addWidget(titleLabel); - + // Repository Settings createRepositorySettings(); mainLayout->addWidget(m_repositoryGroup); - + // Chaotic-AUR Setup createChaoticAurSettings(); mainLayout->addWidget(m_chaoticAurGroup); - + // Maintenance Settings createMaintenanceSettings(); mainLayout->addWidget(m_maintenanceGroup); - + // Status label m_statusLabel = new QLabel(contentWidget); m_statusLabel->setAlignment(Qt::AlignCenter); - m_statusLabel->setProperty("class", "status-msg-info"); - m_statusLabel->hide(); - mainLayout->addWidget(m_statusLabel); - + m_statusLabel->setProperty("class", "status-msg-info"); + m_statusLabel->hide(); + mainLayout->addWidget(m_statusLabel); + // Add stretch at the bottom mainLayout->addStretch(); - + // Set the content widget to the scroll area scrollArea->setWidget(contentWidget); - + // Add scroll area to the outer layout outerLayout->addWidget(scrollArea); - + setLayout(outerLayout); } -void SettingsWidget::createRepositorySettings() { +void SettingsWidget::createRepositorySettings() +{ m_repositoryGroup = new QGroupBox("Package Repositories", this); auto* repoLayout = new QVBoxLayout(m_repositoryGroup); - + // Description - auto* descLabel = new QLabel( - "Select which package repositories to use for searching and installing packages.\n" - "Core and Extra repositories are required and cannot be disabled.", - this); + auto* descLabel = new QLabel("Select which package repositories to use for searching and installing packages.\n" + "Core and Extra repositories are required and cannot be disabled.", + this); descLabel->setWordWrap(true); descLabel->setProperty("class", "settings-description"); repoLayout->addWidget(descLabel); - + // Core repository (always enabled, cannot be disabled) m_coreRepoCheckbox = new QCheckBox("Core - Essential system packages", this); m_coreRepoCheckbox->setChecked(true); m_coreRepoCheckbox->setEnabled(false); m_coreRepoCheckbox->setToolTip("Core repository is required and cannot be disabled"); repoLayout->addWidget(m_coreRepoCheckbox); - + // Extra repository (always enabled, cannot be disabled) m_extraRepoCheckbox = new QCheckBox("Extra - Additional official packages", this); m_extraRepoCheckbox->setChecked(true); m_extraRepoCheckbox->setEnabled(false); m_extraRepoCheckbox->setToolTip("Extra repository is required and cannot be disabled"); repoLayout->addWidget(m_extraRepoCheckbox); - + // Multilib repository (optional, can be enabled/disabled) m_multilibRepoCheckbox = new QCheckBox("Multilib - 32-bit packages on x86_64", this); - m_multilibRepoCheckbox->setToolTip( - "Enable multilib repository for 32-bit applications on 64-bit systems.\n" - "This modifies /etc/pacman.conf and requires administrator privileges."); - connect(m_multilibRepoCheckbox, &QCheckBox::checkStateChanged, - this, &SettingsWidget::onSettingsChanged); + m_multilibRepoCheckbox->setToolTip("Enable multilib repository for 32-bit applications on 64-bit systems.\n" + "This modifies /etc/pacman.conf and requires administrator privileges."); + connect(m_multilibRepoCheckbox, &QCheckBox::checkStateChanged, this, &SettingsWidget::onSettingsChanged); repoLayout->addWidget(m_multilibRepoCheckbox); - + // Chaotic-AUR repository (optional, can be enabled/disabled) m_chaoticAurCheckbox = new QCheckBox("Chaotic-AUR - Pre-built AUR packages", this); - m_chaoticAurCheckbox->setToolTip( - "Enable chaotic-aur repository for pre-built AUR packages.\n" - "This modifies /etc/pacman.conf and requires administrator privileges.\n" - "Note: chaotic-keyring and chaotic-mirrorlist must be installed first."); - connect(m_chaoticAurCheckbox, &QCheckBox::checkStateChanged, - this, &SettingsWidget::onSettingsChanged); + m_chaoticAurCheckbox->setToolTip("Enable chaotic-aur repository for pre-built AUR packages.\n" + "This modifies /etc/pacman.conf and requires administrator privileges.\n" + "Note: chaotic-keyring and chaotic-mirrorlist must be installed first."); + connect(m_chaoticAurCheckbox, &QCheckBox::checkStateChanged, this, &SettingsWidget::onSettingsChanged); repoLayout->addWidget(m_chaoticAurCheckbox); - + // Info label - auto* infoLabel = new QLabel( - "Note: Changes to repositories require modifying system configuration files " - "and may require administrator privileges.", - this); + auto* infoLabel = new QLabel("Note: Changes to repositories require modifying system configuration files " + "and may require administrator privileges.", + this); infoLabel->setWordWrap(true); infoLabel->setProperty("class", "settings-info-italic"); repoLayout->addWidget(infoLabel); - + // Apply button auto* applyLayout = new QHBoxLayout(); applyLayout->addStretch(); - + m_applyButton = new QPushButton("Apply Changes", this); m_applyButton->setMinimumWidth(150); m_applyButton->setEnabled(false); connect(m_applyButton, &QPushButton::clicked, this, &SettingsWidget::onApplyClicked); applyLayout->addWidget(m_applyButton); - + repoLayout->addLayout(applyLayout); - + m_repositoryGroup->setLayout(repoLayout); } -void SettingsWidget::createChaoticAurSettings() { +void SettingsWidget::createChaoticAurSettings() +{ m_chaoticAurGroup = new QGroupBox("Chaotic-AUR Setup", this); auto* chaoticLayout = new QVBoxLayout(m_chaoticAurGroup); - + // Description - auto* descLabel = new QLabel( - "Chaotic-AUR provides pre-built AUR packages, making installation faster and easier.\n" - "Setup requires installing the keyring and mirrorlist packages.", - this); + auto* descLabel = new QLabel("Chaotic-AUR provides pre-built AUR packages, making installation faster and easier.\n" + "Setup requires installing the keyring and mirrorlist packages.", + this); descLabel->setWordWrap(true); descLabel->setProperty("class", "settings-description"); chaoticLayout->addWidget(descLabel); - + // Setup button section auto* setupLayout = new QHBoxLayout(); - + auto* setupLabel = new QLabel("Install Chaotic-AUR:", this); setupLabel->setProperty("class", "settings-section-label"); setupLayout->addWidget(setupLabel); - + setupLayout->addStretch(); - + m_setupChaoticButton = new QPushButton("Setup Chaotic-AUR", this); m_setupChaoticButton->setMinimumWidth(150); - m_setupChaoticButton->setToolTip( - "Install chaotic-keyring and chaotic-mirrorlist packages.\n" - "This will enable access to pre-built AUR packages."); + m_setupChaoticButton->setToolTip("Install chaotic-keyring and chaotic-mirrorlist packages.\n" + "This will enable access to pre-built AUR packages."); connect(m_setupChaoticButton, &QPushButton::clicked, this, &SettingsWidget::onSetupChaoticClicked); setupLayout->addWidget(m_setupChaoticButton); - + chaoticLayout->addLayout(setupLayout); - + // Setup info auto* setupInfoLabel = new QLabel( - "This will install chaotic-keyring and chaotic-mirrorlist from the official Chaotic-AUR repository.", - this); + "This will install chaotic-keyring and chaotic-mirrorlist from the official Chaotic-AUR repository.", this); setupInfoLabel->setWordWrap(true); setupInfoLabel->setProperty("class", "settings-help-text"); - chaoticLayout->addWidget(setupInfoLabel); - + chaoticLayout->addWidget(setupInfoLabel); + // Spacer chaoticLayout->addSpacing(10); - + // Remove button section auto* removeLayout = new QHBoxLayout(); - + auto* removeLabel = new QLabel("Remove Chaotic-AUR:", this); removeLabel->setProperty("class", "settings-section-label"); - removeLayout->addWidget(removeLabel); - + removeLayout->addWidget(removeLabel); + removeLayout->addStretch(); - + m_removeChaoticButton = new QPushButton("Remove Chaotic-AUR", this); m_removeChaoticButton->setMinimumWidth(150); - m_removeChaoticButton->setToolTip( - "Remove chaotic-keyring and chaotic-mirrorlist packages.\n" - "You may need to manually remove the repository from /etc/pacman.conf"); + m_removeChaoticButton->setToolTip("Remove chaotic-keyring and chaotic-mirrorlist packages.\n" + "You may need to manually remove the repository from /etc/pacman.conf"); connect(m_removeChaoticButton, &QPushButton::clicked, this, &SettingsWidget::onRemoveChaoticClicked); removeLayout->addWidget(m_removeChaoticButton); - + chaoticLayout->addLayout(removeLayout); - + // Remove info - auto* removeInfoLabel = new QLabel( - "This will remove the Chaotic-AUR packages. You need to uncheck the Chaotic-AUR option above or manually edit /etc/pacman.conf to remove the repository configuration.", - this); + auto* removeInfoLabel + = new QLabel("This will remove the Chaotic-AUR packages. You need to uncheck the Chaotic-AUR option above or " + "manually edit /etc/pacman.conf to remove the repository configuration.", + this); removeInfoLabel->setWordWrap(true); removeInfoLabel->setProperty("class", "settings-help-text"); - chaoticLayout->addWidget(removeInfoLabel); - + chaoticLayout->addWidget(removeInfoLabel); + m_chaoticAurGroup->setLayout(chaoticLayout); } -void SettingsWidget::createMaintenanceSettings() { +void SettingsWidget::createMaintenanceSettings() +{ m_maintenanceGroup = new QGroupBox("Maintenance", this); auto* maintenanceLayout = new QVBoxLayout(m_maintenanceGroup); - + // Description - auto* descLabel = new QLabel( - "System maintenance and troubleshooting tools.", - this); + auto* descLabel = new QLabel("System maintenance and troubleshooting tools.", this); descLabel->setWordWrap(true); descLabel->setProperty("class", "settings-description"); maintenanceLayout->addWidget(descLabel); - + // Lock file section auto* lockFileLayout = new QHBoxLayout(); - - auto* lockFileLabel = new QLabel( - "Pacman Database Lock:", - this); + + auto* lockFileLabel = new QLabel("Pacman Database Lock:", this); lockFileLabel->setProperty("class", "settings-section-label"); - lockFileLayout->addWidget(lockFileLabel); - + lockFileLayout->addWidget(lockFileLabel); + lockFileLayout->addStretch(); - + m_removeLockButton = new QPushButton("Remove Lock File", this); m_removeLockButton->setMinimumWidth(150); - m_removeLockButton->setToolTip( - "Remove /var/lib/pacman/db.lck if pacman is stuck.\n" - "Only use this if you're sure no other pacman process is running."); + m_removeLockButton->setToolTip("Remove /var/lib/pacman/db.lck if pacman is stuck.\n" + "Only use this if you're sure no other pacman process is running."); connect(m_removeLockButton, &QPushButton::clicked, this, &SettingsWidget::onRemoveLockClicked); lockFileLayout->addWidget(m_removeLockButton); - + maintenanceLayout->addLayout(lockFileLayout); - + // Lock file info - auto* lockInfoLabel = new QLabel( - "If pacman was interrupted, it may leave a lock file that prevents other operations.\n" - "Remove it only if you're certain no package manager is currently running.", - this); + auto* lockInfoLabel + = new QLabel("If pacman was interrupted, it may leave a lock file that prevents other operations.\n" + "Remove it only if you're certain no package manager is currently running.", + this); lockInfoLabel->setWordWrap(true); lockInfoLabel->setProperty("class", "settings-help-text"); maintenanceLayout->addWidget(lockInfoLabel); - + // Spacer maintenanceLayout->addSpacing(15); - + // Sync repositories section auto* syncReposLayout = new QHBoxLayout(); - - auto* syncReposLabel = new QLabel( - "Synchronize Repositories:", - this); - syncReposLabel->setProperty("class", "settings-section-label"); + + auto* syncReposLabel = new QLabel("Synchronize Repositories:", this); + syncReposLabel->setProperty("class", "settings-section-label"); syncReposLayout->addWidget(syncReposLabel); - + syncReposLayout->addStretch(); - + m_syncReposButton = new QPushButton("Sync Repositories", this); m_syncReposButton->setMinimumWidth(150); - m_syncReposButton->setToolTip( - "Manually synchronize package databases (pacman -Sy).\n" - "This updates the list of available packages from all enabled repositories."); + m_syncReposButton->setToolTip("Manually synchronize package databases (pacman -Sy).\n" + "This updates the list of available packages from all enabled repositories."); connect(m_syncReposButton, &QPushButton::clicked, this, &SettingsWidget::onSyncReposClicked); syncReposLayout->addWidget(m_syncReposButton); - + maintenanceLayout->addLayout(syncReposLayout); - + // Sync info auto* syncInfoLabel = new QLabel( "Use this to manually update your package database. This is useful after enabling/disabling repositories\n" @@ -292,21 +281,19 @@ void SettingsWidget::createMaintenanceSettings() { syncInfoLabel->setWordWrap(true); syncInfoLabel->setProperty("class", "settings-help-text"); maintenanceLayout->addWidget(syncInfoLabel); - + // Spacer maintenanceLayout->addSpacing(15); - + // Cancel running process section auto* cancelProcessLayout = new QHBoxLayout(); - - auto* cancelProcessLabel = new QLabel( - "Kill Running Process:", - this); + + auto* cancelProcessLabel = new QLabel("Kill Running Process:", this); cancelProcessLabel->setProperty("class", "settings-section-label"); cancelProcessLayout->addWidget(cancelProcessLabel); - + cancelProcessLayout->addStretch(); - + m_cancelProcessButton = new QPushButton("Kill Process", this); m_cancelProcessButton->setMinimumWidth(150); m_cancelProcessButton->setToolTip( @@ -315,796 +302,862 @@ void SettingsWidget::createMaintenanceSettings() { "This is different from removing the lock file - it actually stops the running process."); connect(m_cancelProcessButton, &QPushButton::clicked, this, &SettingsWidget::onCancelProcessClicked); cancelProcessLayout->addWidget(m_cancelProcessButton); - + maintenanceLayout->addLayout(cancelProcessLayout); - + // Cancel process info - auto* cancelInfoLabel = new QLabel( - "Use this to kill process which is stuck at installation, uninstallation, or update.\n" - "This is useful when you see 'Another operation is already in progress' and want to stop it.", - this); + auto* cancelInfoLabel + = new QLabel("Use this to kill process which is stuck at installation, uninstallation, or update.\n" + "This is useful when you see 'Another operation is already in progress' and want to stop it.", + this); cancelInfoLabel->setWordWrap(true); - cancelInfoLabel->setProperty("class", "settings-help-text"); + cancelInfoLabel->setProperty("class", "settings-help-text"); maintenanceLayout->addWidget(cancelInfoLabel); - + m_maintenanceGroup->setLayout(maintenanceLayout); } -void SettingsWidget::loadCurrentSettings() { +void SettingsWidget::loadCurrentSettings() +{ // Check if multilib is currently enabled bool multilibEnabled = isMultilibEnabledInPacmanConf(); m_multilibRepoCheckbox->setChecked(multilibEnabled); m_originalMultilibState = multilibEnabled; - + // Check if chaotic-aur is enabled bool chaoticAurEnabled = isChaoticAurEnabledInPacmanConf(); m_chaoticAurCheckbox->setChecked(chaoticAurEnabled); m_originalChaoticAurState = chaoticAurEnabled; - - Logger::info(QString("Loaded settings: multilib=%1, chaotic-aur=%2") - .arg(multilibEnabled ? "enabled" : "disabled") - .arg(chaoticAurEnabled ? "enabled" : "disabled")); + + spdlog::info("{}", + (QString("Loaded settings: multilib=%1, chaotic-aur=%2") + .arg(multilibEnabled ? "enabled" : "disabled") + .arg(chaoticAurEnabled ? "enabled" : "disabled")) + .toStdString()); } -bool SettingsWidget::isMultilibEnabledInPacmanConf() const { +bool SettingsWidget::isMultilibEnabledInPacmanConf() const +{ QFile file("/etc/pacman.conf"); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - Logger::error("Failed to open /etc/pacman.conf for reading"); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + spdlog::error("Failed to open /etc/pacman.conf for reading"); return false; } - + QTextStream in(&file); - bool inMultilibSection = false; - - while (!in.atEnd()) { - QString line = in.readLine().trimmed(); - - // Check for [multilib] section header - if (line == "[multilib]") { - inMultilibSection = true; - continue; - } - - // If we found [multilib] section, check if it's not commented - if (inMultilibSection && !line.isEmpty() && !line.startsWith("#")) { - // If we find Include directive, multilib is enabled - if (line.startsWith("Include")) { - file.close(); - return true; - } - } - - // If we hit another section, stop - if (inMultilibSection && line.startsWith("[") && line != "[multilib]") { - break; - } - } - + const QString contents = in.readAll(); file.close(); - return false; + + return PacmanConf::isMultilibEnabled(contents); } -bool SettingsWidget::isChaoticAurEnabledInPacmanConf() const { +bool SettingsWidget::isChaoticAurEnabledInPacmanConf() const +{ QFile file("/etc/pacman.conf"); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - Logger::error("Failed to open /etc/pacman.conf for reading"); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + spdlog::error("Failed to open /etc/pacman.conf for reading"); return false; } - + QTextStream in(&file); - bool inChaoticAurSection = false; - - while (!in.atEnd()) { - QString line = in.readLine().trimmed(); - - // Check for [chaotic-aur] section header - if (line == "[chaotic-aur]") { - inChaoticAurSection = true; - continue; - } - - // If we found [chaotic-aur] section, check if it's not commented - if (inChaoticAurSection && !line.isEmpty() && !line.startsWith("#")) { - // If we find Include or Server directive, chaotic-aur is enabled - if (line.startsWith("Include") || line.startsWith("Server")) { - file.close(); - return true; - } - } - - // If we hit another section, stop - if (inChaoticAurSection && line.startsWith("[") && line != "[chaotic-aur]") { - break; - } - } - + const QString contents = in.readAll(); file.close(); - return false; + + return PacmanConf::isChaoticAurEnabled(contents); } -bool SettingsWidget::enableMultilibInPacmanConf() { +bool SettingsWidget::enableMultilibInPacmanConf() +{ QFile file("/etc/pacman.conf"); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - Logger::error("Failed to open /etc/pacman.conf for reading"); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + spdlog::error("Failed to open /etc/pacman.conf for reading"); return false; } - + QStringList lines; QTextStream in(&file); bool multilibSectionFound = false; - - while (!in.atEnd()) { + + while (!in.atEnd()) + { QString line = in.readLine(); - + // Check if this is a commented [multilib] section - if (line.trimmed() == "#[multilib]") { + if (line.trimmed() == "#[multilib]") + { lines.append("[multilib]"); multilibSectionFound = true; - } + } // Check if the Include line in multilib section is commented - else if (multilibSectionFound && line.trimmed().startsWith("#Include") && - line.contains("mirrorlist")) { - lines.append(line.mid(1)); // Remove the # comment character - multilibSectionFound = false; // Reset flag after processing + else if (multilibSectionFound && line.trimmed().startsWith("#Include") && line.contains("mirrorlist")) + { + lines.append(line.mid(1)); // Remove the # comment character + multilibSectionFound = false; // Reset flag after processing } - else { + else + { lines.append(line); } } file.close(); - + // Write back to file using pkexec for elevated privileges QString tempFile = "/tmp/pacman.conf.tmp"; QFile temp(tempFile); - if (!temp.open(QIODevice::WriteOnly | QIODevice::Text)) { - Logger::error("Failed to create temporary file"); + if (!temp.open(QIODevice::WriteOnly | QIODevice::Text)) + { + spdlog::error("Failed to create temporary file"); return false; } - + QTextStream out(&temp); - for (const QString& line : lines) { + for (const QString& line : lines) + { out << line << "\n"; } temp.close(); - + // Use pkexec to copy the file with elevated privileges QProcess process; process.start("pkexec", QStringList() << "cp" << tempFile << "/etc/pacman.conf"); - process.waitForFinished(30000); // 30 second timeout - - if (process.exitCode() != 0) { - Logger::error("Failed to update pacman.conf with elevated privileges"); + process.waitForFinished(30000); // 30 second timeout + + if (process.exitCode() != 0) + { + spdlog::error("Failed to update pacman.conf with elevated privileges"); QFile::remove(tempFile); return false; } - + QFile::remove(tempFile); - Logger::info("Successfully enabled multilib repository"); + spdlog::info("Successfully enabled multilib repository"); return true; } -bool SettingsWidget::disableMultilibInPacmanConf() { +bool SettingsWidget::disableMultilibInPacmanConf() +{ QFile file("/etc/pacman.conf"); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - Logger::error("Failed to open /etc/pacman.conf for reading"); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + spdlog::error("Failed to open /etc/pacman.conf for reading"); return false; } - + QStringList lines; QTextStream in(&file); bool inMultilibSection = false; - - while (!in.atEnd()) { + + while (!in.atEnd()) + { QString line = in.readLine(); QString trimmedLine = line.trimmed(); - + // Check if this is [multilib] section - if (trimmedLine == "[multilib]") { + if (trimmedLine == "[multilib]") + { lines.append("#[multilib]"); inMultilibSection = true; } // Check if we're in multilib section and this is the Include line - else if (inMultilibSection && trimmedLine.startsWith("Include") && - trimmedLine.contains("mirrorlist")) { + else if (inMultilibSection && trimmedLine.startsWith("Include") && trimmedLine.contains("mirrorlist")) + { lines.append("#" + line); inMultilibSection = false; } // Check if we hit another section - else if (trimmedLine.startsWith("[") && trimmedLine != "[multilib]") { + else if (trimmedLine.startsWith("[") && trimmedLine != "[multilib]") + { lines.append(line); inMultilibSection = false; } - else { + else + { lines.append(line); } } file.close(); - + // Write back to file using pkexec for elevated privileges QString tempFile = "/tmp/pacman.conf.tmp"; QFile temp(tempFile); - if (!temp.open(QIODevice::WriteOnly | QIODevice::Text)) { - Logger::error("Failed to create temporary file"); + if (!temp.open(QIODevice::WriteOnly | QIODevice::Text)) + { + spdlog::error("Failed to create temporary file"); return false; } - + QTextStream out(&temp); - for (const QString& line : lines) { + for (const QString& line : lines) + { out << line << "\n"; } temp.close(); - + // Use pkexec to copy the file with elevated privileges QProcess process; process.start("pkexec", QStringList() << "cp" << tempFile << "/etc/pacman.conf"); - process.waitForFinished(30000); // 30 second timeout - - if (process.exitCode() != 0) { - Logger::error("Failed to update pacman.conf with elevated privileges"); + process.waitForFinished(30000); // 30 second timeout + + if (process.exitCode() != 0) + { + spdlog::error("Failed to update pacman.conf with elevated privileges"); QFile::remove(tempFile); return false; } - + QFile::remove(tempFile); - Logger::info("Successfully disabled multilib repository"); + spdlog::info("Successfully disabled multilib repository"); return true; } -bool SettingsWidget::enableChaoticAurInPacmanConf() { +bool SettingsWidget::enableChaoticAurInPacmanConf() +{ QFile file("/etc/pacman.conf"); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - Logger::error("Failed to open /etc/pacman.conf for reading"); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + spdlog::error("Failed to open /etc/pacman.conf for reading"); return false; } - + QStringList lines; QTextStream in(&file); bool inCommentedChaoticAurSection = false; bool chaoticAurSectionExists = false; - - while (!in.atEnd()) { + + while (!in.atEnd()) + { QString line = in.readLine(); QString trimmedLine = line.trimmed(); - + // Check if chaotic-aur section already exists (uncommented or commented) - if (trimmedLine == "[chaotic-aur]" || trimmedLine == "#[chaotic-aur]") { + if (trimmedLine == "[chaotic-aur]" || trimmedLine == "#[chaotic-aur]") + { chaoticAurSectionExists = true; - + // If it's commented, uncomment it - if (trimmedLine == "#[chaotic-aur]") { + if (trimmedLine == "#[chaotic-aur]") + { lines.append("[chaotic-aur]"); inCommentedChaoticAurSection = true; - } else { + } + else + { // Already uncommented, keep as is lines.append(line); } - } + } // Check if the Include/Server line in chaotic-aur section is commented - else if (inCommentedChaoticAurSection && trimmedLine.startsWith("#") && - (trimmedLine.contains("Include") || trimmedLine.contains("Server"))) { + else if (inCommentedChaoticAurSection && trimmedLine.startsWith("#") + && (trimmedLine.contains("Include") || trimmedLine.contains("Server"))) + { // Remove the # comment character lines.append(line.mid(line.indexOf('#') + 1)); inCommentedChaoticAurSection = false; } // Check if we hit another section, reset flag - else if (trimmedLine.startsWith("[") && trimmedLine != "[chaotic-aur]" && trimmedLine != "#[chaotic-aur]") { + else if (trimmedLine.startsWith("[") && trimmedLine != "[chaotic-aur]" && trimmedLine != "#[chaotic-aur]") + { lines.append(line); inCommentedChaoticAurSection = false; } - else { + else + { lines.append(line); } } file.close(); - + // If chaotic-aur section doesn't exist at all, add it - if (!chaoticAurSectionExists) { + if (!chaoticAurSectionExists) + { lines.append(""); lines.append("[chaotic-aur]"); lines.append("Include = /etc/pacman.d/chaotic-mirrorlist"); } - + // Write back to file using pkexec for elevated privileges QString tempFile = "/tmp/pacman.conf.tmp"; QFile temp(tempFile); - if (!temp.open(QIODevice::WriteOnly | QIODevice::Text)) { - Logger::error("Failed to create temporary file"); + if (!temp.open(QIODevice::WriteOnly | QIODevice::Text)) + { + spdlog::error("Failed to create temporary file"); return false; } - + QTextStream out(&temp); - for (const QString& line : lines) { + for (const QString& line : lines) + { out << line << "\n"; } temp.close(); - + // Use pkexec to copy the file with elevated privileges QProcess process; process.start("pkexec", QStringList() << "cp" << tempFile << "/etc/pacman.conf"); - process.waitForFinished(30000); // 30 second timeout - - if (process.exitCode() != 0) { - Logger::error("Failed to update pacman.conf with elevated privileges"); + process.waitForFinished(30000); // 30 second timeout + + if (process.exitCode() != 0) + { + spdlog::error("Failed to update pacman.conf with elevated privileges"); QFile::remove(tempFile); return false; } - + QFile::remove(tempFile); - Logger::info("Successfully enabled chaotic-aur repository"); + spdlog::info("Successfully enabled chaotic-aur repository"); return true; } -bool SettingsWidget::disableChaoticAurInPacmanConf() { +bool SettingsWidget::disableChaoticAurInPacmanConf() +{ QFile file("/etc/pacman.conf"); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - Logger::error("Failed to open /etc/pacman.conf for reading"); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + spdlog::error("Failed to open /etc/pacman.conf for reading"); return false; } - + QStringList lines; QTextStream in(&file); bool inChaoticAurSection = false; - - while (!in.atEnd()) { + + while (!in.atEnd()) + { QString line = in.readLine(); QString trimmedLine = line.trimmed(); - + // Check if this is [chaotic-aur] section (uncommented or already commented) - if (trimmedLine == "[chaotic-aur]") { + if (trimmedLine == "[chaotic-aur]") + { lines.append("#[chaotic-aur]"); inChaoticAurSection = true; } - else if (trimmedLine == "#[chaotic-aur]") { + else if (trimmedLine == "#[chaotic-aur]") + { // Already commented, keep as is lines.append(line); inChaoticAurSection = false; } // Check if we're in chaotic-aur section and this is the Include/Server line (not already commented) - else if (inChaoticAurSection && !trimmedLine.startsWith("#") && - (trimmedLine.startsWith("Include") || trimmedLine.startsWith("Server"))) { + else if (inChaoticAurSection && !trimmedLine.startsWith("#") + && (trimmedLine.startsWith("Include") || trimmedLine.startsWith("Server"))) + { lines.append("#" + line); inChaoticAurSection = false; } // Check if we hit another section - else if (trimmedLine.startsWith("[") && trimmedLine != "[chaotic-aur]" && trimmedLine != "#[chaotic-aur]") { + else if (trimmedLine.startsWith("[") && trimmedLine != "[chaotic-aur]" && trimmedLine != "#[chaotic-aur]") + { lines.append(line); inChaoticAurSection = false; } - else { + else + { lines.append(line); } } file.close(); - + // Write back to file using pkexec for elevated privileges QString tempFile = "/tmp/pacman.conf.tmp"; QFile temp(tempFile); - if (!temp.open(QIODevice::WriteOnly | QIODevice::Text)) { - Logger::error("Failed to create temporary file"); + if (!temp.open(QIODevice::WriteOnly | QIODevice::Text)) + { + spdlog::error("Failed to create temporary file"); return false; } - + QTextStream out(&temp); - for (const QString& line : lines) { + for (const QString& line : lines) + { out << line << "\n"; } temp.close(); - + // Use pkexec to copy the file with elevated privileges QProcess process; process.start("pkexec", QStringList() << "cp" << tempFile << "/etc/pacman.conf"); - process.waitForFinished(30000); // 30 second timeout - - if (process.exitCode() != 0) { - Logger::error("Failed to update pacman.conf with elevated privileges"); + process.waitForFinished(30000); // 30 second timeout + + if (process.exitCode() != 0) + { + spdlog::error("Failed to update pacman.conf with elevated privileges"); QFile::remove(tempFile); return false; } - + QFile::remove(tempFile); - Logger::info("Successfully disabled chaotic-aur repository"); + spdlog::info("Successfully disabled chaotic-aur repository"); return true; } -void SettingsWidget::onSettingsChanged() { +void SettingsWidget::onSettingsChanged() +{ // Enable apply button when settings change - bool hasChanges = (m_multilibRepoCheckbox->isChecked() != m_originalMultilibState) || - (m_chaoticAurCheckbox->isChecked() != m_originalChaoticAurState); + bool hasChanges = (m_multilibRepoCheckbox->isChecked() != m_originalMultilibState) + || (m_chaoticAurCheckbox->isChecked() != m_originalChaoticAurState); m_applyButton->setEnabled(hasChanges); m_statusLabel->hide(); } -void SettingsWidget::onApplyClicked() { +void SettingsWidget::onApplyClicked() +{ bool currentMultilibState = m_multilibRepoCheckbox->isChecked(); bool currentChaoticAurState = m_chaoticAurCheckbox->isChecked(); bool success = true; bool changesApplied = false; - + // Handle multilib changes - if (currentMultilibState != m_originalMultilibState) { + if (currentMultilibState != m_originalMultilibState) + { // Show confirmation dialog QString message; - if (currentMultilibState) { + if (currentMultilibState) + { message = "This will enable the multilib repository by modifying /etc/pacman.conf.\n" - "You will be prompted for administrator privileges.\n\n" - "After enabling, you should run 'sudo pacman -Sy' to sync the databases.\n\n" - "Do you want to continue?"; - } else { + "You will be prompted for administrator privileges.\n\n" + "After enabling, you should run 'sudo pacman -Sy' to sync the databases.\n\n" + "Do you want to continue?"; + } + else + { message = "This will disable the multilib repository by modifying /etc/pacman.conf.\n" - "You will be prompted for administrator privileges.\n\n" - "Do you want to continue?"; + "You will be prompted for administrator privileges.\n\n" + "Do you want to continue?"; } - - auto reply = QMessageBox::question(this, "Confirm Repository Change", - message, - QMessageBox::Yes | QMessageBox::No); - - if (reply != QMessageBox::Yes) { + + auto reply + = QMessageBox::question(this, "Confirm Repository Change", message, QMessageBox::Yes | QMessageBox::No); + + if (reply != QMessageBox::Yes) + { return; } - + // Apply the change - if (currentMultilibState) { + if (currentMultilibState) + { success = enableMultilibInPacmanConf(); - } else { + } + else + { success = disableMultilibInPacmanConf(); } - - if (success) { + + if (success) + { m_originalMultilibState = currentMultilibState; changesApplied = true; - + // Emit signal to notify other widgets emit multilibStatusChanged(currentMultilibState); - } else { + } + else + { success = false; } } - + // Handle chaotic-aur changes - if (currentChaoticAurState != m_originalChaoticAurState) { + if (currentChaoticAurState != m_originalChaoticAurState) + { // Show confirmation dialog QString message; - if (currentChaoticAurState) { + if (currentChaoticAurState) + { message = "This will enable the chaotic-aur repository by modifying /etc/pacman.conf.\n" - "You will be prompted for administrator privileges.\n\n" - "Note: Make sure chaotic-keyring and chaotic-mirrorlist are installed first.\n\n" - "After enabling, you should run 'sudo pacman -Sy' to sync the databases.\n\n" - "Do you want to continue?"; - } else { + "You will be prompted for administrator privileges.\n\n" + "Note: Make sure chaotic-keyring and chaotic-mirrorlist are installed first.\n\n" + "After enabling, you should run 'sudo pacman -Sy' to sync the databases.\n\n" + "Do you want to continue?"; + } + else + { message = "This will disable the chaotic-aur repository by modifying /etc/pacman.conf.\n" - "You will be prompted for administrator privileges.\n\n" - "Do you want to continue?"; + "You will be prompted for administrator privileges.\n\n" + "Do you want to continue?"; } - - auto reply = QMessageBox::question(this, "Confirm Repository Change", - message, - QMessageBox::Yes | QMessageBox::No); - - if (reply != QMessageBox::Yes) { + + auto reply + = QMessageBox::question(this, "Confirm Repository Change", message, QMessageBox::Yes | QMessageBox::No); + + if (reply != QMessageBox::Yes) + { return; } - + // Apply the change bool chaoticSuccess = false; - if (currentChaoticAurState) { + if (currentChaoticAurState) + { chaoticSuccess = enableChaoticAurInPacmanConf(); - } else { + } + else + { chaoticSuccess = disableChaoticAurInPacmanConf(); } - - if (chaoticSuccess) { + + if (chaoticSuccess) + { m_originalChaoticAurState = currentChaoticAurState; changesApplied = true; - + // Emit signal to notify other widgets emit chaoticAurStatusChanged(currentChaoticAurState); - } else { + } + else + { success = false; } } - + // Show results and offer database sync if changes were applied - if (changesApplied && success) { + if (changesApplied && success) + { m_statusLabel->setText("Settings applied successfully! Please sync package databases."); m_statusLabel->setProperty("class", "status-msg-success"); - m_statusLabel->style()->unpolish(m_statusLabel); + m_statusLabel->style()->unpolish(m_statusLabel); m_statusLabel->style()->polish(m_statusLabel); - m_statusLabel->show(); - + m_statusLabel->show(); + m_applyButton->setEnabled(false); - + // Suggest database sync - auto reply = QMessageBox::question(this, "Sync Package Database", - "Would you like to sync the package database now?\n" - "(This will run 'pkexec pacman -Sy')", - QMessageBox::Yes | QMessageBox::No); - - if (reply == QMessageBox::Yes) { - QProcess process; - m_statusLabel->setText("Syncing package databases..."); - process.start("pkexec", QStringList() << "pacman" << "-Sy"); - process.waitForFinished(60000); // 60 second timeout - - if (process.exitCode() == 0) { - m_statusLabel->setText("Package databases synced successfully!"); - m_statusLabel->setProperty("class", "status-msg-success"); - Logger::info("Package databases synced after repository change"); - - // Refresh ALPM databases to pick up the new repository - AlpmWrapper::instance().refreshDatabases(); - } else { - m_statusLabel->setText("Failed to sync package databases. Please run 'sudo pacman -Sy' manually."); - m_statusLabel->setProperty("class", "status-msg-error"); - } - } else { - // Even if they don't sync now, refresh ALPM to detect the new repo configuration + auto reply = QMessageBox::question(this, + "Sync Package Database", + "Would you like to sync the package database now?\n" + "(This will run 'pkexec pacman -Sy')", + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) + { + QProcess process; + m_statusLabel->setText("Syncing package databases..."); + process.start("pkexec", QStringList() << "pacman" << "-Sy"); + process.waitForFinished(60000); // 60 second timeout + + if (process.exitCode() == 0) + { + m_statusLabel->setText("Package databases synced successfully!"); + m_statusLabel->setProperty("class", "status-msg-success"); + spdlog::info("Package databases synced after repository change"); + + // Refresh ALPM databases to pick up the new repository AlpmWrapper::instance().refreshDatabases(); } - } else if (!success) { + else + { + m_statusLabel->setText("Failed to sync package databases. Please run 'sudo pacman -Sy' manually."); + m_statusLabel->setProperty("class", "status-msg-error"); + } + } + else + { + // Even if they don't sync now, refresh ALPM to detect the new repo configuration + AlpmWrapper::instance().refreshDatabases(); + } + } + else if (!success) + { m_statusLabel->setText("Failed to apply settings. Please check permissions."); m_statusLabel->setProperty("class", "status-msg-error"); m_statusLabel->style()->unpolish(m_statusLabel); m_statusLabel->style()->polish(m_statusLabel); - m_statusLabel->show(); + m_statusLabel->show(); } } -bool SettingsWidget::isMultilibEnabled() const { - return m_multilibRepoCheckbox->isChecked() && - (m_multilibRepoCheckbox->isChecked() == m_originalMultilibState); +bool SettingsWidget::isMultilibEnabled() const +{ + return m_multilibRepoCheckbox->isChecked() && (m_multilibRepoCheckbox->isChecked() == m_originalMultilibState); } -bool SettingsWidget::isChaoticAurEnabled() const { - return m_chaoticAurCheckbox->isChecked() && - (m_chaoticAurCheckbox->isChecked() == m_originalChaoticAurState); +bool SettingsWidget::isChaoticAurEnabled() const +{ + return m_chaoticAurCheckbox->isChecked() && (m_chaoticAurCheckbox->isChecked() == m_originalChaoticAurState); } -void SettingsWidget::applySettings() { +void SettingsWidget::applySettings() +{ onApplyClicked(); } -void SettingsWidget::onRemoveLockClicked() { +void SettingsWidget::onRemoveLockClicked() +{ QString lockFilePath = "/var/lib/pacman/db.lck"; - + // Check if lock file exists QFile lockFile(lockFilePath); - if (!lockFile.exists()) { - QMessageBox::information(this, "Lock File Not Found", - "The pacman lock file does not exist.\n" - "No action needed."); + if (!lockFile.exists()) + { + QMessageBox::information(this, + "Lock File Not Found", + "The pacman lock file does not exist.\n" + "No action needed."); return; } - + // Show warning dialog with checkbox QMessageBox msgBox(this); msgBox.setIcon(QMessageBox::Warning); msgBox.setWindowTitle("Remove Pacman Lock File"); msgBox.setText("Are you sure you want to remove the pacman lock file?"); - msgBox.setInformativeText( - "This will remove: /var/lib/pacman/db.lck\n\n" - "WARNING: Only do this if you are certain that no other package manager " - "(pacman, yay, paru, etc.) is currently running.\n\n" - "Removing the lock file while a package operation is in progress can " - "corrupt your package database!"); - + msgBox.setInformativeText("This will remove: /var/lib/pacman/db.lck\n\n" + "WARNING: Only do this if you are certain that no other package manager " + "(pacman, yay, paru, etc.) is currently running.\n\n" + "Removing the lock file while a package operation is in progress can " + "corrupt your package database!"); + QCheckBox* confirmCheckbox = new QCheckBox("I understand the risks and confirm no package manager is running"); msgBox.setCheckBox(confirmCheckbox); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); - + int ret = msgBox.exec(); - - if (ret == QMessageBox::Yes && confirmCheckbox->isChecked()) { + + if (ret == QMessageBox::Yes && confirmCheckbox->isChecked()) + { // Use pkexec to remove the lock file with elevated privileges QProcess process; process.start("pkexec", QStringList() << "rm" << "-f" << lockFilePath); - process.waitForFinished(30000); // 30 second timeout - - if (process.exitCode() == 0) { + process.waitForFinished(30000); // 30 second timeout + + if (process.exitCode() == 0) + { m_statusLabel->setText("Lock file removed successfully!"); m_statusLabel->setProperty("class", "status-msg-success"); m_statusLabel->style()->unpolish(m_statusLabel); m_statusLabel->style()->polish(m_statusLabel); m_statusLabel->show(); - Logger::info("Pacman lock file removed successfully"); - - QMessageBox::information(this, "Success", - "The pacman lock file has been removed successfully.\n" - "You can now run package operations."); - } else { + spdlog::info("Pacman lock file removed successfully"); + + QMessageBox::information(this, + "Success", + "The pacman lock file has been removed successfully.\n" + "You can now run package operations."); + } + else + { m_statusLabel->setText("Failed to remove lock file. Check permissions."); m_statusLabel->setProperty("class", "status-msg-error"); m_statusLabel->style()->unpolish(m_statusLabel); m_statusLabel->style()->polish(m_statusLabel); m_statusLabel->show(); - Logger::error("Failed to remove pacman lock file"); - - QMessageBox::critical(this, "Error", - "Failed to remove the lock file.\n" - "You may need to run: sudo rm /var/lib/pacman/db.lck"); + spdlog::error("Failed to remove pacman lock file"); + + QMessageBox::critical(this, + "Error", + "Failed to remove the lock file.\n" + "You may need to run: sudo rm /var/lib/pacman/db.lck"); } - } else if (ret == QMessageBox::Yes && !confirmCheckbox->isChecked()) { - QMessageBox::warning(this, "Confirmation Required", - "You must check the confirmation box to proceed."); + } + else if (ret == QMessageBox::Yes && !confirmCheckbox->isChecked()) + { + QMessageBox::warning(this, "Confirmation Required", "You must check the confirmation box to proceed."); } } -void SettingsWidget::onSetupChaoticClicked() { +void SettingsWidget::onSetupChaoticClicked() +{ QMessageBox msgBox(this); msgBox.setIcon(QMessageBox::Question); msgBox.setWindowTitle("Setup Chaotic-AUR"); msgBox.setText("Install Chaotic-AUR repository?"); - msgBox.setInformativeText( - "This will:\n" - "1. Download chaotic-keyring and chaotic-mirrorlist packages\n" - "2. Install them using pacman\n" - "3. Add the repository to /etc/pacman.conf\n\n" - "This requires internet connection and administrator privileges."); + msgBox.setInformativeText("This will:\n" + "1. Download chaotic-keyring and chaotic-mirrorlist packages\n" + "2. Install them using pacman\n" + "3. Add the repository to /etc/pacman.conf\n\n" + "This requires internet connection and administrator privileges."); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::Yes); - - if (msgBox.exec() != QMessageBox::Yes) { + + if (msgBox.exec() != QMessageBox::Yes) + { return; } - + m_statusLabel->setText("Setting up Chaotic-AUR repository..."); m_statusLabel->setProperty("class", "status-msg-info"); m_statusLabel->style()->unpolish(m_statusLabel); m_statusLabel->style()->polish(m_statusLabel); m_statusLabel->show(); m_setupChaoticButton->setEnabled(false); - + // Use a shell script to download and install chaotic-aur packages // This follows the official installation guide from aur.chaotic.cx - QString script = - "cd /tmp && " - "rm -f chaotic-keyring.pkg.tar.zst chaotic-mirrorlist.pkg.tar.zst && " - "curl -L -O https://cdn-mirror.chaotic.cx/chaotic-aur/chaotic-keyring.pkg.tar.zst && " - "curl -L -O https://cdn-mirror.chaotic.cx/chaotic-aur/chaotic-mirrorlist.pkg.tar.zst && " - "pacman -U --noconfirm chaotic-keyring.pkg.tar.zst chaotic-mirrorlist.pkg.tar.zst"; - + QString script = "cd /tmp && " + "rm -f chaotic-keyring.pkg.tar.zst chaotic-mirrorlist.pkg.tar.zst && " + "curl -L -O https://cdn-mirror.chaotic.cx/chaotic-aur/chaotic-keyring.pkg.tar.zst && " + "curl -L -O https://cdn-mirror.chaotic.cx/chaotic-aur/chaotic-mirrorlist.pkg.tar.zst && " + "pacman -U --noconfirm chaotic-keyring.pkg.tar.zst chaotic-mirrorlist.pkg.tar.zst"; + QProcess* process = new QProcess(this); - + // Capture both stdout and stderr for debugging process->setProcessChannelMode(QProcess::MergedChannels); - - connect(process, QOverload::of(&QProcess::finished), - this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) { - QString output = process->readAll(); - - Logger::info(QString("Chaotic-AUR setup exit code: %1, status: %2") - .arg(exitCode) - .arg(exitStatus == QProcess::NormalExit ? "Normal" : "Crashed")); - - if (!output.isEmpty()) { - Logger::debug(QString("Chaotic-AUR setup output:\n%1").arg(output)); - } - - process->deleteLater(); - m_setupChaoticButton->setEnabled(true); - - if (exitCode == 0 && exitStatus == QProcess::NormalExit) { - m_statusLabel->setText("Chaotic-AUR packages installed successfully!"); - m_statusLabel->setProperty("class", "status-msg-success"); - m_statusLabel->style()->unpolish(m_statusLabel); - m_statusLabel->style()->polish(m_statusLabel); - m_statusLabel->show(); - Logger::info("Chaotic-AUR packages installed successfully"); - - // Refresh the chaotic-aur checkbox status - loadCurrentSettings(); - - QMessageBox::information(this, "Success", - "Chaotic-AUR packages installed successfully!\n\n" - "You can now enable the Chaotic-AUR repository using the checkbox above.\n" - "After enabling, remember to sync the package databases."); - } else { - m_statusLabel->setText("Failed to install Chaotic-AUR packages."); - m_statusLabel->setProperty("class", "status-msg-error"); - m_statusLabel->style()->unpolish(m_statusLabel); - m_statusLabel->style()->polish(m_statusLabel); - m_statusLabel->show(); - Logger::error(QString("Failed to install Chaotic-AUR packages. Exit code: %1").arg(exitCode)); - - // Show output in error message if available - QString errorDetails = "Possible reasons:\n" - "• No internet connection\n" - "• Download failed\n" - "• Installation cancelled\n" - "• User denied authentication\n\n"; - - if (!output.isEmpty() && output.length() < 500) { - errorDetails += "Error output:\n" + output; - } - - QMessageBox::critical(this, "Error", - "Failed to install Chaotic-AUR packages.\n\n" + errorDetails); - } - }); - + + connect(process, + QOverload::of(&QProcess::finished), + this, + [this, process](int exitCode, QProcess::ExitStatus exitStatus) + { + QString output = process->readAll(); + + spdlog::info("{}", + (QString("Chaotic-AUR setup exit code: %1, status: %2") + .arg(exitCode) + .arg(exitStatus == QProcess::NormalExit ? "Normal" : "Crashed")) + .toStdString()); + + if (!output.isEmpty()) + { + spdlog::debug("{}", (QString("Chaotic-AUR setup output:\n%1").arg(output)).toStdString()); + } + + process->deleteLater(); + m_setupChaoticButton->setEnabled(true); + + if (exitCode == 0 && exitStatus == QProcess::NormalExit) + { + m_statusLabel->setText("Chaotic-AUR packages installed successfully!"); + m_statusLabel->setProperty("class", "status-msg-success"); + m_statusLabel->style()->unpolish(m_statusLabel); + m_statusLabel->style()->polish(m_statusLabel); + m_statusLabel->show(); + spdlog::info("Chaotic-AUR packages installed successfully"); + + // Refresh the chaotic-aur checkbox status + loadCurrentSettings(); + + QMessageBox::information(this, + "Success", + "Chaotic-AUR packages installed successfully!\n\n" + "You can now enable the Chaotic-AUR repository using the checkbox above.\n" + "After enabling, remember to sync the package databases."); + } + else + { + m_statusLabel->setText("Failed to install Chaotic-AUR packages."); + m_statusLabel->setProperty("class", "status-msg-error"); + m_statusLabel->style()->unpolish(m_statusLabel); + m_statusLabel->style()->polish(m_statusLabel); + m_statusLabel->show(); + spdlog::error( + "{}", + (QString("Failed to install Chaotic-AUR packages. Exit code: %1").arg(exitCode)).toStdString()); + + // Show output in error message if available + QString errorDetails = "Possible reasons:\n" + "• No internet connection\n" + "• Download failed\n" + "• Installation cancelled\n" + "• User denied authentication\n\n"; + + if (!output.isEmpty() && output.length() < 500) + { + errorDetails += "Error output:\n" + output; + } + + QMessageBox::critical(this, "Error", "Failed to install Chaotic-AUR packages.\n\n" + errorDetails); + } + }); + process->start("pkexec", QStringList() << "bash" << "-c" << script); } -void SettingsWidget::onRemoveChaoticClicked() { +void SettingsWidget::onRemoveChaoticClicked() +{ QMessageBox msgBox(this); msgBox.setIcon(QMessageBox::Warning); msgBox.setWindowTitle("Remove Chaotic-AUR"); msgBox.setText("Remove Chaotic-AUR repository?"); - msgBox.setInformativeText( - "This will remove:\n" - "• chaotic-keyring\n" - "• chaotic-mirrorlist\n\n" - "Note: You may need to manually remove the [chaotic-aur] section " - "from /etc/pacman.conf to fully disable the repository."); + msgBox.setInformativeText("This will remove:\n" + "• chaotic-keyring\n" + "• chaotic-mirrorlist\n\n" + "Note: You may need to manually remove the [chaotic-aur] section " + "from /etc/pacman.conf to fully disable the repository."); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); - - if (msgBox.exec() != QMessageBox::Yes) { + + if (msgBox.exec() != QMessageBox::Yes) + { return; } - + m_statusLabel->setText("Removing Chaotic-AUR packages..."); m_statusLabel->setProperty("class", "status-msg-info"); m_statusLabel->style()->unpolish(m_statusLabel); m_statusLabel->style()->polish(m_statusLabel); m_statusLabel->show(); - m_removeChaoticButton->setEnabled(false); + m_removeChaoticButton->setEnabled(false); // Remove chaotic-keyring and chaotic-mirrorlist QProcess* process = new QProcess(this); - connect(process, QOverload::of(&QProcess::finished), - this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) { - process->deleteLater(); - m_removeChaoticButton->setEnabled(true); - - if (exitCode == 0 && exitStatus == QProcess::NormalExit) { - m_statusLabel->setText("Chaotic-AUR packages removed successfully!"); - m_statusLabel->setProperty("class", "status-msg-success"); - m_statusLabel->style()->unpolish(m_statusLabel); - m_statusLabel->style()->polish(m_statusLabel); - m_statusLabel->show(); - Logger::info("Chaotic-AUR packages removed successfully"); - - // Refresh the chaotic-aur checkbox status - loadCurrentSettings(); - - QMessageBox::information(this, "Success", - "Chaotic-AUR packages removed successfully!\n\n" - "To fully disable the repository, you may need to remove or comment out " - "the [chaotic-aur] section in /etc/pacman.conf"); - } else { - m_statusLabel->setText("Failed to remove Chaotic-AUR packages."); - m_statusLabel->setProperty("class", "status-msg-error"); - m_statusLabel->style()->unpolish(m_statusLabel); - m_statusLabel->style()->polish(m_statusLabel); - m_statusLabel->show(); - Logger::error("Failed to remove Chaotic-AUR packages"); - - QMessageBox::critical(this, "Error", - "Failed to remove Chaotic-AUR packages.\n" - "Please check the logs for details."); - } - }); - - process->start("pkexec", QStringList() << "pacman" << "-Rns" << "--noconfirm" - << "chaotic-keyring" << "chaotic-mirrorlist"); + connect(process, + QOverload::of(&QProcess::finished), + this, + [this, process](int exitCode, QProcess::ExitStatus exitStatus) + { + process->deleteLater(); + m_removeChaoticButton->setEnabled(true); + + if (exitCode == 0 && exitStatus == QProcess::NormalExit) + { + m_statusLabel->setText("Chaotic-AUR packages removed successfully!"); + m_statusLabel->setProperty("class", "status-msg-success"); + m_statusLabel->style()->unpolish(m_statusLabel); + m_statusLabel->style()->polish(m_statusLabel); + m_statusLabel->show(); + spdlog::info("Chaotic-AUR packages removed successfully"); + + // Refresh the chaotic-aur checkbox status + loadCurrentSettings(); + + QMessageBox::information(this, + "Success", + "Chaotic-AUR packages removed successfully!\n\n" + "To fully disable the repository, you may need to remove or comment out " + "the [chaotic-aur] section in /etc/pacman.conf"); + } + else + { + m_statusLabel->setText("Failed to remove Chaotic-AUR packages."); + m_statusLabel->setProperty("class", "status-msg-error"); + m_statusLabel->style()->unpolish(m_statusLabel); + m_statusLabel->style()->polish(m_statusLabel); + m_statusLabel->show(); + spdlog::error("Failed to remove Chaotic-AUR packages"); + + QMessageBox::critical(this, + "Error", + "Failed to remove Chaotic-AUR packages.\n" + "Please check the logs for details."); + } + }); + + process->start("pkexec", + QStringList() << "pacman" << "-Rns" << "--noconfirm" + << "chaotic-keyring" << "chaotic-mirrorlist"); } -void SettingsWidget::onSyncReposClicked() { +void SettingsWidget::onSyncReposClicked() +{ QMessageBox msgBox(this); msgBox.setIcon(QMessageBox::Question); msgBox.setWindowTitle("Sync Repositories"); msgBox.setText("Synchronize package databases?"); - msgBox.setInformativeText( - "This will run: pacman -Sy\n\n" - "This updates the list of available packages from all enabled repositories.\n" - "This is useful after enabling/disabling repositories or when you want to " - "ensure you have the latest package information."); + msgBox.setInformativeText("This will run: pacman -Sy\n\n" + "This updates the list of available packages from all enabled repositories.\n" + "This is useful after enabling/disabling repositories or when you want to " + "ensure you have the latest package information."); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::Yes); - - if (msgBox.exec() != QMessageBox::Yes) { + + if (msgBox.exec() != QMessageBox::Yes) + { return; } @@ -1113,91 +1166,103 @@ void SettingsWidget::onSyncReposClicked() { m_statusLabel->style()->unpolish(m_statusLabel); m_statusLabel->style()->polish(m_statusLabel); m_statusLabel->show(); - m_syncReposButton->setEnabled(false); + m_syncReposButton->setEnabled(false); // Run pacman -Sy with pkexec QProcess* process = new QProcess(this); - connect(process, QOverload::of(&QProcess::finished), - this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) { - process->deleteLater(); - m_syncReposButton->setEnabled(true); - - if (exitCode == 0 && exitStatus == QProcess::NormalExit) { - m_statusLabel->setText("Repositories synchronized successfully!"); - m_statusLabel->setProperty("class", "status-msg-success"); - m_statusLabel->style()->unpolish(m_statusLabel); - m_statusLabel->style()->polish(m_statusLabel); - m_statusLabel->show(); - Logger::info("Repositories synchronized successfully"); - - // Refresh ALPM databases - AlpmWrapper::instance().refreshDatabases(); - - QMessageBox::information(this, "Success", - "Package databases synchronized successfully!\n\n" - "The package list has been updated with the latest available packages."); - } else { - m_statusLabel->setText("Failed to synchronize repositories."); - m_statusLabel->setProperty("class", "status-msg-error"); - m_statusLabel->style()->unpolish(m_statusLabel); - m_statusLabel->style()->polish(m_statusLabel); - m_statusLabel->show(); - Logger::error("Failed to synchronize repositories"); - - QMessageBox::critical(this, "Error", - "Failed to synchronize package databases.\n" - "Please check your internet connection and try again."); - } - }); - + connect(process, + QOverload::of(&QProcess::finished), + this, + [this, process](int exitCode, QProcess::ExitStatus exitStatus) + { + process->deleteLater(); + m_syncReposButton->setEnabled(true); + + if (exitCode == 0 && exitStatus == QProcess::NormalExit) + { + m_statusLabel->setText("Repositories synchronized successfully!"); + m_statusLabel->setProperty("class", "status-msg-success"); + m_statusLabel->style()->unpolish(m_statusLabel); + m_statusLabel->style()->polish(m_statusLabel); + m_statusLabel->show(); + spdlog::info("Repositories synchronized successfully"); + + // Refresh ALPM databases + AlpmWrapper::instance().refreshDatabases(); + + QMessageBox::information(this, + "Success", + "Package databases synchronized successfully!\n\n" + "The package list has been updated with the latest available packages."); + } + else + { + m_statusLabel->setText("Failed to synchronize repositories."); + m_statusLabel->setProperty("class", "status-msg-error"); + m_statusLabel->style()->unpolish(m_statusLabel); + m_statusLabel->style()->polish(m_statusLabel); + m_statusLabel->show(); + spdlog::error("Failed to synchronize repositories"); + + QMessageBox::critical(this, + "Error", + "Failed to synchronize package databases.\n" + "Please check your internet connection and try again."); + } + }); + process->start("pkexec", QStringList() << "pacman" << "-Sy"); } -void SettingsWidget::onCancelProcessClicked() { +void SettingsWidget::onCancelProcessClicked() +{ // Check if there's actually a process running - if (!PackageManager::instance().isOperationRunning()) { - QMessageBox::information(this, "No Process Running", - "There is no package operation currently running.\n" - "Nothing to kill."); + if (!PackageManager::instance().isOperationRunning()) + { + QMessageBox::information(this, + "No Process Running", + "There is no package operation currently running.\n" + "Nothing to kill."); return; } - + // Show confirmation dialog QMessageBox msgBox(this); msgBox.setIcon(QMessageBox::Warning); msgBox.setWindowTitle("Kill Running Process"); msgBox.setText("Are you sure you want to kill the running package operation?"); - msgBox.setInformativeText( - "This will stop the current installation, uninstallation, or update process.\n\n" - "WARNING: Killing a package operation may leave your system in an inconsistent state.\n" - "You may need to run the operation again to complete it properly.\n\n" - "It's recommended to only kill operation if the process is truly stuck or unresponsive."); + msgBox.setInformativeText("This will stop the current installation, uninstallation, or update process.\n\n" + "WARNING: Killing a package operation may leave your system in an inconsistent state.\n" + "You may need to run the operation again to complete it properly.\n\n" + "It's recommended to only kill operation if the process is truly stuck or unresponsive."); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); - - if (msgBox.exec() != QMessageBox::Yes) { + + if (msgBox.exec() != QMessageBox::Yes) + { return; } - + m_statusLabel->setText("Killing running process..."); m_statusLabel->setProperty("class", "status-msg-info"); m_statusLabel->style()->unpolish(m_statusLabel); m_statusLabel->style()->polish(m_statusLabel); - m_statusLabel->show(); + m_statusLabel->show(); // Cancel the operation PackageManager::instance().cancelRunningOperation(); - + m_statusLabel->setText("Process killed successfully!"); m_statusLabel->setProperty("class", "status-msg-success"); m_statusLabel->style()->unpolish(m_statusLabel); m_statusLabel->style()->polish(m_statusLabel); m_statusLabel->show(); - Logger::info("User killed running package operation from settings"); - - QMessageBox::information(this, "Process Killed", - "The running package operation has been killed.\n\n" - "If you were in the middle of installing or updating a package, " - "you may need to run the operation again to complete it."); + spdlog::info("User killed running package operation from settings"); + + QMessageBox::information(this, + "Process Killed", + "The running package operation has been killed.\n\n" + "If you were in the middle of installing or updating a package, " + "you may need to run the operation again to complete it."); } diff --git a/src/gui/settings_widget.h b/src/gui/settings_widget.h index 064f2f8..c022c4e 100644 --- a/src/gui/settings_widget.h +++ b/src/gui/settings_widget.h @@ -1,12 +1,12 @@ #ifndef SETTINGS_WIDGET_H #define SETTINGS_WIDGET_H -#include #include +#include #include #include -#include #include +#include /** * @brief Widget for application and repository settings. @@ -14,20 +14,21 @@ * Memory Management: * - All Qt widget members use Qt parent-child ownership (raw pointers are non-owning) */ -class SettingsWidget : public QWidget { +class SettingsWidget : public QWidget +{ Q_OBJECT - + public: explicit SettingsWidget(QWidget* parent = nullptr); ~SettingsWidget() override = default; - + bool isMultilibEnabled() const; bool isChaoticAurEnabled() const; - + signals: void multilibStatusChanged(bool enabled); void chaoticAurStatusChanged(bool enabled); - + private: void setupUi(); void loadCurrentSettings(); @@ -41,35 +42,35 @@ class SettingsWidget : public QWidget { bool enableChaoticAurInPacmanConf(); bool disableChaoticAurInPacmanConf(); void applySettings(); - + // Repository settings (Qt parent-child managed, non-owning pointers) QGroupBox* m_repositoryGroup = nullptr; QCheckBox* m_coreRepoCheckbox = nullptr; QCheckBox* m_extraRepoCheckbox = nullptr; QCheckBox* m_multilibRepoCheckbox = nullptr; QCheckBox* m_chaoticAurCheckbox = nullptr; - + // Chaotic-AUR setup (Qt parent-child managed) QGroupBox* m_chaoticAurGroup = nullptr; QPushButton* m_setupChaoticButton = nullptr; QPushButton* m_removeChaoticButton = nullptr; - + // Maintenance settings (Qt parent-child managed) QGroupBox* m_maintenanceGroup = nullptr; QPushButton* m_removeLockButton = nullptr; QPushButton* m_syncReposButton = nullptr; QPushButton* m_cancelProcessButton = nullptr; - + // Control buttons (Qt parent-child managed) QPushButton* m_applyButton = nullptr; - + // Status (Qt parent-child managed) QLabel* m_statusLabel = nullptr; - + // Track original state bool m_originalMultilibState = false; bool m_originalChaoticAurState = false; - + private slots: void onApplyClicked(); void onSettingsChanged(); @@ -80,4 +81,4 @@ private slots: void onCancelProcessClicked(); }; -#endif // SETTINGS_WIDGET_H +#endif // SETTINGS_WIDGET_H diff --git a/src/gui/updates_widget.cpp b/src/gui/updates_widget.cpp index 0afc1ca..40bbd9e 100644 --- a/src/gui/updates_widget.cpp +++ b/src/gui/updates_widget.cpp @@ -2,81 +2,87 @@ #include "../core/alpm_wrapper.h" #include "../core/aur_helper.h" #include "../core/package_manager.h" -#include "../utils/logger.h" -#include +#include "../utils/logging.h" #include +#include #include -#include -#include #include +#include -class UpdateItem : public QWidget { +class UpdateItem : public QWidget +{ Q_OBJECT - + public: UpdateItem(const UpdateInfo& info, QWidget* parent = nullptr) - : QWidget(parent), m_info(info) { + : QWidget(parent) + , m_info(info) + { // Required for the app-wide stylesheet to paint this custom widget's background setAttribute(Qt::WA_StyledBackground, true); auto* layout = new QHBoxLayout(this); layout->setContentsMargins(10, 10, 10, 10); - + auto* infoLayout = new QVBoxLayout(); - + auto* nameLabel = new QLabel(m_info.name, this); auto nameFont = nameLabel->font(); nameFont.setBold(true); nameFont.setPointSize(12); nameLabel->setFont(nameFont); infoLayout->addWidget(nameLabel); - - auto* versionLabel = new QLabel( - QString("%1 → %2").arg(m_info.oldVersion, m_info.newVersion), this); + + auto* versionLabel = new QLabel(QString("%1 → %2").arg(m_info.oldVersion, m_info.newVersion), this); versionLabel->setProperty("class", "secondary-text"); - infoLayout->addWidget(versionLabel); - + infoLayout->addWidget(versionLabel); + auto* repoLabel = new QLabel(m_info.repository, this); repoLabel->setProperty("class", "dim-text"); - infoLayout->addWidget(repoLabel); - + infoLayout->addWidget(repoLabel); + layout->addLayout(infoLayout); layout->addStretch(); - - if (m_info.downloadSize > 0) { + + if (m_info.downloadSize > 0) + { auto* sizeLabel = new QLabel(formatSize(m_info.downloadSize), this); sizeLabel->setProperty("class", "secondary-text"); - layout->addWidget(sizeLabel); + layout->addWidget(sizeLabel); } - + auto* updateButton = new QPushButton("Update", this); updateButton->setMinimumWidth(100); - connect(updateButton, &QPushButton::clicked, [this]() { - emit updateRequested(m_info.name); - }); + connect(updateButton, &QPushButton::clicked, [this]() { emit updateRequested(m_info.name); }); layout->addWidget(updateButton); - + setLayout(layout); setProperty("class", "update-item"); } - + signals: void updateRequested(const QString& packageName); - + private: UpdateInfo m_info; - - QString formatSize(qint64 bytes) { + + QString formatSize(qint64 bytes) + { const qint64 KB = 1024; const qint64 MB = KB * 1024; const qint64 GB = MB * 1024; - - if (bytes >= GB) { + + if (bytes >= GB) + { return QString("%1 GB").arg(bytes / static_cast(GB), 0, 'f', 2); - } else if (bytes >= MB) { + } + else if (bytes >= MB) + { return QString("%1 MB").arg(bytes / static_cast(MB), 0, 'f', 1); - } else if (bytes >= KB) { + } + else if (bytes >= KB) + { return QString("%1 KB").arg(bytes / static_cast(KB), 0, 'f', 0); } return QString("%1 B").arg(bytes); @@ -98,251 +104,295 @@ UpdatesWidget::UpdatesWidget(QWidget* parent) , m_progressLabel(new QLabel(this)) , m_toggleLogButton(new QPushButton("Show Logs", this)) , m_logWidget(new QWidget(this)) - , m_logViewer(new QTextEdit(this)) { - + , m_logViewer(new QTextEdit(this)) +{ + setupUi(); - + // Connect to PackageManager signals - connect(&PackageManager::instance(), &PackageManager::operationStarted, - this, &UpdatesWidget::onOperationStarted); - connect(&PackageManager::instance(), &PackageManager::operationOutput, - this, &UpdatesWidget::onOperationOutput); - connect(&PackageManager::instance(), &PackageManager::operationCompleted, - this, &UpdatesWidget::onOperationCompleted); - connect(&PackageManager::instance(), &PackageManager::operationError, - this, &UpdatesWidget::onOperationError); + connect(&PackageManager::instance(), &PackageManager::operationStarted, this, &UpdatesWidget::onOperationStarted); + connect(&PackageManager::instance(), &PackageManager::operationOutput, this, &UpdatesWidget::onOperationOutput); + connect( + &PackageManager::instance(), &PackageManager::operationCompleted, this, &UpdatesWidget::onOperationCompleted); + connect(&PackageManager::instance(), &PackageManager::operationError, this, &UpdatesWidget::onOperationError); } -void UpdatesWidget::setupUi() { +void UpdatesWidget::setupUi() +{ auto* mainLayout = new QVBoxLayout(this); - + // Header auto* headerLayout = new QHBoxLayout(); - + auto* titleLabel = new QLabel("Available Updates", this); titleLabel->setObjectName("view-title"); headerLayout->addWidget(titleLabel); - + headerLayout->addStretch(); - + m_countLabel->setProperty("class", "secondary-text"); - headerLayout->addWidget(m_countLabel); - + headerLayout->addWidget(m_countLabel); + m_checkButton->setMinimumHeight(35); connect(m_checkButton, &QPushButton::clicked, this, &UpdatesWidget::checkForUpdates); headerLayout->addWidget(m_checkButton); - + m_updateAllButton->setMinimumHeight(35); m_updateAllButton->setMinimumWidth(120); m_updateAllButton->setProperty("class", "primary-btn"); m_updateAllButton->setEnabled(false); connect(m_updateAllButton, &QPushButton::clicked, this, &UpdatesWidget::onUpdateAll); headerLayout->addWidget(m_updateAllButton); - + mainLayout->addLayout(headerLayout); - + // Search bar m_searchInput->setPlaceholderText("Search updates..."); m_searchInput->setMinimumHeight(35); m_searchInput->setClearButtonEnabled(true); - m_searchInput->setEnabled(false); // Disabled until updates are loaded + m_searchInput->setEnabled(false); // Disabled until updates are loaded connect(m_searchInput, &QLineEdit::textChanged, this, &UpdatesWidget::onSearchTextChanged); mainLayout->addWidget(m_searchInput); - + // Status label m_statusLabel->setAlignment(Qt::AlignCenter); m_statusLabel->setText("Click 'Check for Updates' to scan for available updates"); mainLayout->addWidget(m_statusLabel); - + // Updates area m_scrollArea->setWidget(m_contentWidget); m_scrollArea->setWidgetResizable(true); m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - + m_contentLayout->setSpacing(5); m_contentLayout->setContentsMargins(10, 10, 10, 10); m_contentLayout->addStretch(); - + mainLayout->addWidget(m_scrollArea); - + // Progress bar section (hidden by default) auto* progressLayout = new QVBoxLayout(m_progressWidget); - progressLayout->setContentsMargins(20, 0, 20, 20); + progressLayout->setContentsMargins(20, 0, 20, 20); progressLayout->setSpacing(8); - + m_progressLabel->setProperty("class", "dim-text"); - m_progressLabel->setAlignment(Qt::AlignCenter); + m_progressLabel->setAlignment(Qt::AlignCenter); progressLayout->addWidget(m_progressLabel); - + m_progressBar->setMinimumHeight(20); m_progressBar->setMaximumHeight(20); m_progressBar->setTextVisible(true); m_progressBar->setFormat("%p%"); m_progressBar->setObjectName("operation-progress"); - progressLayout->addWidget(m_progressBar); - + progressLayout->addWidget(m_progressBar); + // Toggle log button m_toggleLogButton->setProperty("class", "link-button"); - connect(m_toggleLogButton, &QPushButton::clicked, this, &UpdatesWidget::toggleLogViewer); + connect(m_toggleLogButton, &QPushButton::clicked, this, &UpdatesWidget::toggleLogViewer); progressLayout->addWidget(m_toggleLogButton, 0, Qt::AlignCenter); - + m_progressWidget->hide(); mainLayout->addWidget(m_progressWidget, 0); - + // Log viewer section (hidden by default) auto* logLayout = new QVBoxLayout(m_logWidget); logLayout->setContentsMargins(20, 0, 20, 20); logLayout->setSpacing(8); - + m_logViewer->setReadOnly(true); m_logViewer->setMaximumHeight(200); - m_logViewer->setObjectName("log-viewer"); + m_logViewer->setObjectName("log-viewer"); logLayout->addWidget(m_logViewer); - + m_logWidget->hide(); mainLayout->addWidget(m_logWidget, 0); - + setLayout(mainLayout); } -void UpdatesWidget::checkForUpdates() { +void UpdatesWidget::checkForUpdates() +{ m_statusLabel->setText("Checking for updates..."); m_statusLabel->show(); m_checkButton->setEnabled(false); m_checkButton->setText("Checking..."); m_updateAllButton->setEnabled(false); - + clearUpdates(); - - (void)QtConcurrent::run([this]() { - auto updates = AlpmWrapper::instance().getAvailableUpdates(); - - // Also check AUR updates - AurHelper aurHelper; - auto aurUpdates = aurHelper.checkAurUpdates(); - updates.append(aurUpdates); - - QMetaObject::invokeMethod(this, [this, updates]() { - m_updates = updates; - m_filteredUpdates = updates; - - m_checkButton->setEnabled(true); - m_checkButton->setText("Check for Updates"); - - if (updates.isEmpty()) { - m_statusLabel->setText("Your system is up to date!"); - m_countLabel->clear(); - m_searchInput->setEnabled(false); - } else { - m_statusLabel->hide(); - m_countLabel->setText(QString("%1 updates available") - .arg(updates.size())); - m_updateAllButton->setEnabled(true); - m_searchInput->setEnabled(true); - - // Apply any existing search filter - QString searchText = m_searchInput->text(); - if (!searchText.isEmpty()) { - filterUpdates(searchText); - } else { - displayUpdates(updates); - } + + // Reassigning a running jthread auto-requests-stop and joins the + // previous one first, so a still-running check gets cancelled here + // rather than running concurrently with this one. + m_updateCheckThread = std::jthread( + [this](std::stop_token stopToken) + { + auto updates = AlpmWrapper::instance().getAvailableUpdates(); + + // Also check AUR updates + AurHelper aurHelper; + auto aurUpdates = aurHelper.checkAurUpdates(stopToken); + updates.append(aurUpdates); + + if (stopToken.stop_requested()) + { + return; } - - Logger::info(QString("Found %1 updates").arg(updates.size())); - }, Qt::QueuedConnection); - }); + + QMetaObject::invokeMethod( + this, + [this, updates]() + { + m_updates = updates; + m_filteredUpdates = updates; + + m_checkButton->setEnabled(true); + m_checkButton->setText("Check for Updates"); + + if (updates.isEmpty()) + { + m_statusLabel->setText("Your system is up to date!"); + m_countLabel->clear(); + m_searchInput->setEnabled(false); + } + else + { + m_statusLabel->hide(); + m_countLabel->setText(QString("%1 updates available").arg(updates.size())); + m_updateAllButton->setEnabled(true); + m_searchInput->setEnabled(true); + + // Apply any existing search filter + QString searchText = m_searchInput->text(); + if (!searchText.isEmpty()) + { + filterUpdates(searchText); + } + else + { + displayUpdates(updates); + } + } + + spdlog::info("{}", (QString("Found %1 updates").arg(updates.size())).toStdString()); + }, + Qt::QueuedConnection); + }); } -void UpdatesWidget::displayUpdates(const QVector& updates) { +void UpdatesWidget::displayUpdates(const QVector& updates) +{ clearUpdates(); - - for (const auto& update : updates) { + + for (const auto& update : updates) + { auto* item = new UpdateItem(update, m_contentWidget); - connect(item, &UpdateItem::updateRequested, - this, &UpdatesWidget::onUpdateSingle); + connect(item, &UpdateItem::updateRequested, this, &UpdatesWidget::onUpdateSingle); m_contentLayout->insertWidget(m_contentLayout->count() - 1, item); } } -void UpdatesWidget::clearUpdates() { - while (m_contentLayout->count() > 1) { +void UpdatesWidget::clearUpdates() +{ + while (m_contentLayout->count() > 1) + { auto* item = m_contentLayout->takeAt(0); - if (auto* widget = item->widget()) { + if (auto* widget = item->widget()) + { widget->deleteLater(); } delete item; } } -void UpdatesWidget::filterUpdates(const QString& searchText) { - if (searchText.isEmpty()) { +void UpdatesWidget::filterUpdates(const QString& searchText) +{ + if (searchText.isEmpty()) + { m_filteredUpdates = m_updates; displayUpdates(m_filteredUpdates); m_countLabel->setText(QString("%1 updates available").arg(m_updates.size())); return; } - + QString lowerSearch = searchText.toLower(); m_filteredUpdates.clear(); - - for (const auto& update : m_updates) { - if (update.name.toLower().contains(lowerSearch)) { + + for (const auto& update : m_updates) + { + if (update.name.toLower().contains(lowerSearch)) + { m_filteredUpdates.append(update); } } - + displayUpdates(m_filteredUpdates); - + // Update count label to show filtered count - if (m_filteredUpdates.size() == m_updates.size()) { + if (m_filteredUpdates.size() == m_updates.size()) + { m_countLabel->setText(QString("%1 updates available").arg(m_updates.size())); - } else { + } + else + { m_countLabel->setText(QString("%1 of %2 updates").arg(m_filteredUpdates.size()).arg(m_updates.size())); } } -void UpdatesWidget::onSearchTextChanged(const QString& text) { +void UpdatesWidget::onSearchTextChanged(const QString& text) +{ filterUpdates(text); } -void UpdatesWidget::onUpdateAll() { - auto reply = QMessageBox::question(this, "Update All", - QString("Are you sure you want to update all %1 packages?") - .arg(m_updates.size()), - QMessageBox::Yes | QMessageBox::No); - - if (reply == QMessageBox::Yes) { +void UpdatesWidget::onUpdateAll() +{ + auto reply + = QMessageBox::question(this, + "Update All", + QString("Are you sure you want to update all %1 packages?").arg(m_updates.size()), + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) + { PackageManager::instance().updateAllPackages(); } } -void UpdatesWidget::onUpdateSingle(const QString& packageName) { - auto reply = QMessageBox::question(this, "Update Package", - QString("Are you sure you want to update %1?").arg(packageName), - QMessageBox::Yes | QMessageBox::No); - - if (reply == QMessageBox::Yes) { +void UpdatesWidget::onUpdateSingle(const QString& packageName) +{ + auto reply = QMessageBox::question(this, + "Update Package", + QString("Are you sure you want to update %1?").arg(packageName), + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) + { PackageManager::instance().updatePackage(packageName); } } -QString UpdatesWidget::formatSize(qint64 bytes) { +QString UpdatesWidget::formatSize(qint64 bytes) +{ const qint64 KB = 1024; const qint64 MB = KB * 1024; const qint64 GB = MB * 1024; - - if (bytes >= GB) { + + if (bytes >= GB) + { return QString("%1 GB").arg(bytes / static_cast(GB), 0, 'f', 2); - } else if (bytes >= MB) { + } + else if (bytes >= MB) + { return QString("%1 MB").arg(bytes / static_cast(MB), 0, 'f', 1); - } else if (bytes >= KB) { + } + else if (bytes >= KB) + { return QString("%1 KB").arg(bytes / static_cast(KB), 0, 'f', 0); } return QString("%1 B").arg(bytes); } -void UpdatesWidget::showProgress(const QString& message) { +void UpdatesWidget::showProgress(const QString& message) +{ m_progressLabel->setText(message); m_progressBar->setRange(0, 100); m_progressBar->setValue(0); @@ -352,7 +402,8 @@ void UpdatesWidget::showProgress(const QString& message) { m_logViewer->clear(); } -void UpdatesWidget::hideProgress() { +void UpdatesWidget::hideProgress() +{ // Hide the progress bar and label, but keep the widget and toggle button visible m_progressBar->hide(); m_progressLabel->hide(); @@ -361,90 +412,110 @@ void UpdatesWidget::hideProgress() { // This allows users to review logs after operation completes } -void UpdatesWidget::toggleLogViewer() { +void UpdatesWidget::toggleLogViewer() +{ m_logVisible = !m_logVisible; - if (m_logVisible) { + if (m_logVisible) + { m_logWidget->show(); m_toggleLogButton->setText("Hide Logs"); - } else { + } + else + { m_logWidget->hide(); m_toggleLogButton->setText("Show Logs"); } } -void UpdatesWidget::onOperationStarted(const QString& message) { +void UpdatesWidget::onOperationStarted(const QString& message) +{ showProgress(message); m_updateAllButton->setEnabled(false); m_checkButton->setEnabled(false); - + // Disable all individual update buttons - for (int i = 0; i < m_contentLayout->count() - 1; ++i) { - if (auto* item = m_contentLayout->itemAt(i)) { - if (auto* widget = item->widget()) { + for (int i = 0; i < m_contentLayout->count() - 1; ++i) + { + if (auto* item = m_contentLayout->itemAt(i)) + { + if (auto* widget = item->widget()) + { widget->setEnabled(false); } } } } -void UpdatesWidget::onOperationOutput(const QString& output) { +void UpdatesWidget::onOperationOutput(const QString& output) +{ m_logViewer->append(output); - + // Auto-scroll to bottom auto cursor = m_logViewer->textCursor(); cursor.movePosition(QTextCursor::End); m_logViewer->setTextCursor(cursor); - + // Try to parse progress information from output // This is a simple implementation - could be enhanced - if (output.contains("downloading", Qt::CaseInsensitive)) { + if (output.contains("downloading", Qt::CaseInsensitive)) + { m_progressLabel->setText("Downloading packages..."); - m_progressBar->setRange(0, 0); // Indeterminate - } else if (output.contains("installing", Qt::CaseInsensitive)) { + m_progressBar->setRange(0, 0); // Indeterminate + } + else if (output.contains("installing", Qt::CaseInsensitive)) + { m_progressLabel->setText("Installing packages..."); - m_progressBar->setRange(0, 0); // Indeterminate + m_progressBar->setRange(0, 0); // Indeterminate } } -void UpdatesWidget::onOperationCompleted(bool success, const QString& message) { +void UpdatesWidget::onOperationCompleted(bool success, const QString& message) +{ Q_UNUSED(success); Q_UNUSED(message); // Refresh ALPM state so subsequent queries reflect the changes AlpmWrapper::instance().release(); AlpmWrapper::instance().initialize(); - + hideProgress(); - + m_updateAllButton->setEnabled(!m_updates.isEmpty()); m_checkButton->setEnabled(true); - + // Re-enable all individual update buttons - for (int i = 0; i < m_contentLayout->count() - 1; ++i) { - if (auto* item = m_contentLayout->itemAt(i)) { - if (auto* widget = item->widget()) { + for (int i = 0; i < m_contentLayout->count() - 1; ++i) + { + if (auto* item = m_contentLayout->itemAt(i)) + { + if (auto* widget = item->widget()) + { widget->setEnabled(true); } } } } -void UpdatesWidget::onOperationError(const QString& error) { +void UpdatesWidget::onOperationError(const QString& error) +{ Q_UNUSED(error); // Refresh ALPM state (best-effort) AlpmWrapper::instance().release(); AlpmWrapper::instance().initialize(); - + hideProgress(); - + m_updateAllButton->setEnabled(!m_updates.isEmpty()); m_checkButton->setEnabled(true); - + // Re-enable all individual update buttons - for (int i = 0; i < m_contentLayout->count() - 1; ++i) { - if (auto* item = m_contentLayout->itemAt(i)) { - if (auto* widget = item->widget()) { + for (int i = 0; i < m_contentLayout->count() - 1; ++i) + { + if (auto* item = m_contentLayout->itemAt(i)) + { + if (auto* widget = item->widget()) + { widget->setEnabled(true); } } diff --git a/src/gui/updates_widget.h b/src/gui/updates_widget.h index 4742f2e..d899a59 100644 --- a/src/gui/updates_widget.h +++ b/src/gui/updates_widget.h @@ -1,35 +1,42 @@ #ifndef UPDATES_WIDGET_H #define UPDATES_WIDGET_H -#include -#include -#include -#include +#include "../utils/types.h" #include -#include +#include #include +#include +#include #include -#include -#include "../utils/types.h" +#include +#include +#include +#include class UpdateItem; /** * @brief Widget for displaying and managing package updates. - * + * * Memory Management: * - All Qt widget members use Qt parent-child ownership (raw pointers are non-owning) * - UpdateItem widgets are dynamically created/destroyed in displayUpdates/clearUpdates + * - m_updateCheckThread: a std::jthread running the AUR/ALPM update check. + * Its destructor auto-requests-stop and joins, so a check in progress is + * cancelled (not left dangling) if the widget is destroyed, and a second + * checkForUpdates() call cancels any still-running previous check instead + * of letting two run concurrently. */ -class UpdatesWidget : public QWidget { +class UpdatesWidget : public QWidget +{ Q_OBJECT - + public: explicit UpdatesWidget(QWidget* parent = nullptr); ~UpdatesWidget() override = default; - + void checkForUpdates(); - + private: void setupUi(); void displayUpdates(const QVector& updates); @@ -39,7 +46,7 @@ class UpdatesWidget : public QWidget { void showProgress(const QString& message); void hideProgress(); void toggleLogViewer(); - + // Qt parent-child managed widgets (non-owning pointers) QLineEdit* m_searchInput = nullptr; QScrollArea* m_scrollArea = nullptr; @@ -49,7 +56,7 @@ class UpdatesWidget : public QWidget { QLabel* m_countLabel = nullptr; QPushButton* m_updateAllButton = nullptr; QPushButton* m_checkButton = nullptr; - + // Progress bar and log viewer (Qt parent-child managed) QWidget* m_progressWidget = nullptr; QProgressBar* m_progressBar = nullptr; @@ -58,10 +65,12 @@ class UpdatesWidget : public QWidget { QWidget* m_logWidget = nullptr; QTextEdit* m_logViewer = nullptr; bool m_logVisible = false; - + QVector m_updates; QVector m_filteredUpdates; - + + std::jthread m_updateCheckThread; + private slots: void onUpdateAll(); void onUpdateSingle(const QString& packageName); @@ -72,4 +81,4 @@ private slots: void onOperationError(const QString& error); }; -#endif // UPDATES_WIDGET_H +#endif // UPDATES_WIDGET_H diff --git a/src/main.cpp b/src/main.cpp index a3f35bd..c8c7fcf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,29 +1,40 @@ #include "gui/mainwindow.h" -#include "utils/logger.h" +#include "utils/logging.h" +#include "utils/version.h" #include #include -int main(int argc, char *argv[]) { +int main(int argc, char* argv[]) +{ + // Parses and strips -v/-vv/-vvv and -D before Qt ever sees argv. + Log::init(argc, argv); + QApplication app(argc, argv); - + + // resources.qrc is compiled into the static `gui` library (0.3.2's + // granular CMake split); a static lib's resource initializer is only + // linked in if something references it, so it must be registered + // explicitly here instead of relying on static init order. + Q_INIT_RESOURCE(resources); + // Set application metadata - app.setApplicationName("ALG App Store"); - app.setApplicationVersion("2.0.0"); + app.setApplicationName("Explorer"); + app.setApplicationVersion(APP_VERSION); app.setOrganizationName("Arch Linux GUI"); - + // Set application style app.setStyle(QStyleFactory::create("Fusion")); - - Logger::info("Starting ALG App Store"); - Logger::info(QString("Qt version: %1").arg(qVersion())); - + + spdlog::info("Starting Explorer"); + spdlog::info("{}", (QString("Qt version: %1").arg(qVersion())).toStdString()); + MainWindow window; window.show(); - - Logger::info("Application window shown"); - + + spdlog::info("Application window shown"); + int result = app.exec(); - - Logger::info("Application exiting"); + + spdlog::info("Application exiting"); return result; } diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt new file mode 100644 index 0000000..aea1eed --- /dev/null +++ b/src/utils/CMakeLists.txt @@ -0,0 +1,26 @@ +# Shared utilities: spdlog-backed logging setup, shared types (header-only). +add_library(utils STATIC + logging.cpp + logging.h + types.h +) + +target_include_directories(utils PUBLIC ${CMAKE_SOURCE_DIR}/src) + +# Generate utils/version.h from the CMake-configured PROJECT_VERSION, which +# itself comes from the repo-root VERSION file — keeps a single source of +# truth instead of a hardcoded version string in application code. +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/version.h.in + ${CMAKE_BINARY_DIR}/generated/utils/version.h + @ONLY +) +target_include_directories(utils PUBLIC ${CMAKE_BINARY_DIR}/generated) + +target_link_libraries(utils PUBLIC Qt6::Core spdlog::spdlog) + +target_compile_options(utils PRIVATE + -Wall + -Wextra + -Wpedantic +) diff --git a/src/utils/logger.h b/src/utils/logger.h deleted file mode 100644 index a8be860..0000000 --- a/src/utils/logger.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef LOGGER_H -#define LOGGER_H - -#include -#include - -namespace Logger { - inline void info(const QString &message) { - qDebug() << "[INFO]" << message; - } - - inline void warning(const QString &message) { - qWarning() << "[WARNING]" << message; - } - - inline void error(const QString &message) { - qCritical() << "[ERROR]" << message; - } - - inline void debug(const QString &message) { - qDebug() << "[DEBUG]" << message; - } -} - -#endif // LOGGER_H diff --git a/src/utils/logging.cpp b/src/utils/logging.cpp new file mode 100644 index 0000000..9696f4f --- /dev/null +++ b/src/utils/logging.cpp @@ -0,0 +1,69 @@ +#include "logging.h" + +#include +#include +#include + +namespace +{ + +spdlog::level::level_enum defaultLevel() +{ +#ifndef NDEBUG + return spdlog::level::debug; +#else + return spdlog::level::info; +#endif +} + +bool isVerbosityFlag(const std::string& arg) +{ + return arg.size() >= 2 && arg[0] == '-' && arg.find_first_not_of('v', 1) == std::string::npos; +} + +} // namespace + +namespace Log +{ + +void init(int& argc, char** argv) +{ + spdlog::level::level_enum level = defaultLevel(); + std::vector remaining; + remaining.push_back(argv[0]); + + for (int i = 1; i < argc; ++i) + { + const std::string arg = argv[i]; + + if (isVerbosityFlag(arg)) + { + const std::size_t vCount = arg.size() - 1; + level = (vCount >= 2) ? spdlog::level::trace : spdlog::level::debug; + continue; + } + + if (arg == "-D" && i + 1 < argc) + { + const int n = std::atoi(argv[++i]); + if (n >= spdlog::level::trace && n <= spdlog::level::off) + { + level = static_cast(n); + } + continue; + } + + remaining.push_back(argv[i]); + } + + argc = static_cast(remaining.size()); + for (std::size_t i = 0; i < remaining.size(); ++i) + { + argv[i] = remaining[i]; + } + + spdlog::set_level(level); + spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v"); +} + +} // namespace Log diff --git a/src/utils/logging.h b/src/utils/logging.h new file mode 100644 index 0000000..5c0ff5e --- /dev/null +++ b/src/utils/logging.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace Log +{ + +// Parses and strips verbosity flags from argv, then configures the default +// spdlog logger's level accordingly. Must be called before constructing +// QApplication, so Qt never sees flags it doesn't recognize. +// +// With no flags: `debug` in dev builds, `info` in Release builds (NDEBUG). +// -v: `debug`. -vv (or more v's): `trace`. +// -D : explicit spdlog::level::level_enum value (0=trace .. 6=off); +// takes precedence over -v when both are given. +void init(int& argc, char** argv); + +} // namespace Log diff --git a/src/utils/types.h b/src/utils/types.h index d4907f8..87ebc8c 100644 --- a/src/utils/types.h +++ b/src/utils/types.h @@ -1,12 +1,13 @@ #ifndef TYPES_H #define TYPES_H +#include #include #include -#include #include -struct PackageInfo { +struct PackageInfo +{ QString name; QString version; QString description; @@ -15,29 +16,43 @@ struct PackageInfo { QString upstreamUrl; QStringList dependList; QDateTime lastUpdated; - + PackageInfo() = default; - - PackageInfo(const QString& name, const QString& version, - const QString& description, const QString& repository) - : name(name), version(version), description(description), - repository(repository) {} + + PackageInfo(const QString& name, const QString& version, const QString& description, const QString& repository) + : name(name) + , version(version) + , description(description) + , repository(repository) + { + } }; -struct UpdateInfo { +struct UpdateInfo +{ QString name; QString oldVersion; QString newVersion; QString repository; qint64 downloadSize; - - UpdateInfo() : downloadSize(0) {} - - UpdateInfo(const QString& name, const QString& oldVersion, - const QString& newVersion, const QString& repository, + + UpdateInfo() + : downloadSize(0) + { + } + + UpdateInfo(const QString& name, + const QString& oldVersion, + const QString& newVersion, + const QString& repository, qint64 downloadSize = 0) - : name(name), oldVersion(oldVersion), newVersion(newVersion), - repository(repository), downloadSize(downloadSize) {} + : name(name) + , oldVersion(oldVersion) + , newVersion(newVersion) + , repository(repository) + , downloadSize(downloadSize) + { + } }; -#endif // TYPES_H +#endif // TYPES_H diff --git a/src/utils/version.h.in b/src/utils/version.h.in new file mode 100644 index 0000000..6120404 --- /dev/null +++ b/src/utils/version.h.in @@ -0,0 +1,5 @@ +#pragma once + +// Generated from version.h.in by CMake — do not edit directly. +// Sourced from the repo-root VERSION file (see CMakeLists.txt). +#define APP_VERSION "@PROJECT_VERSION@" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..0c3aac5 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,22 @@ +# Pure-logic unit tests only (see PLAN.md 0.3.5): AlpmWrapper/PackageManager +# remain hard singletons touching real libalpm/pkexec and are not covered +# here. A full ALPM mock backend is deferred to 0.5.x. +add_executable(explorer_tests + test_aur_helper.cpp + test_pacman_conf.cpp + test_progress_parser.cpp +) + +target_link_libraries(explorer_tests PRIVATE + core + Catch2::Catch2WithMain +) + +target_compile_options(explorer_tests PRIVATE + -Wall + -Wextra + -Wpedantic +) + +include(Catch) +catch_discover_tests(explorer_tests) diff --git a/tests/test_aur_helper.cpp b/tests/test_aur_helper.cpp new file mode 100644 index 0000000..3864419 --- /dev/null +++ b/tests/test_aur_helper.cpp @@ -0,0 +1,63 @@ +#include "../src/core/aur_helper.h" + +#include +#include +#include + +TEST_CASE("AurHelper::parseAurPackage maps basic fields", "[aur_helper]") +{ + QJsonObject obj; + obj["Name"] = "yay"; + obj["Version"] = "12.3.5-1"; + obj["Description"] = "Yet another yogurt - an AUR helper"; + obj["Maintainer"] = "someone"; + obj["URL"] = "https://github.com/Jguer/yay"; + obj["LastModified"] = 1700000000; + + const PackageInfo info = AurHelper::parseAurPackage(obj); + + REQUIRE(info.name == "yay"); + REQUIRE(info.version == "12.3.5-1"); + REQUIRE(info.description == "Yet another yogurt - an AUR helper"); + REQUIRE(info.repository == "AUR"); + REQUIRE(info.maintainer == "someone"); + REQUIRE(info.upstreamUrl == "https://github.com/Jguer/yay"); + REQUIRE(info.lastUpdated.toSecsSinceEpoch() == 1700000000); +} + +TEST_CASE("AurHelper::parseAurPackage combines Depends and MakeDepends", "[aur_helper]") +{ + QJsonObject obj; + obj["Name"] = "example"; + obj["Version"] = "1.0-1"; + + QJsonArray depends; + depends.append("glibc"); + depends.append("openssl"); + obj["Depends"] = depends; + + QJsonArray makeDepends; + makeDepends.append("cmake"); + obj["MakeDepends"] = makeDepends; + + const PackageInfo info = AurHelper::parseAurPackage(obj); + + REQUIRE(info.dependList.size() == 3); + REQUIRE(info.dependList[0] == "glibc"); + REQUIRE(info.dependList[1] == "openssl"); + REQUIRE(info.dependList[2] == "cmake (make)"); +} + +TEST_CASE("AurHelper::parseAurPackage handles missing optional fields", "[aur_helper]") +{ + QJsonObject obj; + obj["Name"] = "minimal"; + obj["Version"] = "1.0-1"; + + const PackageInfo info = AurHelper::parseAurPackage(obj); + + REQUIRE(info.name == "minimal"); + REQUIRE(info.maintainer.isEmpty()); + REQUIRE(info.upstreamUrl.isEmpty()); + REQUIRE(info.dependList.isEmpty()); +} diff --git a/tests/test_pacman_conf.cpp b/tests/test_pacman_conf.cpp new file mode 100644 index 0000000..f16dd66 --- /dev/null +++ b/tests/test_pacman_conf.cpp @@ -0,0 +1,91 @@ +#include "../src/core/pacman_conf.h" + +#include + +TEST_CASE("PacmanConf::isMultilibEnabled detects an active Include line", "[pacman_conf]") +{ + const QString contents = R"( +[options] +Architecture = auto + +[multilib] +Include = /etc/pacman.d/mirrorlist + +[extra] +Include = /etc/pacman.d/mirrorlist +)"; + + REQUIRE(PacmanConf::isMultilibEnabled(contents)); +} + +TEST_CASE("PacmanConf::isMultilibEnabled ignores a commented-out section", "[pacman_conf]") +{ + const QString contents = R"( +[options] +Architecture = auto + +#[multilib] +#Include = /etc/pacman.d/mirrorlist +)"; + + REQUIRE_FALSE(PacmanConf::isMultilibEnabled(contents)); +} + +TEST_CASE("PacmanConf::isMultilibEnabled is false when section header present but body commented", "[pacman_conf]") +{ + const QString contents = R"( +[multilib] +#Include = /etc/pacman.d/mirrorlist + +[extra] +Include = /etc/pacman.d/mirrorlist +)"; + + REQUIRE_FALSE(PacmanConf::isMultilibEnabled(contents)); +} + +TEST_CASE("PacmanConf::isMultilibEnabled is false when section is absent", "[pacman_conf]") +{ + const QString contents = R"( +[options] +Architecture = auto + +[extra] +Include = /etc/pacman.d/mirrorlist +)"; + + REQUIRE_FALSE(PacmanConf::isMultilibEnabled(contents)); +} + +TEST_CASE("PacmanConf::isChaoticAurEnabled detects Server directive", "[pacman_conf]") +{ + const QString contents = R"( +[options] +Architecture = auto + +[chaotic-aur] +Include = /etc/pacman.d/chaotic-mirrorlist +)"; + + REQUIRE(PacmanConf::isChaoticAurEnabled(contents)); +} + +TEST_CASE("PacmanConf::isChaoticAurEnabled detects direct Server line", "[pacman_conf]") +{ + const QString contents = R"( +[chaotic-aur] +Server = https://geo-mirror.chaotic.cx/$repo/$arch +)"; + + REQUIRE(PacmanConf::isChaoticAurEnabled(contents)); +} + +TEST_CASE("PacmanConf::isChaoticAurEnabled is false when section is absent", "[pacman_conf]") +{ + const QString contents = R"( +[options] +Architecture = auto +)"; + + REQUIRE_FALSE(PacmanConf::isChaoticAurEnabled(contents)); +} diff --git a/tests/test_progress_parser.cpp b/tests/test_progress_parser.cpp new file mode 100644 index 0000000..52b75c4 --- /dev/null +++ b/tests/test_progress_parser.cpp @@ -0,0 +1,49 @@ +#include "../src/core/progress_parser.h" + +#include + +TEST_CASE("parseOperationProgress recognizes status keywords", "[progress_parser]") +{ + REQUIRE(parseOperationProgress("downloading foo-1.0-1-x86_64.pkg.tar.zst").statusText == "Downloading packages..."); + REQUIRE(parseOperationProgress("installing foo").statusText == "Installing packages..."); + REQUIRE(parseOperationProgress("Building foo (1/1)").statusText == "Building packages..."); + REQUIRE(parseOperationProgress("checking dependencies...").statusText == "Checking dependencies..."); + REQUIRE(parseOperationProgress("resolving dependencies...").statusText == "Resolving dependencies..."); + REQUIRE_FALSE(parseOperationProgress("some unrelated line").statusText.has_value()); +} + +TEST_CASE("parseOperationProgress extracts (n/total) package progress", "[progress_parser]") +{ + const auto result = parseOperationProgress("(2/5) checking package integrity"); + + REQUIRE(result.currentPackage == 2); + REQUIRE(result.totalPackages == 5); + REQUIRE(result.progressPercent == 40); +} + +TEST_CASE("parseOperationProgress tolerates a space before the numerator", "[progress_parser]") +{ + const auto result = parseOperationProgress("( 1/5) installing foo"); + + REQUIRE(result.currentPackage == 1); + REQUIRE(result.totalPackages == 5); + REQUIRE(result.progressPercent == 20); +} + +TEST_CASE("parseOperationProgress prefers a trailing NN% over (n/total)", "[progress_parser]") +{ + const auto result = parseOperationProgress("(1/5) downloading foo 75%"); + + REQUIRE(result.currentPackage == 1); + REQUIRE(result.totalPackages == 5); + REQUIRE(result.progressPercent == 75); +} + +TEST_CASE("parseOperationProgress returns no progress fields for plain output", "[progress_parser]") +{ + const auto result = parseOperationProgress("nothing interesting here"); + + REQUIRE_FALSE(result.currentPackage.has_value()); + REQUIRE_FALSE(result.totalPackages.has_value()); + REQUIRE_FALSE(result.progressPercent.has_value()); +}