[
  {
    "path": ".clang-format",
    "content": "---\nLanguage:        Cpp\nAccessModifierOffset: -4\nAlignAfterOpenBracket: Align\nAlignArrayOfStructures: None\nAlignConsecutiveAssignments:\n  Enabled:         false\n  AcrossEmptyLines: false\n  AcrossComments:  false\n  AlignCompound:   false\n  PadOperators:    false\nAlignConsecutiveBitFields:\n  Enabled:         false\n  AcrossEmptyLines: false\n  AcrossComments:  false\n  AlignCompound:   false\n  PadOperators:    false\nAlignConsecutiveDeclarations:\n  Enabled:         false\n  AcrossEmptyLines: false\n  AcrossComments:  false\n  AlignCompound:   false\n  PadOperators:    false\nAlignConsecutiveMacros:\n  Enabled:         false\n  AcrossEmptyLines: false\n  AcrossComments:  false\n  AlignCompound:   false\n  PadOperators:    false\nAlignEscapedNewlines: DontAlign\nAlignOperands:   Align\nAlignTrailingComments:\n  Kind:            Always\n  OverEmptyLines:  0\nAllowAllArgumentsOnNextLine: true\nAllowAllParametersOfDeclarationOnNextLine: true\nAllowShortBlocksOnASingleLine: Never\nAllowShortCaseLabelsOnASingleLine: false\nAllowShortEnumsOnASingleLine: true\nAllowShortFunctionsOnASingleLine: All\nAllowShortIfStatementsOnASingleLine: Never\nAllowShortLambdasOnASingleLine: All\nAllowShortLoopsOnASingleLine: false\nAlwaysBreakAfterDefinitionReturnType: None\nAlwaysBreakAfterReturnType: None\nAlwaysBreakBeforeMultilineStrings: false\nAlwaysBreakTemplateDeclarations: Yes\nAttributeMacros:\n  - __capability\nBinPackArguments: false\nBinPackParameters: false\nBitFieldColonSpacing: Both\nBraceWrapping:\n  AfterCaseLabel:  false\n  AfterClass:      true\n  AfterControlStatement: Always\n  AfterEnum:       false\n  AfterExternBlock: false\n  AfterFunction:   true\n  AfterNamespace:  true\n  AfterObjCDeclaration: true\n  AfterStruct:     false\n  AfterUnion:      false\n  BeforeCatch:     true\n  BeforeElse:      true\n  BeforeLambdaBody: false\n  BeforeWhile:     false\n  IndentBraces:    false\n  SplitEmptyFunction: false\n  SplitEmptyRecord: true\n  SplitEmptyNamespace: true\nBreakAfterAttributes: Never\nBreakAfterJavaFieldAnnotations: false\nBreakArrays:     true\nBreakBeforeBinaryOperators: All\nBreakBeforeConceptDeclarations: Always\nBreakBeforeBraces: Custom\nBreakBeforeInlineASMColon: OnlyMultiline\nBreakBeforeTernaryOperators: true\nBreakConstructorInitializers: AfterColon\nBreakInheritanceList: BeforeColon\nBreakStringLiterals: true\nColumnLimit:     100\nCommentPragmas:  '^ IWYU pragma:'\nCompactNamespaces: false\nConstructorInitializerIndentWidth: 4\nContinuationIndentWidth: 4\nCpp11BracedListStyle: true\nDerivePointerAlignment: false\nDisableFormat:   false\nEmptyLineAfterAccessModifier: Never\nEmptyLineBeforeAccessModifier: LogicalBlock\nExperimentalAutoDetectBinPacking: false\nFixNamespaceComments: true\nForEachMacros:\n  - forever\n  - foreach\n  - Q_FOREACH\n  - BOOST_FOREACH\nIfMacros:\n  - KJ_IF_MAYBE\nIncludeBlocks:   Preserve\nIncludeCategories:\n  - Regex:           '^<Q.*'\n    Priority:        200\n    SortPriority:    200\n    CaseSensitive:   true\nIncludeIsMainRegex: '(Test)?$'\nIncludeIsMainSourceRegex: ''\nIndentAccessModifiers: false\nIndentCaseBlocks: false\nIndentCaseLabels: false\nIndentExternBlock: AfterExternBlock\nIndentGotoLabels: true\nIndentPPDirectives: None\nIndentRequiresClause: true\nIndentWidth:     4\nIndentWrappedFunctionNames: false\nInsertBraces:    false\nInsertNewlineAtEOF: false\nInsertTrailingCommas: None\nIntegerLiteralSeparator:\n  Binary:          0\n  Decimal:         0\n  Hex:             0\nJavaScriptQuotes: Leave\nJavaScriptWrapImports: true\nKeepEmptyLinesAtTheStartOfBlocks: false\nLambdaBodyIndentation: Signature\nLineEnding:      DeriveLF\nMacroBlockBegin: ''\nMacroBlockEnd:   ''\nMaxEmptyLinesToKeep: 1\nNamespaceIndentation: None\nObjCBinPackProtocolList: Auto\nObjCBlockIndentWidth: 4\nObjCBreakBeforeNestedBlockParam: true\nObjCSpaceAfterProperty: false\nObjCSpaceBeforeProtocolList: true\nPackConstructorInitializers: Never\nPenaltyBreakAssignment: 150\nPenaltyBreakBeforeFirstCallParameter: 300\nPenaltyBreakComment: 500\nPenaltyBreakFirstLessLess: 400\nPenaltyBreakOpenParenthesis: 0\nPenaltyBreakString: 600\nPenaltyBreakTemplateDeclaration: 10\nPenaltyExcessCharacter: 50\nPenaltyIndentedWhitespace: 0\nPenaltyReturnTypeOnItsOwnLine: 300\nPointerAlignment: Right\nPPIndentWidth:   -1\nQualifierAlignment: Leave\nReferenceAlignment: Pointer\nReflowComments:  true\nRemoveBracesLLVM: false\nRemoveSemicolon: false\nRequiresClausePosition: OwnLine\nRequiresExpressionIndentation: OuterScope\nSeparateDefinitionBlocks: Leave\nShortNamespaceLines: 1\nSortIncludes: Never\nSortJavaStaticImport: Before\nSortUsingDeclarations: Lexicographic\nSpaceAfterCStyleCast: true\nSpaceAfterLogicalNot: false\nSpaceAfterTemplateKeyword: false\nSpaceAroundPointerQualifiers: Default\nSpaceBeforeAssignmentOperators: true\nSpaceBeforeCaseColon: false\nSpaceBeforeCpp11BracedList: false\nSpaceBeforeCtorInitializerColon: true\nSpaceBeforeInheritanceColon: true\nSpaceBeforeParens: ControlStatements\nSpaceBeforeParensOptions:\n  AfterControlStatements: true\n  AfterForeachMacros: true\n  AfterFunctionDefinitionName: false\n  AfterFunctionDeclarationName: false\n  AfterIfMacros:   true\n  AfterOverloadedOperator: false\n  AfterRequiresInClause: false\n  AfterRequiresInExpression: false\n  BeforeNonEmptyParentheses: false\nSpaceBeforeRangeBasedForLoopColon: true\nSpaceBeforeSquareBrackets: false\nSpaceInEmptyBlock: false\nSpaceInEmptyParentheses: false\nSpacesBeforeTrailingComments: 2\nSpacesInAngles:  Never\nSpacesInConditionalStatement: false\nSpacesInContainerLiterals: false\nSpacesInCStyleCastParentheses: false\nSpacesInLineCommentPrefix:\n  Minimum:         1\n  Maximum:         -1\nSpacesInParentheses: false\nSpacesInSquareBrackets: false\nStandard:        Auto\nStatementAttributeLikeMacros:\n  - Q_EMIT\nStatementMacros:\n  - Q_UNUSED\n  - QT_REQUIRE_VERSION\n  - Q_CLASSINFO\n  - Q_ENUM\n  - Q_ENUM_NS\n  - Q_FLAG\n  - Q_FLAG_NS\n  - Q_GADGET\n  - Q_GADGET_EXPORT\n  - Q_INTERFACES\n  - Q_MOC_INCLUDE\n  - Q_NAMESPACE\n  - Q_NAMESPACE_EXPORT\n  - Q_OBJECT\n  - Q_PROPERTY\n  - Q_REVISION\n  - Q_DISABLE_COPY\n  - Q_SET_OBJECT_NAME\n  - QT_BEGIN_NAMESPACE\n  - QT_END_NAMESPACE\n  - QML_ADDED_IN_MINOR_VERSION\n  - QML_ANONYMOUS\n  - QML_ATTACHED\n  - QML_DECLARE_TYPE\n  - QML_DECLARE_TYPEINFO\n  - QML_ELEMENT\n  - QML_EXTENDED\n  - QML_EXTENDED_NAMESPACE\n  - QML_EXTRA_VERSION\n  - QML_FOREIGN\n  - QML_FOREIGN_NAMESPACE\n  - QML_IMPLEMENTS_INTERFACES\n  - QML_INTERFACE\n  - QML_NAMED_ELEMENT\n  - QML_REMOVED_IN_MINOR_VERSION\n  - QML_SINGLETON\n  - QML_UNAVAILABLE\n  - QML_UNCREATABLE\n  - QML_VALUE_TYPE\nTabWidth:        4\nUseTab:          Never\nWhitespaceSensitiveMacros:\n  - BOOST_PP_STRINGIZE\n  - CF_SWIFT_NAME\n  - NS_SWIFT_NAME\n  - PP_STRINGIZE\n  - STRINGIZE\n...\n\n\n"
  },
  {
    "path": ".docker/README.md",
    "content": "# Dockerfiles\n\n\n## Build\n\n```sh\ndocker build --progress=plain -f \"${path}/arch.Dockerfile\" -t albert:arch --platform linux/amd64 .\ndocker build --progress=plain -f \"${path}/fedora.Dockerfile\" -t albert:fedora .\ndocker build --progress=plain -f \"${path}/ubuntu.Dockerfile\" -t albert:ubuntu .\n```\n\n- `--platform linux/amd64`. Arch has no ARM image. Needed on docker for mac to emulate.\n- `--progress=plain` Disables the buildkit output folding\n\n\n## Run using X\n\nDon't forget to install and run [XQuartz](https://www.xquartz.org/) on macOS.\n\n```sh\ndocker run --rm -it \\\n  -e QT_LOGGING_RULES=\"albert*=true\" \\\n  -e DISPLAY=\"host.docker.internal:0\" \\\n  albert:ubuntu -c \"xterm & albert\"\n```\n"
  },
  {
    "path": ".docker/arch.Dockerfile",
    "content": "ARG BASE_IMAGE=archlinux:latest\n\nFROM ${BASE_IMAGE} AS base\nRUN pacman -Syu --verbose --noconfirm \\\n    cmake \\\n    gcc \\\n    libarchive \\\n    libqalculate \\\n    libxml2 \\\n    make \\\n    pkgconf \\\n    python \\\n    qcoro-qt6 \\\n    qt6-base \\\n    qt6-declarative \\\n    qt6-scxml \\\n    qt6-svg \\\n    qt6-tools \\\n    qtkeychain-qt6 \\\n && pacman -Scc --noconfirm\n\nFROM base AS build\nCOPY . /src\nARG build_dir=\"/build\"\nRUN cmake \\\n      -S /src \\\n      -B $build_dir \\\n      -DBUILD_TESTS=ON \\\n && cmake --build $build_dir -j$(nproc) \\\n && cmake --install $build_dir --prefix /usr \\\n && ctest --test-dir $build_dir --output-on-failure \\\n && rm -rf $build_dir\n\nFROM build AS build-plugin\nRUN cmake \\\n      -S /src/plugins/applications \\\n      -B $build_dir \\\n      -DCMAKE_PREFIX_PATH=/usr/lib/$(gcc -dumpmachine)/cmake/ \\\n && cmake --build $build_dir -j$(nproc) \\\n && cmake --install $build_dir --prefix /usr \\\n && ctest --test-dir $build_dir --output-on-failure \\\n && rm -rf $build_dir\n\nENTRYPOINT [\"bash\"]\n"
  },
  {
    "path": ".docker/fedora.Dockerfile",
    "content": "ARG BASE_IMAGE=fedora:latest\n\nFROM ${BASE_IMAGE} AS base\nRUN yum install -y \\\n    cmake \\\n    gcc-c++ \\\n    libarchive-devel \\\n    libqalculate-devel \\\n    pkgconfig \\\n    python3-devel \\\n    qt6-qtbase \\\n    qt6-qtbase-mysql \\\n    qt6-qtbase-odbc \\\n    qt6-qtbase-postgresql \\\n    qt6-qtscxml-devel \\\n    qt6-qtsvg-devel \\\n    qt6-qttools-devel \\\n    qtkeychain-qt6-devel \\\n    qcoro-qt6-devel \\\n    xml2 \\\n && yum clean all \\\n && rm -rf /var/cache/yum/*\n\nFROM base AS build\nCOPY . /src\nARG build_dir=\"/build\"\nRUN cmake \\\n      -S /src \\\n      -B $build_dir \\\n      -DBUILD_TESTS=ON \\\n && cmake --build $build_dir -j$(nproc) \\\n && cmake --install $build_dir --prefix /usr \\\n && ctest --test-dir $build_dir --output-on-failure \\\n && rm -rf $build_dir\n\nFROM build AS build-plugin\nRUN cmake \\\n      -S /src/plugins/applications \\\n      -B $build_dir \\\n      -DCMAKE_PREFIX_PATH=/usr/lib64/cmake \\\n && cmake --build $build_dir -j$(nproc) \\\n && cmake --install $build_dir --prefix /usr \\\n && ctest --test-dir $build_dir --output-on-failure \\\n && rm -rf $build_dir\n\nENTRYPOINT [\"bash\"]\n"
  },
  {
    "path": ".docker/ubuntu.Dockerfile",
    "content": "ARG BASE_IMAGE=ubuntu:24.04\n\nFROM ${BASE_IMAGE} AS base\nRUN export DEBIAN_FRONTEND=noninteractive \\\n && apt-get -qq update \\\n && apt-get install --no-install-recommends -y \\\n    cmake \\\n    g++ \\\n    libarchive-dev \\\n    libgl1-mesa-dev \\\n    libglvnd-dev \\\n    libqalculate-dev \\\n    libqt6opengl6-dev \\\n    libqt6sql6-sqlite \\\n    libqt6svg6-dev \\\n    libxml2-utils \\\n    make \\\n    pkg-config \\\n    python3-dev \\\n    qt6-base-dev \\\n    qt6-scxml-dev  \\\n    qt6-tools-dev \\\n    qt6-tools-dev-tools \\\n    qt6-l10n-tools \\\n    qtkeychain-qt6-dev \\\n    qcoro-qt6-dev \\\n && apt-get clean \\\n && rm -rf /var/lib/apt/lists/*\n\nFROM base AS dev\nRUN export DEBIAN_FRONTEND=noninteractive \\\n && apt-get -qq update \\\n && apt-get install --no-install-recommends -y \\\n    ccache \\\n    clangd \\\n    lldb \\\n    ninja-build \\\n    xterm \\\n && apt-get clean \\\n && rm -rf /var/lib/apt/lists/*\n\nFROM base AS build\nCOPY . /src\nARG build_dir=\"/build\"\nRUN cmake \\\n      -S /src \\\n      -B $build_dir \\\n      -DBUILD_TESTS=ON \\\n && cmake --build $build_dir -j$(nproc) \\\n && cmake --install $build_dir --prefix /usr \\\n && ctest --test-dir $build_dir --output-on-failure \\\n && rm -rf $build_dir\n\nFROM build AS build-plugin\nRUN cmake \\\n      -S /src/plugins/applications \\\n      -B $build_dir \\\n      -DCMAKE_PREFIX_PATH=/usr/lib/$(gcc -dumpmachine)/cmake/ \\\n && cmake --build $build_dir -j$(nproc) \\\n && cmake --install $build_dir --prefix /usr \\\n && ctest --test-dir $build_dir --output-on-failure \\\n && rm -rf $build_dir\n"
  },
  {
    "path": ".dockerignore",
    "content": ".*\ndocumentation\n*build*\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "Stay civil. Inappropriate content will be removed without warning.\n\nSee also the contributing section on the [Albert website](https://albertlauncher.github.io/gettingstarted/contributing/).\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1-bugreport.yml",
    "content": "name: Bug report\ndescription: Report a bug.\nassignees: ManuelSchneid3r\nlabels: [\"Needs triage\"]\nbody:\n- type: dropdown\n  id: Source\n  attributes:\n    label: Source\n    description: Where did you get the build from?\n    options:\n      - Open Build Service \n      - Homebrew\n      - AUR\n      - Built from source\n      - Other (leave a note)\n    default: 0\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: App logs\n    render: Text\n    description: |\n      Run the command below and post its output.\n      **Linux** `QT_LOGGING_RULES='albert*=true' albert`\n      **macOS** `QT_LOGGING_RULES='albert*=true' /Applications/Albert.app/Contents/MacOS/albert`\n    placeholder: \"Post the logs here\"\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: Current Behavior\n    description: A concise description of what you're experiencing.\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: Expected Behavior\n    description: A concise description of what you expected to happen.\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: Anything else?\n    description: Anything that will give more context about the issue you are encountering!\n  validations:\n    required: false\n    \n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/2-missing_terminal.yml",
    "content": "name: Missing terminal\ndescription: Report a missing terminal.\ntitle: \"Missing terminal <name>\"\nassignees: ManuelSchneid3r\nlabels: [\"C: App\", \"Needs triage\"]\nbody:\n- type: input\n  attributes:\n    label: Absolute path to the desktop file\n    description: See <a href=\"https://specifications.freedesktop.org/desktop-entry-spec/latest/\">this</a> if you dont know what a desktop file is.\n    placeholder: /usr/share/applications/…\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: Contents of the desktop file\n    render: INI\n    placeholder: \"[Desktop Entry] …\"\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: App report\n    render: Text\n    description: |\n      Run the command below and post its output.\n      **Linux** `albert --report`\n      **macOS** `/Applications/Albert.app/Contents/MacOS/albert --report`\n    placeholder: \"Post the report here\"\n  validations:\n    required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: Feature request\n    url: https://github.com/orgs/albertlauncher/discussions/1307\n    about: Post a feature request on the wishlist. Dont forget to vote!\n  - name: Chat on Discord\n    url: https://discord.gg/t8G2EkvRZh\n    about: Bridged community chat.\n  - name: Chat on Telegram\n    url: https://telegram.me/albert_launcher_community\n    about: Bridged community chat.\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Configuration for probot-stale - https://github.com/probot/stale\n\n# Number of days of inactivity before an Issue or Pull Request becomes stale\ndaysUntilStale: 365\n\n# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.\n# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.\ndaysUntilClose: 182\n\n# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)\nonlyLabels: []\n\n# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable\nexemptLabels:\n  - Suggestion\n  - Bug P1 Blocker\n  - Bug P2 High\n  - Bug P3 Medium\n\n# Set to true to ignore issues in a project (defaults to false)\nexemptProjects: false\n\n# Set to true to ignore issues in a milestone (defaults to false)\nexemptMilestones: false\n\n# Set to true to ignore issues with an assignee (defaults to false)\nexemptAssignees: false\n\n# Label to use when marking as stale\nstaleLabel: stale\n\n# Comment to post when marking as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\n\n# Comment to post when removing the stale label.\n# unmarkComment: >\n#   Your comment here.\n\n# Comment to post when closing a stale Issue or Pull Request.\n# closeComment: >\n#   Your comment here.\n\n# Limit the number of actions per hour, from 1-30. Default is 30\nlimitPerRun: 30\n\n# Limit to only `issues` or `pulls`\nonly: issues\n\n# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':\n# pulls:\n#   daysUntilStale: 30\n#   markComment: >\n#     This pull request has been automatically marked as stale because it has not had\n#     recent activity. It will be closed if no further activity occurs. Thank you\n#     for your contributions.\n\n# issues:\n#   exemptLabels:\n#     - confirmed\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI/CD\n\non:\n  push:\n    branches: [ \"main\", \"dev\", \"devel\" ]\n    tags: '*'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\njobs:\n\n  SourceArtifact:\n    runs-on: ubuntu-latest\n    steps:\n\n      - name: Checkout source\n        uses: actions/checkout@v3\n        with:\n          submodules: 'recursive'\n          path: source\n\n      - name: Upload source artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: source-artifact\n          include-hidden-files: true\n          path: source\n\n\n  ReleaseSourceArchives:\n    needs: SourceArtifact\n    if: startsWith(github.ref, 'refs/tags/')\n    runs-on: ubuntu-latest\n    steps:\n\n      - name: Download source artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: source-artifact\n          path: albert\n\n      - name: Create archives\n        run: |\n          tar --exclude=\".*\" -czvf ${{ github.ref_name }}.tar.gz albert\n          zip -r ${{ github.ref_name }}.zip albert -x \"*/.*\"\n\n      - name: Release\n        uses: softprops/action-gh-release@v1\n        with:\n          generate_release_notes: true\n          files: |\n            ${{ github.ref_name }}.tar.gz\n            ${{ github.ref_name }}.zip\n\n  LinuxBuilds:\n    needs: SourceArtifact\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        dockerfile: [arch.Dockerfile, fedora.Dockerfile, ubuntu.Dockerfile]\n\n    steps:\n\n      - name: Download source artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: source-artifact\n\n      - name: Build docker test image\n        run: docker build . --file .docker/${{ matrix.dockerfile }} --target build-plugin\n\n  MacBuilds:\n    needs: SourceArtifact\n    name: ${{matrix.buildname}}\n    runs-on: ${{matrix.os}}\n    strategy:\n      matrix:\n        include:\n          - os: macos-15-intel\n            buildname: macOS x86_64\n            arch: 'x86_64'\n\n          - os: macos-15\n            buildname: macOS arm64\n            arch: 'arm64'\n\n    steps:\n\n      - name: Download source artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: source-artifact\n          path: source\n\n      - name: Install dependencies available at homebrew\n        env:\n          #HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1\n          HOMEBREW_NO_AUTO_UPDATE: 1\n          HOMEBREW_NO_INSTALL_CLEANUP: 1\n          HOMEBREW_NO_INSTALL_UPGRADE: 1\n        run: |\n          brew install llvm@18 qt qcoro6\n          brew install --ignore-dependencies libqalculate qtkeychain # python libarchive sparkle\n\n      - name: Build and package\n        run: |\n          cmake -S source -B build \\\n            -DCMAKE_C_COMPILER=$(brew --prefix llvm@18)/bin/clang \\\n            -DCMAKE_CXX_COMPILER=$(brew --prefix llvm@18)/bin/clang++ \\\n            -DCMAKE_OSX_DEPLOYMENT_TARGET=11 \\\n            -DCMAKE_BUILD_TYPE=RelWithDebInfo \\\n            -DCMAKE_OSX_ARCHITECTURES=\"${{ matrix.arch }}\" \\\n            -DBUILD_PLUGIN_DEBUG=OFF \\\n            -DBUILD_PLUGIN_DOCS=OFF\n          cmake --build build\n\n      - name: Build and package\n        run: cd build && cpack -V\n\n      - name: Append suffix\n        run: |\n          set -x\n          dmg=$(echo build/Albert-*.dmg)\n          new_dmg=\"${dmg::${#dmg} - 4}-${{ matrix.arch }}.dmg\"\n          mv \"${dmg}\" \"${new_dmg}\"\n          mv \"${dmg}.sha256\" \"${new_dmg}.sha256\"\n\n      - name: Upload macOS bundle artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: macos-bundle-artifact-${{ matrix.arch }}\n          path: |\n            build/*.dmg\n            build/*.sha256\n            #appcast_item.txt\n\n  RelaseMacBuilds:\n    needs: MacBuilds\n    if: startsWith(github.ref, 'refs/tags/')\n    runs-on: ubuntu-latest\n    steps:\n      - name: Get all build artifacts\n        uses: actions/download-artifact@v4\n        with:\n          pattern: macos-bundle-artifact-*\n          merge-multiple: true\n\n      - name: Upload\n        uses: softprops/action-gh-release@v1\n        with:\n          files: |\n            Albert-*.dmg\n            Albert-*.sha256\n\n  UpdateTap:\n    needs: RelaseMacBuilds\n    if: startsWith(github.ref, 'refs/tags/')\n    runs-on: ubuntu-latest\n    steps:\n\n      - uses: actions/checkout@v4  # deletes .\n        with:\n          repository: albertlauncher/homebrew-albert\n          token: ${{ secrets.PAT }}\n\n      - name: Get all build artifacts\n        uses: actions/download-artifact@v4\n        with:\n          pattern: macos-bundle-artifact-*\n          merge-multiple: true\n\n      - name: Update cask\n        run: |\n          find .\n          ver=${{ github.ref_name }}\n          ver=\"${ver#v}\"  # Remove leading 'v'\n          sha_arm=$(cut -f 1 -d \" \"  Albert-v$ver-arm64.dmg.sha256)\n          sha_intel=$(cut -f 1 -d \" \"  Albert-v$ver-x86_64.dmg.sha256)\n          # Note this is GNU sed\n          sed -i \"s/version .*$/version \\\"$ver\\\"/; s/^  sha256.*$/  sha256 arm: \\\"${sha_arm}\\\", intel: \\\"${sha_intel}\\\"/\" Casks/albert.rb\n          git config --local user.email \"action@github.com\"\n          git config --local user.name \"GitHub Action\"\n          git add Casks/albert.rb\n          git commit -m \"${{ github.ref_name }}\"\n          git push\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n # Appcast:\n #   needs: RelaseMacBuilds\n #   if: startsWith(github.ref, 'refs/tags/')\n #   runs-on: ubuntu-latest\n #   steps:\n #     - name: Checkout website source code\n #       uses: actions/checkout@v3\n #       with:\n #         repository: albertlauncher/documentation\n #         ref: 'master'\n #         token: ${{ secrets.PAT }}\n\n      #- name: list files\n      #  run: find .\n\n      #- run: sed -i -e '/<\\/language>/r appcast_item.txt' src/appcast.xml\n\n      #- name: Push appcast\n      #  run: |\n      #    git config --local user.name \"GitHub Action\"\n      #    git config --local user.email \"action@github.com\"\n      #    git add src/appcast.xml\n      #    git commit -m \"Update appcast\"\n      #    git push origin master\n\n\n#- name: Restore macports dependencies\n#  id: cache-macports  # ref'ed below\n#  uses: actions/cache/restore@v4\n#  with:\n#    path: |\n#      /opt/local/lib\n#      /opt/local/include\n#    key: ${{ matrix.os }}-macports-r2\n\n#- name: Install macports (for universal binaries of libqalculate and libarchive)\n#  if: steps.cache-macports.outputs.cache-hit != 'true'\n#  run: |\n#    case ${{ matrix.os }} in\n#      macos-11)\n#          wget \"https://github.com/macports/macports-base/releases/download/v2.8.1/MacPorts-2.8.1-11-BigSur.pkg\"\n#          sudo installer -pkg ./MacPorts-2.8.1-11-BigSur.pkg -target /\n#      ;;\n#      macos-12)\n#          wget \"https://github.com/macports/macports-base/releases/download/v2.8.1/MacPorts-2.8.1-12-Monterey.pkg\"\n#          sudo installer -pkg ./MacPorts-2.8.1-12-Monterey.pkg -target /\n#      ;;\n#      macos-13)\n#          wget \"https://github.com/macports/macports-base/releases/download/v2.8.1/MacPorts-2.8.1-13-Ventura.pkg\"\n#          sudo installer -pkg ./MacPorts-2.8.1-13-Ventura.pkg -target /\n#      ;;\n#    esac\n#    sudo sh -c 'echo \"\\n+universal\" >> /opt/local/etc/macports/variants.conf'\n\n#- name: Install dependencies using macports\n#  if: steps.cache-macports.outputs.cache-hit != 'true'\n#  run : sudo /opt/local/bin/port install libqalculate libarchive  # increase steps.cache-macports.outputs.cache-primary-key revision on change\n\n#- name: Save macports dependencies\n#  uses: actions/cache/save@v4\n#  with:\n#    path: |\n#      /opt/local/lib\n#      /opt/local/include\n#    key: ${{ steps.cache-macports.outputs.cache-primary-key }}\n\n# - name: Checkout source code\n#   uses: actions/checkout@v3\n#   with:\n#     submodules: recursive\n\n# - name: Get latest CMake and ninja\n#   uses: lukka/get-cmake@latest\n\n#- name: Install Qt dependencies\n#  uses: jurplel/install-qt-action@v3\n#  with:\n#    version: ${{ matrix.qt_version }}\n#    cache: true\n#    modules: 'qtscxml qt5compat qtshadertools'\n\n#- name: Create writable /opt/local\n#  run: sudo install -d -o $UID -m 755 /opt/local\n\n\n#- name: Generate appcast item\n#  env:\n#    EDDSA_PRIVATE_KEY: ${{ secrets.EDDSA_PRIVATE_KEY }}\n#    VERSION: ${{ github.ref_name }}\n#  run: ./dist/macos/generate_appcast_item.sh \"build/Albert-${{ github.ref_name }}.dmg\" \"${VERSION:1}\" \"$EDDSA_PRIVATE_KEY\" appcast_item.txt\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": ".github/workflows/telegram_notify_comments.yml",
    "content": "name: Telegram Notifications\n\non:\n\n  issue_comment:\n    types: [created]\n\njobs:\n  notify:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Send notifications to Telegram\n      run: >\n        curl -s\n        -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_NOTIFIER_BOT_TOKEN }}/sendMessage\n        -d chat_id=${{ secrets.TELEGRAM_ALBERT_CHAT_ID }}\n        -d text=\"${MESSAGE}\"\n        -d parse_mode=HTML\n        -d disable_web_page_preview=true\n        >> /dev/null\n      env:\n        MESSAGE: \"<b>${{ github.event.comment.user.login }}</b> on <a href=\\\"${{ github.event.comment.html_url }}\\\"><b>${{ github.event.repository.name }}#${{ github.event.issue.number }}</b>: <i>${{ github.event.issue.title }}</i></a>%0A${{ github.event.comment.body }}\"\n\n"
  },
  {
    "path": ".github/workflows/telegram_notify_issues.yml",
    "content": "name: Telegram Notifications\n\non:\n  issues:\n    types: [opened, reopened]\n\njobs:\n  notify:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Send notifications to Telegram\n      run: >\n        curl -s\n        -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_NOTIFIER_BOT_TOKEN }}/sendMessage\n        -d chat_id=${{ secrets.TELEGRAM_ALBERT_CHAT_ID }}\n        -d text=\"${MESSAGE}\"\n        -d parse_mode=HTML\n        -d disable_web_page_preview=true\n        >> /dev/null\n      env:\n        MESSAGE: \"New issue:%0A<a href=\\\"${{ github.event.issue.html_url }}\\\"><b>${{ github.event.repository.name }}#${{ github.event.issue.number }}</b>: <i>${{ github.event.issue.title }}</i></a>\"\n\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n/CMakeLists.txt.user*\n/build*\n/documentation\njustfile\n/.qtcreator\n\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"i18n\"]\n\tpath = i18n\n\turl = https://github.com/albertlauncher/i18n.git\n\n[submodule \"lib/QHotkey\"]\n\tpath = lib/QHotkey\n\turl = https://github.com/QtCommunity/QHotkey\n[submodule \"lib/QNotification\"]\n\tpath = lib/QNotification\n\turl = https://github.com/QtCommunity/QNotification\n\n[submodule \"plugins/application\"]\n\tpath = plugins/application\n\turl = https://github.com/albertlauncher/albert-plugin-application.git\n[submodule \"plugins/applications\"]\n\tpath = plugins/applications\n\turl = https://github.com/albertlauncher/albert-plugin-applications.git\n[submodule \"plugins/bluetooth\"]\n\tpath = plugins/bluetooth\n\turl = https://github.com/albertlauncher/albert-plugin-bluetooth.git\n[submodule \"plugins/caffeine\"]\n\tpath = plugins/caffeine\n\turl = https://github.com/albertlauncher/albert-plugin-caffeine.git\n[submodule \"plugins/calculator-qalculate\"]\n\tpath = plugins/calculator-qalculate\n\turl = https://github.com/albertlauncher/albert-plugin-calculator-qalculate.git\n[submodule \"plugins/chromium\"]\n\tpath = plugins/chromium\n\turl = https://github.com/albertlauncher/albert-plugin-chromium.git\n[submodule \"plugins/clipboard\"]\n\tpath = plugins/clipboard\n\turl = https://github.com/albertlauncher/albert-plugin-clipboard.git\n[submodule \"plugins/commandline\"]\n\tpath = plugins/commandline\n\turl = https://github.com/albertlauncher/albert-plugin-commandline.git\n[submodule \"plugins/contacts\"]\n\tpath = plugins/contacts\n\turl = https://github.com/albertlauncher/albert-plugin-contacts.git\n[submodule \"plugins/datetime\"]\n\tpath = plugins/datetime\n\turl = https://github.com/albertlauncher/albert-plugin-datetime.git\n[submodule \"plugins/debug\"]\n\tpath = plugins/debug\n\turl = https://github.com/albertlauncher/albert-plugin-debug.git\n[submodule \"plugins/dictionary\"]\n\tpath = plugins/dictionary\n\turl = https://github.com/albertlauncher/albert-plugin-dictionary.git\n[submodule \"plugins/docs\"]\n\tpath = plugins/docs\n\turl = https://github.com/albertlauncher/albert-plugin-docs.git\n[submodule \"plugins/files\"]\n\tpath = plugins/files\n\turl = https://github.com/albertlauncher/albert-plugin-files.git\n[submodule \"plugins/github\"]\n\tpath = plugins/github\n\turl = https://github.com/albertlauncher/albert-plugin-github.git\n[submodule \"plugins/hash\"]\n\tpath = plugins/hash\n\turl = https://github.com/albertlauncher/albert-plugin-hash.git\n[submodule \"plugins/homebrew\"]\n\tpath = plugins/homebrew\n\turl = https://github.com/albertlauncher/albert-plugin-homebrew.git\n[submodule \"plugins/mediaremote\"]\n\tpath = plugins/mediaremote\n\turl = https://github.com/albertlauncher/albert-plugin-mediaremote.git\n[submodule \"plugins/menubar\"]\n\tpath = plugins/menubar\n\turl = https://github.com/albertlauncher/albert-plugin-menubar.git\n[submodule \"plugins/obsidian\"]\n\tpath = plugins/obsidian\n\turl = https://github.com/albertlauncher/albert-plugin-obsidian.git\n[submodule \"plugins/python\"]\n\tpath = plugins/python\n\turl = https://github.com/albertlauncher/albert-plugin-python.git\n[submodule \"plugins/snippets\"]\n\tpath = plugins/snippets\n\turl = https://github.com/albertlauncher/albert-plugin-snippets.git\n[submodule \"plugins/spotify\"]\n\tpath = plugins/spotify\n\turl = https://github.com/albertlauncher/albert-plugin-spotify.git\n[submodule \"plugins/ssh\"]\n\tpath = plugins/ssh\n\turl = https://github.com/albertlauncher/albert-plugin-ssh.git\n[submodule \"plugins/system\"]\n\tpath = plugins/system\n\turl = https://github.com/albertlauncher/albert-plugin-system.git\n[submodule \"plugins/timer\"]\n\tpath = plugins/timer\n\turl = https://github.com/albertlauncher/albert-plugin-timer.git\n[submodule \"plugins/timezones\"]\n\tpath = plugins/timezones\n\turl = https://github.com/albertlauncher/albert-plugin-timezones.git\n[submodule \"plugins/urlhandler\"]\n\tpath = plugins/urlhandler\n\turl = https://github.com/albertlauncher/albert-plugin-urlhandler.git\n[submodule \"plugins/vpn\"]\n\tpath = plugins/vpn\n\turl = https://github.com/albertlauncher/albert-plugin-vpn.git\n[submodule \"plugins/websearch\"]\n\tpath = plugins/websearch\n\turl = https://github.com/albertlauncher/albert-plugin-websearch.git\n[submodule \"plugins/widgetsboxmodel\"]\n\tpath = plugins/widgetsboxmodel\n\turl = https://github.com/albertlauncher/albert-plugin-widgetsboxmodel.git\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "# See https://pre-commit.com for more information\n# See https://pre-commit.com/hooks.html for more hooks\nrepos:\n#-   repo: https://github.com/pre-commit/pre-commit-hooks\n#    rev: v3.2.0\n#    hooks:\n#    -   id: trailing-whitespace\n#    -   id: end-of-file-fixer\n#    -   id: check-yaml\n#    -   id: check-added-large-files\n-   repo: https://github.com/jorisroovers/gitlint\n    rev:  v0.19.1\n    hooks:\n    -   id: gitlint\n        args:\n            - --ignore=body-is-missing,body-min-length\n            - --contrib=contrib-title-conventional-commits\n            - -c\n            - contrib-title-conventional-commits.types=fix,feat,chore,docs,style,refactor,perf,test,revert,ci,build,api\n            - --msg-filename\n\n\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## v34.0.10 (2026-02-13)\n\n### Features\n\n#### Plugins\n\n- **Widgets BoxModel** · Warn on icon rendering taking too long\n\n### Performance\n\n#### Plugins\n\n- **Widgets BoxModel** · Warm up font rendering\n\n### Fixes\n\n#### Core\n\n- _GeneratorQueryHandler_ · Workaround Qt 6.4 deadlocks\n- _BackgroundExecutor_ · Workaround Qt 6.4 deadlocks\n\n\n## v34.0.9 (2026-02-13)\n\n### Fixes\n\n#### Core\n\n- _widgetsboxmodel_ · Accidentially released dev branch\n\n#### Plugins\n\n- **URL Handler** · Disable implict input action\n\n\n## v34.0.8 (2026-02-13)\n\n### Fixes\n\n#### Plugins\n\n- **Python plugins** · Dependent template breaks almost all builds\n\n### Miscellaneous Tasks\n\n#### Core\n\n- Remove title from cliff templates\n\n#### Plugins\n\n- **GitHub** · Remove leftover debug output\n\n\n## v34.0.7 (2026-02-12)\n\n### Fixes\n\n- **Python plugins** · Default trigger overrides not used\n\n\n## v34.0.6 (2026-02-12)\n\n### Features\n\n#### Plugins\n\n- **Files** · Treat depth 0 as special value (infty)\n- **Python plugins**\n  - Let default trigger be module name\n  - Add prefpanes plugin\n\n### Fixes\n\n#### Core\n\n- Remove unnecessary redundancy in global query log\n- Avoid holding a lock while calling plugin code\n\n#### Plugins\n\n- **Files** · File item completion\n- **Jetbrains projects** · Python 3.12 problems\n- **Python plugins** · _PyPluginLoader_ · Do not hold GIL while emitting `finished` signal\n- **Timers** · Disable auto input action text\n\n### Miscellaneous Tasks\n\n#### Core\n\n- _cliff_ · Ignore plugin release commits\n- Remove unreleased changes from changelog\n\n#### Plugins\n\n- **PacMan** · Add maintainer\n\n\n## v34.0.5 (2026-02-04)\n\n### Features\n\n#### Plugins\n\n- **Jetbrains projects** · Global query handling\n- **VSCode projects** · Support workspaces and singlefile recents\n\n### Performance\n\n#### Plugins\n\n- **Jetbrains projects** · Debounce projects cache update\n\n### Fixes\n\n#### Core\n\n- Run calls to `items()` threaded\n\n#### Plugins\n\n- **Chromium** · GCC13 builds\n- **PacMan** · Query context variable\n- **Pass** · Pass generate could have empty location\n\n### Miscellaneous Tasks\n\n#### Core\n\n- Change motivational text in plugins tab placeholder widget\n\n#### Plugins\n\n- **Bitwarden** · Add maintainer\n- **Jetbrains projects** · Update readme\n\n\n## v34.0.4 (2026-02-02)\n\n### Features\n\n#### Plugins\n\n- **Albert** · Show composed dir icon on albert dirs\n- **Files** · Default to case insensitive matching for fs browsers\n- **Spotify** · Add open settings action on error items\n- **Widgets BoxModel**\n  - Linear-gradient brush support\n  - Add ubuntu prototype theme\n\n### Fixes\n\n#### Core\n\n- Link to extension website\n\n#### Plugins\n\n- **Widgets BoxModel** · Correct `input_hint_color` default\n\n### Miscellaneous Tasks\n\n#### Core\n\n- Update FAQ link\n\n#### Plugins\n\n- **Albert** · Translation correction\n- **Chromium**\n  - Log reason on favicons db copy failure\n  - Add some more data directories\n  - Scan for profiles on init only if none configured\n- **Files**\n  - Drop static dir mime type\n  - Do not store mime types, fetch mimetype on demand\n- **Spotify** · Update german translations\n- **Widgets BoxModel** · Update README\n\n\n## v34.0.3 (2026-01-28)\n\n### Features\n\n#### Plugins\n\n- **Applications**\n  - _macOS_ · Use default terminal for *.command files\n  - Add cosmic-term support\n  - Support guake terminal\n- **Chromium**\n  - [**BREAKING**] · Add profile selection combo box\n  - Add option \"Show favicons\"\n- **Clipboard** · Use clipboard emoji icon\n\n### Performance\n\n#### Plugins\n\n- **Widgets BoxModel** · Cache icon by item identifier\n\n### Fixes\n\n#### Core\n\n- _RankedQueryHandler_ · Do not call rankItems on main thread\n- Segfault on plugin selection of unloaded plugin\n\n#### Plugins\n\n- **Bitwarden** · Return type and attr error\n- **Clipboard** · Deadlock on clipboard change\n- **Date and time** · Timer not started/stopped in same thread\n- **Python plugins**\n  - Empty debug line\n  - Missing dependencies are not automatically installed\n  - Duplicate interpreter config dump\n\n### Miscellaneous Tasks\n\n#### Plugins\n\n- **Applications** · Update translations\n- **Bluetooth** · Add README link\n- **Chromium**\n  - Add README\n  - Platform specific base dirs\n- **Python plugins**\n  - Update translations\n  - Comment config dumps\n\n\n## v34.0.2 (2026-01-21)\n\n### Fixes\n\n#### Plugins\n\n- **Applications** · Non persistent and ineffective applications options\n- **Python plugins**\n  - Pip freeze parsing\n  - Reset venv when Python version changed\n  - Handle errors of process executions\n\n### Documentation\n\n- Give the albert namespace a docstring\n- Remove reference to ExtensionRegistry\n\n### Miscellaneous Tasks\n\n#### Core\n\n- Codesign bundle with local certificate\n\n#### Plugins\n\n- **Python plugins**\n  - Instantiate plugins on main thread\n  - Drop dynamic dependecy installation\n\n\n## v34.0.1 (2026-01-19)\n\n### Fixes\n\n#### Core\n\n- Misleading tooltip in query widget\n- Segfault on initial run\n\n#### Plugins\n\n- **Date and time**\n  - \"Show date on empty query\" not persisent\n  - Segfaults due to timers controlled from different threads\n\n### Documentation\n\n- Fix missing graphemeDefaultBrush docs\n- Remove leftover icon topic\n- Group `App` functions\n\n### Miscellaneous Tasks\n\n- _gitlint_ · Ignore body min length\n\n\n## v34.0.0 (2026-01-19)\n\n### Features\n\n#### Core\n\n- [**BREAKING**] Tokenize strings using unicode word boundaries\n- Add 🕚 inputhint to global query wildcard\n- Add Homebrew plugin\n\n#### Plugins\n\n- **Arch Linux Wiki** · Infinite scrolling\n- **Documentation** · Allow custom docsets\n- **GitHub**\n  - Use a placeholder icon to impove visual appearance\n  - Lazily fetch pages (Infinite scroll)\n- **Kill Process** · Support all platforms\n- **Obsidian**\n  - Flatpak app support\n  - Snapcraft support\n- **Python plugins** · Add firefox plugin\n- **Spotify** · Lazy item generation (Infinite scroll)\n- **Widgets BoxModel**\n  - Customizable window properties\n  - Drop \"quit on close\" option\n- **Wikipedia** · Infinite scrolling\n\n### API\n\n#### Core\n\n- [**BREAKING**] Drop property.h\n- [**BREAKING**] Rename `bind` to `bindWidget`\n- [**BREAKING**] Drop `util` namespace\n- [**BREAKING**] Drop `tryCreateDirectory`\n- [**BREAKING**] Expose the `UsageScoring` class\n- [**BREAKING**] Remove color macros from public interface\n- [**BREAKING**] Simplify `UsageScoring` API\n- [**BREAKING**] Add asynchronous query handling support\n- [**BREAKING**] _BackgroundExecutor_ · Remove any logging or exception handling\n- Make `template<typename T> T* extension(const QString &id)` const\n- [**BREAKING**] Move UsageScoring into the Query\n- [**BREAKING**] Add object oriented global `App` interface\n- [**BREAKING**] Remove const from global query handlers parameter\n- [**BREAKING**] Add `GeneratorQueryHandler` class\n- [**BREAKING**] Drop `GlobalQueryHandler::handleGlobalQuery`\n- [**BREAKING**] Rename `Query` to `QueryContext`\n- [**BREAKING**] Rename `ThreadedQueryHandler` to `RankedQueryHandler`\n- Add class `AsyncGeneratorQueryHandler`\n- [**BREAKING**] Rename `QueryContext::string` to `QueryContext::query`\n- Make `QueryContext::isValid` thread-safe\n- Asynchronous PluginInstance initialization\n- [**BREAKING**] _PluginInstance_ · Remove keychain API\n- [**BREAKING**] Facade `ExtensionRegistry` behind `App` interface\n- [**BREAKING**] Redesign icon API\n\n#### Plugins\n\n- **Python plugins** · [**BREAKING**] · Python plugin interface v5.0\n\n### Performance\n\n#### Plugins\n\n- **Calculator** · Initialize calculator in background thread\n- **Documentation** · Asynchronous initialization\n- **Python plugins** · Asynchronous plugin initialization\n\n### Fixes\n\n#### Core\n\n- No tray icon on xdg platforms\n- Taborder in settingswidget\n- _QueryResults_ · Proper tr context\n- _OAuth_ · Do not silently fail on incorrect credentials\n- Prevent segfaults on plugin unload while window is visible\n- _GlobalQuery_ · Threading bugs\n- Replace leftover bool ref stop token\n- Add icon.h to header file set\n- Use clang for dedicated plugin test builds\n- `configWidget` call on unloaded plugin\n- Workaround Qt 6.4 deadlocks\n- Dead link in CONTRIBUTING.md\n- Mute shadow warnings\n- Mute -Wunused warnings\n\n#### Plugins\n\n- **CopyQ** · Invalid return type\n- **Debug** · Workaround GCC13 bugs\n- **Docker** · Proper tag handling\n- **Documentation** · Extract docsets into the docset directory\n- **Emoji** · Loading error when locale is 'C'\n- **Files** · Apply mime filter to the root item.\n- **GitHub**\n  - Fix typos in logs\n  - Avoid writing keychain on initial read\n  - Workaround GCC13 bugs (Ubuntu 24.04).\n- **Obsidian** · Backward compatibility\n- **Pass** · Correct license\n- **Python plugins**\n  - Python AST API dropped attr `Str`\n  - Fix virtual dispatch of `GlobalQueryHandler::items`\n  - Fix virtual dispatch of `IndexQueryHandler::items`\n  - Fix virtual dispatch of `IndexQueryHandler::rankItems`\n  - Use regular exceptions instead of pybind11_fail\n- **Spotify**\n  - Broken error icon lookup\n  - Avoid unneccesary writes to keychain on initial read\n- **Widgets BoxModel**\n  - Correct window_shadow_size literal\n  - Segfaults on null query\n  - Derive palette from app instead style\n\n### Documentation\n\n#### Core\n\n- Structure by topics\n- Update doxygen topic structure\n- Update query handlers documentation\n- Update ALBERT_PLUGIN macro documentation\n- Fix maintainers field documentation\n- Document how to handle plugin initialization failure\n- Add a note on GCC-13 generator bugs\n- _PluginLoader_ · Move contract into class description\n- Reorder changelog groups\n\n#### Plugins\n\n- **Documentation** · Document the custom docset feature\n\n### Testing\n\n#### Plugins\n\n- **Files** · Fix tests according to d033020b89f0306eaf09fdf95070e1697ed7c633\n- **Python plugins** · Update tests\n\n### Miscellaneous Tasks\n\n#### Core\n\n- Mute `-Wshadow` warnings\n- _docker_ · Build test images with Clang and  Ninja\n- _test_ · Mute `-Wunused-result` warnings\n- _changelog_ · Group plugin commits if >1\n- Rename plugin _path_ to _commandline_\n- Mute clazy-fully-qualified-moc-types\n- Print theme search paths on start\n- _RateLimiter_ · Async acquire support\n- Drop Widgetsboxmodel QSS frontend\n- Git ignore \".qtcreator\"\n- _StandardIconType_ · Add values and documentation\n- _Telemetry_ · Derive QObject, use proper context objects\n- Remove global engine access on item activation\n- _RPCServer_ · Avoid using QtPrivate\n- _QueryEngine_ · Emit signal per handler type\n- Separate plugin macros from `config.h` into `plugin.h`\n- Do not export privately linked dependencies\n- _docker_ · Pull qcoro dependencies\n- _GlobalQuery_ · Print diag and add results of valid queries only\n- Update translations\n- _CI_ · Update macos build system\n- _CI_ · Add QCoro dependency\n- Run dedicated plugin test builds in docker files\n- Do not print plugin exceptions to the root logging category\n- Update changelog template\n- Revert to GCC builds\n- Do not send anything if users opt out telemetry\n\n#### Plugins\n\n- **AUR**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Albert**\n  - Adapt API changes\n  - Use new icon API\n- **Applications**\n  - Adapt API changes\n  - Drop soft hyphen removal\n  - Use new icon API\n- **Arch Linux Wiki**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Bitwarden**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Bluetooth**\n  - Adapt API changes\n  - Use new icon API\n- **Caffeine**\n  - Adapt API changes\n  - Use new icon API\n- **Calculator**\n  - Adapt API changes\n  - Use new icon API\n- **Chromium**\n  - Adapt API changes\n  - Use new icon API\n- **Clipboard**\n  - Adapt API changes\n  - Remove unused include\n  - Adapt new icon API\n- **CoinGecko**\n  - Adapt to API changes\n  - Add readme\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Contacts**\n  - Adapt API changes\n  - Remove legacy shared_ptr holder\n  - Adapt new icon API\n- **CopyQ**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Date and time**\n  - Adapt API changes\n  - Adapt new icon API\n- **Debug**\n  - Adapt API changes\n  - Adapt new icon API\n- **Dictionary**\n  - Adapt API changes\n  - Adapt new icon API\n- **Docker**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Documentation**\n  - Add README content\n  - Document the custom dataset feature\n  - Adapt API changes\n  - Drop legacy workaraound for lacking move semantics\n  - Adapt new icon API\n- **Emoji**\n  - Increase interface version\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Files**\n  - Adapt API changes\n  - Fix test\n  - Remove unused include\n  - Adapt new icon API\n- **GitHub**\n  - Adapt API changes\n  - Move all saved search handling into root handler\n  - Adapt further API changes\n  - Use qtkeychain directly\n  - Adapt new icon API\n- **GoldenDict**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Hash Generator**\n  - Adapt API changes\n  - Adapt new icon API\n- **Jetbrains projects**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Kill Process**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Locate**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Media player remote**\n  - Adapt API changes\n  - Adapt new icon API\n- **Menu bar**\n  - Adapt API changes\n  - Adapt new icon API\n- **Obsidian**\n  - Fix range-loop-detach warning\n  - Print found vaults to console\n  - Adapt API changes\n  - Index all vaults on change\n  - Adapt further API changes\n  - Adapt new icon API\n- **PacMan**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Pass**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Pomodoro**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Python Eval**\n  - Adapt to API changes\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Python plugins**\n  - Drop plugin duckduckgo\n  - Remove unused include\n  - Update plugins\n- **SSH**\n  - Adapt API changes\n  - Adapt new icon API\n- **Snippets**\n  - Adapt API changes\n  - Avoid `-Wunused`\n  - Update translations\n  - Adapt new icon API\n- **Spotify**\n  - Adapt API changes\n  - Use detail::DyamicItem\n  - _TrackItem_ · Show only authors in description\n  - _ArtistItem_ · Show followers and genres in description\n  - _AlbumItem_ · Show only artists in description\n  - _PlaylistItem_ · Show only owner in description\n  - _ShowItem_ · Show only publisher in description\n  - _EpisodeItem_ · Show only description in description\n  - _AudiobookItem_ · Show only authors in description\n  - Further adaption of API changes\n  - Use qtkeychain directly\n  - Adapt new icon API\n- **Syncthing**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **System**\n  - Adapt API changes\n  - Remove unused include\n  - Adapt new icon API\n- **TeX to Unicode**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Time zones**\n  - Adapt API changes\n  - Adapt new icon API\n- **Timers**\n  - Adapt API changes\n  - Adapt new icon API\n- **Translator**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **URL Handler**\n  - Adapt API changes\n  - Adapt new icon API\n- **Unit Converter**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **VPN**\n  - Adapt API changes\n  - Adapt new icon API\n- **VSCode projects**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **VirtualBox**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **Web search**\n  - Adapt API changes\n  - Adapt new icon API\n- **Widgets BoxModel**\n  - Adapt API changes\n  - Mute missing context object warning\n  - Take namespace of the old WBM frontend\n  - Adapt new icon API\n- **Wikipedia**\n  - Add CODEOWNERS\n  - Adapt icon API changes\n- **X Window Switcher**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n  - Add maintainer\n- **Zeal**\n  - Adapt to API changes\n  - Add CODEOWNERS\n  - Adapt icon API changes\n\n\n## v33.0.1 (2025-10-15)\n\n### Core changes\n\n#### Fixes\n\n- Workaround hidpi icon Qt bug\n\n#### Miscellaneous Tasks\n\n- _QIconEngineAdapter_ · Remove drawing code\n\n\n## v33.0.0 (2025-10-14)\n\n### Core changes\n\n#### Features\n\n- _triggersqueryhandler_ · Add fuzzy support\n- _tray_ · Install the app tray icon on xdg\n- Built-in fallback theme support\n- _PluginQueryHandler_ · Add 'Load'/'Reload' actions\n- User customizable PATH\n\n#### Fixes\n\n- _triggersqueryhandler_ · Properly sync triggers\n- _rankitem_ · Correct inverted text length in operator<\n- _globalquery_ · Remove leftover explicit comparator\n- Update wayland faq link\n\n#### Performance\n\n- _rankitem_ · Avoid multiple string allocations in comparison (🚀200%)\n- _globalqueryhandler_ · Instant response times in handleTriggerQuery\n\n#### API\n\n- Add `util::percentEncoded`\n- Add `util::percentDecoded`\n- [**BREAKING**] Add customizable and typesafe icon support\n- _PluginInstance_ · [**BREAKING**] Asynchronous keychain access\n\n#### Documentation\n\n- _cmake-macros_ · Add missing metadata fields\n- _plugininstance_ · Add \\ref, remove \\since, some updates.\n- _extensionplugin_ · Add doxygen references\n- _TriggerQueryHandler_ · Update doxygen documentation\n\n#### Miscellaneous Tasks\n\n- Add api group to gitlint\n- Remove justfile\n- Add git cliff configuration\n- Move frontend resources into frontend repository\n- _Scoring_ · Fetch exceptions thrown from items\n- Replace about text with list of links\n- Notify on major version change\n\n### Plugin changes\n\n#### Features\n\n- **Albert** · Use paths of app data locations as input action text\n- **Applications** · Add sakura terminal support\n- **Bluetooth** · Linux/BlueZ support\n- **Bluetooth** · Device icon based on class of device\n- **Clipboard** · Add fuzzy support\n- **Documentation** · _icon_ · Pixel density dependent icon\n- **Jetbrains projects** · Handle multiple config dir prefixes (#4)\n- **Jetbrains projects** · Update icons to 2025 (#5)\n- **Python Eval** · Customizable list of modules to preload\n- **VirtualBox** · Use SDK manually installed into venv\n\n#### Fixes\n\n- **Applications** · _macOS_ · Index apps in $HOME/Applications\n- **Calculator** · Remove completion on evaluation errors\n- **Emoji** · Do not lower others while capitalizing 1st char\n- **GitHub** · Correct endpoint in \"Show on GitHub\" action\n- **Menu bar** · Mutex item acces\n- **PacMan** · Remove trigger from input action\n- **Python Eval** · Remove trigger from input action\n- **Python plugins** · Add missing `setTrigger` trampoline\n- **System** · Strict standard conform array initialization\n\n#### Performance\n\n- **Spotify** · Select smallest picture greater than 128px\n\n#### API\n\n- **Python plugins** · [**BREAKING**] · Reflect core API changes in the Python API.\n\n#### Documentation\n\n- **Python plugins** · Unify and update links\n- **Python plugins** · Polish stubfile. Minor fixes.\n- **Python plugins** · _IndexQueryHandler_ · Mark final methods `@final`\n\n#### Testing\n\n- **Python plugins** · Add additional tests\n\n#### Miscellaneous Tasks\n\n- **AUR** · Add maintainer\n- **AUR** · Add README\n- **AUR** · Adopt v4 API changes\n- **AUR** · Adopt v4 StandardItem API changes\n- **Albert** · _icon_ · Adapt to updated icon API\n- **Applications** · _icon_ · Adapt to updated icon API\n- **Applications** · Make exec_args usable on all platforms\n- **Arch Linux Wiki** · Adopt v4 API changes\n- **Arch Linux Wiki** · Adopt v4 StandardItem API changes\n- **Bitwarden** · Add README\n- **Bitwarden** · Adopt v4 API changes\n- **Bitwarden** · Adopt v4 StandardItem API changes\n- **Bluetooth** · Platform abstraction\n- **Bluetooth** · _icon_ · Adapt to updated icon API\n- **Caffeine** · _icon_ · Adapt to updated icon API\n- **Calculator** · _icon_ · Adapt to updated icon API\n- **Chromium** · _icon_ · Adapt to updated icon API\n- **Clipboard** · _icon_ · Adapt to updated icon API\n- **CoinGecko** · Adopt v4 API changes\n- **CoinGecko** · Adopt v4 StandardItem API changes\n- **Contacts** · Remove unused code\n- **Contacts** · _icon_ · Adapt to updated icon API\n- **Contacts** · Avoid copyingitems vector on indexing\n- **CopyQ** · Adopt v4 API changes\n- **CopyQ** · Adopt v4 StandardItem API changes\n- **Date and time** · _icon_ · Adapt to updated icon API\n- **Debug** · _icon_ · Adapt to updated icon API\n- **Dictionary** · _icon_ · Adapt to updated icon API\n- **Docker** · Adopt v4 API changes\n- **Docker** · Adopt v4 StandardItem API changes\n- **DuckDuckGo** · Adopt v4 API changes\n- **DuckDuckGo** · Adopt v4 StandardItem API changes\n- **Emoji** · Add maintainers\n- **Emoji** · Adopt v4 API changes\n- **Emoji** · Adopt v4 StandardItem API changes\n- **Files** · _icon_ · Adapt to updated icon API\n- **GitHub** · _icon_ · Adapt to updated icon API\n- **GitHub** · Adapt to async keychain API\n- **GoldenDict** · Add README\n- **GoldenDict** · Adopt v4 API changes\n- **GoldenDict** · Adopt v4 StandardItem API changes\n- **Hash Generator** · _icon_ · Adapt to updated icon API\n- **Jetbrains projects** · Add maintainers\n- **Jetbrains projects** · Adopt v4 API changes\n- **Jetbrains projects** · Add README\n- **Jetbrains projects** · Adopt v4 StandardItem API changes\n- **Kill Process** · Add maintainer\n- **Kill Process** · Adopt v4 API changes\n- **Kill Process** · Adopt v4 StandardItem API changes\n- **Locate** · Adopt v4 API changes\n- **Locate** · Adopt v4 StandardItem API changes\n- **Media player remote** · _icon_ · Adapt to updated icon API\n- **Menu bar** · _icon_ · Adapt to updated icon API\n- **Obsidian** · _icon_ · Adapt to updated icon API\n- **PATH** · _icon_ · Adapt to updated icon API\n- **PacMan** · Add README\n- **PacMan** · Adopt v4 API changes\n- **PacMan** · Adopt v4 StandardItem API changes\n- **Pass** · Add maintainers\n- **Pass** · Adopt v4 API changes\n- **Pass** · Adopt v4 StandardItem API changes\n- **Pomodoro** · Adopt v4 API changes\n- **Pomodoro** · Adopt v4 StandardItem API changes\n- **Python Eval** · Adopt v4 API changes\n- **Python Eval** · Adopt v4 StandardItem API changes\n- **Python plugins** · Drop color plugin\n- **Python plugins** · Drop dice_roll plugin\n- **Python plugins** · Use smart_holders for items\n- **Python plugins** · >3.9 typing annotations\n- **Python plugins** · Update to pybind v3.0.1\n- **Python plugins** · Refactor and minor optimizations.\n- **SSH** · _icon_ · Adapt to updated icon API\n- **Snippets** · _icon_ · Adapt to updated icon API\n- **Spotify** · _icon_ · Adapt to updated icon API\n- **Spotify** · Adopt async keychain API\n- **Syncthing** · Adopt v4 API changes\n- **Syncthing** · Adopt v4 StandardItem API changes\n- **System** · _icon_ · Adapt to updated icon API\n- **TeX to Unicode** · Add maintainers\n- **TeX to Unicode** · Adopt v4 API changes\n- **TeX to Unicode** · Adopt v4 StandardItem API changes\n- **Time zones** · _icon_ · Adapt to updated icon API\n- **Timers** · _icon_ · Adapt to updated icon API\n- **Translator** · Adopt v4 API changes\n- **Translator** · Adopt v4 StandardItem API changes\n- **URL Handler** · _icon_ · Adapt to updated icon API\n- **Unit Converter** · Add maintainers\n- **Unit Converter** · Adopt v4 API changes\n- **Unit Converter** · Adopt v4 StandardItem API changes\n- **VPN** · _icon_ · Adapt to updated icon API\n- **VSCode projects** · Add maintainer\n- **VSCode projects** · Adopt v4 API changes\n- **VSCode projects** · Adopt v4 StandardItem API changes\n- **VirtualBox** · Adopt v4 API changes\n- **VirtualBox** · Adopt v4 StandardItem API changes\n- **Web search** · _icon_ · Adapt to updated icon API\n- **Widgets BoxModel** · _icon_ · Adapt to updated icon API\n- **Widgets BoxModel** · Adopt changes of async keychain\n- **Widgets BoxModel QSS** · _icon_ · Adapt to updated icon API\n- **Widgets BoxModel QSS** · Adopt API changes of async keychain\n- **Wikipedia** · Adopt v4 API changes\n- **Wikipedia** · Adopt v4 StandardItem API changes\n- **X Window Switcher** · Adopt v4 API changes\n- **X Window Switcher** · Adopt v4 StandardItem API changes\n- **Zeal** · Adopt v4 API changes\n- **Zeal** · Adopt v4 StandardItem API changes\n\n\n## v0.32.1 (2025-08-21)\n\n### Albert\n\n- Fix plugins list view borders in breeze styles\n\n### Plugins\n\n- **github**\n  - Add saved search action \"Show on GitHub\"\n- **obsidian**\n  - Fix obsidian.json path on xdg platforms\n- **widgetsboxmodel**\n  - Fix window property signals\n\n\n## v0.32.0 (2025-08-16)\n\n### Albert\n\n- Add plugin: Obsidian\n\n### API\n\n- Let `runAppleScript` return the result and throw on error\n- Add maintainers to metadata\n- Move InputHistory to namespace `detail`\n\n### Plugins\n\n- **clipboard**\n  - Specialize behavior of different platforms\n    Qt on macOS does not deliver clipboard changes reliably. Poll on macos. React on\n    clipboard changes everywhere else.\n- **copyq**\n  - v3 Add macOS support\n- **kill**\n  - Add platform filter.\n- **obsidian** 🆕\n- **python**\n  - API v3.1 Add additional metadata fields\n    - md_maintainers\n    - md_readme_url\n    - md_platforms\n\n\n## v0.31.1 (2025-07-29)\n\nFix pre Qt6.7 builds not supporting QString spaceship operator\n\n\n## v0.31.0 (2025-07-28)\n\n### API\n\n- Move `applyUsageScore` into `TriggerQueryHandler`\n- `GlobalQueryHandler::applyUsageScore` pass by reference\n\n### Plugins\n\n- **python** Add plugins as submodules\n- **ssh** v10\n  - Allow customizing the ssh command\n  - Allow customizing the remote command\n- **locate** v3\n  - Drop update database action\n  - Filter using `Matcher`\n  - Use system file icons\n\n\n## v0.30.1 (2025-07-12)\n\n### Plugins\n\n- **contacts**\n  - Use AppleScript to open contacts which allows setting a selection.\n- **github**\n  - Avoid downloading icons multiple times\n- **python**\n  - Use pip freeze instead of inspect to check for existing packages\n\n\n## v0.30.0 (2025-06-30)\n\nAdds minor API improvements and new plugins Spotify (WIP) and VSCode.\n\n### Albert\n\n- Add construction site emojis to beta plugins title\n\n### API\n\n- Move `Action` into `item.h`. Apply perfect forwarding in ctor.\n- API: Add `QString albert::util::runAppleScript(const QString &script)`\n- Move DesktopEntryParser from app plugin into private albert API\n\n### Plugins\n\n- **files**\n  - Index trash item also by native title\n  - Fix link paths\n- **github**\n  - Fix translations of listview headers\n  - Drop fixed listview height\n- **mediaremote**\n  - v7\n    - Drop public API\n    - Reintroduce multi player support\n    - Nice composed icons\n- **spotify** 🆕\n\n### Python plugins\n\n- **vscode_projects** 🆕\n- **translators** Improve description\n\n\n## v0.29.4 (2025-06-27)\n\n### Albert\n\n- Add optional field \"readme_url\" to metadata. Add a link in the plugin settings if it is available\n- Fix dysfunctional url scheme handler\n\n\n## v0.29.3 (2025-06-26)\n\nFix release.\n\n\n## v0.29.2 (2025-06-25)\n\nHotfix release.\n\n\n## v0.29.1 (2025-06-24)\n\nHotfix gitmodules private link causing all Linux builds to fail\n\n\n## v0.29.0 (2025-06-24)\n\n- New GitHub plugin\n- Next step in making the API more developer friendly\n\n### Albert\n\n- Move AppQueryHandler into plugin\n\n### API\n\n- c++23 👋\n- Make `PluginLoader` asynchronous\n- `StandardItem` changes\n  - Make `input_action_text` the last constructor argument\n  - Apply perfect forwarding in constructor and shared_ptr factory.\n  - The default behavior of `inputActionText()` is to return `name()` if\n    `input_action_text` is the null string. Set `input_action_text` explicitly to\n    the empty string to get no input action text at all.\n- Return `text()` in `Item::inputActionText` base implementation\n- Remove frontend related classes from public API\n- Add `albert::util::toQString(const std::filesystem::path&)`\n- Simplify messagebox functions\n- Add modal parent parameter to messagbox utils\n- Drop `makeRestRequest` from `networkutil.h`\n- Move `ExtensionPlugin` into `util` namespace\n- Move `TelemetryProvider` into private namespace\n- Rename `albert::util::FileDownloader` to `albert::util::Download`\n- Add `shared_ptr<Download> albert::util::Download::unique(const QUrl &url, const QString &path)`\n- Move plugin dependecies into `albert::util` namespace\n\n### Plugins\n\n- **application**\n  - Moved from core to plugin\n- **applications**\n  - Read env variable `ALBERT_APPLICATIONS_COMMAND_PREFIX`\n- **bluetooth**\n  - Fix MRU order\n- **caffeine**\n  - Fix completion behavior\n- **docs**\n  - Index docsets in background thread\n- **github** 🆕\n- **python**\n  - Check and install missing dependencies _before_ loading plugins\n  - Add button that opens terminal in activated venv\n- **ssh**\n  - Fix missing sort in triggered handler\n- **widgetsboxmodel**\n  - Remove deprecated theme info\n  - Support F/B bindings for pgdown/pgup\n\n\n## v0.28.2 (2025-06-20)\n\nHotfix index initialization\n\n\n## v0.28.1 (2025-06-19)\n\nFix release.\n\n\n## v0.28.0 (2025-05-30)\n\n### Albert\n\n- Make global and triggered triggersqueryhandler behave the same\n\n### API\n\n- Force plugins to not convert from ascii.\n- Move system related functions into `systemutil.h`\n  - Remove `void open(const std::string &path);`\n  - Remove default param workdir in `runDetachedProcess`\n  - Add `long long runDetachedProcess(const QStringList&);`\n  - Remove `open(const std::string &path);`\n  - Add `open(const std::filesystem::path &path);`\n- Remove `Query::isFinished()`\n- Changes to `albert::util::InputHistory`\n  - Add `uint limit() const;`\n  - Add `void setLimit(uint);`\n  - Support multiline entries (store in JSON format)\n- Add keychain support\n  - Add `QString PluginInstance::readKeychain(const QString & key) const;`\n  - Add `void writeKeychain(const QString & key, const QString & value);`\n- Iconprovider add `mask:` and `comp:` schemes\n  - mask: Mask a given icon given a radius divisor.\n  - comp: Composes two given icons.\n- Move messagebox utils to `messagebox.h`\n- Move utility symbols into `albert::util` namespace\n- Add class `albert::util::FileDownLoader`\n- Add class `albert::util::OAuthConfigWidget`\n- Add class `albert::util::OAuth`\n- Add `widgetsutil.h`\n  - Add `bind` function for QCheckBox\n  - Add `bind` function for QLineEdit\n  - Add `bind` function for QSpinBox\n  - Add `bind` function for QDoubleSpinBox\n  - Remove `ALBERT_PROPERTY_CONNECT_*` macros\n- Add `networkutil.h`\n  - Move `albert::network()` to `albert::util::network`\n  - Add `waitForFinished(QNetworkReply *reply)`\n  - Add `QNetworkRequest makeRestRequest(…)`\n- Add CMake macros\n  - `albert_plugin_link_qt`\n  - `albert_plugin_dbus_interface`\n  - `albert_plugin_sources`\n  - `albert_plugin_link`\n  - `albert_plugin_include_directories`\n  - `albert_plugin_i18n`\n  - `albert_plugin_generate_metadata`\n  - `albert_plugin_compile_options`\n- Add dynamic items feature\n  - Add interface class `albert::Item::Observer`\n  - Add `void albert::Item::addObserver(Observer *observer);`\n  - Add `void albert::Item::removeObserver(Observer *observer);`\n- Add `albert:` scheme handling support\n  - Add `albert::UrlHandler` extension\n\n### Plugins\n\n- **chromium**\n  - Show the full folder path\n- **datetime**\n  - Add paste action\n  - Dynamic item support\n- **docs**\n  - Compose icon with the books emoji to compensate for its 32x32 size\n  - Fix missing cache location initialization\n  - Fix directory init mismatch\n- **mpris** → **mediaremote**\n  - v6 macOS implementation\n- **path**\n  - Fix trigger behavior\n- **snippets**\n  - Fix https://github.com/albertlauncher/albert-plugin-snippets/issues/1\n  - Use query dependent synopsis\n  - Allow putting text directly into inputline\n  - Add input validation in snippet name input dialog\n- **ssh**\n  - Avoid empty match in global query.\n- **timer**\n  - Use dynamic item feature\n- **timezones**\n  - Fix icon and translations\n  - Use batch add to avoid flicker\n- **vpn**\n  - Use dynamic item feature\n- **websearch**\n  - Fix 6.9 breaking tableview behavior\n- **widgetsboxmodel**\n  - Make use of Query::dataChanged for dynamic items\n  - Disable window properties for now\n  - Add input edit mode\n  - Change animation durations to a more calm behavior\n  - Drop redundant history text equality check\n  - Properly reset input history search on hide.\n  - Restore user input on backward history iteration end\n  - Fix macOS cmd+backspace behavior in multiline editing\n  - Fix weird selection glitch\n  - Fix out of bounds crashes. Trigger length is set async and may exceed the text length.\n  - Fix null query segfaults\n  - Fix comboboxes not showing system theme\n\n## v0.27.8 (2025-04-06)\n\nHotfix release for the frontends.\n\n### Plugins\n- **widgetsboxmodel**\n  - Fix uninitialized trigger length causing crashes\n- **widgetsboxmodel-qss**\n  - Adopt completion bahavior\n  - Non hiding action support.\n  - Themesqueryhandler behavior from wbm\n\n\n## v0.27.7 (2025-04-02)\n\nHotfix git submodule urls.\n\n\n## v0.27.6 (2025-04-02)\n\n### Albert\n- Organize plugins in submodules.\n- Run the empty query on `*`\n- Add *'Open in terminal'* action to AppQueryHandler\n\n### API\n- Add `albert::Action::hide_on_activation`\n- Add `albert::PluginInstance::dataLocations`\n- Add `albert::show(const QString&)`\n- Add standard message box functions modal to the main window.\n  - Add `albert::question(…)`\n  - Add `albert::information(…)`\n  - Add `albert::warning(…)`\n  - Add `albert::critical(…)`\n\n### Plugins\n- **bluetooth**\n  - Add icons for (in)active/(dis)connected items.\n  - Add completions.\n- **caffeine**\n  - Polish translations of durations.\n  - Add natural duration spec (eg 1h1m).\n  - Fix weird trigger completions.\n  - Fix default trigger matching.\n  - Add deactivation notification.\n  - Use special text ∞ in settings.\n- **clipboard** - Avoid flicker by using batch add.\n- **system**\n  - New icon set.\n  - Update macOS logout command.\n- **timer** - Remove timers from empty query without hiding the window.\n- **vpn** - Add icons clearly indicating the state.\n- **websearch** - New default icon.\n- **widgetsboxmodel** and  **widgetsboxmodel-qss**\n\n  QStyleSheetStyle and its style sheets are a mess to work with and pretty limited.\n  They may be suitable to style dedicated widgets but not for entire UIs.\n  This release lays the foundation for a frontend that is not a pain to work with.\n  The widgetsboxmodel as you know it has been forked to `widgetsboxmodel-qss`.\n  But for now it still has the id `widgetsboxmodel` id until the prototype is polished enough.\n  The id of the new widgetsboxmodel frontend without style sheets is widgetsboxmodel-ng.\n  - Theme files support.\n  - Allow multiline input. Shift enter inserts a newline.\n  - Empahsize the trigger.\n  - Add context menu to button\n  - Allow non hiding actions.\n  - Add a handcrafted, buffered windowframe. Drop glitchy Qt shadow.\n  - Add option 'Disable input method'.\n  - Completion and synopsis side by side.\n  - Handcrafted rounded rects that support gradients.\n  - Ads settable window properties like colors, margins, sizes and such.\n  - Put settingsbutton into the input layout. No overlay anymore.\n  - Always show action to set the _current_ mode first (dark, light).\n  - Add debug overlay option\n  - Loads of fixes and mini improvements.\n\n\n## v0.27.5 (2025-03-06)\n\n### Plugins\n- **widgetsboxmodel** - Fix dark mode detection on Gnome\n- **coingecko** - Make sure cache location exists\n- **emoji** - Make sure cache location exists\n\n\n## v0.27.4 (2025-03-05)\n\n### Albert\n- Fix translation file lookup\n\n### Plugins\n- **vpn**\n  - Add xdg platform (network manager)\n  - Listen to state changes\n- **python.vpn** - Archive plugin\n- Append _en to the plural files\n\n\n## v0.27.3 (2025-02-28)\n\nHotfix Qt 6.2 backward compatibility (Ubuntu 22.04 builds).\n\n\n## v0.27.2 (2025-02-28)\n\nHotfix #1517\n\n\n## v0.27.1 (2025-02-27)\n\n### Albert\n- Fixes and minor improvements\n- Update translations\n\n### Plugins\n- **vpn**\n  - Change icons\n  - Fix translations\n- **python**\n  - Redesign the settings widget\n  - Allow users to reset the venv\n  - Avoid initializing venv on every start\n\n\n## v0.27.0 (2025-02-27)\n\nThis is primarily an intermediate release that reverts bad design decisions that make progress difficult.\n\n### API\n- `albert`\n  - Remove class `ItemsModel`\n  - Remove enum `ItemRoles`\n  - Remove `openUrl(const QUrl &url)`\n  - Remove `ExtensionWatcher<T>`\n  - Add class `ResultItem`\n  - Add `const ExtensionRegistry &extensionRegistry();`\n  - Add `tryCreateDirectory(const filesystem::path&)`\n  - `network()`: Return reference\n- `albert::ExtensionRegistry`\n  - Remove `T* extension<T>(const QString &id)`\n- `albert::PluginInstance`\n  - Remove `ExtensionRegistry &registry();`\n  - Remove `createOrThrow(const QString &path)`\n  - Make `cache/config/dataLocation` return filesystem::path\n  - Add `extensions()`\n- `ExtensionPlugin`\n  - Add `ExtensionPlugin::extensions()`\n- `albert::MatchConfig`\n  - Avoid recurring default separator regex instatiation\n  - Change field order\n- `albert::Query`\n  - Add isActive()\n  - Return `vector<ResultItem>` in `matches` and `fallbacks`\n  - Return `bool` in `activate*`\n  - Remove signal `finished`\n  - Add signal `matchesAboutToBeAdded`\n  - Add signal `matchesAdded`\n  - Add signal `invalidated`\n  - Add signal `activeChanged`\n- `albert::TriggerQueryHandler`\n  - Pass queries as reference\n  - `synopsis()` > `synopsis(const QString &query)`\n- `albert::GlobalQueryHandler`\n  - Pass queries as reference\n  - Remove param of `handleEmptyQuery`\n- Rename albert/util.h to albert/albert.h\n\n### Plugins\n- **python**\n  - Add tests.\n  - Google Docstring format stub file.\n  - **Python API v3.0**. See changelog in stub file for details.\n\n\n## v0.26.13 (2025-01-06)\n\nHotfix backward compatibility.\n\n\n## v0.26.12 (2025-01-06)\n\n### Albert\n- Fix device dependent pixmap creation\n- Avoid usage of deprecated QStyle::standardPixmap\n\n### Plugins\n- **applications** - Add ghostty\n\n\n## v0.26.11 (2024-12-30)\n\n### Albert\n- Add a motivating text in the plugin settings placeholder page\n\n### Plugins\n- **timer**\n  - Allow h, m, s durations\n  - Clean obsolete translations\n  - User timer emoji icon\n- **caffeine** - Fix non-persistent default interval\n- **menubar** - Do not retain NSRunningApplication forever.\n- **widgetsboxmodel**\n  - Update icon handling\n  - Do not upscale icons that are smaller than requested\n  - Draw the icon such that it is centered in the icon area\n- **chromium** - Update bookmark icon\n- **websearch** - Change icon sizes which lead to blurry output\n- **applications** - Change missing terminal link to issues choice\n\n\n## v0.26.10 (2024-12-06)\n\n### Albert\n- Add some more precise Match tests\n- Hardcode /usr/local/bin into PATH on macos\n- Fix Matcher type conversion\n- Precompile headers\n\n### Plugins\n- **chromium** - Avoid warnings on emtpy paths\n- **menubar**\n  - Properly display glyphs\n  - Fix modifier conversion\n  - Minor improvements and fixes\n- **bluetooth** - Add open settings action\n\n\n## v0.26.9 (2024-12-02)\n\n- **widgetsboxmodel** - Fix broken input history behavior\n- **applications** - Set ignore_show_in_keys default to true\n- **clipboard** - Fix typo\n- **path** - Show PATH in settings and tr synopsis\n- **docs** - \"Fix\" mac builds\n\n\n## v0.26.8 (2024-11-18)\n\n### Albert\n- Minor improvements around telemetry\n\n### API\n- Add variadic ``Matcher::match(…)``\n\n### Plugins\n- **python** - Interface 2.5\n- **applications**\n  - Recurse app directories on macos\n  - Revert back to command based heuristic to find terminals\n- **menubar** - Show hotkeys\n\n\n## v0.26.7 (2024-11-07)\n\n### Albert\n- CPack drop qt deploy tool\n- Update translations\n- Parse cli params asap for faster hotkeys\n\n### Plugins\n- **menubar** - v1.0\n- **system** - Move sleep inhibition in separate plugin\n- **caffeine** - Separate from system\n\n\n## v0.26.6 (2024-10-23)\n\n### Albert\n- Actually make use of telemetry and send enabled plugins and activated extensions.\n- Add context menu to the plugin list\n  - En/disable plugin\n  - Un/load plugin\n  - Option to sort list by checked state\n- Improve testing\n  - Drop doctest. Use QTest.\n  - Drop doctest and use QTest\n  - Enable CTest for better CI\n\n### API\n- ``albert``\n  - Add ``quit()``\n  - Add ``restart()``\n  - Add ``open(QUrl url)``\n  - Add ``open(QString path)``\n  - Add ``open(filesystem::path path)``\n- ``InputHistory``\n  - Constructor now optionally takes a path\n- Add colors to logging.h\n\n### Plugins\n- **applications**\n  - Send telemetry about available terminals\n  - Log warning on unsupported terminals\n  - Case sensitive desktop ids\n  - Fix desktop entry shadowing\n  - Add terminal org.gnome.ptyxis\n- **bluetooth** - Fix warning on language\n- **files**\n  - Add filebrowser option: Show hidden files\n  - Add filebrowser option: Sort case insensitive\n  - Add filebrowser option: Show directories first\n  - Use QTest\n- **widgetsboxmodel** - Fix weird end of history behavior\n\n\n## v0.26.5 (2024-10-16)\n\n### Plugins\n- **qmlboxmodel** - Archive plugin\n- **applications**\n  - Fix segfaults on Qt 6.8\n  - Add option \"split camel case\"\n  - Add option \"use acronyms\"\n  - Add terminals debian-uxterm and com.alacritty.Alacritty\n- **python** - Do not allow site_import\n- **dictionary** - Implement FallbackHandler\n- **contacts**\n  - Implement IndexQueryHandler\n  - Index contacts in background\n  - Use all available names\n  - Add website addresses actions\n\n\n## v0.26.4 (2024-09-24)\n\n### Albert\n- ItemIndex: Fix access to moved item. Skip emtpy IndexItems.\n\n### Plugins\n- **applications** - Search in /System/Cryptexes/App/System/Applications\n- **files** - Fix f-term action failing on spaces\n- **files** - Fix xfce4-terminal\n- **snippets** - Fix typo\n- **websearch** - Create required data directory on start\n- **syncthing** - Use python-syncthing2. python-syncthing is dead.\n- **unit_converter** - Remove future typehints\n\n\n## v0.26.3 (2024-09-07)\n\n### Plugins\n- **calculator_qalculate**\n  - Add option: Units in global query\n  - Add option: Functions in global query\n- **python**\n  - Proper venv isolation\n  - Fix excluding regex breaking aur builds\n  - No quotes around logs\n\n\n## v0.26.2 (2024-08-21)\n\nHotfix docs plugin cluttering output due to nonunique item id\n\n\n## v0.26.1 (2024-08-20)\n\n### Albert\n- Albert license v1.1\n\n### Plugins\n- **system**\n  - Also match the trigger for sleep inhibition\n  - Allow changing trigger for sleep inhibition\n- **docs**\n  - Proper anchor support for all kinds of docsets\n  - Add the type to the description\n- **files**\n  - Add option \"index file path\"\n  - Also fix persistence for option \"case senstive file browsers\"\n- **applications**\n  - Sort terminals list by caseinsensitive name\n\n\n## v0.26.0 (2024-08-16)\n\n### Albert\n- Give ``QIcon::fromTheme`` another try\n\n### API\n- Remove const from ``GQH::hgq`` and ``GQH::heq``\n- Drop ``albert::runTerminal``. Moved to applications plugin.\n- Make private property available in subclasses\n- Add getter for plugin dependencies\n\n### Plugins\n- **system:10.0** - Add inhibit sleep feature\n- **docs:3.17** - Be more tolerant with anchors\n- **applications:12.0**\n  - Move terminal detection here\n  - Proper flatpak terminal support\n  - Add public interface to run terminals\n  - Proper platform abstraction\n  - Foundation for xdg-terminal-execute\n  - Foundation for URL scheme and mime type handlers\n- **inhibit_sleep** Archive. Moved to system plugin.\n- **docker:3.0** Revert to trigger query handling.\n- **unit_converter:1.6** Port to API v2\n- **jetbrains:2.0** - Add Aqua and Writerside\n- **tex_to_unicode:1.3** Port to v2.3\n\n\n## v0.25.0 (2024-08-02)\n\n### Albert\n- Simplify `MatchConfig`\n- Hardcode `error_tolerance_divisor` to 4\n\n### Plugins\n- **chromium**\n  - Avoid initial double indexing\n  - Fix status message in settings window\n  - Fix warnings on empty paths\n- **bluetooth** - Support fuzzy matching\n- **urlhandler** - Use ``albert::openUrl``. ``QDesktopServices::openUrl`` fails on wayland.\n\n\n## v0.24.3 (2024-07-09)\n\n### Albert\n- Port applications settings to new id\n- Fix telemetry\n\n### Plugins\n- **snippets:5.4** Show a snippet preview in description.\n\n\n## v0.24.2 (2024-07-02)\n\n### Albert\n- Add \"open terminal here\" to app directory items\n- Hotfix #1408\n\n### Plugins\n- **python:4.5** Update python stub file\n- **goldendict:1.5** Remove breaking type hints\n\n\n## v0.24.1 (2024-06-28)\n\n### Plugins\n- **python:4.4** Revert back to using pybind submodule (v2.12.0)\n\n\n## v0.24.0 (2024-06-28)\n\n### Albert\n- Ignore soft hyphens in lookup strings\n- Add TriggersQueryHandler builtin handler\n- Drop PluginConfigQueryHandler\n- Ignore order of query words\n- Do not run fallbacks on empty queries\n- Allow unsetting hotkey on backspace\n- Move about into general tab\n- Use a button for hotkeys such that tab order is usable\n- Cache icons in the fallback handler to avoid laggy resize\n- Set 700 on albert dirs\n- Use same config location and format on all platforms.\n- Show message box on errors while loading enabled plugins\n- Make openUrl working on wayland by using xdg-open\n\n### API\n- Loads of changes around the project structure\n  - `AUTOMOC`, `UIC`, `RCC` per target\n  - Structure sources in folders\n  - Flatten headers\n  - No paths in core source files (rather lots of include dirs)\n  - Finally proper target export such that plugin build in build tree as well as separate projects\n  - Add custom target `global_lupdate`\n- CMake\n  - `albert_plugin(…)` modifications\n    - Add `QT` parameter\n    - Add `I18N_SOURCES` parameter\n    - `SOURCE_FILES` > `SOURCES`\n    - `I18N_SOURCE_FILES` > `I18N_SOURCES`\n    - `INCLUDE_DIRECTORIES` > `INCLUDE`\n    - `LINK_LIBRARIES` > `LINK`\n    - Make `SOURCES` optional. Specify source conventions: `include/*.h`, `src/*.h`, `src/*.cpp`, `src/*.mm`, `src/*.ui` and `<plugin_id>.qrc`\n  - Drop `METADATA` the metadata.json is a mandatory convention now.\n  - Drop `TS_FILES`. Autosource from 'i18n' dir given a naming convention.\n    `<plugin_id>.ts` and `<plugin_id>_<ui_language>.ts`\n  - Add CMake option `BUILD_PLUGINS`\n- General\n  - Move `Q_OBJECT` into `ALBERT_PLUGIN` macro\n  - Remove app functions from API\n  - Rename `albert.h` to `util.h`\n  - `albert::networkManager` -> `albert::network`\n  - Add convenience classes for plugin interdependencies\n  - Allow `RankItem`s to be created using a `Match`\n  - Revert back to per plugin translations. Plugins shall be self contained modules and in principle be packagable in a separate package.\n  - Let `QtPluginLoader` automatically load translations if available.\n  - Add finished and total count to translations metadata\n  - User per target compile options\n  - Add `havePasteSupport()`\n  - Remove `openIssueTracker` from interface\n  - Separate and improve `ALBERT_PLUGIN_PROPERTY` macros\n    - `ALBERT_PROPERTY_GETSET`\n    - `ALBERT_PLUGIN_PROPERTY_GETSET`\n    - `ALBERT_PROPERTY_CONNECT_SPINBOX`\n    - Add param in property changed signal\n- `PluginInstance`\n  - Add `{cache,config,data}Location`. Checks are up to the clients.\n  - Add `createOrThrow` as a utility function for the above functions.\n  - Add weak refs for `PluginLoader` and `ExtensionRegistry`\n  - Drop convenience functions like `id`, `name`, `description`.\n  - Drop `initialize`/`finalize`\n  - Registering extensions can fail\n  - Auto register root extensions\n- Changes to icon provider API\n  - Add `QIcon` support\n  - Make it free functions\n  - Remove caching\n  - Returned size can be smaller than `requestedSize`, but is never larger.\n- `Query`, engine and handlers\n  - Handle handler configuration in core (trigger, fuzzy, enabled).\n    Remove the getters, have only setters to update plugins.\n    - Add `TriggerQueryHandler::setUserTrigger`\n    - Remove `TriggerQueryHandler::trigger()`\n    - Remove `TriggerQueryHandler::fuzzyMatching()`\n  - Do not allow users to disable triggered query handlers.\n    This may end up in states where plugins are loaded but actually not used.\n    Also some handlers may rely on them to be there, like e.g. the files global\n    handler redirects tabs to the triggered handlers.\n  - Remove `const` from handleTriggerQuery\n  - Support ignore diacritics\n  - Support ignore word order\n  - Make `Query` contextually convertible to `QString`\n  - Unify query interface, no more global- and triggerquery\n  - Add parameterizable `Matcher`/`Match` class\n  - Add dedicated empty query handling\n    Empty patterns should match everything. For global queries that's too\n    much. For triggered queries it is desired though. Since a lot of global\n    query handlers relay the `handleTriggerQuery` to `handleGlobalQuery` it is\n    not possible to have both. This introduces a dedicated function for\n    GlobalQueryHandlers that will be called on empty queries:\n\n### Plugins\n- **widgetsboxmodel** - Use QWindow::startSystemMove instead QWidget:move for Wayland Support\n- **websearch**\n  - Add fallback option\n  - Add GPT to default engines\n  - Add fallback section.\n  - Allow inline editing of fallback and trigger withough using the search engine widget.\n  - Use matcher for more tolerant queries\n  - Complete to trigger instead of name\n- **timezones**\n- **timer**\n- **telegram** - Archive failed telegram quick access approach\n- **path** - Rename from 'terminal'\n- **system** - System commands update for KDE Plasma 6\n- **ssh** - Allow params only in triggered handler\n- **sparkle** - Archive for now\n- **snippets** - Check if paste is supported at all\n- **qmlboxmodel** - Port\n- **python**\n  - Namespace plugin id\n  - Compensate the API changes gracefully to defer a breaking API change\n  - Ship stub file with the plugin\n  - Add buttons for stubfile and user plugin dir\n  - API 2.3\n    - Deprecate obscure module attached md_id. Use PluginInstance.id.\n    - Expose function albert.havePasteSupport\n    - Expose class albert.Matcher\n    - Expose class albert.Match\n    - Expose method albert.TQH.handleTriggerQuery\n    - Expose method albert.GQH.handleGlobalQuery\n    - albert.PluginInstance:\n        - Add read only property id\n        - Add read only property name\n        - Add read only property description\n        - Add instance method registerExtension(…)\n        - Add instance method deregisterExtension(…)\n        - Deprecate initialize(…). Use __init__(…).\n        - Deprecate finalize(…). Use __del__(…).\n        - Deprecate __init__ extensions parameter. Use (de)registerExtension(…).\n        - Auto(de)register plugin extension. (if isinstance(Plugin, Extension))\n    - albert.Notification:\n        - Add property title\n        - Add property text\n        - Add instance method send()\n        - Add instance method dismiss()\n    - Minor breaking change that is probably not even in use:\n        Notification does not display unless send(…) has been called\n- **mpris** - Rewrite using xml interface files\n- **exprtk** - Archive exprtk prototype\n- **docs**\n  - Fix XML based docs.\n  - Do not upscale icons\n  - Fix leak on plugin unloading\n- **dictionary** - Drop resources, use Dictionary.app icon\n- **datetime**\n  - Separate timetzonehandler\n  - Add \"show_date_on_empty_query\" option\n- **clipboard**\n  - Check if paste is supported at all\n  - Use albert::WeakDependency\n- **chromium**\n  - Add completion\n  - Display bookmark folder\n- **bluetooth** New extension on macos\n  - Enable disable Bluetooth\n  - Connect to paired devices\n- **applications**\n  - Add non localized option on macos\n  - Merge applications_macos and applications_xdg\n  - Add completion\n- All python plugins: Minor fixes and port to API 2.3\n- **zeal** - Add fallback extension\n- **wikipedia** - Add fuzzy search support\n- **tr** - Check paste support\n- **timer** - Move to archive\n- **syncthing** - Initial prototype\n- **goldendict** - Support flatpaks and goldendict-ng\n- **emoji** - Check paste support\n\n\n## v0.23.0 (2024-03-03)\n\n### Albert\n- i18n\n- Make fallback order settable in new query tab.\n- Load native plugins threaded.\n- Add --no-load cli param\n- Use hashmap and avoid exceptions. Twice as fast 🚀\n- Add german translation\n- Make \"Show settings\" action the default for plugin items\n\n### API\n- Change frontend interface design\n- drop extensions() from PluginInstance interface.\n  Extensions can now bei registered dynamically at any time.\n- Reduce the plugin system interfaces to the bare minimum\n- Allow hard plugin dependencies.\n- Private destructors for interfaces\n- Refactoring\n  - ExtensionRegistry add > registerExtension\n  - ExtensionRegistry remove > deregisterExtension\n- Make UI strings in the metadata required.\n- Allow plugins to have public interfaces\n- Revert to authors. Drop maintainers. (plugin metadata)\n- Remove polymorphism in PluginInstance id/name/description\n- Remove dynamic allocation of cache/config/dataDir()\n- Drop template parameter QOBJECT\n- Frontend is not an extension\n- Support localized metadata\n- CMake interface\n  - Drop long_description from metadata\n  - Add TS_FILES parameter to albert_plugin macro.\n  - Revert back to json metadata file again\n  - Complete metadata using cmake project details\n  - Move Qt::Widgets into the public link interface\n\n### Plugins\n- Support i18n\n- **qmlboxmodel** archive wip\n- **widgetsboxmodel**\n  - Fix animation on linux\n  - Dark theme support\n  - Themes update\n  - Reproducible style (fusion)\n  - Fix history search\n  - Move persistent window position to state\n  - Clear icon cache on hide\n  - Archive unlicensed themes\n  - Remove \"Show fallbacks on empty result\" option\n  - Drop fonts from themes\n- **websearch:8.0**\n  - Capital You_T_ube\n  - Add Google translate default engine\n- **ssh:8.0**\n  - Reduce complexity of this overengineered plugin\n    - Remove quick connect\n    - Remove known hosts\n    - Remove file watchers (configs change not that often)\n    - remove indexer mutexes\n    - remove fuzzy index\n- **snippets:5.0**\n  - Public extension interface \"Add snippet\"\n- **qmlboxmodel:3.0**\n  - Archived\n- **python**\n  - Open external links in config labels by default\n  - API v2.2\n  - Drop source watches. a plugin provider cant just reload without notifying the plugin registry\n  - API 2.2\n    - PluginInstance.configWidget supports 'label'\n    - __doc__ is not used anymore, since 0.23 drops long_description metadata\n    - md_maintainers not used anymore\n    - md_authors new optional field\n- **dictionary:3.0** Former platform_services\n  - Rename plugin platform services to dict\n- **clipboard:3.0**\n  - use snippets interface\n- **applications_macos:5.0**\n  - Use KVO to track NSQuery results\n- **virtualbox:1.6** Add info on vboxapi requirement\n- **docker:2.0** Show error on conn failure.\n- **pomodoro:1.5** Fix notifications\n- **inhibit_sleep:1.0** Similar to caffeine, theine, amphetamine etc…\n\n\n## v0.22.17 (2023-11-26)\n\n### Albert\n- Prepend albert to logging categories, default filter debug\n- Remove logging rules cli arguments\n  Dont work on some systems and there is QT_LOGGING_RULES for it\n- Differentiate terminator terminals suffering bug 660\n\n### Plugins\n- **mpris:2.0** Ported\n\n\n## v0.22.16 (2023-11-18)\n\n### Albert\n- Remove the visual warning on crashes.\n- Remove autostart option\n- Add \"report\" RPC\n\n### Plugins\n- **python:2.1.0** - Improve UX while installing dependencies\n- **calc:1.4** - Threadsafe and aborting calculations\n- **system:1.8** - Dont prompt on gnome session logout\n- **app_xdg:1.8** - Use Ubuntu gettext domains\n- **bitwarden** - Add copy-username action\n\n\n## v0.22.15 (2023-11-08)\n\n### Albert\n- Fix missing smooth transform in icon provider\n- Add style information to report\n- Use X-GNOME-Autostart-Delay\n- Add proper unix signal handling using self pipe trick\n- Revert printing to logfile\n- Give enough time to connect to other instance.\n\n### Plugins\n- **system:1.8** - Dont prompt on gnome session logout\n- **wbm:1.6** - Remove unnecessary cast that may introduce segfaults\n- **app_xdg:1.8** - Use Ubuntu gettext domains\n- **python** - Fix links in stub\n\n\n## v0.22.14 (2023-10-06)\n\nLet RPCServer take care of crash reports. This is a hotfix to remove the recurring crash report on start, if the app is run more than once, e.g. because the session manager restores a session including albert, but albert is also configured to be autostarted.\n\n\n## v0.22.13 (2023-10-05)\n\n### Albert\n- Hotfix create missing application paths\n- Fix pixmaps path\n\n### Plugins\n- **qml** Fix version branching logic\n\n\n## v0.22.12 (2023-10-03)\n\n### Albert\n- CI/CD: Fix path in sed expression\n\n### Plugins\n- **sparkle** Add macos updater plugin prototype\n- **jetbrains** Add RustRover editor\n\n\n## v0.22.11 (2023-10-03)\n\n### Albert\n- Add missing \"-executable=\" for macdeployqt plugin parameters\n\n### Plugins\n- **py** Hotfix: Workaround https://bugreports.qt.io/browse/QTBUG-117153\n\n\n## v0.22.10 (2023-10-03)\n\n### Albert\n- CI/CD: Appcast prototype\n- Store log in cache dir\n- Add loadtype NOUNLOAD\n  There are some plugins that dont like to be unloaded (Sparkle, Python).\n  Add a mechanism to let plugins prohibit users to unload it at runtime.\n\n### Plugins\n- **python** Fix 6.5.2 only QLogCat quirks. Fixes arch builds\n\n\n## v0.22.9 (2023-09-28)\n\n- CD: upload on tag\n- Revert. NO_SONAME makes troubles on other platforms.\n\n\n## v0.22.8 (2023-09-28)\n\n- Hotfix fixing RPM based builds\n\n\n## v0.22.7 (2023-09-27)\n\n- Restore 6.2 backward compatibility\n\n\n## v0.22.6 (2023-09-26)\n\n- Proper tab navigation in handler widget\n- NativePluginProvider: Use absolute file paths.\n\n### Plugins\n- **files** Fix \"rel. dirpaths of depth 1 have dot prepended\" issue\n- **docs** Fix recent changes to download urls\n- **qalc** Fix tab order\n\n\n## v0.22.5 (2023-09-22)\n\n### Albert\n- CMake: On macOS include the macports lookup path\n- Fix segfaults on busywait\n- Hardcode /usr/local/bin to PATH\n- Move last report ts from settings to state\n- Add iconlookup in /usr/local/share although not standardized\n\n### Plugins\n- **qml** Add hack around lacking DropShadow.samples in Qt <6.4\n- **apps_macos**\n    - Find all apps in home dir\n    - Keep apps up to date unsing online search\n    - Localized app names\n    - Add prefpanes\n- **docs** Disable build on macOS. Licensing does not allow usage on macOS.\n- **files** Add emtpy trash action on macos\n- **muparser** Archive muparser. One calculator is enough.\n- **qml** Fix shadow clipping\n- **qml** Fix clear on hide breaking history search\n- **goldendict** Fix import issue\n- **pass** Add OTP feature\n\n\n## v0.22.4 (2023-08-30)\n\n### Plugins\n- **docs**\n  - Add cache for docset list\n  - Use find_program to find brew for ootb cmake config\n- **muparser** Use find_program to find brew for ootb cmake config\n- **python**\n  - Silently skip dirs and files that are no python modules\n  - iid v2.1: Add config facilities\n- **qalcualte** Use find_program to find brew for ootb cmake config\n- **qml** Add Cmd/Ctrl+Enter/Return to show actions\n- **snippets** Port old snippets\n- **googletrans** Archive. py-googletrans is broken.\n- **translators** Add \"translators\" plugin\n- **emoji** Add \"Use derived emojis\" option\n- **dice_roll** iid 2.0\n\n\n## v0.22.3 (2023-08-17)\n\n### Albert\n- Dont show version notification before app is fully initialized\n\n\n## v0.22.2 (2023-08-14)\n\n### Albert\n- Fix logging filters\n- Proper database move\n\n### Plugins\n- **ws** Fix websearch breaking users search engines config\n- **ws** Fix websearch not applying icon when selected from file dialog\n\n\n## v0.22.1 (2023-08-14)\n\n### Albert\n\n- Freedesktop notification implementation\n- Adopt generic Notification interface on macOS\n- Fix Linux paste action\n\n### Plugins\n- **apps_xdg** Default trigger \"apps\"\n- **python**\n  - Update notification function\n  - Fix function warn > warning\n- **clipboard** Add paste action\n\n### Python plugins\n- **pint,yt** Archived. Require maintenance\n- **timer** Adopt notification api changes\n\n\n## v0.22.0 (2023-08-12)\n\n### Albert\n\n- Add commandline option for logging filter rules\n- Add contour terminal\n- Add settingswindow shortcut action for plugin settings\n- Add feature copy and paste\n- Add \"Run empty query\" option\n- Add handler configurations tab\n- Sort fallbacks\n- LexSort items having equal score\n- Doxygen documentation\n\n### API\n\n- `TriggerQueryHandler`\n    - Add bool `supportsFuzzyMatching()`\n    - Add bool `fuzzyMatchingEnabled()`\n    - Add void `setFuzzyMatchingEnabled(bool)`\n    - Add `QString trigger()` (the user configured one)\n- `GlobalQueryHandler`\n    - Add `applyUsageScore(…)`.\n    - Inherit `TQH`, i.e. every handler is a `TQH`\n- `IndexQueryHandler`\n    - Reimplement TQH `fuzzy` methods\n    - Default `synopsis` `<filter>`\n- Plugin system\n    - Revert multithreaded plugin loading (Qt makes problems everywhere)\n    - Statically inject metadata, use it for PluginInstances\n    - Move native plugin interface into plugin:: namespace\n    - Cache/Conf/Data dirs per plugin only (were per Extension)\n    - Add `PluginInstance::extensions()`\n    - Add Template based `ExtensionPlugin(Instance)`\n    - Make native plugin a template class to allow subclassing any `QObject`\n- Frontend:\n    - Add `Frontend::winId`, Move the window quirks to the core\n    - Use app-wide input history file\n    - Add generic qml/widgets icon provider to interface\n    - Add generic icon provider, creating icons on the fly\n- Fuctions and macros:\n    - Put all free functions in `albert.h`\n    - Add `openUrl` QUrl overload\n    - Add convenience macros for user property definition\n    - Require albert logging category to pass the name\n    - Add state file\n    - Add global settings factory\n- Rename `History` to `InputHistory`\n- Drop `QueryHandler` convenience class\n- Drop global `albert.h` include\n\n#### Plugins\n\n- **clipboard** - Add paste action\n- **wbm**\n    - Remove option \"display icon\"\n    - Appwide input history\n- **websearch**\n    - Adopt to sorted fallbacks, drop dragndrop in listview\n    - Add drag'n'drop image feature\n- **snippets** - Add paste action\n- **qml:2.0** - Revamped QML frontend\n- **python**\n    - Mimic internal api as close as possible\n    - Attach logging functions to plugin modules\n    - Expose albert::setClipboardTextAndPaste\n    - Expose albert::Notification\n    - Interface v2.0 stub\n- **files**\n    - Show filePath instead path in subtext\n    - Add option for case sentivity of fs browsers.\n    - Add user property for inline config\n- **emoji** - New generic and platform agnostic emoji implementation\n- **duckduckgo** - Add extension\n- **color** - Add extension\n\n\n## v0.21.1 (2023-06-27)\n\n### Albert\n\n- Add cmd/ctrl + number tab navigation in settings\n- Automatically add hpp and qml files to plugin projects\n\n### Plugins\n\n- **docs:1.2** - Fix misleading comment in config widget\n- **tex_to_unicode** Fix crash due to wrong type annotation\n- **emoji** Fix #179. Call cacheLocation as method of self.\n\n\n## v0.21.0 (2023-06-23)\n\n### Albert\n\n- Settings window\n  - Add a new search widget in settingswindow\n  - Make handlers of all types optionable\n  - Make window and search widgets tabs in the settings window\n- Change usagedatabase location to datadir\n- Change IPC socket path to `$CACHEDIR/albert/ipc_socket`. Was `$CACHEDIR/albert_socket`.\n- Fix triggered global query MRU sort\n\n### API\n\n- Remove `Item::hasActions`\n- Add global config, cache and data location functions\n- Change `RankItem::score` type to float (0,1**\n- Make queries pointers in handler functions\n- Add function to get global network manager\n- Use explicit named query handling methods (no parameter overloading) `handleTriggerQuery` and\n  `handleGlobalQuery`. This reduces confusion, avoids annoying extra boilerplate to disambiguate\n  methods to avoid hide-virtual warnings and serves as a lowest common denominator on a\n  language/naming level since these features may not be supported by script languages (e.g. Python).\n\n### Plugins\n- Add\n  - **docs** Reduced set of Zeal docsets at hands\n  - **clipboard** Clipboard history\n  - **coingecko** v1.0\n  - **mathematica** iid:1.0 port\n- **contacts:1.2**\n  - Formatting: Remove Apple specific braces\n- **snip:1.3**\n  - Add \"Add\" and \"Remove\" button in config widget\n  - Add \"Add snippet\" item on \"add\" query\n  - Add \"Remove\" action to snippet items\n- **python:1.8** Adopt API v0.21. **New interface version iid 1.0**\n  - Add Extension.cache-, config- and dataLocation\n  - Expose FallbackHandler\n  - Proper multi extension registration\n  - Move interface spec into python stub file (yay!)\n  - Expose TriggerQueryHandler\n  - Expose GlobalQueryHandler\n  - Expose QueryHandler\n  - Expose IndexQueryHandler\n  - Expose Item class entirely such that plugins can subclass it\n  - Use pointer for queries\n  - Remove global cache/config/data dir functions\n  - Add stub file for type hinting and documentation in IDEs\n\n\n## v0.20.14 (2023-05-01)\n\n### Albert\n\n- Sort triggerwidget by name rather than id\n- Avoid segfaults when setting hotkey failed.\n\n### Plugins\n\n- **ws** - Fix oversized text in config\n- **sys:1.6** - Dynamic default commands.\n- **app_xdg** - Remove content margins of settings widget\n- **system** - Add lxqt defaults\n- **python_eval:1.3** - Fix type of result in item subtext\n- **locate:1.7** - Fix lambda capture\n- **api_test** - Drop plugin\n- **aur:1.6** - Fix install action\n- **jetbrains_projects** - handle missing projectOpenTimestamp\n\n\n## v0.20.13 (2023-03-30)\n\n### Plugins\n- **ws** - Show space markers in trigger section\n- **vbox:v1.3** - Port iid:0.5\n- **dice_roll** - iid:0.5 v1.0\n- **emoji** - iid:0.5 v1.0\n\n\n## v0.20.12 (2023-03-29)\n\n### Plugins\n- **system:1.4** Make items checkable and titles customizeable\n\n\n## v0.20.11 (2023-03-27)\n\n### Albert\n- Respect whitespaces in rpcs\n\n### Plugins\n- **wbm** Add option \"Center on active screen\"\n- **app_xdg** Add action \"reveal desktop entry\"\n- **files** Workaround Qt appending slash to root paths\n- **bitwarden** 1.1 (iid: 0.5)\n- **vpn** Add wireguard to connection types\n- **pacman** Fix out of scope lambda vars\n\n\n## v0.20.10 (2023-03-20)\n\n- **vpn** Add wireguard to connection types\n\n\n## v0.20.9 (2023-03-13)\n\n### Albert\n- Update supported terminals (add st and blackbox, remove tilda)\n\n### Plugins\n- **wbm** Hide task bar entry\n- **ws** Add google scholar to defaults\n\n\n## v0.20.8 (2023-02-11)\n\n### Albert\n- Tilda support\n- Print font in report\n\n### Plugins\n- **contacts_mac** v1.0\n- **wbm** Dont hide window when control modifier is hold\n- **xdgapps** Do not inherit QT_QPA_PLATFORM to launched apps\n\n\n## v0.20.7 (2023-02-10)\n\n### Albert\n- Clear icon cache if unused for a minute.\n\n### Plugins\n- **wbm** Postpone query deletion until hide event to prevent busy wait for destruction\n\n\n## v0.20.6 (2023-02-08)\n\n### Albert\n- Close settingswindow on ctrl+w\n\n### Plugins\n- **wbm** Avoid segfaults on failing screenAt()\n- **jetbrains:1.1** Polish. Fix Macos.\n\n\n## v0.20.5 (2023-02-01)\n\n### Albert\n- Drop usage weight. Add option to prioritize perfect matches. See #695\n\n\n## v0.20.4 (2023-01-31)\n\n### Albert\n- Reintroduce telemetry\n- Fix disfunctional link in settings\n\n### Plugins\n- **tex_to_unicode** py interface 0.5\n- **vpn** 1.1 (iid: 0.5)\n- **yt** v1.3 create tmp dirs lazily\n- **jetbrains** 1.0 (iid:0.5)\n\n\n## v0.20.3 (2023-01-27)\n\n### Albert\n- Remove plugin registry from global search\n- Add -Wno-inline\n- Fix line breaks in errors displayed in settings\n- Tray icon isMask\n\n### Plugins\n- **platform_mac** 1.0\n- **py** Add button to open the dependency dir\n- **pint** 1.0 (currency converter)\n\n\n## v0.20.2 (2023-01-25)\n\n### Plugins\n- **py** Quote cd path\n- **pacman** v1.6 iid:0.5\n- **timer** v1.4 iid:0.5\n\n\n## v0.20.1 (2023-01-25)\n\n### Albert\n- Fix pedantic warnings\n- BW tray\n- Use env vars to set default locale\n- Strech config widget\n- Fix segfaults on empty icon name lookup\n\n### Plugins\n- Lots of UI polishing\n- **qalc** Fix precision probles\n- **websearch** Add google maps to defaults\n- **datetime** Use default locale\n\n\n## v0.20.0 (2023-01-24)\n\n### Albert\n- Make Triggerwidget edit trigger on double click anywhere\n\n### API\n- Config widget per plugin (v0.20)\n\n### Plugins\n- **chromium:1.4** Add path reset button\n- **locate** 1.6\n- **docker** 1.3\n\n\n## v0.19.4 (2023-01-22)\n\n### Plugins\n- **qalc** v1.0 Prototype\n\n## v0.19.3 (2023-01-22)\n\n### Albert\n- **md** Use content if long description is a file path\n- Use both, extension and item id, as icon cache key\n- Add standard pixmaps support to iconprovider\n- Workaround terminator bug #702\n\n### Plugins\n- **wbm** Add Nord theme\n- **calc** Respect LC_*\n- **chromium** Fix filewatcher does not watch bookmarks\n- **wbm** Do not exit on missing themes\n- **wbm** Use generic placeholder color for input hint\n- **app:xdg** Add exec key option. Also exclude 'env' in exec keys.\n- **wbm** Fix clipped label\n- **WBM** fix open theme file action\n- **files** Provide trash item\n- **wbm** Fix list view height margins\n- **trash** Drop. Provided by files plugin now.\n\n\n## v0.19.2 (2023-01-18)\n\n### Plugins\n- **datetime** v1.0\n- **urlhandler** Fix tld validation\n\n\n## v0.19.1 (2023-01-18)\n\n### Albert\n- Fix recurring new version info\n- Allow copyconstruction of rank and index items\n\n### Plugins\n- **calc** 1.5\n  - Inline evaluation\n  - Default trigger '='\n  - Synopsis\n- **wbm** Add item activation using Ctrl+O\n\n\n## v0.19.0 (2023-01-18)\n\n### Albert\n- Add reload actinon for plugins\n- Support Console term\n- Fix backgroundexecutor not using move semantics\n- Refactoring\n- Show plugin header files in IDEs\n- Use handcrafted icon lookup again\n\n### API\n- Revert to dedicated FallbackHandler\n- Clean interface using opaque pointers\n- GlobalQueryHandler::rankItems -> handlyQuery\n- IndexQueryHandlers have to set items directly\n\n### Plugins\n- **wbs** 1.3 add query handler providing themes\n- **apps_xdg** 1.5 Remove desktop indexing\n- **ssh** 1.5\n  - Fix ssh connect containing user or port\n  - Allow specifying a command to send to the host\n  - Add action (keep/close term)\n- **yt** v1.2 (iid:0.5)\n- **kill** v1.1 (iid:0.5)\n\n\n## v0.18.13 (2023-01-13)\n\nFix invalid submodule link breaking OBS builds\n\n### Plugins\n- **chromium** Fix config loading\n- **goldendict** 1.1 (0.18)\n\n\n## v0.18.12 (2023-01-13)\n\n### Albert\n- Always print report in debug mode\n- Add platform, lang and locale to report\n- Support Terminology\n\n### Plugins\n- **mac_apps** - Dont show system service apps\n- **python** - ! Add default md_id if not available\n\n\n## v0.18.11 (2023-01-11)\n\n### Albert\n- Add missing long description in plugin metadata.\n- Add metadata LONG_DESCRIPTON to docs\n\n### Plugins\n- **urlhandler** - Handcraft tld validation. Make handler global.\n- **py** - Create site-packages dir if necessary\n- **snippets** - Fix open snippet\n\n\n## v0.18.10 (2023-01-09)\n\nFixes, minor changes and requests\n\n\n## v0.18.9 (2023-01-07)\n\n### Plugins\n- **py** Ask user to install missing python dependencies in terminal\n- **googletrans** 1.0\n- **pass** 1.2\n\n\n## v0.18.8 (2023-01-07)\n\n### Albert\n- Give sensible defaults for usage history\n- Fix memory weight not being loaded\n- Merge frontend tab into general\n- Support foot terminal\n- Check for other instances _before_ laoding plugins\n\n### Plugins\n- **files** - Avoid starting indexing on file index serialization\n- **googletrans** 1.0\n- **pass** 1.2\n\n\n## v0.18.7 (2023-01-05)\n\nDrop albertctl. Back to `albert <command>`\n\n\n## v0.18.6 (2023-01-05)\n\n### Albert\n- sendTrayNotification(…) add time parameter\n- Support wezterm.\n\n### Plugins\n- **Python** 1.5\n  - sendTrayNotification(…) add ms parameter\n- **Hash** 1.5\n  - Global query handler\n  - Add copy 8 char action\n- **Pomodoro** 1.1\n- **CopyQ** 1.2\n\n\n## v0.18.5 (2023-01-04)\n\n### Albert\n- Support Kitty terminal\n- Support Alacritty terminal\n### Plugins\n- **wbm** Show synopsis in tooltip\n\n\n## v0.18.4 (2023-01-03)\n\n### Albert\n- fix single instance mechanism\n\n### Python plugins\n- **docker** -  Archive, curious segfaults\n- **aur** - Port 0.5\n- **awiki** - Port 0.5\n\n\n## v0.18.3 (2023-01-02)\n\n### Plugins\n- **wbm** Fix theme dir paths\n\n\n## v0.18.2 (2023-01-02)\n\n- Better diagnostics on frontend loading\n\n\n## v0.18.1 (2023-01-01)\n\n- Fix armhf builds\n\n\n## v0.18.0 (2022-12-31)\n\nNote that there have been some breaking changes. The new plugin id format changed settings keys and\nconfig/cache/data paths. If you want to keep your old plugin settings you have to adjust the section\nnames in the config file and adjust the paths in your config/cache/data dirs. (e.g. from\n`org.albert.files` to `files`). I'd recommend to start from scratch though, since too much changed.\n\n### Albert\n\n- Shorter plugin ids.\n- Customizeable triggers (if the extension permits)\n- Central plugin management\n- More useful plugin metadata\n- User customizable scoring parameters\n  - Add user option memory decay\n  - Add user option memory weight\n- Finally scoring for _all_ items\n- Inputline history goes to a file now\n- Settingswidget overhaul\n- Hello Qt6, C++20 👋\n- Entirely new interface (see header files)\n- Value typed Action class based on std::function\n- Drop all former *Action classes\n  - Free functions replace and extend action subclass functionality\n- Updates to Item interface\n- New and extended query handling interface classes\n- Extended frontend interface\n- New abstract plugin provider interface\n  - Common plugin metadata\n  - Maintainership is a thing now!\n- Add StandardItem factory for better type deduction and readability\n- Add bgexecutor class\n- Add timeprinter\n- Leaner logging\n- Query design change (realtime, global, indexed)\n- Add extension watcher template class\n- Move XDG into the lib.\n\n### Plugins\n\n- **python** 1.4 (0.18)\n  - Use system pybind\n  - 0.5 interface\n  - auto pip dependencies\n- **files** 1.2 (0.18)\n  - Drop bashlike completions. We have items.\n  - Settings per root path\n  - Add name filter dialog\n  - Add option watch filesystem\n  - Add option max depth\n- **snippets** 1.1 (0.18)\n  - files instead database\n- **widgetsboxmodel** 1.2 (0.18)\n  - Fading busy indicating settingsbutton\n  - Drop rich text\n  - Proper async query without flicker using statemachines\n  - Add input hint\n  - Add option show fallbacks on empty query\n  - Add option history search\n- Archived\n  - **firefox**\n  - **qml box model**\n  - **mpris**\n  - **vbox**\n\n\n## v0.17.6 (2022-10-08)\n\n- Let users choose the chromium bookmarks path\n- Fix #978\n\n\n## v0.17.5 (2022-10-05)\n\nFix #1064.\n\n\n## v0.17.4 (2022-10-04)\n\nFix #1117\n\n\n## v0.17.3 (2022-07-05)\n\nSloppy hotfix #1088. 0.18 will change DB entirely anyway.\n\n\n## v0.17.2 (2020-12-24)\n\nDrop telemetry\n\n### Plugins\n- **wbm** Fix completion\n\n\n## v0.17.1 (2020-12-21)\n\n### Albert\n- Fix OBS builds\n- Several fixes\n- Archive virtualbox python extension\n\n\n## v0.17.0 (2020-12-17)\n\n### Albert\n- Again break init order of Item for the sake of less boilerplate. Presumed this frequency indexStrings > actions > completion > urgency.\n- Let shells handle splitting/quoting\n- Add core as QueryHandler. Add restart, quit, settings action. Also to tray and cli.\n- Drop shutil:: and let shells handle lexing\n\n### Plugins\n- FINALLY ARCHIVE EXTERNAL EXTENSIONS.\n- New extension state : MissingDependencies\n- Disable settings items of exts in this new state\n- Use pybind v2.6.1\n- **term** v1.1 Let shells handle lexing\n- **calc** Add muparserInt option for hex calculations\n- Use QLoggingCategory in all extensions\n- Implicit dependency check for executables and Python modules\n- **Pyv1.3** Adopt core changes. PyAPIv0.4. Changes to the API:\n  - embedded module is called 'albert' now\n  - Reflect core api changes:\n    - Positional arguments of the standard item changes\n    - New semantics of the term action constructors\n      - String commandline will be executed in a shell\n      - StringList commandline will be executed without shell\n  - Add core version of iconLookup(StringList)\n  - New metadata labels:\n    - __version__: new versioning scheme iid_maj.iid_min.ext_version\n    - __title__: former __prettyname__\n    - __authors__: string or list\n    - __exec_deps__: string or list\n    - __py_deps__: string or list\n    - __triggers__: string or list\n  - Allow multiple triggers\n  - Allow multiple authors\n- **locate** ' for basename '' for full path lookups\n- **timer** Make notification stay.\n- **baseconv** Python-style base prefixes to detect source base\n- **texdoc** Add texdoc plugin\n- **aur** add yay helper\n\n\n## v0.16.4 (2020-12-10)\n\n### Albert\n- Fix tab order\n\n### Plugins\n- **chromium** Chromium v1.1\n- **docker** New extension prototype\n- **timer** Use dbus instead of notify-send\n- **units** v1.2 including to time conversion\n\n\n## v0.16.3 (2020-12-03)\n\n- Hotfix for #955\n- Archive defunct CoinMarketCap and Bitfinex extensions\n\n\n## v0.16.2 (2020-11-26)\n\n### Albert\n- Allow multiple instances of albert on different X sessions\n- Fix super key not registering\n- Add terms: Elementary, Tilix, QTerminal, Termite\n- Fix build on FreeBSD\n- Dont show fallbacks on triggered queries\n\n### Plugins\n- **Applications** Index desktop files on desktop\n- **firefox** Rework v2\n- **ssh** Respect the Include keyword\n- **ssh** Allow hyphens to be part of hostnames\n- **chromium** Add brave-browser to list of chromium based browsers.\n- New:\n  - **emoji**\n  - **bitwarden**\n  - **xkcd**\n  - **node.js evaluator**\n  - **php evaluator**\n\n\n## v0.16.1 (2018-12-31)\n\n### Albert\n\n- Fix default plugin lookup path\n- Fix flicker when changing frontends\n- Fix \"Terminal option resets after a restart\"\n- Link libglobalshortcut statically\n- Add a build flag for QtCharts\n- Drop debug options if favor of QLoggingCategory env vars\n\n### Plugins\n\n- **ssh** Fix input regex. Sort by length then lexically.\n- **ssh** Use backward compatible ssh url syntax\n- **qml** Consistent form layout\n- **aur** Sort items by length first\n\n\n## v0.16.0 (2018-12-28)\n\n### Albert\n\n- Add jekyll website as submodule\n- New project structure (minimal shared lib)\n- Let travis build against Ubuntu 18.04 and 16.04\n- Backward compatibility for Ubuntu 16.04\n- Let fuzzy require an additional character. Tolerance: floor((wordlen - 1)/3))\n- Print logging category to stdout QT_LOGGING_RULES=\"*debug=false;driver.usb.debug=true\"\n\n### Plugins\n- **Term** Change terminal action order: Let \"Run w/o term\" be the last one\n- **VBox** Set default build switch for VirtualBox to OFF\n- **Files** Add fancy icons to mime dialog\n- **Py** Use ast to read metadata without loading the modules\n- **Py** Additional constraint: Metadata have to be string literals (for ast)\n- **Py** Additional constraint: Name modules according PEP8\n- **Py/WinSwitch** Add close win action\n- **Py/VBox** Add VirtualBox extension\n\n\n## v0.15.0 (2018-12-16)\n\nUsage graph in the settings (QtCharts (>=5.6))\n\n### Plugins\n- **QML**\n  - Frontend plugin requires ()5.9\n  - History search of the input now allows substring matching (Type and navigate)\n  - Store user input of every session\n- New Python extension: **Fortune**\n- New Python extension: **Window switcher**\n\n\n## v0.14.22 (2018-09-21)\n\n- Telemetry is now opt-in.\n- New themes\n- New Python extension: **Pidgin**\n\n\n## v0.14.21 (2018-06-08)\n\nBugfixes\n\n\n## v0.14.20 (2018-06-04)\n\nBugfixes\n\n\n## v0.14.19 (2018-05-15)\n\n- New Python extension: **Datetime**. (Time display and conversion. Supersedes the external extension)\n- New Python extension: **Bitfinex**. (Quickly access Bitfinex markets)\n- The file browse mode finally mimics bash completion behavior.\n\n\n## v0.14.18 (2018-03-23)\n\n- Hotfix release\n\n\n## v0.14.17 (2018-03-23)\n\n### Plugins\n- **applications**\n  - New option in applications extension: Use keywords for lookup\n  - New option in applications extension: Use generic name for lookup\n- New Python extension: **Arch Wiki**\n- The _kvstore_ extension was renamed to **Snippets** and got an improved config UI.\n\n\n## v0.14.16 (2018-03-09)\n\n### Plugins\n- **Gnome dictionary** (nikhilwanpal)\n- **Mathematica** (Asger Hautop Drewsen)\n- **TeX to unicode** (Asger Hautop Drewsen)\n- **IP address** (Benedict Dudel)\n- **Multi Translate** (David Britt)\n- **Emoji lookup** (David Britt)\n- **Kaomoji lookup** (David Britt)\n- **Timer**\n- **Binance**\n\n\n## v0.14.15 (2018-01-26)\n\n### Plugins\n- **python** - API PythonInterface/v0.2 (`configLocation()`, `dataLocation()`,`cacheLocation()`).\n- New:\n  - **CoinMarketCap**\n  - **Trash**\n  - **Pomodoro**\n  - **Epoch**\n  - **Packagist**\n\n\n## v0.14.14 (2017-12-06)\n\nNew Python extension: **npm** (Benedict Dudel)\n\n\n## v0.14.13 (2017-11-25)\n\n- Rich text support\n### Plugins\n- New:\n  - **AUR**\n  - **scrot**\n\n\n## v0.14.12 (2017-11-23)\n\n- New **CopyQ** Python extension (Ported from external extension)\n\n\n## v0.14.11 (2017-11-19)\n\n- New **locate** Python extension\n\n\n## v0.14.10 (2017-11-16)\n\nBugfixes\n\n\n## v0.14.9 (2017-11-16)\n\n- Better HiDPI support\n- New commandline option for debug output (-d)\n\n\n## v0.14.8 (2017-11-14)\n\n### Plugins\n- New\n  - **Gnote** (Ported from external extension)\n  - **Tomboy** (Ported from external extension)\n  - **Pacman**\n  - **Pass**\n  - **Kill**\n\n\n## v0.14.7 (2017-11-03)\n\nBugfixes\n\n\n## v0.14.6 (2017-10-31)\n\nNew **Wikipedia** Python extension\n\n\n## v0.14.5 (2017-10-30)\n\nBugfixes\n\n\n## v0.14.4 (2017-10-25)\n\nNew **base converter** Python extension\n\n\n## v0.14.3 (2017-10-21)\n\nNew **Google Translate** Python extension\n\n\n## v0.14.2 (2017-10-20)\n\nBugfixes\n\n\n## v0.14.1 (2017-10-19)\n\nBugfixes\n\n\n## v0.14.0 (2017-10-18)\n\n### Plugins\n- New\n  - **Python Embedding**\n  - **Python Eval**\n  - **Debugging**\n  - **Zeal**\n  - **GoldenDict**\n  - **Unit Converter**\n  - **Currency Converter**\n\n\n## v0.13.1 (2017-00-30)\n\nBugfixes\n\n\n## v0.13.0 (2017-09-28)\n\n- Modular frontends\n- QML frontend\n- Tree structure in file index and \"smart\" indexing\n- Shell like completion for terminal extension\n\n### Plugins\n- New: KeyValue\n- New: Hash Generator\n\n\n## v0.12.0 (2017-06-09)\n\n- Git-like ignore files\n- Dedicated dialog for websearch editing.\n\n\n## v0.11.3 (2017-05-28)\n\nBugfixes\n\n\n## v0.11.2 (2017-05-13)\n\n- <kbd>Home</kbd> and <kbd>End</kbd> now work for the results list holding <kbd>Ctrl</kbd>.\n\n\n## v0.11.1 (2017-04-16)\n\nBugfixes\n\n\n## v0.11.0 (2017-04-16)\n\n- Fine-grained control of the MIME types to be indexed.\n- Extensions can now have multiple triggers.\n- The sorting algorithm is now stable.\n- Browse mode now lists the results in lexicographical order with directories before the files.\n- The use of fallbacks has been disabled for triggered queries.\n- Further the websearch extension now contains an URL handler.\n- The qss property `selection-color` works as expected now.\n\n\n## v0.10.4 (2017-04-14)\n\nBugfixes\n\n\n## v0.10.3 (2017-04-02)\n\n- Terminal extension does no more show suggestions.\n- Any shell querying dropped.\n- Bugfixes\n\n\n## v0.10.2 (2017-03-24)\n\nBugfixes\n\n\n## v0.10.1 (2017-03-20)\n\nBugfixes\n\n\n## v0.10.0 (2017-03-19)\n\n- Tab completion.\n- Hovering over the input box the mouse wheel can now be used to browse the history.\n- Spotlight themes (Bright, Dark and Space).\n- The terminal extension now provides the shell aliases too.\n- File browse mode.\n- The application extension allows to ignore the `OnlyShowIn`/`NotShowIn` keys.\n- The bash script to clone the template extension is now deprecated and replaced by a Python script.\n\n### Plugins\n- New: MPRIS\n- New: Secure Shell\n\n\n## v0.9.5 (2017-03-13)\n\nBugfixes\n\n\n## v0.9.4 (2017-03-01)\n\nBugfixes\n\n\n## v0.9.3 (2017-02-05)\n\nBugfixes\n\n\n## v0.9.2 (2017-01-30)\n\nBugfixes\n\n\n## v0.9.1 (2017-01-23)\n\n- Chooseable terminal command of a list of installed terminals.\n- The app icon now uses a theme icon\n- _Open terminal here_ action.\n- Mostly fixes\n\n\n## v0.9.0 (2017-01-17)\n\n- New extension system architecture\n\n### Plugins\n- External extensions\n- Firefox\n- Improved VirtualBox extension\n\n\n## v0.8.11 (2016-09-29)\n\n- Plugin abstraction architecture\n- New plugin support: Native C++ QPlugins\n- New plugin support: Executables\n- Single click activation of items\n- Action modifiers\n- Multithreading\n- Core is now responsible for usage counting (obsoletes most serialization)\n- Asynchronous query \"live\" results\n- Tray icon\n- New option: Show tray icon\n- New extension: Debug\n\n\n## v0.8.10 (2016-06-23)\n\n- New extension: Virtual Box\n- Basic IPC\n- Graceful termination on SIGHUP\n\n\n## v0.8.9 (2016-05-12)\n\n- New option: Hide on close\n- New option: Display shadow\n- Graceful termination on SIGINT\n\n\n## v0.8.8 (2016-04-28)\n\n- Single instance\n\n\n## v0.8.7.3 (2016-04-27), v0.8.7.2 (2016-04-03), v0.8.7.1 (2016-03-31)\n\n- Hotfixes\n\n\n## v0.8.7 (2016-03-31)\n\n- New extension: Terminal\n- Project structure: Use libraries\n\n\n## v0.8.6 (2016-03-28)\n\n- Restructured settings widget\n\n\n## v0.8.5 (2016-03-25)\n\n- Custom icon lookup\n- Make main window movable\n- Show alternative actions o Tab\n- New option: Always on top\n- New option: Hide on focus out\n- New option: Display icons\n- New option: Display scrollbar\n- Dozens of new themes: Dark, Bright, SolarizedDark, SolarizedBright in several colors.\n\n\n## v0.8.4 (2016-03-15)\n\n- Show message box after ungraceful termination\n\n\n## v0.8.3 (2016-03-13)\n\n- Restructured settings widget\n\n\n## v0.8.2 (2016-03-09)\n\n- New option: Group separators for calculator\n- New themes: Arc\n- New theme: Numix\n\n\n## v0.8.1 (2016-03-04)\n\n- Minor tasks and improvements\n\n\n## v0.8.0 (2015-10-27)\n\n- New extension: System control\n\n\n## v0.7.7 (2015-10-16)\n\n- Bring settings window to front if it is already open.\n\n\n## v0.7.6 (2015-10-15)\n\n- Reorderable websearches\n- Allow exclusive queries by trigger\n\n\n## v0.7.5 (2015-10-12)\n\n- Graceful termination on SIGINT\n\n\n## v0.7.1 (2015-10-06), v0.7.2 (2015-10-07), v0.7.3 (2015-10-07), v0.7.4 (2015-10-08)\n\n- Tasks, Hotfixes, minor changes\n\n\n## v0.7.0 (2015-10-05)\n\n- Implement plugin architecture\n- Port the modules\n- Ignore file (\".albertignore\")\n- Actions\n- Threaded background indexing\n- New themes\n\n\n## v0.6.0 (2014-11-12)\n\n- Make action modifications configurable\n- Command history\n\n\n## v0.5.0 (2014-11-06)\n\n- Add configuration widget to configure the modules\n- Make user interface themable\n- Provide proof-of-concept themes\n- Make actions modifiable\n- Show action modifications in the list\n- Use CMake build system\n\n\n## v0.4.0 (2014-10-16)\n\n- Add settings widget\n- Implement indexing and search algorithms 'prefixmatch' and 'fuzzy'\n\n\n## v0.3.0 (2014-09-12)\n\n- Implement serialization of indexes\n- New module:\n  - Applications\n  - Bookmarks\n  - Calculator\n  - Web search\n\n\n## v0.2.0 (2014-09-08)\n\n- Abstract module architecture\n- New module: Files\n\n\n## v0.1.0 (2014-09-01)\n\n- Basic user interface\n- Hotkeymanager\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "# SPDX-FileCopyrightText: 2024 Manuel Schneider\n\ncmake_minimum_required(VERSION 3.26)  # Required by BUILD_LOCAL_INTERFACE generator expression\n\n# dont touch! set by metatool\nset(PROJECT_VERSION 34.0.10)\n\nproject(albert\n    VERSION ${PROJECT_VERSION}\n    DESCRIPTION \"Keyboard launcher\"\n    HOMEPAGE_URL \"https://albertlauncher.github.io\"\n    LANGUAGES CXX\n)\nif(APPLE)\n    enable_language(OBJCXX)  # used by pch\nendif()\nset(PROJECT_DISPLAY_NAME \"Albert\")\n\n# Local cmake modules (also CMake uses this in a pretty obscure way, e.g. for the plist)\nlist(APPEND CMAKE_MODULE_PATH \"${PROJECT_SOURCE_DIR}/cmake\")\n\n# Put the binaries in dedicated toplevel directories\nset(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)\nset(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)\n\n\n### Dependencies ##############################################################\n\n# QNotification\n\nadd_subdirectory(lib/QNotification EXCLUDE_FROM_ALL)\n\n# QHotkey\n\nset(QT_DEFAULT_MAJOR_VERSION 6)\nset(BUILD_SHARED_LIBS FALSE)\nset(QHOTKEY_INSTALL OFF CACHE BOOL \"\" FORCE)\nadd_subdirectory(lib/QHotkey EXCLUDE_FROM_ALL)\ntarget_compile_options(qhotkey\n    PRIVATE\n        -Wno-gnu-zero-variadic-macro-arguments\n        -Wno-unused-parameter\n        -Wno-shadow\n        -Wno-elaborated-enum-base\n        -Wno-deprecated-declarations\n        -Wno-nullability-completeness\n)\n\n#  QCoro\n\nfind_package(QCoro6 REQUIRED COMPONENTS Coro Core)\nqcoro_enable_coroutines()\n\n\n### Lib  ######################################################################\n\nset(TARGET_LIB lib${PROJECT_NAME})\n\nset(LIB_PUBLIC_HEADER\n    ${PROJECT_BINARY_DIR}/include/albert/config.h  # generated\n    ${PROJECT_BINARY_DIR}/include/albert/plugin.h  # generated\n    ${PROJECT_BINARY_DIR}/include/albert/export.h  # generated\n    include/albert/app.h\n    include/albert/asyncgeneratorqueryhandler.h\n    include/albert/extension.h\n    include/albert/extensionplugin.h\n    include/albert/fallbackhandler.h\n    include/albert/frontend.h\n    include/albert/generatorqueryhandler.h\n    include/albert/globalqueryhandler.h\n    include/albert/icon.h\n    include/albert/item.h\n    include/albert/plugindependency.h\n    include/albert/plugininstance.h\n    include/albert/pluginloader.h\n    include/albert/pluginmetadata.h\n    include/albert/pluginprovider.h\n    include/albert/query.h\n    include/albert/querycontext.h\n    include/albert/queryexecution.h\n    include/albert/queryhandler.h\n    include/albert/queryresults.h\n    include/albert/rankedqueryhandler.h\n    include/albert/rankitem.h\n    include/albert/telemetryprovider.h\n    include/albert/urlhandler.h\n    include/albert/usagescoring.h\n\n    # util\n    include/albert/backgroundexecutor.h\n    include/albert/download.h\n    include/albert/indexitem.h\n    include/albert/indexqueryhandler.h\n    include/albert/inputhistory.h\n    include/albert/logging.h\n    include/albert/matchconfig.h\n    include/albert/matcher.h\n    include/albert/messagebox.h\n    include/albert/networkutil.h\n    include/albert/notification.h\n    include/albert/oauth.h\n    include/albert/oauthconfigwidget.h\n    include/albert/ratelimiter.h\n    include/albert/standarditem.h\n    include/albert/systemutil.h\n    include/albert/timeit.h\n    include/albert/widgetsutil.h\n)\n\nset(LIB_SRC\n    src/app/application.cpp\n    src/app/application.h\n    src/app/messagehandler.cpp\n    src/app/messagehandler.h\n    src/app/pathmanager.cpp\n    src/app/pathmanager.h\n    src/app/pluginqueryhandler.cpp\n    src/app/pluginqueryhandler.h\n    src/app/qtpluginloader.cpp\n    src/app/qtpluginloader.h\n    src/app/qtpluginprovider.cpp\n    src/app/qtpluginprovider.h\n    src/app/report.cpp\n    src/app/report.h\n    src/app/rpcserver.cpp\n    src/app/rpcserver.h\n    src/app/systemtrayicon.cpp\n    src/app/systemtrayicon.h\n    src/app/telemetry.cpp\n    src/app/telemetry.h\n    src/app/telemetryprovider.cpp\n    src/app/triggersqueryhandler.cpp\n    src/app/triggersqueryhandler.h\n    src/app/urlhandler.cpp\n    src/common/extension.cpp\n    src/common/item.cpp\n    src/common/rankitem.cpp\n    src/config.h.in\n    src/frontend/frontend.cpp\n    src/frontend/session.cpp\n    src/frontend/session.h\n    src/icon/composedicon.cpp\n    src/icon/composedicon.h\n    src/icon/filetypeicon.cpp\n    src/icon/filetypeicon.h\n    src/icon/graphemeicon.cpp\n    src/icon/graphemeicon.h\n    src/icon/icon.cpp\n    src/icon/iconifiedicon.cpp\n    src/icon/iconifiedicon.h\n    src/icon/imageicon.cpp\n    src/icon/imageicon.h\n    src/icon/qiconicon.cpp\n    src/icon/qiconicon.h\n    src/icon/recticon.cpp\n    src/icon/recticon.h\n    src/icon/standardicon.cpp\n    src/icon/standardicon.h\n    src/icon/themeicon.cpp\n    src/icon/themeicon.h\n    src/platform/platform.h\n    src/platform/signalhandler.h\n    src/plugin.h.in\n    src/plugin/extensionregistry.cpp\n    src/plugin/extensionregistry.h\n    src/plugin/plugininstance.cpp\n    src/plugin/pluginloader.cpp\n    src/plugin/pluginprovider.cpp\n    src/plugin/pluginregistry.cpp\n    src/plugin/pluginregistry.h\n    src/plugin/topologicalsort.hpp\n    src/query/asyncgeneratorqueryhandler.cpp\n    src/query/fallbackhandler.cpp\n    src/query/generatorqueryhandler.cpp\n    src/query/globalquery.cpp\n    src/query/globalquery.h\n    src/query/globalqueryexecution.cpp\n    src/query/globalqueryexecution.h\n    src/query/globalqueryhandler.cpp\n    src/query/query.cpp\n    src/query/queryengine.cpp\n    src/query/queryengine.h\n    src/query/queryexecution.cpp\n    src/query/queryhandler.cpp\n    src/query/queryresults.cpp\n    src/query/rankedqueryhandler.cpp\n    src/query/usagedatabase.cpp\n    src/query/usagedatabase.h\n    src/query/usagescoring.cpp\n    src/settings/pluginswidget/pluginsmodel.cpp\n    src/settings/pluginswidget/pluginsmodel.h\n    src/settings/pluginswidget/pluginssortproxymodel.cpp\n    src/settings/pluginswidget/pluginssortproxymodel.h\n    src/settings/pluginswidget/pluginswidget.cpp\n    src/settings/pluginswidget/pluginswidget.h\n    src/settings/pluginswidget/pluginwidget.cpp\n    src/settings/pluginswidget/pluginwidget.h\n    src/settings/querywidget/fallbacksmodel.cpp\n    src/settings/querywidget/fallbacksmodel.h\n    src/settings/querywidget/queryhandlermodel.cpp\n    src/settings/querywidget/queryhandlermodel.h\n    src/settings/querywidget/querywidget.cpp\n    src/settings/querywidget/querywidget.h\n    src/settings/settingswindow.cpp\n    src/settings/settingswindow.h\n    src/util/color.h\n    src/util/download.cpp\n    src/util/extensionplugin.cpp\n    src/util/indexitem.cpp\n    src/util/indexqueryhandler.cpp\n    src/util/inputhistory.cpp\n    src/util/itemindex.cpp\n    src/util/itemindex.h\n    src/util/levenshtein.cpp\n    src/util/levenshtein.h\n    src/util/matcher.cpp\n    src/util/messagebox.cpp\n    src/util/networkutil.cpp\n    src/util/notification.cpp\n    src/util/oauth.cpp\n    src/util/oauthconfigwidget.cpp\n    src/util/qiconengineadapter.cpp\n    src/util/qiconengineadapter.h\n    src/util/querypreprocessing.cpp\n    src/util/querypreprocessing.h\n    src/util/ratelimiter.cpp\n    src/util/standarditem.cpp\n    src/util/systemutil.cpp\n)\n\nif (WIN32)\n    list(APPEND LIB_SRC\n        src/platform/win/signalhandler.cpp\n    )\nelseif (APPLE)\n    list(APPEND LIB_SRC\n        src/platform/unix/signalhandler.cpp\n        src/platform/mac/platform.mm\n    )\nelseif(UNIX)  # assume xdg\n    list(APPEND LIB_PUBLIC_HEADER\n        include/albert/desktopentryparser.h\n    )\n    list(APPEND LIB_SRC\n        src/platform/unix/signalhandler.cpp\n        src/platform/xdg/iconlookup.cpp\n        src/platform/xdg/iconlookup.h\n        src/platform/xdg/platform.cpp\n        src/platform/xdg/desktopentryparser.cpp\n        src/platform/xdg/themefileparser.cpp\n        src/platform/xdg/themefileparser.h\n    )\nendif()\n\nset(LIB_UI\n    src/settings/querywidget/querywidget.ui\n    src/settings/settingswindow.ui\n)\n\nadd_library(${TARGET_LIB} SHARED\n    ${LIB_PUBLIC_HEADER}\n    ${LIB_SRC}\n    ${LIB_UI}\n    resources/resources.qrc\n)\n\nadd_library(\"${PROJECT_NAME}::${TARGET_LIB}\" ALIAS ${TARGET_LIB})\n\ninclude(GNUInstallDirs)\ninclude(GenerateExportHeader)\n\ngenerate_export_header(${TARGET_LIB}\n    BASE_NAME ${PROJECT_NAME}\n    EXPORT_FILE_NAME \"${PROJECT_BINARY_DIR}/include/albert/export.h\"\n)\n\n# target_precompile_headers(${TARGET_LIB} PRIVATE ${LIB_PUBLIC_HEADER})\n\ntarget_include_directories(${TARGET_LIB}\n    PUBLIC\n        \"$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/include>\"\n        \"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>\"\n        \"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}>\"\n        \"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>\"\n        \"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}>\"\n    PRIVATE\n        \"${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}\"\n        \"${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}\"\n        src\n        src/app\n        src/common\n        src/frontend\n        src/handlers\n        src/platform\n        src/platform/mac\n        src/platform/unix\n        src/platform/xdg\n        src/plugin\n        src/query\n        src/settings\n        src/settings/pluginswidget\n        src/settings/querywidget\n        src/util\n)\n\nconfigure_file(\n    \"${PROJECT_SOURCE_DIR}/src/config.h.in\"\n    \"${PROJECT_BINARY_DIR}/include/albert/config.h\"\n    @ONLY\n)\n\nconfigure_file(\n    \"${PROJECT_SOURCE_DIR}/src/plugin.h.in\"\n    \"${PROJECT_BINARY_DIR}/include/albert/plugin.h\"\n    @ONLY\n)\n\nfind_package(Qt6 6.4 REQUIRED COMPONENTS # QString::operator\"\"_s\n    Core\n    Concurrent\n    Network\n    Sql\n    Svg\n    Widgets\n    LinguistTools\n)\n\ntarget_link_libraries(${TARGET_LIB}\n    PRIVATE\n        $<BUILD_LOCAL_INTERFACE:QHotkey::QHotkey>\n        $<BUILD_LOCAL_INTERFACE:QNotifications::QNotifications>\n        QCoro6::Coro\n        Qt6::Concurrent\n        Qt6::Core\n        Qt6::Network\n        Qt6::Sql\n        Qt6::Widgets\n)\n\nif(APPLE)\n    target_link_libraries(${TARGET_LIB} PRIVATE objc \"-framework Cocoa\")\n    target_compile_options(${TARGET_LIB} PRIVATE \"-fobjc-arc\")\n    set_source_files_properties(src/platform/mac/platform.mm PROPERTIES COMPILE_FLAGS \"-fobjc-arc\")\nelseif(UNIX)\n    if (DEFINED CMAKE_LIBRARY_ARCHITECTURE)\n        target_compile_definitions(${TARGET_LIB}\n            PRIVATE -DMULTIARCH_TUPLE=\"${CMAKE_LIBRARY_ARCHITECTURE}\"\n        )\n    endif()\nendif()\n\ntarget_compile_features(${TARGET_LIB} PUBLIC cxx_std_23)\n\nset_target_properties(${TARGET_LIB} PROPERTIES\n    #INSTALL_RPATH \"$ORIGIN\"\n    CXX_STANDARD 23\n    CXX_STANDARD_REQUIRED ON\n    CXX_EXTENSIONS OFF\n    CXX_VISIBILITY_PRESET hidden\n    FRAMEWORK TRUE\n    FRAMEWORK_VERSION A\n    MACOSX_FRAMEWORK_BUNDLE_VERSION \"${PROJECT_VERSION}\"\n    MACOSX_FRAMEWORK_IDENTIFIER \"org.albertlauncher.albert.interface\"\n    MACOSX_FRAMEWORK_SHORT_VERSION_STRING ${PROJECT_VERSION}\n    OUTPUT_NAME \"${PROJECT_NAME}\"\n    PUBLIC_HEADER \"${LIB_PUBLIC_HEADER}\"\n    SOVERSION \"${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}\"  # also mach-o compat version\n    # NO_SONAME true # do _not_ add. linkers other that ldd will make troubles using non relative paths\n    VERSION ${PROJECT_VERSION}\n    VISIBILITY_INLINES_HIDDEN 1\n    AUTOMOC ON\n    AUTOUIC ON\n    AUTORCC ON\n)\n\ntarget_compile_options(${TARGET_LIB}\n    PUBLIC\n        #-Werror\n        -Wall\n        -Wextra\n        -Wpedantic\n\n        # warnings that should be errors\n        -Werror=return-type  # silently buils but crashes at runtime\n        -Werror=float-conversion  # Implicit cast may loose precision (e.g. match scores)\n\n        -Wno-deprecated-enum-enum-conversion  # bitwise operation between different enumeration types used by drawText\n        -Wno-attributes  # mute visibility warnings\n\n        # -Wconversion\n        # -Weffc++\n        -Winline\n        -Wmissing-field-initializers\n        -Wshadow\n        -Wstrict-aliasing\n        -Winvalid-pch\n\n        # Use this from time to time\n        # -Weverything\n        # -Wdocumentation\n        # -Wno-c++98-compat\n        # -Wno-c++20-compat\n        # -Wno-c++98-compat-pedantic\n\n        # ??\n        # -Wl,-undefined\n)\n\n\n### Internationalization\n\nfile(GLOB TS_FILES i18n/*.ts)\n\n# Source files to be translated (separate because some files may be masked)\nfile(GLOB_RECURSE TRANSLATION_SOURCE_FILES\n    include/.h\n    src/*.h\n    src/*.ui\n    src/*.mm\n    src/*.cpp\n)\n\nif(Qt6_VERSION VERSION_GREATER_EQUAL \"6.7.0\")\n    message(STATUS \"Using new qt_add_translations\")\n    qt_add_translations(\n        ${TARGET_LIB}\n        TS_FILES ${TS_FILES}\n        SOURCES ${TRANSLATION_SOURCE_FILES}\n        LUPDATE_OPTIONS\n          #-no-obsolete\n          -locations none\n        IMMEDIATE_CALL\n    )\nelse()\n    message(STATUS \"Using old qt_add_translations\")\n    qt_add_translations(\n        ${TARGET_LIB}\n        TS_FILES ${TS_FILES}\n        SOURCES ${TRANSLATION_SOURCE_FILES}\n        LUPDATE_OPTIONS\n          # -no-obsolete\n          -locations none\n    )\nendif()\n\n# For convenience, QtCreator does not show the umbrella target\nadd_custom_target(global_lupdate DEPENDS update_translations)\n\n\n### Export/Install\n\ninclude(CMakePackageConfigHelpers)\n\nset(LIB_EXPORT_NAME \"${PROJECT_NAME}-targets\")\nset(LIB_TARGETS_FILE \"${LIB_EXPORT_NAME}.cmake\")\nset(LIB_CONFIG_FILE \"${PROJECT_NAME}-config.cmake\")\nset(LIB_VERSION_FILE \"${PROJECT_NAME}-config-version.cmake\")\nset(LIB_MACROS_FILE \"${PROJECT_NAME}-macros.cmake\")\nset(LIB_FIND_ALBERT_FILE \"FindAlbert.cmake\")\nset(LIB_CMAKE_MODULE_DIR \"${PROJECT_SOURCE_DIR}/cmake\")\nset(INSTALL_CONFIGDIR \"${CMAKE_INSTALL_LIBDIR}/cmake/Albert\")\n\n# Install the target\n# https://cmake.org/cmake/help/latest/command/install.html#targets\ninstall(\n    TARGETS ${TARGET_LIB}\n    EXPORT ${LIB_EXPORT_NAME}  # association for install(EXPORT …) and export(EXPORT …)\n    FRAMEWORK DESTINATION .\n    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}\n    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}\n    PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}\n)\n\n# Create a targets file in install tree\n# https://cmake.org/cmake/help/latest/command/install.html#export\n# By default the generated file will be called <export-name>.cmake\ninstall(\n    EXPORT ${LIB_EXPORT_NAME} # association from above\n    NAMESPACE ${PROJECT_NAME}::\n    DESTINATION ${INSTALL_CONFIGDIR}\n)\n\n# Create a targets file in build tree (matching install(EXPORT))\n# https://cmake.org/cmake/help/latest/command/export.html#exporting-targets-matching-install-export\n# Seems like a shorthand for export(TARGETS)\nexport(\n   EXPORT ${LIB_EXPORT_NAME} # association from above\n   NAMESPACE ${PROJECT_NAME}::\n   FILE \"${PROJECT_BINARY_DIR}/${LIB_TARGETS_FILE}\"\n)\n\ninclude(CMakePackageConfigHelpers)\n\n# Create version file in build tree\nwrite_basic_package_version_file(\n    \"${PROJECT_BINARY_DIR}/${LIB_VERSION_FILE}\"\n    VERSION \"${PROJECT_VERSION}\"\n    COMPATIBILITY AnyNewerVersion\n)\n\n# Create config file in build tree\nconfigure_package_config_file(\n   \"${LIB_CMAKE_MODULE_DIR}/${LIB_CONFIG_FILE}.in\"\n   \"${PROJECT_BINARY_DIR}/${LIB_CONFIG_FILE}\"\n   INSTALL_DESTINATION ${INSTALL_CONFIGDIR}\n)\n\n# Copy the macros to the build directory\nconfigure_file(\n    \"${LIB_CMAKE_MODULE_DIR}/${LIB_MACROS_FILE}\"\n    \"${PROJECT_BINARY_DIR}/${LIB_MACROS_FILE}\"\n    COPYONLY\n)\n\n# Copy the dummy FindAlbert to the build directory\nconfigure_file(\n    \"${LIB_CMAKE_MODULE_DIR}/${LIB_FIND_ALBERT_FILE}.in\"\n    \"${PROJECT_BINARY_DIR}/${LIB_FIND_ALBERT_FILE}\"\n    @ONLY\n)\n\ninstall(FILES\n    \"${PROJECT_BINARY_DIR}/${LIB_CONFIG_FILE}\"\n    \"${PROJECT_BINARY_DIR}/${LIB_VERSION_FILE}\"\n    \"${PROJECT_BINARY_DIR}/${LIB_MACROS_FILE}\"\n    DESTINATION ${INSTALL_CONFIGDIR}\n)\n\n\n### App  ######################################################################\n\nset(TARGET_APP ${CMAKE_PROJECT_NAME})\n\nadd_executable(${TARGET_APP} MACOSX_BUNDLE \"src/main.cpp\")\n\nif (APPLE)\n\n\n    set(ICON_NAME \"albert\")\n    set(ICON_PATH \"dist/macos/${ICON_NAME}.icns\")\n    target_sources(${TARGET_APP} PRIVATE ${ICON_PATH})\n\n    set_source_files_properties(${ICON_PATH} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)\n\n    set_target_properties(${TARGET_APP} PROPERTIES\n        BUNDLE True\n        OUTPUT_NAME \"${PROJECT_DISPLAY_NAME}${DEV_BUNDLE_SUFFIX}\"\n        MACOSX_BUNDLE TRUE\n        MACOSX_BUNDLE_BUNDLE_NAME \"${PROJECT_DISPLAY_NAME}\"  # \"${PROJECT_NAME}\"\n        MACOSX_BUNDLE_BUNDLE_VERSION \"${CMAKE_PROJECT_VERSION}\"\n        MACOSX_BUNDLE_COPYRIGHT \"Copyright (c) 2024 Manuel Schneider\"\n        MACOSX_BUNDLE_GUI_IDENTIFIER \"org.albertlauncher.albert${DEV_BUNDLE_ID_SUFFIX}\"\n        MACOSX_BUNDLE_ICON_FILE \"${ICON_NAME}\"\n        MACOSX_BUNDLE_INFO_STRING \"Albert keyboard launcher\"\n        MACOSX_BUNDLE_SHORT_VERSION_STRING \"${CMAKE_PROJECT_VERSION}\"\n    )\n\n    # Avoid regranting permissions all the time\n    option(BUILD_DEV_BUNDLE \"Build a separate dev bundle (saves some headaches on permissions)\" OFF)\n    if (BUILD_DEV_BUNDLE)\n        set(DEV_BUNDLE_SUFFIX \"Dev\")\n        set(DEV_BUNDLE_ID_SUFFIX \".dev\")\n\n        set(CODESIGN_IDENTITY \"Albert Dev Code Signing\")\n\n        add_custom_command(\n          TARGET ${TARGET_APP} POST_BUILD\n          COMMAND codesign --force --deep --sign \"${CODESIGN_IDENTITY}\" \"$<TARGET_BUNDLE_DIR:${TARGET_APP}>\"\n        )\n    endif()\n\nelseif (UNIX)\n\n    install(FILES dist/xdg/albert.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)\n    install(FILES resources/albert.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps)\n    install(FILES resources/albert-tray.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps)\n\nendif()\n\ntarget_link_libraries(${TARGET_APP} PRIVATE ${TARGET_LIB})\n\n\n    #INSTALL_RPATH_USE_LINK_PATH TRUE\n    #INSTALL_RPATH \"${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}\"\n    #BUILD_WITH_INSTALL_RPATH FALSE\n    #INSTALL_RPATH \"$ORIGIN/../${CMAKE_INSTALL_LIBDIR}/albert/\"  # Set the RPATH for the library lookup\n    #INSTALL_RPATH \"$ORIGIN/../Frameworks/albert/\"  # Set the RPATH for the library lookup\n\ninstall(\n    TARGETS ${TARGET_APP}\n    BUNDLE DESTINATION .\n    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}\n)\n\n\n### Plugins ###################################################################\n\n# Since the cmake module path of this project contains a dummy FindAlbert.cmake\n# CMake uses this one instead of searching systemwide.\nlist(APPEND CMAKE_MODULE_PATH \"${PROJECT_BINARY_DIR}\")\n\n# # on macOS include the macports lookup path\n# if (APPLE)\n#     list(APPEND CMAKE_PREFIX_PATH \"/opt/local\")\n#     message(STATUS \"CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}\")\n# endif()\n#2024: Drop ports use brew only\n# if (APPLE)\n#     list(APPEND CMAKE_PREFIX_PATH \"/opt/homebrew\")\n#     message(STATUS \"CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}\")\n# endif()\n\noption(BUILD_PLUGINS \"Build plugins\" ON)\nset(PLUGINS_DIR \"${CMAKE_CURRENT_LIST_DIR}/plugins\")\nif (BUILD_PLUGINS)\n    file(GLOB ENTRIES RELATIVE \"${PLUGINS_DIR}\" plugins/*)\n    list(FILTER ENTRIES EXCLUDE REGEX \"^\\\\..+\")\n    foreach(ENTRY ${ENTRIES})\n        if(IS_DIRECTORY \"${PLUGINS_DIR}/${ENTRY}\")\n            string(TOUPPER ${ENTRY} UPPER)\n            option(BUILD_PLUGIN_${UPPER} \"Build plugin ${ENTRY}\" ON)\n            if (BUILD_PLUGIN_${UPPER})\n                add_subdirectory(\"${PLUGINS_DIR}/${ENTRY}\")\n            endif()\n        endif()\n    endforeach()\nendif()\n\n\n### Test ######################################################################\n\noption(BUILD_TESTS \"Build tests (Requires QTest)\" OFF)\nif (BUILD_TESTS)\n    include(CTest)\n    find_package(Qt6 REQUIRED COMPONENTS Test)\n\n    get_target_property(SRC_TST ${TARGET_LIB} SOURCES)\n    get_target_property(INC_TST ${TARGET_LIB} INCLUDE_DIRECTORIES)\n    get_target_property(LIBS_TST ${TARGET_LIB} LINK_LIBRARIES)\n    get_target_property(CXX_STD_TST ${TARGET_LIB} CXX_STANDARD)\n\n    set(TARGET_TST ${CMAKE_PROJECT_NAME}_test)\n\n    add_executable(${TARGET_TST} ${SRC_TST} test/test.h test/test.cpp)\n\n    target_include_directories(${TARGET_TST} PRIVATE ${INC_TST} test)\n    target_link_libraries(${TARGET_TST} PRIVATE ${LIBS_TST} Qt6::Test)\n    set_target_properties(${TARGET_TST} PROPERTIES\n        CXX_STANDARD ${CXX_STD_TST}\n        AUTOMOC ON\n        AUTOUIC ON\n        AUTORCC ON\n    )\n    add_test(NAME ${TARGET_TST} COMMAND ${TARGET_TST})\n\nendif()\n\n\n### Packaging #################################################################\n\n\nif (APPLE)\n    set(CPACK_GENERATOR \"DragNDrop\")\n    set(CPACK_PACKAGE_CHECKSUM \"SHA256\")\n    set(CPACK_PACKAGE_DESCRIPTION_FILE \"${PROJECT_SOURCE_DIR}/README.md\")\n    set(CPACK_PACKAGE_EXECUTABLES \"${PROJECT_NAME}\" \"${PROJECT_DISPLAY_NAME} executable\")\n    set(CPACK_PACKAGE_FILE_NAME \"${PROJECT_DISPLAY_NAME}-v${PROJECT_VERSION}\")\n    set(CPACK_PACKAGE_NAME \"${PROJECT_DISPLAY_NAME}\")\n    #set(CPACK_PACKAGE_ICON \"${PROJECT_SOURCE_DIR}/${ICON_PATH}\")\n    set(CPACK_PACKAGE_INSTALL_DIRECTORY \"${CMAKE_PROJECT_NAME}\")\n    set(CPACK_PACKAGE_VENDOR \"manuelschneid3r\")\n    set(CPACK_RESOURCE_FILE_LICENSE \"${PROJECT_SOURCE_DIR}/LICENSE.md\")\n    set(CPACK_RESOURCE_FILE_README \"${PROJECT_SOURCE_DIR}/README.md\")\n    set(CPACK_RESOURCE_FILE_WELCOME \"${PROJECT_SOURCE_DIR}/README.md\")\n    set(CPACK_STRIP_FILES TRUE)\n    set(CPACK_MONOLITHIC_INSTALL TRUE)\n\n    configure_file(\"cmake/bundle-macos.cmake.in\" \"${PROJECT_BINARY_DIR}/bundle-macos.cmake\" @ONLY)\n    install(SCRIPT \"${PROJECT_BINARY_DIR}/bundle-macos.cmake\")\n\n    include(CPack)\nendif()\n"
  },
  {
    "path": "LICENSE.md",
    "content": "Copyright 2021 Manuel Schneider\nAlbert license v1.1\n\nPersonal use in source and binary forms, with or without modification, and\nredistribution in source and binary forms without modification \nare permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must not target Microsoft or Apple platforms, reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\nInformal note:\n\nThis is basically a BSD-2-Clause without the permission for redistribution of modifications and an explicit exclusion of Microsoft or Apple platforms.\nI am not a lawyer and it is hard to properly regulate redistribution of modifications so I simply do not permit it. \nHowever, if you just want to help with e.g. a public pull request, I will turn a blind eye.\n"
  },
  {
    "path": "README.md",
    "content": "# Albert launcher\n\n[![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/albertlauncher/albert)](https://github.com/albertlauncher/albert/tags)\n[![Docker CI Status](https://github.com/albertlauncher/albert/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/albertlauncher/albert/actions/workflows/ci.yml) \n[![OBS status](https://build.opensuse.org/projects/home:manuelschneid3r/packages/albert/badge.svg?type=percent)](https://build.opensuse.org/package/show/home:manuelschneid3r/albert)\n[![Telegram community chat](https://img.shields.io/badge/chat-telegram-0088cc.svg?style=flat)](https://telegram.me/albert_launcher_community)\n[![Discord](https://img.shields.io/badge/chat-discord-7289da.svg?style=flat)](https://discord.gg/t8G2EkvRZh)\n\nAlbert is a plugin-based, desktop-agnostic C++/Qt keyboard launcher.\n\nVisit the [*website*](https://albertlauncher.github.io/) for more information.\n\nEnjoy using Albert! If you find it helpful, feel free to [tip me](https://albertlauncher.github.io/donation/).\n"
  },
  {
    "path": "cmake/FindAlbert.cmake.in",
    "content": "# FindAlbert dummy package\n\n# This file serves the purpose to allow plugins to use find_package even if\n# they are part of the same project as Albert. This is useful for convenient\n# development such that the project itsself includes all upstream plugins\n# while still allowing the plugins to be build as separate projects.\n\ninclude(\"${CMAKE_CURRENT_LIST_DIR}/@LIB_MACROS_FILE@\")\n"
  },
  {
    "path": "cmake/MacOSXBundleInfo.plist.in",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\n    <key>CFBundleDisplayName</key>\n    <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>\n    <key>CFBundleExecutable</key> <!-- Do not put in one line or else cmakes heuristics fail -->\n    <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>\n    <key>CFBundleGetInfoString</key>\n    <string>${MACOSX_BUNDLE_INFO_STRING}</string>\n    <key>CFBundleIconFile</key>\n    <string>${MACOSX_BUNDLE_ICON_FILE}</string>\n    <key>CFBundleIdentifier</key>\n    <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>\n    <key>CFBundleLongVersionString</key>\n    <string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>\n    <key>CFBundleName</key>\n    <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>\n    <key>CFBundleShortVersionString</key>\n    <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>\n    <key>CFBundleVersion</key>\n    <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>\n    <key>NSHumanReadableCopyright</key>\n    <string>${MACOSX_BUNDLE_COPYRIGHT}</string>\n\n    <!--<key>CFBundleDevelopmentRegion</key><string>English</string>-->\n    <key>CFBundleAllowMixedLocalizations</key><true/>\n    <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>\n    <key>CFBundlePackageType</key><string>APPL</string>\n    <key>LSUIElement</key><true/>\n    <key>LSRequiresNativeExecution</key><true/>\n    <key>NSHighResolutionCapable</key><string>True</string>\n    <!--https://stackoverflow.com/questions/17893849/qml-causing-switch-to-discrete-graphics-->\n    <key>NSSupportsAutomaticGraphicsSwitching</key><true/>\n\n\n    <key>NSContactsUsageDescription</key><string>Contacts plugin access.</string>\n    <key>NSBluetoothAlwaysUsageDescription</key><string>Bluetooth plugin access.</string>\n    <key>NSDesktopFolderUsageDescription</key><string>Files plugin access.</string>\n    <key>NSDocumentsFolderUsageDescription</key><string>Files plugin access.</string>\n    <key>NSDownloadsFolderUsageDescription</key><string>Files plugin access.</string>\n    <key>NSNetworkVolumesUsageDescription</key><string>Files plugin access.</string>\n    <key>NSRemovableVolumesUsageDescription</key><string>Files plugin access.</string>\n    <key>NSLocationUsageDescription</key><string>Required to access WLAN names.</string>\n    <key>NSAppleEventsUsageDescription</key><string>Applescript support.</string>\n\n    <key>CFBundleURLTypes</key>\n    <array>\n        <dict>\n            <key>CFBundleURLSchemes</key>\n            <array><string>${PROJECT_NAME}</string></array>\n        </dict>\n    </array>\n\n\n</dict>\n</plist>\n"
  },
  {
    "path": "cmake/MacOSXFrameworkInfo.plist.in",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n        <!--<key>CFBundleDevelopmentRegion</key>\n        <string>English</string>-->\n\t<key>CFBundleExecutable</key>\n\t<string>${MACOSX_FRAMEWORK_NAME}</string>\n\t<key>CFBundleIconFile</key>\n\t<string>${MACOSX_FRAMEWORK_ICON_FILE}</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>${MACOSX_FRAMEWORK_IDENTIFIER}</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundlePackageType</key>\n\t<string>FMWK</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>${MACOSX_FRAMEWORK_BUNDLE_VERSION}</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>${MACOSX_FRAMEWORK_SHORT_VERSION_STRING}</string>\n\t<key>CSResourcesFileMapped</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "cmake/albert-config.cmake.in",
    "content": "@PACKAGE_INIT@\n\ninclude(\"${CMAKE_CURRENT_LIST_DIR}/@LIB_TARGETS_FILE@\")\ninclude(\"${CMAKE_CURRENT_LIST_DIR}/@LIB_MACROS_FILE@\")\n"
  },
  {
    "path": "cmake/albert-macros.cmake",
    "content": "# - Albert cmake macros\n#\n# Use albert_plugin() to add a plugin.\n#\n# albert_plugin(\n#      [SOURCES ...]\n#      [I18N_SOURCES ...]\n#      [INCLUDE ...]\n#      [LINK ...]\n#      [QT ...]\n# )\n#\n# Create a plugin target with the given name.\n#\n# Expects a metadata.json file in the source directory. Supported metadata keys are:\n#\n# |            Parameter |     Type     | Notes                                                                     |\n# |---------------------:|:------------:|---------------------------------------------------------------------------|\n# |                   id |   Reserved   | PROJECT_NAME added by CMake.                                              |\n# |              version |   Reserved   | PROJECT_VERSION added by CMake.                                           |\n# |         translations |   Reserved   | Added by CMake.                                                           |\n# |                 name | local string | Human readable name.                                                      |\n# |          description | local string | Brief, imperative description, e.g. \"Open files\".                         |\n# |              license |    string    | SPDX license identifier. E.g. BSD-2-Clause, MIT, LGPL-3.0-only, …         |\n# |                  url |    string    | Browsable online source code                                              |\n# |           readme_url |    string    | Online README                                                             |\n# |              authors | string list  | List of copyright holders. Preferably using mentionable GitHub usernames. |\n# |          maintainers | string list  | List of maintainers. Preferably using mentionable GitHub usernames.       |\n# | runtime_dependencies | string list  | Default: `[]`. Required libraries.                                        |\n# |  binary_dependencies | string list  | Default: `[]`. Required executables.                                      |\n# |  plugin_dependencies | string list  | Default: `[]`. Required plugins.                                          |\n# |              credits | string list  | Default: `[]`. Attributions, mentions, third party library licenses, …    |\n# |             loadtype |    string    | Default: `user`. `frontend` or `user`.                                    |\n#\n# Note: Local string types can be used to localize the metadata. (e.g. \"name[de]\": \"Anwendungen\")\n#\n# Translations files in a directory named 'i18n' are added automatically.\n# The filenames must have the pattern <plugin_id>_<language_code>.ts.\n# <plugin_id>.ts is the native plurals file.\n#\n# SOURCES\n#     List source files.\n#     Supports (nonrecursive) globbing patterns.\n#     If unspecified the default is a recursive globbing pattern for:\n#     include/*.h, src/*.h, src/*.cpp, src/*.mm, src/*.ui and <plugin_id>.qrc\n#\n# I18N_SOURCES\n#     List translation source files.\n#     Supports (nonrecursive) globbing patterns.\n#     Use if some of the source files are disabled on certain platforms.\n#     If unspecified defaults to the target sources.\n#\n# INCLUDE\n#     List directories to include.\n#     Shorthand for CMake target_include_directories(plugin_target …\n#\n# LINK\n#     List of libraries to link.\n#     Shorthand for CMake target_link_libraries(plugin_target …\n#\n# QT\n#     List of Qt components to link.\n#     Finds and links the given Qt components.\n#\n\ncmake_minimum_required(VERSION 3.19)  # string(JSON…\n\nmacro(albert_plugin_link_qt)\n    set(options REQUIRED)\n    set(one_value_args VERSION)\n    set(multi_value_args QT)\n    cmake_parse_arguments(ARG_QT \"${options}\" \"${one_value_args}\" \"${multi_value_args}\" ${ARGV})\n\n    set(_qt_find_args COMPONENTS ${ARG_QT_UNPARSED_ARGUMENTS})\n    if(ARG_QT_REQUIRED)\n        list(APPEND _qt_find_args REQUIRED)\n    endif()\n    if(ARG_QT_VERSION)\n        list(PREPEND _qt_find_args ${ARG_QT_VERSION})\n    endif()\n\n    find_package(Qt6 ${_qt_find_args})\n\n    foreach(MODULE IN LISTS ARG_QT_UNPARSED_ARGUMENTS)\n        target_link_libraries(${PROJECT_NAME} PRIVATE \"Qt6::${MODULE}\")\n    endforeach()\nendmacro()\n\n#\n# albert_plugin_add_dbus_interface(<xml_interface_spec>)\n#\n# Adds a DBus interface to the plugin from an XML interface specification.\n#\n# The the DBUS_SRCS are generated in the PROJECT_BINARY_DIR.\n#\nmacro(albert_plugin_dbus_interface)\n    set(options)\n    set(one_value_args XML INCLUDE)\n    set(multi_value_args)\n    cmake_parse_arguments(ARG_DBUS \"${options}\" \"${one_value_args}\" \"${multi_value_args}\" ${ARGV})\n\n    get_filename_component(ARG_DBUS_XML_BASENAME ${ARG_DBUS_XML} NAME_WE)\n\n    set_source_files_properties(${ARG_DBUS_XML} PROPERTIES NO_NAMESPACE ON)\n\n    if(ARG_DBUS_INCLUDE)\n        set_source_files_properties(${ARG_DBUS_XML} PROPERTIES INCLUDE ${ARG_DBUS_INCLUDE})\n    endif()\n\n    qt_add_dbus_interface(DBUS_SRCS ${ARG_DBUS_XML} ${ARG_DBUS_XML_BASENAME})\n\n    target_sources(${PROJECT_NAME} PRIVATE\n        ${ARG_DBUS_XML}\n        ${DBUS_SRCS}\n    )\nendmacro()\n\nmacro(albert_plugin_sources)\n    file(GLOB SOURCES ${ARGV})\n    target_sources(${PROJECT_NAME} PRIVATE ${SOURCES})\nendmacro()\n\nmacro(albert_plugin_link)\n    target_link_libraries(${PROJECT_NAME} ${ARGV})\nendmacro()\n\nmacro(albert_plugin_include_directories)\n    target_include_directories(${PROJECT_NAME} ${ARGV})\nendmacro()\n\nmacro(albert_plugin_compile_options)\n    target_compile_options(${PROJECT_NAME} ${ARGV})\nendmacro()\n\nmacro(albert_plugin_i18n)\n    find_package(Qt6 6.0 REQUIRED COMPONENTS\n        Core  # required by LinguistTools\n        LinguistTools\n    )\n\n    # TODO ubuntu 26.04\n    # qt_add_translations improves greatly with 6.7/6.8\n    # for now plural files are full translation files with empty singulars\n    # sure one could build custom targets and such but since this already\n    # implemented in recent qt versions it's not worth the effort\n\n    file(GLOB TS_FILES \"i18n/${PROJECT_NAME}*.ts\")\n\n    if (NOT ARG_I18N_SOURCES)\n        get_target_property(ARG_I18N_SOURCES ${PROJECT_NAME} SOURCES)\n    else()\n        file(GLOB ARG_I18N_SOURCES ${ARG_I18N_SOURCES})\n    endif()\n\n    if (TS_FILES)\n        qt_add_translations(\n            ${PROJECT_NAME}\n            TS_FILES ${TS_FILES}\n            SOURCES ${ARG_I18N_SOURCES}\n            LUPDATE_OPTIONS\n              # -no-obsolete\n              -locations none\n            # QM_FILES_OUTPUT_VARIABLE QM_FILES\n        )\n    endif()\n\n    # install(FILES ${QM_FILES} DESTINATION \"${CMAKE_INSTALL_DATADIR}/albert/i18n\")\n\n    # Prepare a list of translations for the metadata\n    foreach(TS_FILE ${TS_FILES})\n        get_filename_component(BASENAME ${TS_FILE} NAME_WLE)\n\n        if (NOT ${BASENAME} STREQUAL ${PROJECT_NAME})\n\n            execute_process(\n                COMMAND xmllint --xpath \"count(//translation[not(@type='unfinished')])\" ${TS_FILE}\n                OUTPUT_VARIABLE FINISHED_COUNT\n                COMMAND_ERROR_IS_FATAL ANY\n                OUTPUT_STRIP_TRAILING_WHITESPACE\n            )\n\n            execute_process(\n                COMMAND xmllint --xpath \"count(//translation)\" ${TS_FILE}\n                OUTPUT_VARIABLE TOTAL_COUNT\n                COMMAND_ERROR_IS_FATAL ANY\n                OUTPUT_STRIP_TRAILING_WHITESPACE\n            )\n\n            string(REPLACE \"${PROJECT_NAME}_\" \"\" LANGUAGE_CODE ${BASENAME})\n\n            list(APPEND TRANSLATIONS \"${LANGUAGE_CODE} (${FINISHED_COUNT}/${TOTAL_COUNT})\")\n\n        endif()\n    endforeach()\nendmacro()\n\nmacro(albert_plugin_generate_metadata)\n    if (NOT DEFINED PROJECT_VERSION)\n        message(FATAL_ERROR \"Plugin version is undefined\")\n    endif()\n\n    file(READ \"${PROJECT_SOURCE_DIR}/metadata.json\" MD)\n\n    string(JSON MD SET ${MD} \"id\" \"\\\"${PROJECT_NAME}\\\"\")\n\n    string(JSON MD SET ${MD} \"version\" \"\\\"${PROJECT_VERSION}\\\"\")\n\n    if (TRANSLATIONS)\n        list(JOIN TRANSLATIONS \"\\\", \\\"\" TRANSLATIONS_CSV)\n        string(JSON MD SET ${MD} \"translations\" \"[\\\"${TRANSLATIONS_CSV}\\\"]\")\n    endif()\n\n    # get_target_property(LIB_DEPENDENCIES ${PROJECT_NAME} LINK_LIBRARIES)\n    # if (DEFINED LIB_DEPENDENCIES)\n    #     list(JOIN LIB_DEPENDENCIES \"\\\", \\\"\" LIB_DEPENDENCIES_CSV)\n    #     string(JSON MD SET ${MD} \"lib_deps\" \"[\\\"${LIB_DEPENDENCIES_CSV}\\\"]\")\n    # endif()\n\n    file(WRITE \"${CMAKE_CURRENT_BINARY_DIR}/metadata.json\" \"${MD}\")\n\n    target_sources(${PROJECT_NAME} PRIVATE\n        \"${PROJECT_SOURCE_DIR}/metadata.json\"\n        \"${PROJECT_BINARY_DIR}/metadata.json\"\n    )\nendmacro()\n\n# This macro creates a plugin target with the given name.\nmacro(albert_plugin)\n    set(arg_bool )\n    set(arg_vals )\n    set(arg_list SOURCES I18N_SOURCES INCLUDE LINK QT)\n    cmake_parse_arguments(ARG \"${arg_bool}\" \"${arg_vals}\" \"${arg_list}\" ${ARGV})\n\n    if (NOT DEFINED ARG_SOURCES)\n        file(GLOB_RECURSE ARG_SOURCES\n            src/*.h\n            src/*.hpp\n            src/*.cpp\n            src/*.mm\n            src/*.ui\n            include/*.h\n            ${PROJECT_NAME}.qrc\n        )\n    else()\n        file(GLOB ARG_SOURCES ${ARG_SOURCES})\n    endif()\n\n    add_library(${PROJECT_NAME} SHARED ${ARG_SOURCES})\n    add_library(albert::${PROJECT_NAME} ALIAS ${PROJECT_NAME})\n\n    set_target_properties(${PROJECT_NAME} PROPERTIES\n        CXX_VISIBILITY_PRESET hidden\n        VISIBILITY_INLINES_HIDDEN ON\n        PREFIX \"\"  # no libfoo\n        AUTOMOC ON\n        AUTOUIC ON\n        AUTORCC ON\n    )\n\n    target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_CAST_FROM_ASCII)\n\n    # Append. There are defaults.\n    set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY AUTOMOC_MACRO_NAMES \"ALBERT_PLUGIN\")\n\n    target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_BINARY_DIR})\n\n    albert_plugin_sources(README.md)  # if readme exists add to sources\n\n    if (DEFINED ARG_INCLUDE)\n        albert_plugin_include_directories(${ARG_INCLUDE})\n    endif()\n\n    if (DEFINED ARG_LINK)\n        albert_plugin_link(${ARG_LINK})\n    endif()\n    albert_plugin_link(PRIVATE albert::libalbert)\n\n    if (DEFINED ARG_QT)\n        albert_plugin_link_qt(${ARG_QT})\n    endif()\n\n    install(\n        TARGETS ${PROJECT_NAME}\n        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/albert\n    )\n\n    # after add_target, uses SOURCES\n    # before metadata, defines TRANSLATIONS\n    albert_plugin_i18n()\n    albert_plugin_generate_metadata()\nendmacro()\n"
  },
  {
    "path": "cmake/bundle-macos.cmake.in",
    "content": "set(PROJECT_NAME \"@PROJECT_NAME@\")  # bundle path\nset(CMAKE_PREFIX_PATH \"@CMAKE_PREFIX_PATH@\")  # library lookup\nset(CMAKE_INSTALL_LIBDIR \"@CMAKE_INSTALL_LIBDIR@\")  # library paths\nset(CMAKE_INSTALL_DATADIR \"@CMAKE_INSTALL_DATADIR@\")\nset(CMAKE_SOURCE_DIR \"@CMAKE_SOURCE_DIR@\")  # qml lib lookup dir for macdeployqt\nset(CMAKE_LIBRARY_OUTPUT_DIRECTORY \"@CMAKE_LIBRARY_OUTPUT_DIRECTORY@\")\nset(BUNDLE_PATH \"${CMAKE_INSTALL_PREFIX}/${PROJECT_NAME}.app\")\n\n\n\n# PRINT CMAKE ENV\n\nmessage(STATUS \"--- ENV ---\")\nget_cmake_property(_variableNames VARIABLES)\nlist (SORT _variableNames)\nforeach (_variableName ${_variableNames})\n    message(STATUS \"${_variableName}=${${_variableName}}\")\nendforeach()\nmessage(STATUS \"--- ENV END ---\")\n\n\n\n\n# MOVE FILES INTO BUNDLE\n\n\n## Library\nfile(MAKE_DIRECTORY \"${BUNDLE_PATH}/Contents/Frameworks\")\nfile(RENAME\n    \"${CMAKE_INSTALL_PREFIX}/albert.framework\"\n    \"${BUNDLE_PATH}/Contents/Frameworks/albert.framework\"\n)\n\n## Plugins\nfile(MAKE_DIRECTORY \"${BUNDLE_PATH}/Contents/PlugIns\")\nfile(RENAME\n    ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/albert\n    ${BUNDLE_PATH}/Contents/PlugIns/albert\n)\n\n## Resources\nset(RES_SRC ${CMAKE_INSTALL_PREFIX}/share/albert)\nfile(GLOB children RELATIVE ${RES_SRC} ${RES_SRC}/*)\nforeach(child ${children})\n    file(RENAME\n        ${RES_SRC}/${child}\n        ${BUNDLE_PATH}/Contents/Resources/${child}\n    )\nendforeach()\n\nfile(REMOVE_RECURSE \"${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}\")\nfile(REMOVE_RECURSE \"${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}\")\n\n\n\n# ADD RPATHS TO EXECUTABLE\n\nexecute_process(\n    COMMAND install_name_tool\n        -add_rpath \"@executable_path/../Frameworks/\"  # in bundle\n        -add_rpath \"/opt/local/lib/\"                  # RPATH macports\n        -add_rpath \"/opt/homebrew/lib/\"               # RPATH homebrew arm\n        -add_rpath \"/usr/local/lib/\"                  # RPATH homebrew intel\n        \"${BUNDLE_PATH}/Contents/MacOS/${PROJECT_NAME}\"\n    COMMAND_ECHO STDOUT\n    COMMAND_ERROR_IS_FATAL ANY\n)\n\n\n\n# DEPLOY (APPROACH #1, use qt_deploy_runtime_dependencies)\n\n# set(QT_DEPLOY_SUPPORT \"@QT_DEPLOY_SUPPORT@\")\n# message(STATUS \"QT_DEPLOY_SUPPORT ${QT_DEPLOY_SUPPORT}\")\n# include(\"${QT_DEPLOY_SUPPORT}\")\n\n# ## Get a list of plugin files\n# # file(GLOB PLUGINS \"${BUNDLE_PATH}/Contents/MacOS/plugins/*\")\n# # message(STATUS PLUGINS: ${PLUGINS})\n\n# qt_deploy_runtime_dependencies(\n#     EXECUTABLE ${BUNDLE_PATH}\n#     # ADDITIONAL_EXECUTABLES ${PLUGINS}\n#     GENERATE_QT_CONF\n#     VERBOSE\n# )\n\n\n\n# DEPLOY (APPROACH #2, call macdeployqt)\n\n# find_program(MACDEPLOYQT NAMES macdeployqt macdeployqt REQUIRED)\n# get_filename_component(QT_BIN_DIR \"${MACDEPLOYQT}\" DIRECTORY)\n# get_filename_component(QT_DIR \"${QT_BIN_DIR}\" DIRECTORY)\n# set(QT_LIB_DIR \"${QT_DIR}/${CMAKE_INSTALL_LIBDIR}\")\n\n# # Build list of -executable= parameters for plugins consumed by macdeployqt\n# FILE(GLOB PLUGINS \"${BUNDLE_PATH}/Contents/PlugIns/albert/*\")\n# foreach(PLUGIN ${PLUGINS})\n#     list(APPEND PLUGINS_EXEC_PARAMS \"-executable=${PLUGIN}\")\n# endforeach()\n\n# message(STATUS \"MACDEPLOYQT ${MACDEPLOYQT}\")\n# message(STATUS \"QT_DIR ${QT_DIR}\")\n# message(STATUS \"QT_BIN_DIR ${QT_BIN_DIR}\")\n# message(STATUS \"QT_LIB_DIR ${QT_LIB_DIR}\")\n\n# execute_process(\n#     COMMAND \"${MACDEPLOYQT}\"\n#     \"${BUNDLE_PATH}\"\n#     \"-executable=${BUNDLE_PATH}/Contents/Frameworks/albert.framework/Versions/A/albert\"\n#     ${PLUGINS_EXEC_PARAMS}\n#     #\"-libpath=${CMAKE_INSTALL_PREFIX}\"  # albert lib\n#     #\"-libpath=${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}\"  # albert lib\n#     #\"-libpath=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}\"  # albert lib\n#     \"-libpath=/opt/local/lib\"  # macports dependencies\n#     #\"-qmldir=${CMAKE_SOURCE_DIR}/plugins/qmlboxmodel/resources/qml\"\n#     \"-appstore-compliant\"\n#     \"-verbose=1\"\n#     #\"-dmg\"\n#     COMMAND_ECHO STDOUT\n#     COMMAND_ERROR_IS_FATAL ANY\n# )\n\n\n\n# CODE SIGN\n\n# execute_process(\n#     COMMAND xattr -d com.apple.quarantine \"${BUNDLE_PATH}\"\n#     COMMAND_ECHO STDOUT\n#     COMMAND_ERROR_IS_FATAL ANY\n# )\nexecute_process(\n    COMMAND codesign --force --deep --sign - \"${BUNDLE_PATH}\"\n    COMMAND_ECHO STDOUT\n    COMMAND_ERROR_IS_FATAL ANY\n)\n\n\n\n## CLEANUP\n\n# include(BundleUtilities)\n# verify_app(\"${BUNDLE_PATH}\")\n\n\n# Older, possibly useful stuff\n\n## Build library lookup paths from CMAKE_PREFIX_PATH\n#foreach(PATH ${CMAKE_PREFIX_PATH})\n#    list(APPEND DIRS \"${PATH}/lib\")\n#endforeach()\n#list(APPEND DIRS \"${CMAKE_LIBRARY_OUTPUT_DIRECTORY}\") # albert lib\n#list(APPEND DIRS \"/Library/Developer/CommandLineTools/Library/Frameworks/\") # system python\n\n\n\n\n#include(BundleUtilities)\n#fixup_bundle(\"${BUNDLE_PATH}\" \"${LIBS}\" \"${DIRS}\")\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "dist/cliff.toml",
    "content": "# https://git-cliff.org/docs/configuration\n\n[changelog]\ntrim = false\nconventional_commits = true\nfilter_unconventional = false\nrequire_conventional = false\n\n[git]\ncommit_parsers = [\n    { message = \"^feat\", group = \"<!-- 0 -->Features\" },\n    { message = \"^api\", group = \"<!-- 1 -->API\" },\n    { message = \"^perf\", group = \"<!-- 2 -->Performance\" },\n    { message = \"^fix\", group = \"<!-- 3 -->Fixes\" },\n    { message = \"^doc\", group = \"<!-- 4 -->Documentation\" },\n    { message = \"^style\", group = \"<!-- 5 -->Styling\" },\n    { message = \"^test\", group = \"<!-- 6 -->Testing\" },\n    { message = \"^refactor\", skip = true },\n    { message = \"^chore\\\\(release\\\\)\", skip = true },\n    { message = \"^chore\\\\(plugins\\\\)\", skip = true },\n    { message = \"^chore\\\\(deps.*\\\\)\", skip = true },\n    { message = \"^chore\\\\(pr\\\\)\", skip = true },\n    { message = \"^chore\\\\(pull\\\\)\", skip = true },\n    { message = \"^chore|^ci\", group = \"<!-- 7 -->Miscellaneous Tasks\" },\n    { message = \".*\", group = \"<!-- 8 -->Other\" },\n]\n\n"
  },
  {
    "path": "dist/cliff_template_minimal.md",
    "content": "{% if extra.plugin_contexts -%}\n{% set_global commits = commits -%}\n{% for context in extra.plugin_contexts -%}\n{% set_global commits = commits | concat(with=context.commits) -%}\n{% endfor -%}\n\n{% for group, commits in commits | group_by(attribute=\"group\") %}\n### {{ group | striptags | trim | upper_first }}\n\n{% set_global core_commits = [] -%}\n{% for commit in commits -%}\n{% if not commit.extra.plugin_name -%}\n{% set_global core_commits = core_commits | concat(with=commit) -%}\n{% endif -%}\n{% endfor -%}\n\n{% set_global plugin_commits = [] -%}\n{% for commit in commits -%}\n{% if commit.extra.plugin_name -%}\n{% set_global plugin_commits = plugin_commits | concat(with=commit) -%}\n{% endif -%}\n{% endfor -%}\n\n{% if core_commits -%}\n#### Core\n\n{% for commit in core_commits -%}\n- {% if commit.breaking %}[**BREAKING**] {% endif -%}\n  {% if commit.scope %}_{{ commit.scope }}_ · {% endif -%}\n  {{ commit.message | upper_first }}\n{% endfor -%}\n{% endif -%}\n\n{% if plugin_commits %}\n#### Plugins\n\n{% for name, commits in plugin_commits | group_by(attribute=\"extra.plugin_name\") -%}\n{% if commits | length > 1 -%}\n- **{{ name }}**\n{%- for commit in commits %}\n  - {% if commit.breaking %}[**BREAKING**] · {% endif %}\n    {%- if commit.scope %}_{{ commit.scope }}_ · {% endif %}\n    {%- if commit.message %}{{ commit.message | upper_first }}{% endif -%}\n{% endfor %}\n{% else -%}\n{% for commit in commits -%}\n- **{{ name }}**\n  {%- if commit.breaking %} · [**BREAKING**]{% endif %}\n  {%- if commit.scope %} · _{{ commit.scope }}_{% endif %}\n  {%- if commit.message %} · {{ commit.message | upper_first }}{% endif %}\n{% endfor -%}\n{% endif -%}\n{% endfor -%}\n{% endif -%}\n\n{% endfor -%}\n{% endif -%}\n"
  },
  {
    "path": "dist/cliff_template_rich.md",
    "content": "{% if extra.plugin_contexts -%}\n{% set_global commits = commits -%}\n{% for context in extra.plugin_contexts -%}\n{% set_global commits = commits | concat(with=context.commits) -%}\n{% endfor -%}\n\n{% for group, commits in commits | group_by(attribute=\"group\") %}\n### {{ group | striptags | trim | upper_first }}\n\n{% set_global core_commits = [] -%}\n{% for commit in commits -%}\n{% if not commit.extra.plugin_name -%}\n{% set_global core_commits = core_commits | concat(with=commit) -%}\n{% endif -%}\n{% endfor -%}\n\n{% set_global plugin_commits = [] -%}\n{% for commit in commits -%}\n{% if commit.extra.plugin_name -%}\n{% set_global plugin_commits = plugin_commits | concat(with=commit) -%}\n{% endif -%}\n{% endfor -%}\n\n{% if core_commits -%}\n#### Core\n\n{% for commit in core_commits -%}\n- {% if commit.breaking %}[**BREAKING**] {% endif -%}\n  {% if commit.scope %}_{{ commit.scope }}_ · {% endif -%}\n  [{{ commit.message | upper_first }}](https://github.com/albertlauncher/albert/commit/{{ commit.id}})\n{% endfor -%}\n{% endif -%}\n\n{% if plugin_commits %}\n#### Plugins\n\n{% for name, commits in plugin_commits | group_by(attribute=\"extra.plugin_name\") -%}\n{% if commits | length > 1 -%}\n- **{{ name }}**\n{%- for commit in commits %}\n  - {% if commit.breaking %}[**BREAKING**] · {% endif %}\n    {%- if commit.scope %}_{{ commit.scope }}_ · {% endif %}\n    {%- if commit.message %}[{{ commit.message | upper_first }}](https://github.com/albertlauncher/{{ commit.extra.repo_name }}/commit/{{ commit.id}}){% endif -%}\n{% endfor %}\n{% else -%}\n{% for commit in commits -%}\n- **{{ name }}**\n  {%- if commit.breaking %} · [**BREAKING**]{% endif %}\n  {%- if commit.scope %} · _{{ commit.scope }}_{% endif %}\n  {%- if commit.message %} · [{{ commit.message | upper_first }}](https://github.com/albertlauncher/{{ commit.extra.repo_name }}/commit/{{ commit.id}}){% endif %}\n{% endfor -%}\n{% endif -%}\n{% endfor -%}\n{% endif -%}\n\n{% endfor -%}\n{% endif -%}\n"
  },
  {
    "path": "dist/flatpak/README.md",
    "content": "This is a disfunctional prototype.\nFlatpak does not yet provide permissions to run albert in way that makes sense for a launcher.\n\nqalculate needs flathub shared modules\n\n```\ngit clone https://github.com/flathub/shared-modules.git\n```\n"
  },
  {
    "path": "dist/flatpak/org.albertlauncher.Albert.yml",
    "content": "app-id: org.albertlauncher.Albert\nruntime: org.kde.Platform\nruntime-version: '6.4'\nsdk: org.kde.Sdk\ncommand: bootstrap-albert.sh\nrename-desktop-file: albert.desktop\nrename-icon: albert\ncopy-icon: true\nfinish-args:\n  - --device=dri\n  - --filesystem=host\n  #- --filesystem=xdg-cache\n  #- --filesystem=xdg-config\n  #- --filesystem=xdg-documents\n  - --share=ipc\n  - --share=network\n  - --socket=session-bus\n  - --socket=wayland\n  - --socket=x11\nmodules:\n  - name: Albert\n    buildsystem: cmake-ninja\n    builddir: true\n    prefix: /usr\n    post-install:\n      - echo 'albert -d -p /app/lib/albert \"$@\"' > /app/bin/bootstrap-albert.sh\n        chmod +x /app/bin/bootstrap-albert.sh\n    config-opts:\n      - -DCMAKE_BUILD_TYPE=RelWithDebInfo\n      - -DBUILD_CALCULATOR_QALCULATE=OFF\n    sources:\n      - type: git\n        url: https://github.com/albertlauncher/albert\n        branch: master\n    modules:\n      - name: pybind11\n        buildsystem: cmake-ninja\n        builddir: true\n        sources:\n          - type: git\n            url: https://github.com/pybind/pybind11\n            branch: v2.10.4\n        modules:\n          - name: cpython\n            build-options:\n              cflags: -fPIC\n              ldflags: -fpic\n            sources:\n              - type: archive\n                url: https://www.python.org/ftp/python/3.10.10/Python-3.10.10.tar.xz\n                md5: 7bf85df71bbe7f95e5370b983e6ae684\n      - name: libmuparser\n        buildsystem: cmake-ninja\n        sources:\n        - type: git\n          url: https://github.com/beltoforion/muparser\n          branch: v2.3.4\n     # - name: libqalculate\n     #   build-options:\n     #     arch:\n     #       aarch64:\n     #         cxxflags: -fsigned-char\n     #   config-opts:\n     #     - --disable-static\n     #     - --enable-maintainer-mode\n     #     - --enable-compiled-definitions\n     #   sources:\n     #     - type: archive\n     #       url: https://github.com/Qalculate/libqalculate/releases/download/v4.6.0/libqalculate-4.6.0.tar.gz\n     #       sha256: 07b11dba19a80e8c5413a6bb25c81fb30cc0935b54fa0c9090c4ff8661985e08\n\n"
  },
  {
    "path": "dist/macos/albert_icns.sh",
    "content": "#!/bin/sh\n\ninput_path=../../resources/albert.svg\noutput_path=output.iconset\nmkdir -p \"$output_path\"\n\n# the convert command comes from imagemagick\nfor size in 32 64 128 256; do\n  double=\"$(($size * 2))\"\n  convert -background none -density $size -resize x$size ${input_path} $output_path/icon_${size}x${size}.png\n  convert -background none -density $size -resize x$double ${input_path} $output_path/icon_${size}x${size}@2x.png\ndone\n\niconutil -c icns $output_path -o albert.icns\n"
  },
  {
    "path": "dist/macos/generate_appcast_item.sh",
    "content": "#!/usr/bin/env bash\n\nset -exu\n\nDMG_PATH=\"$1\"\nVERSION=\"$2\"\nSPARKLE_ED_PRIVATE_KEY=\"$3\"\nOUTPUT=\"$4\"\nBREW_PREFIX=\"$(brew --prefix)\"\n\necho \"\n    <item>\n      <title>Version $VERSION</title>\n      <pubDate>$(date +'%a, %d %b %Y %H:%M:%S %z')</pubDate>\n      <link>https://albertlauncher.github.io/</link>\n      <sparkle:version>2.0</sparkle:version>\n      <sparkle:minimumSystemVersion>11.0.0</sparkle:minimumSystemVersion>\n      <sparkle:releaseNotesLink>https://albertlauncher.github.io/news/</sparkle:releaseNotesLink>\n      <enclosure\n        url=\\\"https://github.com/albertlauncher/albert/releases/download/v$VERSION/Albert-v$VERSION.dmg\\\"\n        sparkle:version=\\\"$VERSION\\\"\n        sparkle:shortVersionString=\\\"$VERSION\\\"\n        $($BREW_PREFIX/Caskroom/sparkle/2.5.0/bin/sign_update -s \"$SPARKLE_ED_PRIVATE_KEY\" \"$DMG_PATH\")\n        type=\\\"application/octet-stream\\\"/>\n    </item>\n\" > \"$OUTPUT\"\n\n"
  },
  {
    "path": "dist/xdg/albert.desktop",
    "content": "#!/usr/bin/env xdg-open\n[Desktop Entry]\nCategories=Utility;\nComment=A desktop agnostic launcher\nExec=albert --platform xcb %u\nGenericName=Launcher\nIcon=albert\nName=Albert\nStartupNotify=false\nType=Application\nVersion=1.0\nMimeType=x-scheme-handler/albert\n# Give desktop environments time to init. Otherwise QGnomePlatform does not correctly pick up the palette.\nX-GNOME-Autostart-Delay=3\n\n"
  },
  {
    "path": "include/albert/app.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n/// \\defgroup core Core API\n/// Core classes and functions.\n\n/// \\defgroup core_extension Extension\n/// Extension system and built-in extension interfaces classes.\n/// \\ingroup core\n\n/// \\defgroup core_plugin Plugin system\n/// \\ingroup core\n/// Classes and functions of the plugin system.\n\n/// \\defgroup core_query Query handling\n/// \\ingroup core\n/// Classes and functions of the query system.\n\n/// \\defgroup util Utility API\n/// Utility classes and helper functions.\n\n/// \\defgroup util_plugin Plugin\n/// \\ingroup util\n/// Utility for plugins.\n\n/// \\defgroup util_query Query\n/// \\ingroup util\n/// Utility for indexing and query handling.\n\n/// \\defgroup util_net Network\n/// \\ingroup util\n/// Network utility.\n\n/// \\defgroup util_system System\n/// \\ingroup util\n/// System/Desktop utility.\n\n/// \\defgroup util_ui UI\n/// \\ingroup util\n/// UI utility.\n\n#pragma once\n#include <QObject>\n#include <QString>\n#include <albert/export.h>\n#include <filesystem>\n#include <map>\n#include <memory>\nclass QNetworkAccessManager;\nclass QSettings;\nclass QUrl;\n\n/// The Albert namespace.\nnamespace albert\n{\nclass Extension;\nclass UsageScoring;\n\n///\n/// The public app instance interface.\n///\n/// \\ingroup core\n///\nclass ALBERT_EXPORT App : public QObject\n{\n    Q_OBJECT\npublic:\n    /// Returns the global app instance.\n    static App &instance();\n\n\n    /// @name Main window\n    /// @{\n\n    /// Shows the frontend and optionally sets the text to _input_text_.\n    virtual void show(const QString &input_text = {}) = 0;\n\n    /// Shows the settings window and optionally selects the plugin with _plugin_id_.\n    virtual void showSettings(QString plugin_id = {}) = 0;\n\n    /// @}\n\n    /// @name Extensions\n    /// @{\n\n    /// Get map of all registered extensions\n    virtual const std::map<QString,Extension*> &extensions() const = 0;\n\n    /// Get map of all extensions of type T\n    template<typename T>\n    std::map<QString, T*> extensions() const\n    {\n        std::map<QString, T*> results;\n        for (auto &[id, extension] : extensions())\n            if (T *t = dynamic_cast<T*>(extension))\n                results.emplace(id, t);\n        return results;\n    }\n\n    /// Get extension by id implicitly dynamic_cast'ed to type T.\n    template<typename T>\n    T* extension(const QString &id) const\n    {\n        try {\n            return dynamic_cast<T*>(extensions().at(id));\n        } catch (const std::out_of_range &) {\n            return nullptr;\n        }\n    }\n\n    /// @}\n\n    /// @name Application control\n    /// @{\n\n    ///\n    /// Restarts the application.\n    ///\n    /// This function is thread-safe.\n    ///\n    static void restart();\n\n    ///\n    /// Quits the application.\n    ///\n    /// This function is thread-safe.\n    ///\n    static void quit();\n\n    /// @}\n\n    /// @name Persistence\n    /// @{\n\n    ///\n    /// Returns a `QSettings` object initialized with the application configuration file path.\n    ///\n    /// As `unique_ptr` for the sake of movability.\n    ///\n    /// This function is thread-safe.\n    ///\n    static std::unique_ptr<QSettings> settings();\n\n    ///\n    /// Returns a `QSettings` object initialized with the application state file path.\n    ///\n    /// As `unique_ptr` for the sake of movability.\n    ///\n    /// This function is thread-safe.\n    ///\n    static std::unique_ptr<QSettings> state();\n\n    ///\n    /// Returns the path to the application config directory.\n    ///\n    /// This function is thread-safe.\n    ///\n    static const std::filesystem::path &configLocation();\n\n    ///\n    /// Returns the path to the application cache directory.\n    ///\n    /// This function is thread-safe.\n    ///\n    static const std::filesystem::path &cacheLocation();\n\n    ///\n    /// Returns the path to the application data directory.\n    ///\n    /// This function is thread-safe.\n    ///\n    static const std::filesystem::path &dataLocation();\n\n    /// @}\n\nsignals:\n\n    /// Emitted when an extension has been registered.\n    void added(albert::Extension*);\n\n    /// Emitted when an extension has been deregistered.\n    void removed(albert::Extension*);\n\nprotected:\n\n    App();\n    virtual ~App();\n\n};\n\n} // namespace albert\n"
  },
  {
    "path": "include/albert/asyncgeneratorqueryhandler.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/queryhandler.h>\n#include <memory>\n#include <vector>\nnamespace QCoro { template<typename T> class AsyncGenerator; }\n\nnamespace albert\n{\nclass Item;\n\nusing AsyncItemGenerator = QCoro::AsyncGenerator<std::vector<std::shared_ptr<albert::Item>>>;\n\n///\n/// Coroutine-based asynchronous generator query handler.\n///\n/// Convenience base class for implementing triggered query handlers using C++ coroutines. Results\n/// are produced lazily via an asynchronous item generator. The items are displayed in the order\n/// they are yielded.\n///\n/// This class is suitable for I/O-bound query handling (e.g. network requests, subprocessing,\n/// etc.). For CPU-bound work, prefer \\ref GeneratorQueryHandler or its subclasses.\n///\n/// If you derive this class you want to link against QCoro which provides coroutine support for Qt\n/// classes. Note that QCoro is still in development.\n///\n/// \\ingroup util_query\n///\nclass ALBERT_EXPORT AsyncGeneratorQueryHandler : public QueryHandler\n{\npublic:\n    ///\n    /// Yields batches of items for _context_ asynchronously and lazily.\n    ///\n    /// The batch size is defined by the implementation.\n    ///\n    /// \\note GCC-13 does not support returning temporary values in generators.\n    ///       So for as long as Ubuntu 24.04 is supported, we have to return lvalues.\n    ///\n    /// \\note Called from main thread. Do not run blocking operations in it.\n    ///\n    virtual AsyncItemGenerator items(QueryContext &context) = 0;\n\nprotected:\n    /// Destructs the handler.\n    ~AsyncGeneratorQueryHandler() override;\n\n    /// Returns an asynchronous generator query execution for _context_.\n    std::unique_ptr<QueryExecution> execution(QueryContext &context) override;\n};\n}  // namespace albert\n"
  },
  {
    "path": "include/albert/backgroundexecutor.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QFutureWatcher>\n#include <QtConcurrentRun>\n#include <atomic>\n#include <functional>\n\nnamespace albert\n{\n\n///\n/// Convenience class for recurring indexing tasks.\n///\n/// Takes care of the QtConcurrent boilerplate code to start, abort and schedule restarts of threads.\n///\n/// \\ingroup util_query\n///\ntemplate<typename T>\nclass BackgroundExecutor\n{\n    std::unique_ptr<QFutureWatcher<T>> future_watcher_;\n    bool rerun_ = false;\n    std::atomic_bool stop_ = false;\n\npublic:\n\n    ///\n    /// The task to be executed in a thread.\n    ///\n    /// Return the results of type `T`.  Abort if _abort_ is `true`.\n    ///\n    std::function<T(const bool &abort)> parallel;\n\n    ///\n    /// The finish callback.\n    ///\n    /// When the \\ref parallel function finished, this function will be called in the main thread.\n    /// Use \\ref BackgroundExecutor::takeResult to get the _results_ returned from \\ref parallel.\n    ///\n    std::function<void()> finish;\n\n\n    /// Constructs the background executor.\n    BackgroundExecutor() = default;\n\n    ///\n    /// Destructs the background executor.\n    ///\n    /// Silently blocks execution until a running task is finished.\n    /// See \\ref isRunning() and \\ref waitForFinished().\n    ///\n    ~BackgroundExecutor()\n    {\n        stop_ = true;\n        rerun_ = false;\n\n        // Qt 6.4 QFutureWatcher is broken.\n        // isFinished returns wrong values and waitForFinished blocks forever on finished futures.\n        // TODO(26.04): Remove workaround when dropping Qt < 6.5 support.\n        if (future_watcher_\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n            && !future_watcher_->isFinished())\n#else\n            && future_watcher_->isRunning())\n#endif\n            future_watcher_->waitForFinished();\n    }\n\n    ///\n    /// Run or schedule a rerun of the task.\n    ///\n    /// If a task is running this function sets the abort flag and schedules a rerun.\n    /// \\ref finish will not be called for the cancelled run.\n    ///\n    void run()\n    {\n        if (isRunning())\n        {\n            stop_ = true;\n            rerun_ = true;\n        }\n        else\n        {\n            stop_ = false;\n            rerun_ = false;\n\n            future_watcher_ = std::make_unique<QFutureWatcher<T>>();\n\n            QObject::connect(future_watcher_.get(), &QFutureWatcher<T>::finished,\n                             future_watcher_.get(), [this]\n            {\n                if (rerun_)\n                {\n                    future_watcher_.reset();\n                    run();  // discard results and rerun\n                }\n                else\n                {\n                    try {\n                        finish();  // may throw\n                    } catch (...) {}\n                    future_watcher_.reset();\n                }\n            });\n\n            future_watcher_->setFuture(QtConcurrent::run([this]{ return parallel(stop_); }));\n\n        }\n    }\n\n    /// Stops the current execution.\n    inline void stop() { stop_ = true; }\n\n    /// Returns `true` if the asynchronous computation is currently running; otherwise returns `false`.\n    inline bool isRunning() const { return future_watcher_.get(); }\n\n    /// Blocks until the current task finished.\n    inline void waitForFinished()\n    {\n        if (future_watcher_)\n            future_watcher_->waitForFinished();\n    }\n\n    ///\n    /// Takes the result from the future.\n    ///\n    /// Must be called from \\ref finish only. Rethrows any exception thrown in \\ref parallel.\n    ///\n    inline T takeResult() { return future_watcher_->future().takeResult(); }\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/desktopentryparser.h",
    "content": "// Copyright (c) 2024-2024 Manuel Schneider\n\n#pragma once\n#include <QLocale>\n#include <QString>\n#include <albert/export.h>\n#include <map>\n#include <optional>\n\nnamespace albert::detail {\n\n/// Desktop entry parser\n/// http://standards.freedesktop.org/desktop-entry-spec/latest/\nclass ALBERT_EXPORT DesktopEntryParser\n{\npublic:\n\n    DesktopEntryParser(const QString &path);\n\n    /// Get and escape string according to spec\n    ///\n    /// Values of type string may contain all ASCII characters except for\n    /// control characters.\n    ///\n    /// @returns The escaped string of the key in section\n    /// @param section The section to get the value from\n    /// @param key The key to the value for\n    /// @throws out_of_range if lookup failed\n    QString getString(const QString &section, const QString &key) const;\n\n    /// Get localestring according to spec\n    ///\n    /// Values of type localestring are user displayable, and are encoded in UTF-8.\n    ///\n    /// @returns The localestring of the key in section\n    /// @param section The section to get the value from\n    /// @param key The key to the value for\n    /// @throws out_of_range if lookup failed\n    QString getLocaleString(const QString &section, const QString &key);\n\n    /// Get iconstring according to spec\n    ///\n    /// Values of type iconstring are the names of icons; these may be\n    /// absolute paths, or symbolic names for icons located using the\n    /// algorithm described in the Icon Theme Specification. Such values\n    /// are not user-displayable, and are encoded in UTF-8.\n    ///\n    /// @returns The iconstring of the key in section\n    /// @param section The section to get the value from\n    /// @param key The key to the value for\n    /// @throws out_of_range if lookup failed\n    QString getIconString(const QString &section, const QString &key);\n\n    /// Get boolean according to spec\n    ///\n    /// Values of type boolean must either be the string true or false.\n    ///\n    /// @returns The boolean of the key in section\n    /// @param section The section to get the value from\n    /// @param key The key to the value for\n    /// @throws out_of_range if lookup failed\n    bool getBoolean(const QString &section, const QString &key);\n\n    /// Get numeric according to spec\n    ///\n    /// Values of type numeric must be a valid floating point number as\n    /// recognized by the %f specifier for scanf in the C locale.\n    ///\n    /// @returns The numeric of the key in section\n    /// @param section The section to get the value from\n    /// @param key The key to the value for\n    /// @throws out_of_range if lookup failed\n    double getNumeric(const QString &, const QString &);\n\n    /// Split an Exec string according to spec\n    static std::optional<QStringList> splitExec(const QString &s) noexcept;\n\nprivate:\n\n    /// Get a raw, unescaped value for a section and key.\n    ///\n    /// @returns The raw, unescaped string of the key in section\n    /// @param section The section to get the value from\n    /// @param key The key to the value for\n    /// @throws out_of_range if lookup failed\n    QString getRawValue(const QString &section, const QString &key) const;\n\n    /// Get an escaped value for a section and key.\n    ///\n    /// The escape sequences \\s, \\n, \\t, \\r, and \\\\ are supported for values of\n    /// type string, localestring and iconstring, meaning ASCII space, newline,\n    /// tab, carriage return, and backslash, respectively.\n    ///\n    /// @returns The escaped string of the key in section\n    /// @param section The section to get the value from\n    /// @param key The key to the value for\n    /// @throws out_of_range if lookup failed\n    QString getEscapedValue(const QString &section, const QString &key) const;\n\n    std::map<QString, std::map<QString,QString>> data;\n    QLocale locale;\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/download.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QObject>\n#include <albert/export.h>\n#include <memory>\nclass QNetworkReply;\nclass QString;\nclass QUrl;\n\nnamespace albert\n{\n\n///\n/// Downloads a file from the given URL to the given path.\n///\n/// Does not check if the file exists. The download will fail to save the file in this case.\n///\n/// \\ingroup util_net\n///\nclass ALBERT_EXPORT Download : public QObject\n{\n    Q_OBJECT\npublic:\n\n    ///\n    /// Constructs a download with the given _url_, _path_ and _parent_.\n    ///\n    Download(const QUrl &url, const QString &path, QObject *parent = nullptr);\n\n    ///\n    /// Destructs this download.\n    ///\n    ~Download();\n\n    ///\n    /// Returns a unique download for _url_ and _path_.\n    ///\n    /// If a download for the same URL and path already exists, it is returned;\n    /// otherwise a new download is created and returned.\n    /// The download created is automatically started and lives in the main thread.\n    ///\n    /// This function is thread-safe.\n    ///\n    static std::shared_ptr<Download> unique(const QUrl &url, const QString &path);\n\n    ///\n    /// Returns the url of the download.\n    ///\n    /// This function is thread-safe.\n    ///\n    const QUrl &url();\n\n    ///\n    /// Returns the destination path of the download.\n    ///\n    /// This function is thread-safe.\n    ///\n    const QString &path();\n\n    ///\n    /// Returns true if the download is active.\n    ///\n    /// This function is thread-safe.\n    ///\n    bool isActive();\n\n    ///\n    /// Returns the error of the download, if any.\n    ///\n    /// This function is thread-safe.\n    ///\n    const QString &error();\n\n    ///\n    /// Starts the download.\n    ///\n    /// Do **not** move the download to another thread **after** calling this method.\n    ///\n    void start();\n\nsignals:\n\n    ///\n    /// Emitted when the download has finished.\n    ///\n    void finished();\n\nprivate:\n\n    class Private;\n    std::unique_ptr<Private> d;\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/extension.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/export.h>\nclass QString;\n\nnamespace albert\n{\n\n///\n/// Abstract extension class.\n///\n/// \\ingroup core_extension\n///\nclass ALBERT_EXPORT Extension\n{\npublic:\n    ///\n    /// Returns the extension identifier.\n    ///\n    /// To avoid naming conflicts use the namespace of your plugin,\n    /// e.g. files (root extension), files.rootbrowser, files.homebrowser, …\n    ///\n    virtual QString id() const = 0;\n\n    ///\n    /// Returns the pretty, human readable extension name.\n    ///\n    virtual QString name() const = 0;\n\n    ///\n    /// Returns the brief extension description.\n    ///\n    virtual QString description() const = 0;\n\nprotected:\n    ///\n    /// Destructs the extension.\n    ///\n    virtual ~Extension();\n};\n\n}  // namespace albert\n"
  },
  {
    "path": "include/albert/extensionplugin.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QObject>\n#include <albert/extension.h>\n#include <albert/plugininstance.h>\n\nnamespace albert\n{\n\n///\n/// Extension plugin base class.\n///\n/// Implements pure virtual functions of \\ref Extension and \\ref PluginInstance.\n///\n/// \\ingroup util_plugin\n///\nclass ALBERT_EXPORT ExtensionPlugin : public PluginInstance,\n                                      virtual public Extension\n{\npublic:\n    ///\n    /// Returns \\ref PluginMetadata::id.\n    ///\n    QString id() const override;\n\n    ///\n    /// Returns \\ref PluginMetadata::name.\n    ///\n    QString name() const override;\n\n    ///\n    /// Returns \\ref PluginMetadata::description.\n    ///\n    QString description() const override;\n\n    ///\n    /// Returns `this` extension.\n    ///\n    std::vector<albert::Extension*> extensions() override;\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/fallbackhandler.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QString>\n#include <albert/export.h>\n#include <albert/extension.h>\n#include <albert/item.h>\n#include <memory>\n#include <vector>\n\nnamespace albert\n{\nclass Item;\n\n///\n/// Abstract fallback item provider.\n///\n/// \\ingroup core_extension\n///\nclass ALBERT_EXPORT FallbackHandler : virtual public Extension\n{\npublic:\n\n    ///\n    /// Returns fallback items for _query_.\n    ///\n    virtual std::vector<std::shared_ptr<Item>> fallbacks(const QString &) const = 0;\n\nprotected:\n\n    ///\n    /// Destructs the fallback handler.\n    ///\n    ~FallbackHandler() override;\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/frontend.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QObject>\n#include <QString>\n#include <albert/export.h>\n#include <albert/plugininstance.h>\nclass QWidget;\n\nnamespace albert\n{\nnamespace detail\n{\nclass Query;\n\n///\n/// The interface for albert frontends.\n///\nclass ALBERT_EXPORT Frontend : public albert::PluginInstance\n{\n    Q_OBJECT\n\npublic:\n\n    ///\n    /// Visibility of the frontend\n    ///\n    virtual bool isVisible() const = 0;\n\n    ///\n    /// Set the visibility state of the frontend\n    ///\n    virtual void setVisible(bool visible) = 0;\n\n    ///\n    /// Input line text\n    ///\n    virtual QString input() const = 0;\n\n    ///\n    /// Input line text setter\n    ///\n    virtual void setInput(const QString&) = 0;\n\n    ///\n    /// The native window id. Used to apply platform quirks.\n    ///\n    virtual unsigned long long winId() const = 0;\n\n    ///\n    /// The config widget show in the window settings tab\n    ///\n    virtual QWidget *createFrontendConfigWidget() = 0;\n\n    ///\n    /// The query setter\n    ///\n    virtual void setQuery(Query *query) = 0;\n\nsignals:\n\n    void inputChanged(QString);\n    void visibleChanged(bool);\n\nprotected:\n\n    ~Frontend() override;\n\n};\n\n}\n}\n"
  },
  {
    "path": "include/albert/generatorqueryhandler.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/queryhandler.h>\n#include <memory>\n#include <vector>\nnamespace QCoro { template<typename T> class Generator; }\n\nnamespace albert\n{\nclass Item;\n\nusing ItemGenerator = QCoro::Generator<std::vector<std::shared_ptr<albert::Item>>>;\n\n///\n/// Coroutine-based synchronous generator query handler.\n///\n/// Convenience base class for implementing triggered query handlers using C++ coroutines. Results\n/// are produced lazily via a synchronous item generator. Item production is executed in a worker\n/// thread, allowing CPU-bound work without blocking the main thread. The items are displayed in the\n/// order they are yielded.\n///\n/// This class is intended for computational workloads. For I/O-bound or event-driven tasks, prefer\n/// \\ref AsyncGeneratorQueryHandler.\n///\n/// \\ingroup util_query\n///\nclass ALBERT_EXPORT GeneratorQueryHandler : public QueryHandler\n{\npublic:\n    ///\n    /// Yields batches of items for _context_ lazily.\n    ///\n    /// The batch size is defined by the implementation.\n    ///\n    /// \\note Executed in a background thread.\n    ///\n    /// \\note GCC-13 does not support returning temporary values in generators.\n    ///       So for as long as Ubuntu 24.04 is supported, we have to return lvalues.\n    ///\n    virtual ItemGenerator items(QueryContext &context) = 0;\n\nprotected:\n    /// Destructs the handler.\n    ~GeneratorQueryHandler() override;\n\n    /// Returns a threaded synchronous generator query execution for _context_.\n    std::unique_ptr<QueryExecution> execution(QueryContext &context) override;\n};\n}  // namespace albert\n"
  },
  {
    "path": "include/albert/globalqueryhandler.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/rankedqueryhandler.h>\n#include <albert/rankitem.h>\n#include <memory>\n#include <vector>\n\nnamespace albert\n{\n\n///\n/// Query handler participating in the global search.\n///\n/// By design, every global query handler is also a triggered query handler. Therefore this class\n/// inherits \\ref RankedQueryHandler and as such inherits its contract. I.e. the handler returns a\n/// complete set of match-scored items eagerly. The provided match scores will be combined with the\n/// usage-based scoring weighted by user configuration. Finally the items (of all global handlers)\n/// will be yielded lazily in order of their final score.\n///\n/// Note: Global queries are expected to complete within a few milliseconds.\n///\n/// \\ingroup core_extension\n///\nclass ALBERT_EXPORT GlobalQueryHandler : public albert::RankedQueryHandler\n{\npublic:\n    ///\n    /// Returns a list of special items that should show up on an emtpy query.\n    ///\n    /// The empty pattern matches everything. For triggered queries this is desired and by design\n    /// lots of triggered handlers reuse GlobalQueryHandler::rankItems. The empty global query is\n    /// not executed. This function allows dedicated empty global query handling.\n    ///\n    virtual std::vector<std::shared_ptr<Item>> handleEmptyQuery();\n\nprotected:\n    /// Destructs the handler.\n    ~GlobalQueryHandler() override;\n};\n\n}  // namespace albert\n"
  },
  {
    "path": "include/albert/icon.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QString>\n#include <albert/export.h>\n#include <memory>\n#include <filesystem>\nclass QBrush;\nclass QIcon;\nclass QPainter;\nclass QPixmap;\nclass QRect;\nclass QSize;\nclass QString;\n\nnamespace albert\n{\n\n///\n/// Abstract icon engine.\n///\n/// \\ingroup core_query\n///\nclass ALBERT_EXPORT Icon\n{\npublic:\n\n    /// Destructs the icon.\n    virtual ~Icon();\n\n    /// Returns a clone of this icon.\n    virtual std::unique_ptr<Icon> clone() const = 0;\n\n    ///\n    /// Returns the device independent size of the available icon for the given\n    /// _device_independent_size_ and _device_pixel_ratio_.\n    ///\n    /// The base implementations returns _device_independent_size_.\n    ///\n    virtual QSize actualSize(const QSize &device_independent_size, double device_pixel_ratio);\n\n    ///\n    /// Returns a pixmap for the requested _device_independent_size_ and _device_pixel_ratio_.\n    ///\n    /// The base implementation creates a transparent pixmap of \\ref actualSize and calls \\ref paint on it.\n    ///\n    virtual QPixmap pixmap(const QSize &device_independent_size, double device_pixel_ratio);\n\n    /// Uses the given _painter_ to paint the icon into the rectangle _rect_.\n    virtual void paint(QPainter *painter, const QRect &rect) = 0;\n\n    ///\n    /// Returns `true` if the icon is valid; otherwise returns `false`.\n    ///\n    /// The base implementation returns `false`.\n    ///\n    virtual bool isNull();\n\n    /// Returns a URL representation of the icon.\n    virtual QString toUrl() const = 0;\n\n    ///\n    /// Returns the cache key of the icon.\n    ///\n    /// The base implementation calls \\ref toUrl. Reimplement to get faster lookups.\n    ///\n    virtual QString cacheKey();\n\n    /// Returns a `QIcon` using _icon_ as icon engine.\n    static QIcon qIcon(std::unique_ptr<albert::Icon> icon);\n\n    /// Returns a built-in icon for the given _url_.\n    static std::unique_ptr<Icon> iconFromUrl(const QString &url);\n\n    /// Returns a built-in icon for the given _urls_.\n    static std::unique_ptr<Icon> iconFromUrls(const QStringList &urls);\n\n    /// @name Image icon\n    /// @{\n\n    /// Returns an icon from an image file at _path_.\n    static std::unique_ptr<Icon> image(const QString &path);\n\n    /// @copydoc image(const QString &)\n    static std::unique_ptr<Icon> image(const std::filesystem::path &path);\n\n    /// @}\n\n    /// @name File type icon\n    /// @{\n\n    /// Returns an icon representing the file type of the file at _path_.\n    static std::unique_ptr<Icon> fileType(const QString &path);\n\n    /// @copydoc fileType(const QString &)\n    static std::unique_ptr<Icon> fileType(const std::filesystem::path &path);\n\n    /// @}\n\n    /// @name Theme icon ([XDG icon lookup](https://specifications.freedesktop.org/icon-theme/latest/))\n    /// @{\n\n    /// Returns an icon from the current icon theme with the given _icon_name_.\n    static std::unique_ptr<Icon> theme(const QString &icon_name);\n\n    /// @}\n\n    /// @name Standard icon (QStyle standard pixmap)\n    /// @{\n    ///\n    /// This enum describes the available standard icons.\n    ///\n    /// See [Qt documentation](https://doc.qt.io/qt-6/qstyle.html#StandardPixmap-enum) for more details.\n    ///\n    enum StandardIconType\n    {\n        TitleBarMinButton = 1,                 ///< Menu button on a title bar.\n        TitleBarMenuButton = 0,                ///< Minimize button on title bars (e.g., in QMdiSubWindow).\n        TitleBarMaxButton = 2,                 ///< Maximize button on title bars.\n        TitleBarCloseButton = 3,               ///< Close button on title bars.\n        TitleBarNormalButton = 4,              ///< Normal (restore) button on title bars.\n        TitleBarShadeButton = 5,               ///< Shade button on title bars.\n        TitleBarUnshadeButton = 6,             ///< Unshade button on title bars.\n        TitleBarContextHelpButton = 7,         ///< The Context help button on title bars.\n        MessageBoxInformation = 9,             ///< The \"information\" icon.\n        MessageBoxWarning = 10,                ///< The \"warning\" icon.\n        MessageBoxCritical = 11,               ///< The \"critical\" icon.\n        MessageBoxQuestion = 12,               ///< The \"question\" icon.\n        DesktopIcon = 13,                      ///< The \"desktop\" icon.\n        TrashIcon = 14,                        ///< The \"trash\" icon.\n        ComputerIcon = 15,                     ///< The \"My computer\" icon.\n        DriveFDIcon = 16,                      ///< The floppy icon.\n        DriveHDIcon = 17,                      ///< The harddrive icon.\n        DriveCDIcon = 18,                      ///< The CD icon.\n        DriveDVDIcon = 19,                     ///< The DVD icon.\n        DriveNetIcon = 20,                     ///< The network icon.\n        DirHomeIcon = 56,                      ///< The home directory icon.\n        DirOpenIcon = 21,                      ///< The open directory icon.\n        DirClosedIcon = 22,                    ///< The closed directory icon.\n        DirIcon = 38,                          ///< The directory icon.\n        DirLinkIcon = 23,                      ///< The link to directory icon.\n        DirLinkOpenIcon = 24,                  ///< The link to open directory icon.\n        FileIcon = 25,                         ///< The file icon.\n        FileLinkIcon = 26,                     ///< The link to file icon.\n        FileDialogStart = 29,                  ///< The \"start\" icon in a file dialog.\n        FileDialogEnd = 30,                    ///< The \"end\" icon in a file dialog.\n        FileDialogToParent = 31,               ///< The \"parent directory\" icon in a file dialog.\n        FileDialogNewFolder = 32,              ///< The \"create new folder\" icon in a file dialog.\n        FileDialogDetailedView = 33,           ///< The detailed view icon in a file dialog.\n        FileDialogInfoView = 34,               ///< The file info icon in a file dialog.\n        FileDialogContentsView = 35,           ///< The contents view icon in a file dialog.\n        FileDialogListView = 36,               ///< The list view icon in a file dialog.\n        FileDialogBack = 37,                   ///< The back arrow in a file dialog.\n        DockWidgetCloseButton = 8,             ///< Close button on dock windows (see also QDockWidget).\n        ToolBarHorizontalExtensionButton = 27, ///< Extension button for horizontal toolbars.\n        ToolBarVerticalExtensionButton = 28,   ///< Extension button for vertical toolbars.\n        DialogOkButton = 39,                   ///< Icon for a standard OK button in a QDialogButtonBox.\n        DialogCancelButton = 40,               ///< Icon for a standard Cancel button in a QDialogButtonBox.\n        DialogHelpButton = 41,                 ///< Icon for a standard Help button in a QDialogButtonBox.\n        DialogOpenButton = 42,                 ///< Icon for a standard Open button in a QDialogButtonBox.\n        DialogSaveButton = 43,                 ///< Icon for a standard Save button in a QDialogButtonBox.\n        DialogCloseButton = 44,                ///< Icon for a standard Close button in a QDialogButtonBox.\n        DialogApplyButton = 45,                ///< Icon for a standard Apply button in a QDialogButtonBox.\n        DialogResetButton = 46,                ///< Icon for a standard Reset button in a QDialogButtonBox.\n        DialogDiscardButton = 47,              ///< Icon for a standard Discard button in a QDialogButtonBox.\n        DialogYesButton = 48,                  ///< Icon for a standard Yes button in a QDialogButtonBox.\n        DialogNoButton = 49,                   ///< Icon for a standard No button in a QDialogButtonBox.\n        ArrowUp = 50,                          ///< Icon arrow pointing up.\n        ArrowDown = 51,                        ///< Icon arrow pointing down.\n        ArrowLeft = 52,                        ///< Icon arrow pointing left.\n        ArrowRight = 53,                       ///< Icon arrow pointing right.\n        ArrowBack = 54,                        ///< Equivalent to SP_ArrowLeft when the current layout direction is Qt::LeftToRight, otherwise SP_ArrowRight.\n        ArrowForward = 55,                     ///< Equivalent to SP_ArrowRight when the current layout direction is Qt::LeftToRight, otherwise SP_ArrowLeft.\n        CommandLink = 57,                      ///< Icon used to indicate a Vista style command link glyph.\n        VistaShield = 58,                      ///< Icon used to indicate UAC prompts on Windows Vista. This will return a null pixmap or icon on all other platforms.\n        BrowserReload = 59,                    ///< Icon indicating that the current page should be reloaded.\n        BrowserStop = 60,                      ///< Icon indicating that the page loading should stop.\n        MediaPlay = 61,                        ///< Icon indicating that media should begin playback.\n        MediaStop = 62,                        ///< Icon indicating that media should stop playback.\n        MediaPause = 63,                       ///< Icon indicating that media should pause playback.\n        MediaSkipForward = 64,                 ///< Icon indicating that media should skip forward.\n        MediaSkipBackward = 65,                ///< Icon indicating that media should skip backward.\n        MediaSeekForward = 66,                 ///< Icon indicating that media should seek forward.\n        MediaSeekBackward = 67,                ///< Icon indicating that media should seek backward.\n        MediaVolume = 68,                      ///< Icon indicating a volume control.\n        MediaVolumeMuted = 69,                 ///< Icon indicating a muted volume control.\n        LineEditClearButton = 70,              ///< Icon for a standard clear button in a QLineEdit.\n        DialogYesToAllButton = 71,             ///< Icon for a standard YesToAll button in a QDialogButtonBox.\n        DialogNoToAllButton = 72,              ///< Icon for a standard NoToAll button in a QDialogButtonBox.\n        DialogSaveAllButton = 73,              ///< Icon for a standard SaveAll button in a QDialogButtonBox.\n        DialogAbortButton = 74,                ///< Icon for a standard Abort button in a QDialogButtonBox.\n        DialogRetryButton = 75,                ///< Icon for a standard Retry button in a QDialogButtonBox.\n        DialogIgnoreButton = 76,               ///< Icon for a standard Ignore button in a QDialogButtonBox.\n        RestoreDefaultsButton = 77,            ///< Icon for a standard RestoreDefaults button in a QDialogButtonBox.\n        TabCloseButton = 78,                   ///< Icon for the close button in the tab of a QTabBar.\n    };\n    /// Returns a standard icon for the given _type_.\n    static std::unique_ptr<Icon> standard(StandardIconType type);\n    /// @}\n\n    /// @name Grapheme icon\n    /// @{\n\n    /// Returns the window text color of the current application palette.\n    static QBrush graphemeDefaultBrush();\n\n    /// Returns an icon rendering the given _grapheme_ with \\ref graphemeDefaultBrush, scaled by _scalar_.\n    static std::unique_ptr<Icon> grapheme(const QString &grapheme, double scalar = 1.0);\n\n    /// Returns an icon rendering the given _grapheme_, scaled by _scalar_ and colored with _brush_.\n    static std::unique_ptr<Icon> grapheme(const QString &grapheme,\n                                          double scalar,\n                                          const QBrush &brush);\n\n    /// @}\n\n    /// @name Iconified icon\n    /// @{\n\n    /// Returns the default background brush (a top down gradient from white to some darker white).\n    static const QBrush &iconifiedDefaultBackgroundBrush();\n\n    /// Returns the default border color (a gradient slightly darker than the default background).\n    static const QBrush &iconifiedDefaultBorderBrush();\n\n    ///\n    /// Returns iconified _icon_. i.e. drawn in a colored rounded rectangle with a border.\n    ///\n    /// _color_ specifies the background color, _border_width_ the border width in device independent pixels,\n    /// _border_radius_ the relative border radius (0.0 - 1.0), and _border_color_ the border color.\n    ///\n    static std::unique_ptr<Icon> iconified(\n        std::unique_ptr<Icon> icon,\n        const QBrush &background_brush = iconifiedDefaultBackgroundBrush(),\n        double border_radius = 1.0,\n        int border_width = 1,\n        const QBrush &border_color = iconifiedDefaultBorderBrush());\n\n    /// @}\n\n    /// @name Composed icon\n    /// @{\n\n    ///\n    /// Returns a composed icon from _icon1_ and _icon2_.\n    ///\n    /// _size1_ and _size2_ specify the relative sizes (0.0 - 1.0) of the icons.\n    /// _x1_, _y1_, _x2_, and _y2_ specify the relative positions (0.0 - 1.0, 0.5 is centered) of the icons.\n    ///\n    static std::unique_ptr<Icon> composed(std::unique_ptr<Icon> icon1,\n                                          std::unique_ptr<Icon> icon2,\n                                          double size1 = 0.7,\n                                          double size2 = 0.7,\n                                          double x1 = 0.0,\n                                          double y1 = 0.0,\n                                          double x2 = 1.0,\n                                          double y2 = 1.0);\n\n    /// @}\n\n};\n\n}  // namespace albert\n"
  },
  {
    "path": "include/albert/indexitem.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QString>\n#include <albert/export.h>\n#include <albert/item.h>\n#include <memory>\n\nnamespace albert\n{\n\n///\n/// An item utlized by ItemIndex\n///\n/// \\ingroup util_query\n///\nclass ALBERT_EXPORT IndexItem\n{\npublic:\n    /// Constructs an index item with the given _item_ and _string_.\n    IndexItem(std::shared_ptr<Item> item, QString string);\n\n    /// The item to be indexed\n    std::shared_ptr<Item> item;\n\n    /// The corresponding lookup string\n    QString string;\n};\n\n}\n"
  },
  {
    "path": "include/albert/indexqueryhandler.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QString>\n#include <albert/globalqueryhandler.h>\n#include <albert/indexitem.h>\n#include <memory>\n#include <vector>\n\nnamespace albert\n{\n\n///\n/// Index-based global query handler.\n///\n/// Convenience base class for global query handlers backed by a precomputed index. Query execution\n/// is performed against the index and provides fast, deterministic matching suitable for the global\n/// search. Implementations are responsible for providing the indexed items and keeping the index up\n/// to date when the underlying data changes.\n///\n/// \\ingroup util_query\n///\nclass ALBERT_EXPORT IndexQueryHandler : public GlobalQueryHandler\n{\npublic:\n    /// Returns `true`\n    bool supportsFuzzyMatching() const override;\n\n    /// Sets the fuzzy matching mode to _enabled_ and triggers \\ref updateIndexItems().\n    void setFuzzyMatching(bool enabled) override;\n\n    /// Returns a list of scored matches for _context_ using the index.\n    std::vector<RankItem> rankItems(QueryContext &context) override;\n\n    ///\n    /// Updates the index.\n    ///\n    /// Called when the index needs to be updated, i.e. for initialization, on user changes to the\n    /// index config (fuzzy, etc…) and probably by the client itself if the items changed. This\n    /// function should call \\ref setIndexItems to update the index.\n    ///\n    /// @note Do not call this method on plugin initialization. It will be called once loaded.\n    ///\n    virtual void updateIndexItems() = 0;\n\n    /// Sets the items of the index to _index_items_.\n    void setIndexItems(std::vector<IndexItem> &&index_items);\n\nprotected:\n    /// Constructs an index query handler.\n    IndexQueryHandler();\n\n    /// Destructs the index query handler.\n    ~IndexQueryHandler() override;\n\nprivate:\n    class Private;\n    std::unique_ptr<Private> d;\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/inputhistory.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QObject>\n#include <QString>\n#include <albert/export.h>\n#include <memory>\n\nnamespace albert::detail\n{\n\n///\n/// Input history class.\n///\n/// Stores input strings and provides a search iterator.\n///\nclass ALBERT_EXPORT InputHistory final : public QObject\n{\n    Q_OBJECT\n\npublic:\n\n    InputHistory(const QString &path = {});\n    ~InputHistory() override;\n\n    ///\n    /// Adds text to history search.\n    ///\n    /// Skips empty strings and drops duplicates.\n    ///\n    /// @param str The string to add.\n    ///\n    Q_INVOKABLE void add(const QString& str);\n\n    ///\n    /// Gets next history item matching the pattern.\n    ///\n    /// @param pattern A pattern used to filter the history items.\n    /// @returns The next history item matching the pattern or empty string.\n    ///\n    Q_INVOKABLE QString next(const QString &pattern = QString{});\n\n    ///\n    /// Gets previous history item matching the pattern.\n    ///\n    /// @param pattern A pattern used to filter the history items.\n    /// @returns The previous history item matching the pattern or empty string.\n    ///\n    Q_INVOKABLE QString prev(const QString &pattern = QString{});\n\n    ///\n    /// Resets history search.\n    ///\n    Q_INVOKABLE void resetIterator();\n\n    ///\n    /// Clears the history.\n    ///\n    Q_INVOKABLE void clear();\n\n    ///\n    /// Returns the maximum amount of history entries.\n    ///\n    Q_INVOKABLE uint limit() const;\n\n    ///\n    /// Sets the maximum amount of history entries.\n    ///\n    Q_INVOKABLE void setLimit(uint);\n\nprivate:\n\n    class Private;\n    std::unique_ptr<Private> d;\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/item.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QStringList>\n#include <albert/export.h>\n#include <functional>\n#include <vector>\n\nnamespace albert\n{\nclass Icon;\n\n///\n/// Action used by result items (\\ref Item).\n///\n/// \\ingroup core_query\n///\nclass ALBERT_EXPORT Action final\n{\npublic:\n\n    // ///\n    // /// Constructs an \\ref Action with the contents initialized with the data passed.\n    // /// \\param id_ \\copybrief id\n    // /// \\param text_ \\copybrief text\n    // /// \\param function_ \\copybrief function\n    // /// \\param hide_on_activation_ \\copybrief hide_on_activation\n    // ///\n    // template<typename T1 = QString,\n    //          typename T2 = QString,\n    //          typename T3 = std::function<void()>>\n    // Action(T1 &&id_,\n    //        T2 &&text_,\n    //        T3 &&function_,\n    //        bool hide_on_activation_ = true) noexcept :\n    //     id(std::forward<T1>(id_)),\n    //     text(std::forward<T2>(text_)),\n    //     function(std::forward<T3>(function_)),\n    //     hide_on_activation(hide_on_activation_)\n    // {}\n\n    ///\n    /// The identifier.\n    ///\n    QString id;\n\n    ///\n    /// The description.\n    ///\n    QString text;\n\n    ///\n    /// The function executed on activation.\n    ///\n    std::function<void()> function;\n\n    ///\n    /// The activation behavior.\n    ///\n    bool hide_on_activation = true;\n};\n\n\n///\n/// Result items displayed in the query results list\n///\n/// \\ingroup core_query\n///\nclass ALBERT_EXPORT Item\n{\npublic:\n\n    ///\n    /// Destructs the item.\n    ///\n    virtual ~Item();\n\n    ///\n    /// Returns the item identifier.\n    ///\n    /// Has to be unique per extension. This function is involved in several time critical\n    /// operartion such as indexing and sorting. It is therefore recommended to return a string that\n    /// is as short as possible as fast as possible.\n    ///\n    virtual QString id() const = 0;\n\n    ///\n    /// Returns the item text.\n    ///\n    /// Primary text displayed emphasized in a list item. The string must not be empty, since the\n    /// text length is used as divisor for scoring. Return as fast as possible. No checks are\n    /// performed. If empty you get undefined behavior.\n    ///\n    virtual QString text() const = 0;\n\n    ///\n    /// Returns the item subtext.\n    ///\n    /// Secondary descriptive text displayed in a list item.x\n    ///\n    virtual QString subtext() const = 0;\n\n    ///\n    /// Returns the item input action text.\n    ///\n    /// Used as input text replacement (usually by pressing Tab).\n    ///\n    /// The base implementation returns \\ref text().\n    ///\n    virtual QString inputActionText() const;\n\n    ///\n    /// Returns the item icon.\n    ///\n    /// Do _not_ clone a stored icon in this function. Icons can be a heavy resource. Instead return\n    /// a new instance every time this function is called. The view will cache the icon instance.\n    ///\n    virtual std::unique_ptr<Icon> icon() const = 0;\n\n    ///\n    /// Returns the item actions.\n    ///\n    /// These are the actions a users can choose to activate. The base implementation returns an empty vector.\n    ///\n    virtual std::vector<Action> actions() const;\n\n    ///\n    /// Interface class for item observers\n    ///\n    class Observer\n    {\n    public:\n\n        ///\n        /// Notifies the observer about any changes in _item_.\n        ///\n        virtual void notify(const albert::Item *item) = 0;\n\n    protected:\n\n        ///\n        /// Destructs the observer.\n        ///\n        virtual ~Observer();\n    };\n\n    ///\n    /// Starts notifying _observer_ about any changes.\n    ///\n    virtual void addObserver(Observer *observer);\n\n    ///\n    /// Stops notifying _observer_ about any changes.\n    ///\n    virtual void removeObserver(Observer *observer);\n\n};\n\nnamespace detail\n{\nclass ALBERT_EXPORT DynamicItem : public Item\n{\npublic:\n\n    DynamicItem();\n    ~DynamicItem() override;\n\n    void dataChanged() const;\n\n    void addObserver(Observer *) override;\n    void removeObserver(Observer *) override;\n\nprivate:\n\n    class Private;\n    std::unique_ptr<Private> d;\n\n};\n}\n\n/// A shared pointer to an \\ref Item or subclass thereof\ntemplate<typename T>\nconcept ItemPtr\n    = std::is_base_of_v<Item, typename std::decay_t<T>::element_type>\n      && std::same_as<std::shared_ptr<typename std::decay_t<T>::element_type>, std::decay_t<T>>;\n\n/// A range of \\ref ItemPtr\ntemplate<typename R>\nconcept ItemRange = std::ranges::range<R> && ItemPtr<std::ranges::range_value_t<R>>;\n\n}  // namespace albert\n\n"
  },
  {
    "path": "include/albert/logging.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QLoggingCategory>\n\nQ_DECLARE_LOGGING_CATEGORY(AlbertLoggingCategory)\n\n///\n/// Defines the logging category with the name _name_.\n///\n#define ALBERT_LOGGING_CATEGORY(name) Q_LOGGING_CATEGORY(AlbertLoggingCategory, \"albert.\" name)\n\n///\n/// Creates a log object (level debug) you can use to pipe text into (<<).\n///\n#define DEBG qCDebug(AlbertLoggingCategory,).noquote()\n\n///\n/// Creates a log object (level info) you can use to pipe text into (<<).\n///\n#define INFO qCInfo(AlbertLoggingCategory,).noquote()\n\n///\n/// Creates a log object (level warning) you can use to pipe text into (<<).\n///\n#define WARN qCWarning(AlbertLoggingCategory,).noquote()\n\n///\n/// Creates a log object (level critial) you can use to pipe text into (<<).\n///\n#define CRIT qCCritical(AlbertLoggingCategory,).noquote()\n"
  },
  {
    "path": "include/albert/matchconfig.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/export.h>\n\nnamespace albert\n{\n\n///\n/// Configuration for string matching.\n///\n/// Initialize with designated initializers to avoid hard to find bugs on future API changes.\n///\n/// \\sa \\ref Matcher, \\ref IndexQueryHandler\n///\n/// \\ingroup util_query\n///\nclass ALBERT_EXPORT MatchConfig\n{\npublic:\n\n    ///\n    /// Match strings error tolerant.\n    ///\n    bool fuzzy = false;\n\n    ///\n    /// Match strings case insensitive.\n    ///\n    bool ignore_case = true;\n\n    ///\n    /// Match strings independent of their order.\n    ///\n    bool ignore_word_order = true;\n\n    ///\n    /// Match strings normalized.\n    ///\n    bool ignore_diacritics = true;\n\n};\n\n} // namespace albert\n"
  },
  {
    "path": "include/albert/matcher.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QRegularExpression>\n#include <QString>\n#include <albert/export.h>\n#include <albert/matchconfig.h>\n#include <ranges>\nclass MatcherPrivate;\n\nnamespace albert\n{\n\n///\n/// Augmented match score.\n///\n/// Some nifty features:\n/// - The bool type conversion evaluates to \\ref isMatch()\n/// - The Score/double type conversion seamlessly uses the \\ref score()\n///\n/// @sa \\ref Matcher\n///\n/// \\ingroup util_query\n///\nclass ALBERT_EXPORT Match final\n{\npublic:\n    using Score = double;\n\n    ///\n    /// Constructs an invalid match.\n    ///\n    Match() : score_(-1.) {}\n\n    ///\n    /// Constructs a match with the given `score`.\n    ///\n    Match(const Score score) : score_(score) {}\n\n    ///\n    /// Constructs a #Match with the score of `other`.\n    ///\n    Match(const Match &o) = default;\n\n    ///\n    /// Replaces the score with that of `other`.\n    ///\n    Match &operator=(const Match &o) = default;\n\n    ///\n    /// Returns `true` if this is a match, otherwise returns `false`.\n    ///\n    inline bool isMatch() const { return score_ >= 0.0; }\n\n    ///\n    /// Returns `true` if this is a zero score match, otherwise returns `false`.\n    ///\n    inline bool isEmptyMatch() const { return qFuzzyCompare(score_, 0.0); }\n\n    ///\n    /// Returns `true` if this is a perfect match, otherwise returns `false`.\n    ///\n    inline bool isExactMatch() const { return qFuzzyCompare(score_, 1.0); }\n\n    ///\n    /// Returns the score.\n    ///\n    inline Score score() const { return score_; }\n\n    ///\n    /// Returns `true` if this is a match, otherwise returns `false`.\n    ///\n    inline explicit operator bool() const { return isMatch(); }\n\n    ///\n    /// Returns the score.\n    ///\n    inline operator Score() const { return score_; }\n\nprivate:\n\n    Score score_;\n};\n\n\n///\n/// Configurable string matcher.\n///\n/// @sa \\ref MatchConfig, \\ref Match\n///\n/// \\ingroup util_query\n///\nclass ALBERT_EXPORT Matcher final\n{\npublic:\n    ///\n    /// Constructs a Matcher with the given _string_ and match _config_.\n    ///\n    /// If _config_ is not provided, a default constructed config is used.\n    ///\n    Matcher(const QString &string, MatchConfig config = {});\n\n    ///\n    /// Constructs a Matcher with the contents of _other_ using move semantics.\n    ///\n    Matcher(Matcher &&o);\n\n    ///\n    /// Replaces the contents with those of _other_ using move semantics.\n    ///\n    Matcher &operator=(Matcher &&o);\n\n    ///\n    /// Destructs the Matcher.\n    ///\n    ~Matcher();\n\n    ///\n    /// Returns the string matched against.\n    ///\n    const QString &string() const;\n\n    ///\n    /// Returns a \\ref Match for _string_.\n    ///\n    Match match(const QString &string) const;\n\n    ///\n    /// Returns the max \\ref Match for the strings _first_ and _remainder_.\n    ///\n    Match match(QString first, auto... remainder) const\n    { return std::max(match(first), match(remainder...)); }\n\n    ///\n    /// Returns the max \\ref Match in the range of _strings_.\n    ///\n    Match match(std::ranges::range auto &&strings) const\n         requires std::same_as<std::ranges::range_value_t<decltype(strings)>, QString>\n    {\n        if (strings.empty())\n            return Match();\n        return std::ranges::max(\n            strings | std::views::transform([this](const QString &s) { return this->match(s); }));\n    }\n\n\nprivate:\n\n    class Private;\n    std::unique_ptr<Private> d;\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/messagebox.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/export.h>\nclass QWidget;\nclass QString;\n\nnamespace albert\n{\n\n/// @name Message boxes\n/// @addtogroup util_ui\n/// @{\n\n///\n/// Shows a question message box with Yes and No buttons.\n///\n/// The title of the message box is set to the application name and the message to _text_.\n/// The message box will appear modal to _parent_ or the main window if undefined.\n/// Returns \\c true if the user pressed yes, \\c false otherwise.\n///\nALBERT_EXPORT bool question(const QString &text, QWidget *parent = nullptr);\n\n///\n/// Shows an information message box with a single Ok button.\n///\n/// The title of the message box is set to the application name and the message to _text_.\n/// The message box will appear modal to _parent_ or the main window if undefined.\n///\nALBERT_EXPORT void information(const QString &text, QWidget *parent = nullptr);\n\n///\n/// Shows a warning message box with a single Ok button.\n///\n/// The title of the message box is set to the application name and the message to _text_.\n/// The message box will appear modal to _parent_ or the main window if undefined.\n///\nALBERT_EXPORT void warning(const QString &text, QWidget *parent = nullptr);\n\n///\n/// Shows a critical message box with a single Ok button.\n///\n/// The title of the message box is set to the application name and the message to _text_.\n/// The message box will appear modal to _parent_ or the main window if undefined.\n///\nALBERT_EXPORT void critical(const QString &text, QWidget *parent = nullptr);\n\n/// @}\n\n}\n"
  },
  {
    "path": "include/albert/networkutil.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/export.h>\nclass QNetworkAccessManager;\nclass QNetworkReply;\nclass QString;\n\nnamespace albert\n{\n\n/// \\addtogroup util_net\n/// @{\n\n///\n/// Returns a global, threadlocal QNetworkAccessManager.\n///\nALBERT_EXPORT QNetworkAccessManager &network();\n\n///\n/// Blocks until _reply_ is finished.\n///\nALBERT_EXPORT QNetworkReply *await(QNetworkReply *reply);\n\n///\n/// Returns _string_ percent encoded.\n///\nALBERT_EXPORT QString percentEncoded(const QString &string);\n\n///\n/// Returns _string_ percent decoded.\n///\nALBERT_EXPORT QString percentDecoded(const QString &string);\n\n/// @}\n\n}\n"
  },
  {
    "path": "include/albert/notification.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QObject>\n#include <albert/export.h>\n\nnamespace albert\n{\n\n///\n/// The notification class.\n///\n/// This is basically a wrapper around the QNotification class.\n/// @see https://github.com/QtCommunity/QNotification\n///\n/// \\ingroup util_system\n///\nclass ALBERT_EXPORT Notification final : public QObject\n{\n    Q_OBJECT\n\npublic:\n\n    ///\n    /// Constructs a notification with the given _title_ and _text_.\n    ///\n    Notification(const QString &title = {},\n                 const QString &text = {},\n                 QObject *parent = nullptr);\n\n    ///\n    /// Destructs the notification.\n    ///\n    ~Notification();\n\n    ///\n    /// Returns the title of the notification.\n    ///\n    const QString &title() const;\n\n    ///\n    /// Sets the title of the notification to _title_.\n    ///\n    void setTitle(const QString &title);\n\n    ///\n    /// Returns the text of the notification.\n    ///\n    const QString &text() const;\n\n    ///\n    /// Sets the text of the notification to _text_.\n    ///\n    void setText(const QString &text);\n\n    ///\n    /// Send the notification to the notification server.\n    ///\n    /// This will add the notification to the notification server\n    /// and present it to the user (Subject to the users settings).\n    ///\n    void send();\n\n    ///\n    /// Dismiss the notification.\n    ///\n    /// This will remove the notification from the notification server.\n    ///\n    void dismiss();\n\nsignals:\n\n    ///\n    /// Emitted when the notification is activated, i.e. the user clicked on the notification.\n    ///\n    void activated();\n\nprivate:\n\n    class ALBERT_NO_EXPORT Private;\n    std::unique_ptr<Private> d;\n    friend class ALBERT_NO_EXPORT QNotificationManager;\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/oauth.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QDateTime>\n#include <QObject>\n#include <QString>\n#include <albert/export.h>\n#include <memory>\nclass QUrl;\n\nnamespace albert\n{\n\n///\n/// Provides OAuth2 authentication with support for the Authorization Code Flow with PKCE and\n/// refresh tokens.\n///\n/// See also \\ref OAuthConfigWidget.\n///\n/// \\ingroup util_net\n///\nclass ALBERT_EXPORT OAuth2 : public QObject\n{\n    Q_OBJECT\npublic:\n\n    ///\n    /// Constructs an \\ref OAuth2.\n    ///\n    OAuth2();\n\n    ///\n    /// Destruct the \\ref OAuth2.\n    ///\n    ~OAuth2();\n\n    ///\n    /// Requests access, i.e. starts the Authorization Code Flow to obtain an access token.\n    ///\n    void requestAccess();\n\n    ///\n    /// Updates the access token.\n    ///\n    void updateTokens();\n\n    ///\n    /// Returns the client identifier.\n    ///\n    const QString &clientId() const;\n\n    ///\n    /// Sets the client identifier to _id_.\n    ///\n    void setClientId(const QString &id);\n\n    ///\n    /// Returns the client secret.\n    ///\n    const QString &clientSecret() const;\n\n    ///\n    /// Sets the client secret to _secret_.\n    ///\n    void setClientSecret(const QString &secret);\n\n    ///\n    /// Returns the OAuth scope to request permissions for.\n    ///\n    const QString &scope() const;\n\n    ///\n    /// Sets the OAuth scope to request permissions for to _scope_.\n    ///\n    void setScope(const QString &scope);\n\n    ///\n    /// Returns the authorization URL.\n    ///\n    const QString &authUrl() const;\n\n    ///\n    /// Sets the authorization URL to _url_.\n    ///\n    void setAuthUrl(const QString &url);\n\n    ///\n    /// Returns the redirect URI.\n    ///\n    const QString &redirectUri() const;\n\n    ///\n    /// Sets the redirect URI to _uri_.\n    ///\n    void setRedirectUri(const QString &uri);\n\n    ///\n    /// Returns true if PKCE is enabled, false otherwise.\n    ///\n    bool isPkceEnabled() const;\n\n    ///\n    /// Sets whether PKCE is enabled or not.\n    ///\n    void setPkceEnabled(bool enabled);\n\n    ///\n    /// Returns the token URL.\n    ///\n    const QString &tokenUrl() const;\n\n    ///\n    /// Sets the token URL to _url_.\n    ///\n    void setTokenUrl(const QString &url);\n\n    ///\n    /// Returns the access token.\n    ///\n    const QString &accessToken() const;\n\n    ///\n    /// Returns the access token.\n    ///\n    const QString &refreshToken() const;\n\n    ///\n    /// Returns the access token.\n    ///\n    const QDateTime &tokenExpiration() const;\n\n    ///\n    /// Sets the access token, refresh token and expiration date.\n    ///\n    void setTokens(const QString &access_token,\n                   const QString &refresh_token = {},\n                   const QDateTime &expiration = {});\n\n    ///\n    /// Returns the error message if any.\n    ///\n    const QString &error() const;\n\n    enum class State {\n        NotAuthorized,  ///< Not yet authorized.\n        Awaiting,  ///< Waiting for user interaction to authorize.\n        Granted  ///< Authorization granted and access token available.\n    };\n\n    ///\n    /// Returns the state of the authorization flow.\n    ///\n    State state() const;\n\n    ///\n    /// Handles the redirect callback URL from the OAuth2 provider.\n    ///\n    void handleCallback(const QUrl &callback);\n\nsignals:\n\n    void clientIdChanged(const QString &);  ///< Emitted when the client ID changes.\n    void clientSecretChanged(const QString &);  ///< Emitted when the client secret changes.\n    void scopeChanged(const QString &);  ///< Emitted when the scope changes.\n    void authUrlChanged(const QString &);  ///< Emitted when the authorization URL changes.\n    void redirectUriChanged(const QString &);  ///< Emitted when the redirect URI changes.\n    void tokenUrlChanged(const QString &);  ///< Emitted when the token URL changes.\n    void tokensChanged();  ///< Emitted when the access token, refresh token or expiration date changes.\n    void stateChanged(State);  ///< Emitted when the state changes.\n\nprivate:\n\n    class Private;\n    std::unique_ptr<Private> d;\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/oauthconfigwidget.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QString>\n#include <QWidget>\n#include <albert/export.h>\n\nnamespace albert\n{\nclass OAuth2;\n\n///\n/// Ready to use OAuth login widget.\n///\n/// \\ingroup util_ui\n///\nclass ALBERT_EXPORT OAuthConfigWidget : public QWidget\n{\npublic:\n\n    ///\n    /// Constructs an \\ref OAuthConfigWidget for _oauth_.\n    ///\n    OAuthConfigWidget(OAuth2 &oauth);\n\n    ///\n    /// Destructs the \\ref OAuthConfigWidget.\n    ///\n    ~OAuthConfigWidget();\n\nprivate:\n\n    class Private;\n    std::unique_ptr<Private> d;\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/plugindependency.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QCoreApplication>\n#include <albert/app.h>\n#include <albert/export.h>\n#include <albert/logging.h>\n\nnamespace albert\n{\n\ntemplate<class T>\nclass Dependency\n{\npublic:\n\n    inline operator T() const { return dependency_; }\n    inline operator bool() const { return dependency_ != nullptr; }\n    const T* operator->() const { return dependency_; }\n    T* operator->() { return dependency_; }\n    const T* get() const { return dependency_; }\n    T* get() { return dependency_; }\n\nprotected:\n\n    Dependency() = default;\n    ~Dependency() = default;\n\n    T *dependency_ = nullptr;\n};\n\n///\n/// Convenience holder class for hard plugin dependencies.\n///\n/// Fetches and holds a weak pointer an extension `id` of type `T`. This class is intended to be\n/// initialized on plugin constuction without a try-catch block.\n///\n/// @note Hard dependencies have to be listed in the plugin metadata, such that the plugin loader is\n/// able to manage the loading order.\n///\n/// \\ingroup util_plugin\n///\ntemplate<class T>\nclass ALBERT_EXPORT StrongDependency final : public Dependency<T>\n{\npublic:\n\n    ///\n    /// Constructs a StrongDependency with `id`.\n    ///\n    /// @throws std::runtime_error if the dependency is not available or not of type `T`.\n    ///\n    StrongDependency(QString id)\n    {\n        try\n        {\n            this->dependency_ = dynamic_cast<T*>(App::instance().extensions().at(id));\n\n            if (!this->dependency_)\n                throw std::runtime_error(\n                        QCoreApplication::translate(\n                            \"Dependency\",\n                            \"Extension '%1' is available, but it is not of the expected type.\"\n                        ).arg(id).toStdString());\n        }\n        catch (const std::out_of_range &)\n        {\n            throw std::runtime_error(\n                        QCoreApplication::translate(\n                            \"Dependency\",\n                            \"The required extension '%1' is not available.\"\n                        ).arg(id).toStdString());\n        }\n    }\n};\n\n\n///\n/// Convenience holder class for soft plugin dependencies.\n///\n/// Watches for (de)registration of an extension `id` of type `T`. On (de)registration of the\n/// dependency the pointer is updated and the callback called.\n///\n/// If you use this class you may want to lock a mutex against a query handler or an action.\n/// This should not be necessary since the plugin loader and the extension registry must not be\n/// accessed while a session query is running. Take care though if you defer the execution of\n/// the action using a timer or similar.\n///\n/// \\ingroup util_plugin\n///\ntemplate<class T>\nclass ALBERT_EXPORT WeakDependency final : public Dependency<T>\n{\npublic:\n\n    ///\n    /// Constructs a WeakDependency with `id` and callback `on_registered`.\n    ///\n    explicit WeakDependency(const QString &id, std::function<void(bool)> on_registered = {}):\n        callback(on_registered),\n        id_(id)\n    {\n        try {\n            this->dependency_ = dynamic_cast<T*>(App::instance().extensions().at(id));\n            if (!this->dependency_)\n                WARN << QStringLiteral(\"Found '%1' but failed casting to expected type.\").arg(id);\n        } catch (const std::out_of_range &) { /* okay, optional */ }\n\n        conn_add_ = QObject::connect(&App::instance(), &App::added,\n                                     [this](Extension *e){ onRegistered(e);});\n\n        conn_rem_ = QObject::connect(&App::instance(), &App::removed,\n                                     [this](Extension *e){ onDeregistered(e);});\n    }\n\n    ~WeakDependency()\n    {\n        QObject::disconnect(conn_add_);\n        QObject::disconnect(conn_rem_);\n    }\n\n    std::function<void(bool)> callback;\n\nprivate:\n\n    void onRegistered(Extension *e)\n    {\n        if (e->id() != this->id_)\n            return;\n\n        if (!this->dependency_)\n        {\n            if (auto *d = dynamic_cast<T*>(e); d)\n            {\n                this->dependency_ = d;\n                if (callback)\n                    callback(true);\n            }\n            else\n                WARN << QStringLiteral(\"Failed casting '%1' to expected type.\").arg(this->id_);\n        }\n        else\n            CRIT << \"WeakDependency already set. Internal logic error?\";\n    }\n\n    void onDeregistered(Extension *e)\n    {\n\n        if (e->id() != this->id_)\n            return;\n\n        if (this->dependency_)\n        {\n            if (auto *d = dynamic_cast<T*>(e); d)\n            {\n                if (callback)\n                    callback(false);  // the dependency should still be usable in the callback\n                this->dependency_ = nullptr;\n            }\n            else\n                WARN << QStringLiteral(\"Failed casting '%1' to expected type.\").arg(this->id_);\n        }\n        else\n            CRIT << \"WeakDependency already unset. Internal logic error?\";\n    }\n\n    QMetaObject::Connection conn_add_;\n    QMetaObject::Connection conn_rem_;\n    QString id_;\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/plugininstance.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QObject>\n#include <QString>\n#include <albert/export.h>\n#include <albert/plugin.h>\n#include <filesystem>\n#include <memory>\n#include <vector>\nclass QSettings;\nclass QWidget;\n\nnamespace albert\n{\nclass Extension;\nclass PluginLoader;\n\n///\n/// Abstract plugin instance class.\n///\n/// The class every plugin has to inherit.\n///\n/// If the plugin instantiation fails you are supposed to print errors in english to the logs and\n/// throw a localized message that will be shown to the user.\n///\n/// \\ingroup core_plugin\n///\nclass ALBERT_EXPORT PluginInstance : public QObject\n{\n    Q_OBJECT\n\npublic:\n\n    ///\n    /// Triggers the asynchronous initialization.\n    ///\n    /// Implementations have to emit \\ref initialized() or call base::initialize() when done\n    /// initializing.\n    ///\n    virtual void initialize();\n\n    ///\n    /// Creates a widget that can be used to configure the plugin properties.\n    ///\n    /// The caller takes ownership of the returned object.\n    ///\n    virtual QWidget *buildConfigWidget();\n\n    ///\n    /// Returns the extensions provided by this plugin.\n    ///\n    /// The caller does **not** take ownership of the returned objects.\n    ///\n    virtual std::vector<albert::Extension *> extensions();\n\npublic:\n    /// Returns the loader of this plugin.\n    [[nodiscard]] const PluginLoader &loader() const;\n\n    /// Returns the writable cache location for this plugin.\n    [[nodiscard]] std::filesystem::path cacheLocation() const;\n\n    /// Returns the writable config location for this plugin.\n    [[nodiscard]] std::filesystem::path configLocation() const;\n\n    /// Returns the writable data location for this plugin.\n    [[nodiscard]] std::filesystem::path dataLocation() const;\n\n    ///\n    /// Returns the existing data locations for this plugin.\n    ///\n    /// This includes user, vendor, and system locations.\n    ///\n    [[nodiscard]] std::vector<std::filesystem::path> dataLocations() const;\n\n    ///\n    /// Creates a preconfigured `QSettings` object for plugin config data.\n    ///\n    /// Configured to use the group <plugin-id> in \\ref albert::config().\n    ///\n    [[nodiscard]] std::unique_ptr<QSettings> settings() const;\n\n    ///\n    /// Creates a preconfigured `QSettings` object for plugin state data.\n    ///\n    /// Configured to use the group <plugin-id> in \\ref albert::state().\n    ///\n    [[nodiscard]] std::unique_ptr<QSettings> state() const;\n\nsignals:\n\n    /// Emitted when the plugin has completed initialization.\n    void initialized();\n\nprotected:\n    /// Constructs a plugin instance.\n    PluginInstance();\n\n    /// Destructs the plugin instance.\n    virtual ~PluginInstance();\n\nprivate:\n    class Private;\n    std::unique_ptr<Private> d;\n};\n\n}  // namespace albert\n"
  },
  {
    "path": "include/albert/pluginloader.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QObject>\n#include <QString>\n#include <albert/export.h>\n\nnamespace albert\n{\nclass PluginInstance;\nclass PluginMetadata;\n\n///\n/// Asynchronous plugin loader.\n///\n/// Turns a physical plugin into a logical plugin instance.\n///\n/// Errors are intentionally not logged. Thats the responsibility of the plugin implementation. On\n/// errors, implementations are expected to throw a localized message and print english logs using\n/// their logging category.\n///\n/// Implementations have to set \\ref current_loader before calling the constructor of the plugin\n/// instance. This avoids injection mechanisms and therefore reduces boilerplate for \\ref\n/// PluginInstance implementations.\n///\n/// Implementations have to emit \\ref finished(), when the loading process finished. On success \\ref\n/// instance() returns a valid \\ref PluginInstance. _info_ contains additional information. On error\n/// \\ref instance() returns a nullptr, i.e. the plugin failed to load, _info_ is an error message.\n///\n/// \\ingroup core_plugin\n///\nclass ALBERT_EXPORT PluginLoader : public QObject\n{\n    Q_OBJECT\n\npublic:\n    /// Returns the path to the plugin.\n    virtual QString path() const = 0;\n\n    /// Returns the plugin metadata.\n    virtual const PluginMetadata &metadata() const = 0;\n\n    /// Starts asynchronous loading process of the plugin.\n    virtual void load() = 0;\n\n    /// Unloads the plugin.\n    virtual void unload() = 0;\n\n    /// Returns the \\ref PluginInstance if the plugin is loaded, else `nullptr`.\n    virtual albert::PluginInstance *instance() = 0;\n\n    /// The static injection pointer.\n    static thread_local PluginLoader *current_loader;\n\nsignals:\n\n    /// Emitted when the loading process finished.\n    void finished(QString info);\n\nprotected:\n\n    virtual ~PluginLoader();\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/pluginmetadata.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QStringList>\n#include <albert/export.h>\n\nnamespace albert\n{\n\n///\n/// Common plugin metadata.\n///\n/// \\ingroup core_plugin\n///\nclass ALBERT_EXPORT PluginMetadata\n{\npublic:\n\n    ///\n    /// Plugin interface identifier.\n    ///\n    /// The core app API version used.\n    ///\n    QString iid;\n\n    ///\n    /// Unique identifier.\n    ///\n    /// No duplicates allowed. To avoid name conflicts implementations should prefix their plugins\n    /// ids with the id of the loader id.\n    ///\n    QString id;\n\n    ///\n    /// [Semantic version](https://semver.org/).\n    ///\n    QString version;\n\n    ///\n    /// Human readable name.\n    ///\n    QString name;\n\n    ///\n    /// Brief, imperative description.\n    ///\n    QString description;\n\n    ///\n    /// [SPDX short-form license identifier](https://spdx.org/licenses/).\n    ///\n    QString license;\n\n    ///\n    /// Browsable source.\n    ///\n    QString url;\n\n    ///\n    /// Online readme.\n    ///\n    QString readme_url;\n\n    ///\n    /// Available translations.\n    ///\n    QStringList translations;\n\n    ///\n    /// The copyright holders.\n    ///\n    QStringList authors;\n\n    ///\n    /// The current maintainers.\n    ///\n    QStringList maintainers;\n\n    ///\n    /// Required libraries.\n    ///\n    QStringList runtime_dependencies;\n\n    ///\n    /// Required executables.\n    ///\n    QStringList binary_dependencies;\n\n    ///\n    /// Required plugins.\n    ///\n    QStringList plugin_dependencies;\n\n    ///\n    /// Third party credits and license notes.\n    ///\n    QStringList third_party_credits;\n\n    ///\n    /// List of supported platforms.\n    ///\n    /// If empty all platforms are supported.\n    ///\n    QStringList platforms;\n\n    ///\n    /// The load type of the plugin.\n    ///\n    /// Some plugins have to be treated differently when loading.\n    /// E.g. a frontends are an integral part of the app, there has to be\n    /// exactly one frontend which will be loaded before any other plugins.\n    /// Some other pluins cannot be safely unloaded at runtime (e.g. Python).\n    ///\n    enum class LoadType {\n        User,          ///< Plugin should be user (un-)loadable.\n        Frontend,      ///< Loading handled by the core. Requires Frontend interface.\n    };\n\n\n    LoadType load_type; ///< \\copybrief LoadType\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/pluginprovider.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/extension.h>\n#include <vector>\n\nnamespace albert\n{\nclass PluginLoader;\n\n///\n/// Plugin provider interface class.\n///\n/// \\ingroup core_extension\n///\nclass ALBERT_EXPORT PluginProvider : virtual public Extension\n{\npublic:\n\n    ///\n    /// Returns references to the plugins provided by this plugin provider.\n    ///\n    /// The calles does **not** take ownership of the returned plugin loaders.\n    ///\n    virtual std::vector<PluginLoader*> plugins() = 0;\n\nprotected:\n\n    ///\n    /// Destructs the plugin provider.\n    ///\n    virtual ~PluginProvider();\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/query.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QString>\n#include <albert/export.h>\n#include <albert/querycontext.h>\nclass QueryEngine;\nnamespace albert\n{\nclass QueryHandler;\nclass QueryResult;\nclass QueryResults;\nclass QueryExecution;\n}  // namespace albert\n\nnamespace albert::detail\n{\n\n/// The query implementation.\nclass ALBERT_EXPORT Query : public albert::QueryContext\n{\npublic:\n    /// Constructs a query.\n    Query(UsageScoring usage_scoring,\n          std::vector<albert::QueryResult> &&fallbacks,\n          QueryHandler &handler,\n          QString trigger,\n          QString string);\n\n    /// Destructs the query.\n    ~Query();\n\n    /// \\copydoc albert::Query::isValid\n    bool isValid() const override;\n\n    /// \\copydoc albert::Query::handler\n    QueryHandler &handler() const override;\n\n    /// \\copydoc albert::Query::trigger\n    QString trigger() const override;\n\n    /// \\copydoc albert::Query::query\n    QString query() const override;\n\n    /// \\copydoc albert::Query::usageScoring\n    const UsageScoring &usageScoring() const override;\n\n    /// Returns the execution of this query if running; else nullptr.\n    QueryExecution &execution() const;\n\n    /// Stops the query execution.\n    void cancel();\n\n    /// Returns the matches.\n    QueryResults &matches();\n\n    /// Returns the fallbacks.\n    QueryResults &fallbacks();\n\nprivate:\n    class Private;\n    std::unique_ptr<Private> d;\n};\n\n}  // namespace albert::detail\n"
  },
  {
    "path": "include/albert/querycontext.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QString>\n#include <albert/export.h>\n\nnamespace albert\n{\nclass QueryHandler;\nclass UsageScoring;\n\n///\n/// Query interface.\n///\n/// \\ingroup core_query\n///\nclass ALBERT_EXPORT QueryContext\n{\npublic:\n\n    ///\n    /// Returns `true` if the query is valid; `false` if it has been cancelled.\n    ///\n    /// This function is thread-safe.\n    ///\n    virtual bool isValid() const = 0;\n\n    /// Returns the handler of this query.\n    virtual const QueryHandler &handler() const = 0;\n\n    /// Returns the trigger string of the query.\n    virtual QString trigger() const = 0;\n\n    /// Returns the query string of the query.\n    virtual QString query() const = 0;\n\n    /// Returns the usage scoring.\n    virtual const UsageScoring &usageScoring() const = 0;\n\n    /// Implicit QString context conversion.\n    operator QString() const { return query(); }\n\nprotected:\n\n    virtual ~QueryContext() = default;\n};\n\n}\n"
  },
  {
    "path": "include/albert/queryexecution.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QObject>\n#include <albert/export.h>\n#include <albert/queryresults.h>\n\nnamespace albert\n{\n\n///\n/// Abstract asynchronous query execution interface.\n///\n/// Controls the execution of a query, reports busy state and allows to fetch results on demand.\n///\n/// \\ingroup core_query\n///\nclass ALBERT_EXPORT QueryExecution : public QObject\n{\n    Q_OBJECT\n\npublic:\n\n    /// Constructs a query execution for _context_\n    QueryExecution(QueryContext &context);\n\n    /// The unique id of this query execution.\n    const uint id;\n\n    /// The query context of this query execution.\n    QueryContext &context;\n\n    /// The results of this query.\n    QueryResults results;\n\n    /// Cancels the query processing.\n    virtual void cancel() = 0;\n\n    /// Fetches more results.\n    virtual void fetchMore() = 0;\n\n    /// Returns `true` if there are more results to fetch, otherwise returns `false`.\n    virtual bool canFetchMore() const = 0;\n\n    /// Returns `true` if the query is being processed, otherwise returns `false`.\n    virtual bool isActive() const = 0;\n\nsignals:\n\n    ///\n    /// Emitted when query processing started or finished.\n    ///\n    /// @note The UI state machine expects results to be added only while active is `true`.\n    ///\n    void activeChanged(bool active);\n\n};\n\n}  // namespace albert\n"
  },
  {
    "path": "include/albert/queryhandler.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/export.h>\n#include <albert/extension.h>\n#include <albert/querycontext.h>\n#include <memory>\nclass QString;\nclass QueryEngine;\n\nnamespace albert\n{\nclass QueryExecution;\nclass QueryResults;\n\n///\n/// Base query handler interface for triggered queries.\n///\n/// This class defines the fundamental contract between the core and a query\n/// handler. It is used for triggered queries and is selected exclusively when\n/// its trigger matches the user input.\n///\n/// Implementations are responsible for executing the query asynchronously and\n/// providing results via a \\ref QueryExecution created by \\ref execution.\n///\n/// This interface is low-level and intentionally flexible. Implement it only\n/// if your use case is not covered by the convenience subclasses.\n///\n/// \\ingroup core_extension\n///\nclass ALBERT_EXPORT QueryHandler : virtual public Extension\n{\npublic:\n    ///\n    /// Returns the input hint for the given _query_.\n    ///\n    /// The returned string will be displayed in the input line if space permits.\n    ///\n    /// The base class implementation returns an empty string.\n    ///\n    virtual QString synopsis(const QString &query) const;\n\n    ///\n    /// Returns `true` if the user is allowed to set a custom trigger, otherwise returns `false`.\n    ///\n    /// The base class implementation returns `true`.\n    ///\n    virtual bool allowTriggerRemap() const;\n\n    ///\n    /// Returns the default trigger.\n    ///\n    /// The base class implementation returns \\ref Extension::id() with a space appended.\n    ///\n    virtual QString defaultTrigger() const;\n\n    ///\n    /// Returns `true` if the handler supports fuzzy matching, otherwise returns `false`.\n    ///\n    /// If `true`, the user can enable fuzzy matching for this handler and \\ref\n    /// setFuzzyMatching(bool) should be implemented accordingly.\n    ///\n    /// The base class implementation returns `false`.\n    ///\n    virtual bool supportsFuzzyMatching() const;\n\n    ///\n    /// Creates a query execution for the given _context_.\n    ///\n    /// The results are added to _results_ as they become available.\n    ///\n    virtual std::unique_ptr<QueryExecution> execution(QueryContext &context) = 0;\n\nprotected:\n    /// Destructs the handler.\n    ~QueryHandler() override;\n\n    ///\n    /// Sets the fuzzy matching mode to _enabled_.\n    ///\n    /// This function is called when the user toggles fuzzy matching for this handler.\n    ///\n    /// The base class implementation does nothing.\n    ///\n    virtual void setFuzzyMatching(bool enabled);\n\n    ///\n    /// Notifies that the user-defined trigger has changed to _trigger_.\n    ///\n    /// This function is called when the user changes the trigger for this handler.\n    ///\n    /// The base class implementation does nothing.\n    ///\n    virtual void setTrigger(const QString &trigger);\n\n    friend class ::QueryEngine;\n};\n\n}\n"
  },
  {
    "path": "include/albert/queryresults.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QObject>\n#include <albert/export.h>\n#include <albert/item.h>\n#include <albert/querycontext.h>\n#include <memory>\n#include <ranges>\n#include <vector>\n\nnamespace albert\n{\nclass Extension;\n\n///\n/// Result item associating an item with an extension.\n///\n/// \\ingroup core_query\n///\nclass ALBERT_EXPORT QueryResult\n{\npublic:\n    const Extension *extension; ///< The extension providing the item.\n    std::shared_ptr<Item> item; ///< The item.\n};\n\n///\n/// Query results container.\n///\n/// Holds the results of a \\ref Query and emits signals as expected by the UI models.\n///\n/// \\ingroup core_query\n///\nclass ALBERT_EXPORT QueryResults : public QObject, private Item::Observer\n{\n    Q_OBJECT\n\npublic:\n\n    /// Constructs query results with the _context_ it belongs to.\n    QueryResults(const QueryContext &context);\n\n    /// Destructs the query results.\n    ~QueryResults() override;\n\n    /// Returns the result at index _index_.\n    inline QueryResult &operator[](size_t index) { return results[index]; }\n\n    /// @copybrief operator[](size_t)\n    inline const QueryResult &operator[](size_t index) const { return results[index]; }\n\n    /// Returns the number of results.\n    inline uint count() const { return results.size(); }\n\n    /// Activates the action at _action_index_ of the result item at _item_index_.\n    bool activate(uint item_index, uint action_index = 0);\n\n    ///\n    /// Appends a \\ref QueryResult constructed from _extension_ and _item_.\n    ///\n    /// Use the range add methods to avoid UI flicker.\n    ///\n    void add(const Extension &extension, ItemPtr auto &&item)\n    {\n        emit resultsAboutToBeInserted(results.size(), results.size());\n        item->addObserver(this);\n        results.emplace_back(&extension, std::forward<decltype(item)>(item));\n        emit resultsInserted();\n    }\n\n    ///\n    /// Appends a \\ref QueryResult constructed from _item_ and the handler this results belong to.\n    ///\n    /// Use the range add methods to avoid UI flicker.\n    ///\n    void add(ItemPtr auto &&item) { add(context.handler(), std::forward<decltype(item)>(item)); }\n\n    /// Appends _query_results_ to the results.\n    void add(std::ranges::range auto &&query_results)\n        requires(std::same_as<QueryResult, std::ranges::range_value_t<decltype(query_results)>>)\n    {\n        if (!query_results.empty())\n        {\n            const auto count = std::ranges::distance(query_results);\n            results.reserve(results.size() + count);\n            emit resultsAboutToBeInserted(results.size(), results.size() + count - 1);\n            for (auto&& query_result : query_results)\n            {\n                query_result.item->addObserver(this);\n                // results.emplace_back(std::forward_like<decltype(query_results)>(query_result));\n                // TODO remove if forward_like is available everywhere (26.04)\n                if constexpr (std::is_lvalue_reference_v<decltype(query_results)>)\n                    results.emplace_back(query_result);\n                else\n                    results.emplace_back(std::move(query_result));\n            }\n            emit resultsInserted();\n        }\n    }\n\n    /// Appends \\ref QueryResult's constructed from _extension_ and _items_.\n    void add(const Extension &extension, ItemRange auto &&items)\n    {\n        if (!items.empty())\n        {\n            const auto count = std::ranges::distance(items);\n            results.reserve(results.size() + count);\n            emit resultsAboutToBeInserted(results.size(), results.size() + count - 1);\n            for (auto&& item : items)\n            {\n                item->addObserver(this);\n                // results.emplace_back(std::forward_like<decltype(query_results)>(query_result));\n                // TODO remove if forward_like is available everywhere (26.04)\n                if constexpr (std::is_lvalue_reference_v<decltype(items)>)\n                    results.emplace_back(&extension, item);\n                else\n                    results.emplace_back(&extension, std::move(item));\n            }\n            emit resultsInserted();\n        }\n    }\n\n    /// Appends \\ref QueryResult's constructed from _items_ and the handler this results belong to.\n    void add(ItemRange auto &&items){ add(context.handler(), std::forward<decltype(items)>(items)); }\n\n    /// Removes _count_ results starting from _index_.\n    void remove(uint index, uint count = 1)\n    {\n        for (auto i = index; i < index + count; ++i)\n            results[i].item->removeObserver(this);\n\n        emit resultsAboutToBeRemoved(index, index + count - 1);\n        results.erase(results.begin() + index,results.begin() + index + count);\n        emit resultsRemoved();\n\n    }\n\n    /// Removes all results.\n    void reset()\n    {\n        for (auto&& result : results)\n            result.item->removeObserver(this);\n        emit resultsAboutToBeReset();\n        results.clear();\n        emit resultsReset();\n    }\n\nsignals:\n\n    /// Emitted before results are inserted.\n    void resultsAboutToBeInserted(int first, int last);\n\n    /// Emitted after results have been inserted.\n    void resultsInserted();\n\n    /// Emitted before results are removed.\n    void resultsAboutToBeRemoved(int first, int last);\n\n    /// Emitted after results have been removed.\n    void resultsRemoved();\n\n    /// Emitted before results are moved.\n    void resultsAboutToBeMoved(int srcFirst, int srcLast, int dst);\n\n    /// Emitted after results have been moved.\n    void resultsMoved();\n\n    /// Emitted before all results are reset.\n    void resultsAboutToBeReset();\n\n    /// Emitted after all results have been reset.\n    void resultsReset();\n\n    /// Emitted when a result changed.\n    void resultChanged(uint i);\n\n    /// Emitted when a result was activated.\n    void resultActivated(QString query, QString extension_id, QString item_id, QString action_id);\n\nprivate:\n\n    void notify(const albert::Item *item) override;\n\n    const QueryContext &context;\n    std::vector<QueryResult> results;\n\n};\n\n}\n"
  },
  {
    "path": "include/albert/rankedqueryhandler.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/generatorqueryhandler.h>\n#include <albert/rankitem.h>\n#include <vector>\n\nnamespace albert\n{\n\n///\n/// Usage-ranked query handler.\n///\n/// Convenience base class for triggered query handlers that return a complete set of match-scored\n/// items eagerly. \\ref rankItems is executed in a worker thread, allowing CPU-bound work without\n/// blocking the main thread. The provided match scores will be combined with the usage-based\n/// scoring weighted by user configuration. Finally the items will be yielded lazily in order of\n/// their final score.\n///\n/// \\ingroup util_query\n///\nclass ALBERT_EXPORT RankedQueryHandler : public GeneratorQueryHandler\n{\npublic:\n    ///\n    /// Returns a list of scored matches for _context_.\n    ///\n    /// The match score should make sense and often is the fraction of matched characters (legth of\n    /// query string / length of matched string). The empty pattern matches everything and returns\n    /// all items with a score of 0.\n    ///\n    /// \\note Executed in a background thread.\n    ///\n    virtual std::vector<RankItem> rankItems(QueryContext &context) = 0;\n\n    /// Yields _rank_items_ lazily sorted.\n    static ItemGenerator lazySort(std::vector<RankItem> rank_items);\n\n    /// Yields result of \\ref rankItems for _context_ usage scored and lazily sorted.\n    ItemGenerator items(QueryContext &context) override;\n\nprotected:\n    /// Destructs the handler.\n    ~RankedQueryHandler() override;\n};\n\n}  // namespace albert\n"
  },
  {
    "path": "include/albert/rankitem.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/export.h>\n#include <albert/item.h>\n#include <memory>\n\nnamespace albert\n{\n\n///\n/// An Item with a score.\n///\n/// Used to rank item results of mutliple handlers\n///\n/// \\ingroup core_query\n///\nclass ALBERT_EXPORT RankItem\n{\npublic:\n\n    ///\n    /// Constructs a RankItem with the given `item` and `score`.\n    ///\n    explicit RankItem(const std::shared_ptr<Item> &item, double score) noexcept;\n\n    ///\n    /// Constructs a RankItem with the given `item` and `score` using move semantics.\n    ///\n    explicit RankItem(std::shared_ptr<Item> &&item, double score) noexcept;\n\n    ///\n    /// The less operator\n    ///\n    bool operator<(const RankItem &other) const;\n\n    ///\n    /// The greater operator\n    ///\n    bool operator>(const RankItem &other) const;\n\n    ///\n    /// The matched item\n    ///\n    std::shared_ptr<Item> item;\n\n    ///\n    /// The match score.\n    ///\n    /// The match score should make sense in the context of the matched item. Often \"make sense\"\n    /// means it should be the fraction of matched characters over length of the string matched\n    /// agaist. The empty string should yield a match with a score of 0.\n    ///\n    /// Must be in the range (0,1]. Not checked for performance.\n    ///\n    double score;\n};\n\n}\n"
  },
  {
    "path": "include/albert/ratelimiter.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QObject>\n#include <albert/export.h>\n#include <memory>\n\nnamespace albert::detail\n{\nclass RateLimiterPrivate;\n\nclass ALBERT_EXPORT Acquire : public QObject\n{\n    Q_OBJECT\npublic:\n    Acquire();\n    ~Acquire() override;\n\n    bool isGranted();\n\n    bool await(std::function<bool()> stop_requested);\n\nsignals:\n    void granted();\n\nprivate:\n    class Private;\n    std::unique_ptr<Private> d;\n\n    friend class RateLimiterPrivate;\n};\n\nclass ALBERT_EXPORT RateLimiter : public QObject\n{\n    Q_OBJECT\npublic:\n    RateLimiter(uint delay);\n    ~RateLimiter() override;\n\n    void setDelay(uint delay);\n    uint delay() const;\n\n    std::unique_ptr<Acquire> acquire();\n\nprivate:\n    std::unique_ptr<RateLimiterPrivate> d;\n};\n\n}  // namespace albert::detail\n"
  },
  {
    "path": "include/albert/standarditem.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/item.h>\n#include <memory>\n#include <vector>\nnamespace albert{ class Icon; }\n\nnamespace albert\n{\n\n///\n/// General purpose \\ref Item implementation.\n///\n/// \\ingroup util_query\n///\nclass ALBERT_EXPORT StandardItem : public Item\n{\npublic:\n\n    ///\n    /// Constructs a \\ref StandardItem with the contents initialized with the data passed.\n    ///\n    template<typename T_ID = QString,\n             typename T_TEXT = QString,\n             typename T_SUBTEXT = QString,\n             typename T_ICON_FACTORY = std::function<std::unique_ptr<Icon>()>,\n             typename T_ACTIONS = std::vector<Action>,\n             typename T_INPUTACTION = QString>\n        requires(std::same_as<std::remove_cvref_t<T_ID>, QString>\n                 && std::same_as<std::remove_cvref_t<T_TEXT>, QString>\n                 && std::same_as<std::remove_cvref_t<T_SUBTEXT>, QString>\n                 && std::convertible_to<std::remove_cvref_t<T_ICON_FACTORY>, std::function<std::unique_ptr<Icon>()>>\n                 && std::same_as<std::remove_cvref_t<T_ACTIONS>, std::vector<Action>>\n                 && std::same_as<std::remove_cvref_t<T_INPUTACTION>, QString>)\n    StandardItem(T_ID &&id,\n                 T_TEXT &&text,\n                 T_SUBTEXT &&subtext,\n                 T_ICON_FACTORY &&icon_factory,\n                 T_ACTIONS &&actions = std::vector<Action>{},\n                 T_INPUTACTION &&input_action_text = QString{}) noexcept :\n        id_(std::forward<T_ID>(id)),\n        text_(std::forward<T_TEXT>(text)),\n        subtext_(std::forward<T_SUBTEXT>(subtext)),\n        icon_factory_(std::forward<T_ICON_FACTORY>(icon_factory)),\n        actions_(std::forward<T_ACTIONS>(actions)),\n        input_action_text_(std::forward<T_INPUTACTION>(input_action_text))\n    {}\n\n\n    ///\n    /// Constructs a `shared_ptr` holding a \\ref StandardItem with the contents initialized with the\n    /// data passed.\n    ///\n    /// Convenience function for readability. See the \\ref StandardItem::StandardItem for details.\n    ///\n    template<typename T_ID = QString,\n             typename T_TEXT = QString,\n             typename T_SUBTEXT = QString,\n             typename T_ICON_FACTORY = std::function<std::unique_ptr<Icon>()>,\n             typename T_ACTIONS = std::vector<Action>,\n             typename T_INPUTACTION = QString>\n        requires(std::same_as<std::decay_t<T_ID>, QString>\n                 && std::same_as<std::decay_t<T_TEXT>, QString>\n                 && std::same_as<std::decay_t<T_SUBTEXT>, QString>\n                 && std::convertible_to<std::decay_t<T_ICON_FACTORY>, std::function<std::unique_ptr<Icon>()>>\n                 && std::same_as<std::decay_t<T_ACTIONS>, std::vector<Action>>\n                 && std::same_as<std::decay_t<T_INPUTACTION>, QString>)\n    static std::shared_ptr<StandardItem> make(T_ID &&id,\n                                              T_TEXT &&text,\n                                              T_SUBTEXT &&subtext,\n                                              T_ICON_FACTORY &&icon_factory,\n                                              T_ACTIONS &&actions = std::vector<Action>{},\n                                              T_INPUTACTION &&input_action_text = QString{}) noexcept\n    {\n        return std::make_shared<StandardItem>(std::forward<T_ID>(id),\n                                              std::forward<T_TEXT>(text),\n                                              std::forward<T_SUBTEXT>(subtext),\n                                              std::forward<T_ICON_FACTORY>(icon_factory),\n                                              std::forward<T_ACTIONS>(actions),\n                                              std::forward<T_INPUTACTION>(input_action_text));\n    }\n\n    StandardItem(const StandardItem &) = delete;\n    StandardItem& operator=(const StandardItem&) = delete;\n\n    /// Constructs a \\ref StandardItem with the contents of _other_ using move semantics.\n    StandardItem(StandardItem &&other) noexcept = default;\n\n    /// Replaces the contents with those of _other_ using move semantics.\n    StandardItem &operator=(StandardItem &&other) noexcept = default;\n\n    /// Destructs the \\ref StandardItem.\n    ~StandardItem();\n\n    /// Sets the item identifier to _id_.\n    void setId(QString id);\n\n    /// Sets the item text to _text_.\n    void setText(QString text);\n\n    /// Sets the item subtext to _text_.\n    void setSubtext(QString text);\n\n    /// Sets the item factory to _icon_factory_.\n    void setIconFactory(std::function<std::unique_ptr<Icon>()> icon_factory);\n\n    /// Returns the item icon factory.\n    std::function<std::unique_ptr<Icon>()> iconFactory();\n\n    /// Sets the item actions to _actions_.\n    void setActions(std::vector<Action> actions);\n\n    /// Sets the item input action text to _text_.\n    void setInputActionText(QString text);\n\n    QString id() const override;\n    QString text() const override;\n    QString subtext() const override;\n    QString inputActionText() const override;\n    std::unique_ptr<Icon> icon() const override;\n    std::vector<Action> actions() const override;\n\nprotected:\n    QString id_;\n    QString text_;\n    QString subtext_;\n    std::function<std::unique_ptr<Icon>()> icon_factory_;\n    std::vector<Action> actions_;\n    QString input_action_text_;\n};\n\n}\n"
  },
  {
    "path": "include/albert/systemutil.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QtGlobal>\n#include <albert/export.h>\n#include <filesystem>\nclass QString;\nclass QUrl;\ntemplate <typename T> class QList;\ntypedef QList<QString> QStringList;\n\nnamespace albert\n{\n\n/// @name System utility\n/// @addtogroup util_system\n/// @{\n\n///\n/// Opens _url_ with the default handler for the scheme.\n///\n/// Does nothing if _url_ is not a valid URL.\n///\nALBERT_EXPORT void openUrl(const QString &url);\n\n/// Opens _url_ with the default handler for the scheme.\nALBERT_EXPORT void open(const QUrl &url);\n\n/// Opens a file at _path_ with the associated default application.\nALBERT_EXPORT void open(const QString &path);\n\n/// Opens a file at _path_ with the associated default application.\nALBERT_EXPORT void open(const std::filesystem::path &path);\n\n/// Sets the system clipboard to _text_.\nALBERT_EXPORT void setClipboardText(const QString &text);\n\n/// Returns the `true` if the platform supports pasting, else `false`.\nALBERT_EXPORT bool havePasteSupport();\n\n///\n/// Sets the system clipboard to _text_ and pastes _text_ to the front-most window.\n///\n/// Check \\ref albert::havePasteSupport before using this function.\n///\nALBERT_EXPORT void setClipboardTextAndPaste(const QString &text);\n\n///\n/// Starts the _commandline_ in a new process, and detaches from it.\n///\n/// Returns the PID on success; otherwise returns 0.\n/// The working directory is the users home directory.\n///\nALBERT_EXPORT long long runDetachedProcess(const QStringList &commandline);\n\n///\n/// Starts the _commandline_ in a new process, and detaches from it.\n///\n/// Returns the PID on success; otherwise returns 0.\n/// The process will be started in the directory `working_dir`.\n/// If `working_dir` is empty, the working directory is the users home directory.\n///\nALBERT_EXPORT long long runDetachedProcess(const QStringList &commandline, const QString &working_dir);\n\n/// Returns a QString representation of _path_.\nALBERT_EXPORT QString toQString(const std::filesystem::path &path);\n\n#ifdef Q_OS_MAC\n///\n/// Execute the AppleScript _script_. Returns any return value of the script.\n///\n/// Throws runtime_error in case of an error.\n/// Available on macOS only.\n///\nALBERT_EXPORT QString runAppleScript(const QString &script);\n#endif\n\n/// @}\n\n}\n"
  },
  {
    "path": "include/albert/telemetryprovider.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QJsonObject>\n#include <albert/export.h>\n#include <albert/extension.h>\n\n\n// PRIVATE API - DO NOT USE!\n\n\nnamespace albert::detail\n{\nclass ALBERT_EXPORT TelemetryProvider : virtual public Extension\n{\npublic:\n    virtual QJsonObject telemetryData() const = 0;\n\nprotected:\n    ~TelemetryProvider() override;\n};\n}\n"
  },
  {
    "path": "include/albert/timeit.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n\n#pragma once\n#include <QDebug>\n#include <QString>\n#include <chrono>\n#include <albert/logging.h>\n\n// Private API\nnamespace albert::detail\n{\n\nstruct TimeIt\n{\n    QString name_;\n    std::chrono::system_clock::time_point start_;\n\n    [[nodiscard]] TimeIt(const QString &name = {}):\n        name_(name),\n        start_(std::chrono::system_clock::now())\n    {}\n\n    ~TimeIt()\n    {\n        auto end = std::chrono::system_clock::now();\n        auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end - start_).count();\n        CRIT << QStringLiteral(\"\\x1b[36m%L1 µs | %2\").arg(dur, 8).arg(name_);\n    }\n};\n\n}\n"
  },
  {
    "path": "include/albert/urlhandler.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <albert/export.h>\n#include <albert/extension.h>\nclass QUrl;\n\nnamespace albert\n{\n\n///\n/// Albert scheme URL handler interface.\n///\n/// Use this interface to register `albert:` URL handlers based on \\ref Extension::id.\n/// URLs with the host matching this extension's id are passed to the \\ref handle() method.\n/// E.g. the URL `albert://github/?…` will be redirected to the GitHub extension.\n///\n/// \\ingroup core_extension\n///\nclass ALBERT_EXPORT UrlHandler : virtual public Extension\n{\npublic:\n\n    ///\n    /// Handles the _url_ received.\n    ///\n    virtual void handle(const QUrl &url) = 0;\n\nprotected:\n\n    ~UrlHandler() override;\n};\n}\n"
  },
  {
    "path": "include/albert/usagescoring.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QString>\n#include <albert/export.h>\n#include <unordered_map>\n#include <vector>\n\nnamespace albert\n{\nclass RankItem;\n\n\nstruct ALBERT_EXPORT ItemKey\n{\n    QString extension_id;\n    QString item_id;\n    bool operator==(const ItemKey&) const = default;\n};\n\n\n///\n/// Modifies match scores according to user usage history and preferences.\n///\n/// \\ingroup core_query\n///\nclass ALBERT_EXPORT UsageScoring\n{\npublic:\n\n    /// Returns the modified _match_score_ for an item identified by _key_.\n    double modifiedMatchScore(const ItemKey &key, double match_score) const;\n\n    /// Modifies the match score of _rank_item_ for an item identified by _key_ in-place.\n    void modifyMatchScores(const QString &extension_id, std::vector<albert::RankItem> &rank_items) const;\n\n    /// If `true` perfect matches should be prioritized even if their usage score is lower.\n    bool prioritize_perfect_match;\n\n    /// The exponential decay applied to usage scores based on recency.\n    /// This value adjusts the influence of recent item activations using a geometric weighting\n    /// scheme: each activation contributes a weight of 1 / (memory_decay^recency).\n    /// A value of 1.0 disables decay, assigning equal weight to all activations and the score of an\n    /// item is the sum of its activations (Most Frequently Used).\n    /// A value of 0.5 implies that for any activation a_i in history the sum of all older\n    /// activations can not exceed the weight of a_i (Most Recently Used).\n    /// Valid range: [0.5, 1.0]\n    double memory_decay;\n\n    /// The usage scores.\n    std::shared_ptr<const std::unordered_map<ItemKey, double>> usage_scores;\n\n};\n\n}\n\n// Hashing specialization for ItemKey\ntemplate <>\nstruct std::hash<albert::ItemKey>\n{\n    // https://stackoverflow.com/questions/17016175/c-unordered-map-using-a-custom-class-type-as-the-key#comment39936543_17017281\n    inline std::size_t operator()(const albert::ItemKey& key) const\n    { return (qHash(key.extension_id) ^ (qHash(key.item_id)<< 1)); }\n};\n"
  },
  {
    "path": "include/albert/widgetsutil.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QCheckBox>\n#include <QDoubleSpinBox>\n#include <QLineEdit>\n#include <QSpinBox>\n#include <QString>\n#include <albert/export.h>\n\nnamespace albert\n{\n\n/// @name Property editor bindings\n/// @addtogroup util_ui\n/// @{\n\n///\n/// Binds a property of type `bool` of _object_ to _checkbox_.\n///\n/// Initializes _checkbox_ using _get_ and connects the `toggled` signal to _set_.\n///\ntemplate<typename T, typename GET, typename SET>\nvoid bindWidget(QCheckBox *checkbox, T *object, GET get, SET set)\n{\n    checkbox->setChecked((object->*get)());\n    QObject::connect(checkbox, &QCheckBox::toggled, object, set);\n}\n\n///\n/// Binds a property of type `bool` of _object_ to _checkbox_.\n///\n/// Initializes _checkbox_ using _get_,\n/// connects the `toggled` signal to _set_ and\n/// connects the signal _sig_ to `setChecked`.\n///\ntemplate<typename T, typename GET, typename SET, typename SIG>\nvoid bindWidget(QCheckBox *checkbox, T *object, GET get, SET set, SIG sig)\n{\n    bindWidget(checkbox, object, get, set);\n    QObject::connect(object, sig, checkbox, &QCheckBox::setChecked);\n}\n\n///\n/// Binds a property of type `QString` of _object_ to _lineedit_.\n///\n/// Initializes _lineedit_ using _get_ and\n/// connects the `editingFinished` signal to _set_.\n///\ntemplate<typename T, typename GET, typename SET>\nvoid bindWidget(QLineEdit *lineedit, T *object, GET get, SET set)\n{\n    lineedit->setText((object->*get)());\n    QObject::connect(lineedit, &QLineEdit::editingFinished,\n                     object, [lineedit, object, set] { (object->*set)(lineedit->text()); });\n}\n\n///\n/// Binds a property of type `QString` of _object_ to _lineedit_.\n///\n/// Initializes _lineedit_ using _get_,\n/// connects the `editingFinished` signal to _set_ and\n/// connects the signal _sig_ to `setText`.\n///\ntemplate<typename T, typename GET, typename SET, typename SIG>\nvoid bindWidget(QLineEdit *lineedit, T *object, GET get, SET set, SIG sig)\n{\n    bindWidget(lineedit, object, get, set);\n    QObject::connect(object, sig, lineedit, &QLineEdit::setText);\n}\n\n///\n/// Binds a property of type `int` of _object_ to _spinbox_.\n///\n/// Initializes _spinbox_ using _get_ and\n/// connects the `valueChanged` signal to _set_.\n///\ntemplate<typename T, typename GET, typename SET>\nvoid bindWidget(QSpinBox *spinbox, T *object, GET get, SET set)\n{\n    spinbox->setValue((object->*get)());\n    QObject::connect(spinbox, QOverload<int>::of(&QSpinBox::valueChanged), object, set);\n}\n\n///\n/// Binds a property of type `int` of _object_ to _spinbox_.\n///\n/// Initializes _spinbox_ using _get_,\n/// connects the `valueChanged` signal to _set_ and\n/// connects the signal _sig_ to `setValue`.\n///\ntemplate<typename T, typename GET, typename SET, typename SIG>\nvoid bindWidget(QSpinBox *spinbox, T *object, GET get, SET set, SIG sig)\n{\n    bindWidget(spinbox, object, get, set);\n    QObject::connect(object, sig, spinbox, &QSpinBox::setValue);\n}\n\n///\n/// Binds a property of type `double` of _object_ to _spinbox_.\n///\n/// Initializes _spinbox_ using _get_ and\n/// connects the `valueChanged` signal to _set_.\n///\ntemplate<typename T, typename GET, typename SET>\nvoid bindWidget(QDoubleSpinBox *spinbox, T *object, GET get, SET set)\n{\n    spinbox->setValue((object->*get)());\n    QObject::connect(spinbox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), object, set);\n}\n\n///\n/// Binds a property of type `double` of _object_ to _spinbox_.\n///\n/// Initializes _spinbox_ using _get_,\n/// connects the `valueChanged` signal to _set_ and\n/// connects the signal _sig_ to `setValue`.\n///\ntemplate<typename T, typename GET, typename SET, typename SIG>\nvoid bindWidget(QDoubleSpinBox *spinbox, T *object, GET get, SET set, SIG sig)\n{\n    bindWidget(spinbox, object, get, set);\n    QObject::connect(object, sig, spinbox, &QDoubleSpinBox::setValue);\n}\n\n/// @}\n\n}\n"
  },
  {
    "path": "resources/index.theme",
    "content": "# This is the builtin fallback theme.\n# Its purpose is to make plugin development easy.\n# Put your icons into it and query them on any platform.\n# On xdg the system icon theme is prefered.\n\n[Icon Theme]\nName=fallback\nComment=The builtin fallback icon theme\n\nDirectories=scalable\n\n[scalable]\nMinSize=1\nSize=128\nMaxSize=256\nType=Scalable\n"
  },
  {
    "path": "resources/resources.qrc",
    "content": "<RCC>\n    <qresource prefix=\"/icons/fallback/scalable\">\n        <file>albert-tray.svg</file>\n        <file>albert.svg</file>\n    </qresource>\n    <qresource prefix=\"/icons/fallback\">\n        <file>index.theme</file>\n    </qresource>\n</RCC>\n"
  },
  {
    "path": "src/app/application.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"application.h\"\n#include \"config.h\"\n#include \"extensionregistry.h\"\n#include \"frontend.h\"\n#include \"logging.h\"\n#include \"messagehandler.h\"\n#include \"pathmanager.h\"\n#include \"platform.h\"\n#include \"plugininstance.h\"\n#include \"pluginloader.h\"\n#include \"pluginmetadata.h\"\n#include \"pluginqueryhandler.h\"\n#include \"pluginregistry.h\"\n#include \"pluginswidget.h\"\n#include \"qtpluginprovider.h\"\n#include \"queryengine.h\"\n#include \"querywidget.h\"\n#include \"report.h\"\n#include \"rpcserver.h\"\n#include \"session.h\"\n#include \"settingswindow.h\"\n#include \"signalhandler.h\"\n#include \"systemtrayicon.h\"\n#include \"systemutil.h\"\n#include \"telemetry.h\"\n#include \"triggersqueryhandler.h\"\n#include \"urlhandler.h\"\n#include <QByteArray>\n#include <QCommandLineParser>\n#include <QDesktopServices>\n#include <QDir>\n#include <QFile>\n#include <QHotkey>\n#include <QJsonArray>\n#include <QJsonDocument>\n#include <QLibraryInfo>\n#include <QMessageBox>\n#include <QObject>\n#include <QPluginLoader>\n#include <QPointer>\n#include <QSettings>\n#include <QStandardPaths>\n#include <QTimer>\n#include <QTranslator>\n#include <iostream>\nQ_LOGGING_CATEGORY(AlbertLoggingCategory, \"albert\")\nusing namespace Qt::StringLiterals;\nusing namespace albert::detail;\nusing namespace albert;\nusing namespace std;\n\n\nnamespace {\nApp *app_instance = nullptr;\nstatic const char *STATE_LAST_USED_VERSION = \"last_used_version\";\nstatic const char *CFG_FRONTEND_ID = \"frontend\";\nstatic const char *DEF_FRONTEND_ID = \"widgetsboxmodel\";\nstatic const char *CFG_HOTKEY = \"hotkey\";\nstatic const char *DEF_HOTKEY = \"Ctrl+Space\";\n}\n\n// -------------------------------------------------------------------------------------------------\n\nApp::App()\n{\n    if (app_instance)\n        qFatal(\"There can be only one app instance.\");\n    app_instance = this;\n}\n\nApp::~App() { app_instance = nullptr; }\n\nApp &App::instance() { return *app_instance; }\n\nvoid App::restart()\n{ QMetaObject::invokeMethod(qApp, \"exit\", Qt::QueuedConnection, Q_ARG(int, -1)); }\n\nvoid App::quit()\n{ QMetaObject::invokeMethod(qApp, \"quit\", Qt::QueuedConnection); }\n\nconst filesystem::path &App::cacheLocation()\n{\n    static const auto path = filesystem::path(\n        QStandardPaths::writableLocation(QStandardPaths::CacheLocation).toStdString());\n    return path;\n}\n\nconst filesystem::path &App::configLocation()\n{\n    static const auto path = filesystem::path(\n        QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation).toStdString());\n    return path;\n}\n\nconst filesystem::path &App::dataLocation()\n{\n    static const auto path = filesystem::path(\n        QStandardPaths::writableLocation(QStandardPaths::AppDataLocation).toStdString());\n    return path;\n}\n\nunique_ptr<QSettings> App::settings()\n{\n    return make_unique<QSettings>(\n        QString::fromStdString((configLocation() / \"config\").string()),\n        QSettings::IniFormat\n    );\n}\n\nunique_ptr<QSettings> App::state()\n{\n    return make_unique<QSettings>(\n        QString::fromStdString((dataLocation() / \"state\").string()),\n        QSettings::IniFormat\n    );\n}\n\n// -------------------------------------------------------------------------------------------------\n\nclass Application::Private\n{\npublic:\n\n    Private(Application &app,\n            const QStringList &additional_plugin_paths,\n            bool load_enabled,\n            QSettings &settings,\n            QSettings &state);\n    ~Private();\n\n    void initHotkey(QSettings &settings);\n    void initRPC();\n    void initFrontend(QSettings &settings);\n\n    QString loadFrontend(albert::PluginLoader *);\n    void notifyVersionChange(QSettings &state);\n\npublic:\n\n    Application &app;\n\n    // As early as possible\n    RPCServer rpc_server; // Check for other instances first\n    SignalHandler unix_signal_handler;\n    PathManager path_manager;\n\n    // Core\n    albert::ExtensionRegistry extension_registry;\n    PluginRegistry plugin_registry;\n    QtPluginProvider plugin_provider;\n    QueryEngine query_engine;\n    Telemetry telemetry;\n    SystemTrayIcon tray_icon;\n\n    // Weak, lazy or optional\n    albert::PluginLoader *frontend_plugin{nullptr};\n    albert::detail::Frontend *frontend{nullptr};\n    std::unique_ptr<QHotkey> hotkey{nullptr};\n    std::unique_ptr<Session> session{nullptr};\n    QPointer<SettingsWindow> settings_window{nullptr};\n\n    PluginQueryHandler plugin_query_handler;\n    TriggersQueryHandler triggers_query_handler;\n\n};\n\n\nApplication::Private::Private(Application &q,\n                              const QStringList &additional_plugin_paths,\n                              bool load_enabled,\n                              QSettings &settings,\n                              QSettings &state):\n    app(q),\n    path_manager(settings),\n    plugin_registry(extension_registry, load_enabled),\n    plugin_provider(additional_plugin_paths),\n    query_engine(extension_registry),\n    telemetry(plugin_registry, extension_registry),\n    tray_icon(settings),\n    plugin_query_handler(plugin_registry),\n    triggers_query_handler(query_engine)\n{\n    platform::initPlatform();\n\n    // Install scheme handler\n    QDesktopServices::setUrlHandler(\"albert\", &app, \"handleUrl\");\n\n    initFrontend(settings);\n\n\n    connect(frontend, &Frontend::visibleChanged,\n            &app, [this]{\n                if (frontend->isVisible())\n                    session = make_unique<Session>(query_engine, *frontend);\n                else\n                    session.reset();\n            });\n\n    auto reset_session = [this] {\n        if (frontend->isVisible()) {\n            session.reset();  // Make sure session is deleted _before_ creating a new one\n            session = make_unique<Session>(query_engine, *frontend);\n        }\n    };\n\n    connect(&query_engine, &QueryEngine::queryHandlerAdded,\n            &app, reset_session);\n\n    connect(&query_engine, &QueryEngine::queryHandlerRemoved,\n            &app, reset_session, Qt::QueuedConnection);\n\n    initRPC(); // Also may trigger frontend\n\n    initHotkey(settings);  // Connect hotkey after! frontend has been loaded else segfaults\n\n    notifyVersionChange(state);\n\n    extension_registry.registerExtension(&plugin_query_handler);\n    extension_registry.registerExtension(&triggers_query_handler);\n\n    // Load plugins not before loop is executing\n    QTimer::singleShot(0, [this] { extension_registry.registerExtension(&plugin_provider); });\n}\n\nApplication::Private::~Private()\n{\n    QDesktopServices::unsetUrlHandler(\"albert\");\n\n    frontend->disconnect();\n    query_engine.disconnect();\n\n    if (hotkey)\n    {\n        hotkey.get()->disconnect();\n        hotkey->setRegistered(false);\n    }\n\n    delete settings_window.get();\n    session.reset();\n\n    extension_registry.deregisterExtension(&plugin_provider);  // unloads plugins\n    extension_registry.deregisterExtension(&triggers_query_handler);\n    extension_registry.deregisterExtension(&plugin_query_handler);\n\n    frontend_plugin->unload();\n}\n\nvoid Application::Private::initHotkey(QSettings &settings)\n{\n    if (!QHotkey::isPlatformSupported())\n    {\n        INFO << \"Hotkeys are not supported on this platform.\";\n        return;\n    }\n\n    auto s_hk = settings.value(CFG_HOTKEY, DEF_HOTKEY).toString();\n\n    if (s_hk.isEmpty())\n    {\n        DEBG << \"Hotkey explicitly unset.\";\n        return;\n    }\n\n    auto kc_hk = QKeySequence::fromString(s_hk)[0];\n\n    if (auto hk = make_unique<QHotkey>(kc_hk);\n        hk->setRegistered(true))\n    {\n        hotkey = ::move(hk);\n        connect(hotkey.get(), &QHotkey::activated,\n                frontend, [this]{ app.toggle(); });\n        INFO << \"Hotkey set to\" << s_hk;\n    }\n    else\n    {\n        auto t = QT_TR_NOOP(\"Failed to set the hotkey '%1'\");\n        WARN << QString::fromUtf8(t).arg(s_hk);\n        QMessageBox::warning(nullptr, qApp->applicationDisplayName(),\n                             tr(t).arg(QKeySequence(kc_hk)\n                                       .toString(QKeySequence::NativeText)));\n        app.showSettings();\n    }\n}\n\nvoid Application::Private::initRPC()\n{\n    auto messageHandler = [this](const QByteArray bytes) -> QByteArray\n    {\n        INFO << \"Received RPC message:\" << bytes;\n\n        const auto array = QJsonDocument::fromJson(bytes).array();\n\n        QStringList args;\n        for (const QJsonValue &value : array)\n            args << value.toString();\n\n        if (args.size() == 0)\n        {\n            WARN << \"Received Invalid message expected json array of strings.\";\n            return \"Invalid message expected json array of strings.\";\n        }\n\n        else if (args[0] == \"show\")\n        {\n            if (args.size() > 2)\n                return \"'show' expects zero or one argument.\";\n\n            else if (args.size() == 2)\n                app.show(args[1]);\n\n            else // if (args.size() == 1)\n                app.show();\n        }\n\n        else if (args[0] == \"hide\")\n\n            if (args.size() == 1)\n                app.hide();\n            else\n                return \"'hide' expects no arguments.\";\n\n        else if (args[0] == \"toggle\")\n\n            if (args.size() == 1)\n                app.toggle();\n            else\n                return \"'toggle' expects no arguments.\";\n\n        else if (args[0] == \"settings\")\n        {\n            if (args.size() > 2)\n                return \"'settings' expects zero or one argument.\";\n\n            else if (args.size() == 2)\n                app.showSettings(args[1]);\n\n            else // if (args.size() == 1)\n                app.showSettings();\n        }\n\n        else if (args[0] == \"restart\")\n\n            if (args.size() == 1)\n                app.restart();\n            else\n                return \"'restart' expects no arguments.\";\n\n        else if (args[0] == \"quit\")\n\n            if (args.size() == 1)\n                app.quit();\n            else\n                return \"'quit' expects no arguments.\";\n\n        else if (args[0] == \"report\")\n\n            if (args.size() == 1)\n                return report().join('\\n').toLocal8Bit();\n            else\n                return \"'report' expects no arguments.\";\n\n        else if (QUrl url(args[0]); url.isValid())\n            for (const auto &arg : as_const(args))\n                app.handleUrl(arg);\n\n        else\n        {\n            WARN << \"Invalid RPC message\" << bytes;\n        }\n\n        return {};\n    };\n\n    rpc_server.setMessageHandler(messageHandler);\n}\n\nvoid Application::Private::initFrontend(QSettings &settings)\n{\n    auto loaders = plugin_provider.frontendPlugins();\n    const auto id = settings.value(CFG_FRONTEND_ID, DEF_FRONTEND_ID).toString();\n\n    DEBG << u\"Try loading the configured frontend '%1'.\"_s.arg(id);\n\n    if (auto it = ranges::find(loaders, id, [&](auto loader){ return loader->metadata().id; });\n        it != loaders.end())\n        if (auto err = loadFrontend(*it); err.isNull())\n            return;\n        else\n        {\n            WARN << u\"Loading configured frontend '%1' failed: %2.\"_s.arg(id, err);\n            loaders.erase(it);\n        }\n    else\n        WARN << u\"Configured frontend plugin '%1' does not exist.\"_s.arg(id);\n\n    for (auto &loader : loaders)\n    {\n        WARN << u\"Try loading '%1'.\"_s.arg(loader->metadata().id);\n\n        if (auto err = loadFrontend(loader); err.isNull())\n        {\n            INFO << u\"Using '%1' as fallback.\"_s.arg(loader->metadata().id);\n            return;\n        }\n        else\n            WARN << u\"Failed loading '%1'.\"_s.arg(loader->metadata().id);\n    }\n\n    qFatal(\"Could not load any frontend.\");\n}\n\nQString Application::Private::loadFrontend(PluginLoader *loader)\n{\n    using enum Plugin::State;\n\n    // Blocking load\n    QEventLoop loop;\n\n    connect(loader, &PluginLoader::finished, &loop, [&](QString info) {\n        if (!info.isEmpty())\n            DEBG << info;\n        loop.quit();\n    });\n    QTimer::singleShot(0, loader, [loader]{ loader->load(); });\n    loop.exec();\n\n    connect(loader->instance(), &PluginInstance::initialized,\n            &loop, [&] { loop.quit(); });\n    QTimer::singleShot(0, loader, [loader]{ loader->instance()->initialize(); });\n    loop.exec();\n\n    if (frontend = dynamic_cast<Frontend*>(loader->instance()); frontend)\n    {\n        platform::initNativeWindow(frontend->winId());\n\n        for (auto *ext : loader->instance()->extensions())\n            extension_registry.registerExtension(ext);\n\n        frontend_plugin = loader;\n\n        return {};\n    }\n    else\n        return QString(\"Failed casting plugin instance to albert::Frontend: %1\")\n            .arg(loader->metadata().id);\n}\n\nvoid Application::Private::notifyVersionChange(QSettings &state)\n{\n    auto current_version = qApp->applicationVersion();\n    auto last_used_version = state.value(STATE_LAST_USED_VERSION).toString();\n\n    // First run\n    if (last_used_version.isNull())\n    {\n        auto text = tr(\"This is the first time you've launched Albert. Albert is \"\n                       \"plugin based. You have to enable some plugins you want to use.\");\n\n        QMessageBox::information(nullptr, qApp->applicationDisplayName(), text);\n\n        QTimer::singleShot(0, &app, [&]{ app.showSettings(); });\n    }\n    else if (current_version.section('.', 0, 0) != last_used_version.section('.', 0, 0))\n    {\n        auto text = tr(\"You are now using Albert %1. The major version changed. \"\n                       \"Some parts of the API might have changed. \"\n                       \"Check the <a href=\\\"https://albertlauncher.github.io/news/\\\">news</a>.\"\n                       ).arg(current_version);\n\n        QMessageBox::information(nullptr, qApp->applicationDisplayName(), text);\n    }\n\n    if (last_used_version != current_version)\n        state.setValue(STATE_LAST_USED_VERSION, current_version);\n}\n\n// -------------------------------------------------------------------------------------------------\n\nApplication::Application(const QStringList &additional_plugin_paths, bool load_enabled) :\n    d(make_unique<Private>(*this,\n                           additional_plugin_paths,\n                           load_enabled,\n                           *App::settings(),\n                           *App::state()))\n{\n    connect(&d->extension_registry, &ExtensionRegistry::added, this, &Application::added);\n    connect(&d->extension_registry, &ExtensionRegistry::removed, this, &Application::removed);\n}\n\nApplication::~Application() {}\n\nApplication &Application::instance() { return static_cast<Application&>(App::instance()); }\n\nvoid Application::handleUrl(const QUrl &url)\n{\n    DEBG << \"Handle url\" << url.toString();\n    if (url.scheme() == qApp->applicationName())\n    {\n        if (url.authority().isEmpty())\n        {\n            // ?\n        }\n        else if (auto h = extension<UrlHandler>(url.authority()); h)\n            h->handle(url);\n        else\n            WARN << \"URL handler not available: \" + url.authority().toLocal8Bit();\n    }\n    else\n        WARN << \"Invalid URL scheme\" << url.scheme();\n}\n\nPluginRegistry &Application::pluginRegistry() { return d->plugin_registry; }\n\nQueryEngine &Application::queryEngine() { return d->query_engine; }\n\nTelemetry &Application::telemetry() { return d->telemetry; }\n\nSystemTrayIcon &Application::systemTrayIcon() { return d->tray_icon; }\n\nPathManager &Application::pathManager() { return d->path_manager; }\n\nconst map<QString, Extension *> &Application::extensions() const\n{ return d->extension_registry.extensions(); }\n\nvoid Application::showSettings(QString plugin_id)\n{\n    if (!d->settings_window)\n        d->settings_window = new SettingsWindow(*this);\n    hide();\n    d->settings_window->bringToFront(plugin_id);\n}\n\nvoid Application::show(const QString &text)\n{\n    if (!text.isNull())\n        d->frontend->setInput(text);\n    d->frontend->setVisible(true);\n}\n\nvoid Application::hide() { d->frontend->setVisible(false); }\n\nvoid Application::toggle() { d->frontend->setVisible(!d->frontend->isVisible()); }\n\nFrontend *Application::frontend() { return d->frontend; }\n\nQString Application::currentFrontend() { return d->frontend_plugin->metadata().name; }\n\nQStringList Application::availableFrontends()\n{\n    QStringList ret;\n    for (const auto *loader : d->plugin_provider.frontendPlugins())\n        ret << loader->metadata().name;\n    return ret;\n}\n\nvoid Application::setFrontend(uint i)\n{\n    auto fp = d->plugin_provider.frontendPlugins().at(i);\n    settings()->setValue(CFG_FRONTEND_ID, fp->metadata().id);\n\n    auto text = tr(\"Changing the frontend requires a restart. \"\n                   \"Do you want to restart Albert?\");\n\n    if (QMessageBox::question(nullptr, qApp->applicationDisplayName(), text) == QMessageBox::Yes)\n        restart();\n}\n\nconst QHotkey *Application::hotkey() const { return d->hotkey.get(); }\n\nvoid Application::setHotkey(unique_ptr<QHotkey> hk)\n{\n    if (!hk)\n    {\n        d->hotkey.reset();\n        settings()->setValue(CFG_HOTKEY, QString{});\n    }\n    else if (hk->isRegistered())\n    {\n        d->hotkey = ::move(hk);\n        connect(d->hotkey.get(), &QHotkey::activated,\n                d->frontend, [this]{ toggle(); });\n        settings()->setValue(CFG_HOTKEY, d->hotkey->shortcut().toString());\n    }\n    else\n        WARN << \"Set unregistered hotkey. Ignoring.\";\n}\n\nnamespace albert {\n\nint ALBERT_EXPORT run(int argc, char **argv)\n{\n    if (qApp != nullptr)\n        qFatal(\"Calling run more than once is not allowed.\");\n\n    QLoggingCategory::setFilterRules(\"*.debug=false\");\n    qInstallMessageHandler(messageHandler);\n\n    // Initialize Qt application\n\n    QApplication qapp(argc, argv);\n    QApplication::setApplicationName(\"albert\");\n    QApplication::setApplicationDisplayName(\"Albert\");\n    QApplication::setApplicationVersion(ALBERT_VERSION_STRING);\n    QApplication::setWindowIcon(QIcon::fromTheme(\"albert\"));\n    QApplication::setQuitOnLastWindowClosed(false);\n\n\n    // Parse command line (asap for fast cli commands)\n\n    struct {\n        QStringList plugin_dirs;\n        bool autoload;\n    } config;\n\n    {\n        auto opt_p = QCommandLineOption({\"p\", \"plugin-dirs\"},\n                                        Application::tr(\"Set the plugin dirs to use. Comma separated.\"),\n                                        Application::tr(\"directories\"));\n        auto opt_r = QCommandLineOption({\"r\", \"report\"},\n                                        Application::tr(\"Print report and quit.\"));\n        auto opt_n = QCommandLineOption({\"n\", \"no-autoload\"},\n                                        Application::tr(\"Do not implicitly load enabled plugins.\"));\n\n        QCommandLineParser parser;\n        parser.addOptions({opt_p, opt_r, opt_n});\n        parser.addPositionalArgument(Application::tr(\"command\"),\n                                     Application::tr(\"RPC command to send to the running instance.\"),\n                                     Application::tr(\"[command [params...]]\"));\n        parser.addVersionOption();\n        parser.addHelpOption();\n        parser.setApplicationDescription(Application::tr(\"Launch Albert or control a running instance.\"));\n        parser.process(qapp);\n\n        // TODO If not running? Continue and use? Makes sense for albert show but not for URLs.\n        if (const auto args = parser.positionalArguments(); !args.isEmpty())\n            try {\n                QJsonDocument d(QJsonArray::fromStringList(args));\n                auto bytes = d.toJson(QJsonDocument::Compact);\n                bytes = RPCServer::sendMessage(bytes);\n                cout << bytes.data() << endl;\n                return EXIT_SUCCESS;\n            } catch (const exception &e) {\n                cout << e.what() << endl;\n                return EXIT_FAILURE;\n            }\n\n        if (parser.isSet(opt_r)) {\n            for (const auto &line : report())\n                std::cout << line.toStdString() << std::endl;\n            ::exit(EXIT_SUCCESS);\n        } else\n            for (const auto &line : report())\n                DEBG << line;\n\n        config = {\n            .plugin_dirs = parser.value(opt_p).split(',', Qt::SkipEmptyParts),\n            .autoload    = !parser.isSet(opt_n),\n        };\n    }\n\n\n    // Initialize app directories\n\n    for (const auto &path : { App::cacheLocation(), App::configLocation(), App::dataLocation() })\n        try {\n            filesystem::create_directories(path);\n            QFile::setPermissions(path, QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);\n        } catch (...) {\n            qFatal(\"Failed creating directory: %s\", path.c_str());\n        }\n\n\n    // Load translators\n\n    {\n        DEBG << \"Loading translations\";\n\n        auto *t = new QTranslator(&qapp);\n        if (t->load(QLocale(), \"qtbase\", \"_\", QLibraryInfo::path(QLibraryInfo::TranslationsPath)))\n        {\n            DEBG << \" -\" << t->filePath();\n            qapp.installTranslator(t);\n        }\n        else\n            delete t;\n\n        t = new QTranslator(&qapp);\n        if (t->load(QLocale(), qapp.applicationName(), \"_\", \":/i18n\"))\n        {\n            DEBG << \" -\" << t->filePath();\n            qapp.installTranslator(t);\n        }\n        else\n            delete t;\n    }\n\n\n    // Initialize theme icon lookup\n\n    {\n        // QIcon::setThemeSearchPaths({\":/icons\"});  // implicitly set\n        // See https://bugreports.qt.io/browse/QTBUG-140639\n        QIcon::setFallbackThemeName(\"fallback\");\n        DEBG << \"Theme search paths:\" << QIcon::themeSearchPaths();\n    }\n\n\n    // Run app\n\n    Application app(config.plugin_dirs, config.autoload);\n    int return_value = qapp.exec();\n\n    if (return_value == -1 && runDetachedProcess(qApp->arguments(), QDir::currentPath()))\n        return_value = EXIT_SUCCESS;\n\n    return return_value;\n}\n\n}\n"
  },
  {
    "path": "src/app/application.h",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#pragma once\n#include \"app.h\"\n#include <QObject>\n#include <memory>\nclass PathManager;\nclass PluginRegistry;\nclass QHotkey;\nclass QueryEngine;\nclass SystemTrayIcon;\nclass Telemetry;\nnamespace albert {\nnamespace detail { class Frontend; }\nclass ExtensionRegistry;\nint run(int, char**);\n}\n\n\nclass Application final : public albert::App\n{\n    Q_OBJECT\n\npublic:\n\n    // Public interface\n    void show(const QString &text = {}) override;\n    void showSettings(QString plugin_id = {}) override;\n    const std::map<QString, albert::Extension *> &extensions() const override;\n\n    const std::filesystem::path &settingsFilePath() const;\n    const std::filesystem::path &stateFilePath() const;\n\n    void hide();\n    void toggle();\n    Q_INVOKABLE void handleUrl(const QUrl &url);\n\n    PluginRegistry &pluginRegistry();\n    QueryEngine &queryEngine();\n    Telemetry &telemetry();\n    SystemTrayIcon &systemTrayIcon();\n    PathManager &pathManager();\n\n    const QHotkey *hotkey() const;\n    void setHotkey(std::unique_ptr<QHotkey> hotkey);\n\n    QStringList availableFrontends();\n    QString currentFrontend();\n    void setFrontend(uint i);\n    albert::detail::Frontend *frontend();\n\n    static Application &instance();\n\nprivate:\n\n    explicit Application(const QStringList &additional_plugin_paths, bool load_enabled);\n    ~Application() override;\n\n    friend int albert::run(int, char**);\n\n    class Private;\n    std::unique_ptr<Private> d;\n\n};\n"
  },
  {
    "path": "src/app/messagehandler.cpp",
    "content": "// Copyright (c) 2024 Manuel Schneider\n\n#include \"messagehandler.h\"\n#include <QMessageBox>\n#include <QString>\n#include <QTime>\n\nvoid messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)\n{\n    // Todo use std::format as soon as apple gets it off the ground\n    switch (type) {\n    case QtDebugMsg:\n        fprintf(stdout, \"%s \\x1b[34m[debg:%s]\\x1b[0m %s\\x1b[0m\\n\",\n                QTime::currentTime().toString().toLocal8Bit().constData(),\n                context.category,\n                message.toLocal8Bit().constData());\n        break;\n    case QtInfoMsg:\n        fprintf(stdout, \"%s \\x1b[32m[info:%s]\\x1b[0m %s\\n\",\n                QTime::currentTime().toString().toLocal8Bit().constData(),\n                context.category,\n                message.toLocal8Bit().constData());\n\n        break;\n    case QtWarningMsg:\n        fprintf(stdout, \"%s \\x1b[33m[warn:%s]\\x1b[0m %s\\x1b[0m\\n\",\n                QTime::currentTime().toString().toLocal8Bit().constData(),\n                context.category,\n                message.toLocal8Bit().constData());\n        break;\n    case QtCriticalMsg:\n        fprintf(stdout, \"%s \\x1b[31m[crit:%s] %s\\x1b[0m\\n\",\n                QTime::currentTime().toString().toLocal8Bit().constData(),\n                context.category,\n                message.toLocal8Bit().constData());\n        break;\n    case QtFatalMsg:\n        fprintf(stderr, \"%s \\x1b[41;30;4m[fatal:%s]\\x1b[0;1m %s  --  [%s]\\x1b[0m\\n\",\n                QTime::currentTime().toString().toLocal8Bit().constData(),\n                context.category,\n                message.toLocal8Bit().constData(),\n                context.function);\n        QMessageBox::critical(nullptr, \"Fatal error\", message);\n        exit(1);\n    }\n    fflush(stdout);\n}\n"
  },
  {
    "path": "src/app/messagehandler.h",
    "content": "// Copyright (c) 2024 Manuel Schneider\n\n#pragma once\n#include <QString>\n\nvoid messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);\n"
  },
  {
    "path": "src/app/pathmanager.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"pathmanager.h\"\n#include \"logging.h\"\n#include \"app.h\"\n#include <QStringList>\n#include <QSettings>\n#include <QtGlobal>\nusing namespace albert;\nusing namespace std;\n\nstatic const char *CFG_ADDITIONAL_PATH_ENTRIES = \"additional_path_entires\";\n\nclass PathManager::Private\n{\npublic:\n    const QStringList original_path_entries = qEnvironmentVariable(\"PATH\").split(u':', Qt::SkipEmptyParts);\n    QStringList additional_path_entries;\n};\n\nPathManager::PathManager(const QSettings &settings) : d(make_unique<Private>())\n{\n    d->additional_path_entries = settings.value(CFG_ADDITIONAL_PATH_ENTRIES).toStringList();\n    auto effective_path_entries = QStringList() << d->additional_path_entries\n                                                << d->original_path_entries;\n    auto new_path = effective_path_entries.join(u':').toUtf8();\n    qputenv(\"PATH\", new_path);\n    DEBG << \"Effective PATH: \" << new_path;\n}\n\nPathManager::~PathManager() {}\n\nconst QStringList &PathManager::originalPathEntries() const { return d->original_path_entries; }\n\nconst QStringList &PathManager::additionalPathEntries() const { return d->additional_path_entries; }\n\nvoid PathManager::setAdditionalPathEntries(const QStringList &entries)\n{\n    if (entries != d->additional_path_entries)\n    {\n        d->additional_path_entries = entries;\n        App::instance().settings()->setValue(CFG_ADDITIONAL_PATH_ENTRIES, entries);\n    }\n}\n"
  },
  {
    "path": "src/app/pathmanager.h",
    "content": "// Copyright (C) 2022-2025 Manuel Schneider\n\n#pragma once\n#include <memory>\n#include <QStringList>\nclass QSettings;\n\nclass PathManager\n{\npublic:\n\n    PathManager(const QSettings &settings);\n    ~PathManager();\n\n    const QStringList &originalPathEntries() const;\n    const QStringList &additionalPathEntries() const;\n    void setAdditionalPathEntries(const QStringList &entries);\n\nprivate:\n    class Private;\n    std::unique_ptr<Private> d;\n\n};\n"
  },
  {
    "path": "src/app/pluginqueryhandler.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"albert/app.h\"\n#include \"icon.h\"\n#include \"pluginloader.h\"\n#include \"pluginmetadata.h\"\n#include \"pluginqueryhandler.h\"\n#include \"pluginregistry.h\"\n#include <QWidget>\nusing enum Plugin::State;\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\n\nclass PluginItem : public Item\n{\n    PluginRegistry &plugin_registry_;\n    const Plugin &plugin_;\npublic:\n\n    PluginItem(PluginRegistry &plugin_registry, const Plugin &plugin):\n        plugin_registry_(plugin_registry), plugin_(plugin) {}\n\n    QString id() const override { return plugin_.id; }\n\n    QString text() const override\n    { return QString(\"%1 (%2)\").arg(plugin_.metadata.name, plugin_.id); }\n\n    QString subtext() const override\n    {\n        QString state;\n        if (plugin_.state == Loaded)\n        {\n            static const auto tr_loaded = PluginQueryHandler::tr(\"Loaded\");\n            state = tr_loaded;\n        }\n        else\n        {\n            static const auto tr_unloaded = PluginQueryHandler::tr(\"Unloaded\");\n            state = tr_unloaded;\n        }\n\n        if (!plugin_.state_info.isEmpty())\n            state.append(QString(\" (%1)\").arg(plugin_.state_info));\n\n        static const auto tr_config = PluginQueryHandler::tr(\"Configuration\");\n        static const auto tr_enabled = PluginQueryHandler::tr(\"Enabled\");\n        static const auto tr_disabled = PluginQueryHandler::tr(\"Disabled\");\n        static const auto tr_state = PluginQueryHandler::tr(\"State\");\n        return QString(\"%1: %2, %3: %4\")\n            .arg(tr_config, plugin_.enabled ? tr_enabled : tr_disabled,\n                 tr_state, state);\n    }\n\n    QString inputActionText() const override\n    { return plugin_.metadata.name; }\n\n    unique_ptr<Icon> icon() const override\n    {\n        if(!plugin_.enabled)\n            return Icon::grapheme(u\"🧩\"_s);\n        else if (plugin_.state == Loaded)\n            return Icon::composed(Icon::grapheme(u\"🧩\"_s), Icon::grapheme(u\"✅\"_s), 1.0, 0.5);\n        else if (plugin_.state_info.isEmpty())\n            return Icon::composed(Icon::grapheme(u\"🧩\"_s), Icon::grapheme(u\"⏳\"_s), 1.0, 0.5);\n        else\n            return Icon::composed(Icon::grapheme(u\"🧩\"_s), Icon::grapheme(u\"⚠️\"_s), 1.0, 0.5);\n    }\n\n    vector<Action> actions() const override\n    {\n        vector<Action> actions;\n\n        actions.emplace_back(\n            \"settings\",\n            PluginQueryHandler::tr(\"Open settings\"),\n            [this] { App::instance().showSettings(id()); }\n            );\n\n        actions.emplace_back(\n            plugin_.enabled ? \"disable\" : \"enable\",\n            plugin_.enabled ? PluginQueryHandler::tr(\"Disable\")\n                            : PluginQueryHandler::tr(\"Enable\"),\n            [this] { plugin_registry_.setEnabledWithUserConfirmation(plugin_.id, !plugin_.enabled); }\n            );\n\n        if (plugin_.state == Loaded)\n            actions.emplace_back(\n                u\"relaod\"_s,\n                PluginQueryHandler::tr(\"Reload\"),\n                [this] {\n                    plugin_registry_.setLoaded(plugin_.id, false);\n                    plugin_registry_.setLoaded(plugin_.id, true);\n                });\n        else if (plugin_.state == Unloaded)\n            actions.emplace_back(\n                u\"laod\"_s,\n                PluginQueryHandler::tr(\"Load\"),\n                [this] { plugin_registry_.setLoaded(plugin_.id, true); });\n\n        return actions;\n    }\n};\n\n\nPluginQueryHandler::PluginQueryHandler(PluginRegistry &plugin_registry) : plugin_registry_(plugin_registry)\n{\n    QObject::connect(&plugin_registry_, &PluginRegistry::pluginsChanged,\n                     &plugin_registry_, [this] { setIndexItems({}); updateIndexItems(); });\n}\n\nQString PluginQueryHandler::id() const { return u\"pluginregistry\"_s; }\n\nQString PluginQueryHandler::name() const { return tr(\"Plugins\"); }\n\nQString PluginQueryHandler::description() const { return tr(\"Manage plugins\"); }\n\nQString PluginQueryHandler::defaultTrigger() const { return u\"plugin \"_s; }\n\nvoid PluginQueryHandler::updateIndexItems()\n{\n    vector<IndexItem> items;\n    for (auto &[id, plugin] : plugin_registry_.plugins()){\n        auto item = make_shared<PluginItem>(plugin_registry_, plugin);\n        items.emplace_back(item, id);\n        items.emplace_back(item, plugin.metadata.name);\n    }\n    setIndexItems(::move(items));\n}\n"
  },
  {
    "path": "src/app/pluginqueryhandler.h",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#pragma once\n#include \"indexqueryhandler.h\"\n#include <QCoreApplication>\nclass PluginRegistry;\n\nclass PluginQueryHandler : public albert::IndexQueryHandler\n{\n    Q_DECLARE_TR_FUNCTIONS(PluginQueryHandler)\n\npublic:\n    PluginQueryHandler(PluginRegistry &plugin_registry);\n\n    QString id() const override;\n    QString name() const override;\n    QString description() const override;\n    QString defaultTrigger() const override;\n    void updateIndexItems() override;\n\nprivate:\n    PluginRegistry &plugin_registry_;\n};\n"
  },
  {
    "path": "src/app/qtpluginloader.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"config.h\"\n#include \"logging.h\"\n#include \"plugininstance.h\"\n#include \"qtpluginloader.h\"\n#include <QCoreApplication>\n#include <QFutureWatcher>\n#include <QPluginLoader>\n#include <QTranslator>\n#include <QtConcurrentRun>\n#include <chrono>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std::chrono;\nusing namespace std;\n\nstatic QString fetchLocalizedMetadata(const QJsonObject &json, const QString &key)\n{\n    auto locale = QLocale();\n\n    auto k = u\"%1[%2]\"_s.arg(key, locale.name());\n    if (auto v = json[k].toString(); !v.isEmpty())\n        return v;\n\n    k = u\"%1[%2]\"_s.arg(key, QLocale::languageToCode(locale.language()));\n    if (auto v = json[k].toString(); !v.isEmpty())\n        return v;\n\n    return json[key].toString();\n}\n\n\nQtPluginLoader::QtPluginLoader(const QString &path)\n    : loader_(path)\n    , instance_(nullptr)\n{\n    //\n    // Check interface\n    //\n\n    auto iid = loader_.metaData().value(\"IID\"_L1).toString();\n\n    if (iid.isEmpty())\n        throw runtime_error(\"Not a Qt plugin\");\n\n    static const auto regex_iid = QRegularExpression(R\"R(org.albert.PluginInterface/(\\d+).(\\d+))R\");\n    auto iid_match = regex_iid.match(iid);\n\n    if (!iid_match.hasMatch())\n    {\n        auto msg = QCoreApplication::translate(\n            \"QtPluginLoader\", \"Invalid interface identifier (IID) pattern : '%1'. Expected '%2'.\");\n        msg = msg.arg(iid_match.captured(), iid_match.regularExpression().pattern());\n        throw runtime_error(msg.toStdString());\n    }\n\n    if (auto plugin_iid_major = iid_match.captured(1).toUInt();\n        plugin_iid_major != ALBERT_VERSION_MAJOR)\n    {\n        auto msg = QCoreApplication::translate(\n            \"QtPluginLoader\", \"Incompatible major version: %1. Expected: %2.\");\n        msg = msg.arg(iid_match.captured(), iid_match.regularExpression().pattern());\n        throw runtime_error(msg.toStdString());\n    }\n\n    if (auto plugin_iid_minor = iid_match.captured(2).toUInt();\n        plugin_iid_minor > ALBERT_VERSION_MINOR)\n    {\n        auto msg = QCoreApplication::translate(\n            \"QtPluginLoader\", \"Incompatible minor version: %1. Supported up to: %2.\");\n        msg = msg.arg(iid_match.captured(), iid_match.regularExpression().pattern());\n        throw runtime_error(msg.toStdString());\n    }\n\n    //\n    // Extract metadata\n    //\n\n    auto rawMetadata = loader_.metaData().value(\"MetaData\"_L1).toObject();\n\n    auto load_type = PluginMetadata::LoadType::User;\n    if (auto lts = rawMetadata[\"loadtype\"_L1].toString();\n        lts == \"frontend\"_L1)\n        load_type = PluginMetadata::LoadType::Frontend;\n    else if (!lts.isEmpty() && lts != \"user\"_L1)\n        WARN << u\"Invalid load type '%1'. Default to 'user'.\"_s.arg(lts);\n\n    metadata_ = albert::PluginMetadata\n    {\n        .iid                  = iid,\n        .id                   = rawMetadata[\"id\"_L1].toString(),\n        .version              = rawMetadata[\"version\"_L1].toString(),\n        .name                 = fetchLocalizedMetadata(rawMetadata, \"name\"_L1),\n        .description          = fetchLocalizedMetadata(rawMetadata, \"description\"_L1),\n        .license              = rawMetadata[\"license\"_L1].toString(),\n        .url                  = rawMetadata[\"url\"_L1].toString(),\n        .readme_url           = rawMetadata[\"readme_url\"_L1].toString(),\n        .translations         = rawMetadata[\"translations\"_L1].toVariant().toStringList(),\n        .authors              = rawMetadata[\"authors\"_L1].toVariant().toStringList(),\n        .maintainers          = rawMetadata[\"maintainers\"_L1].toVariant().toStringList(),\n        .runtime_dependencies = rawMetadata[\"runtime_dependencies\"_L1].toVariant().toStringList(),\n        .binary_dependencies  = rawMetadata[\"binary_dependencies\"_L1].toVariant().toStringList(),\n        .plugin_dependencies  = rawMetadata[\"plugin_dependencies\"_L1].toVariant().toStringList(),\n        .third_party_credits  = rawMetadata[\"credits\"_L1].toVariant().toStringList(),\n        .platforms{},\n        .load_type = load_type\n    };\n\n    //\n    // Set load hints\n    //\n    // ExportExternalSymbolsHint:\n    // Some python libs do not link against python. Export the python symbols to the main app.\n    // (this comment is like 10y old, TODO check if necessary)\n    //\n    // PreventUnloadHint:\n    // To be able to unload we have to make sure that there is no object of this library alive.\n    // This is nearly impossible with the current design. Frontends keep queries alive over\n    // sessions which then segfault on deletion when the code has been unloaded.\n    //\n    // TODO: Design something that ensures that no items/actions will be alive when plugins get\n    // unloaded. (e.g. Session class, owning queries, injected into frontends when shown).\n    //\n    // Anyway atm frontends keep queries alive over session, which is just poor design.\n    // However not unloading is an easy fix for now and theres more important stuff to do.\n    //\n    // Update 2024:\n    //\n    // Althought the design _does_ handle object lifetime correctly now the app still segfaults\n    // when unloading plugins. Probably due to qt internal connection handling. One example that\n    // proved to sefault guaranteed is the WeakDependency class whose connections (at least on\n    // macos) call into unloaded code although all connections have been properly disconnected.\n    //\n    // Probably this should be reported as a bug to Qt. But well, … PreventUnload\n    //\n\n    loader_.setLoadHints(QLibrary::ExportExternalSymbolsHint | QLibrary::PreventUnloadHint);\n}\n\nQtPluginLoader::~QtPluginLoader()\n{\n    if (loader_.isLoaded())\n    {\n        WARN << \"QtPluginLoader destroyed in loaded state:\" << metadata_.id;\n        unload();\n    }\n}\n\nQString QtPluginLoader::path() const { return loader_.fileName(); }\n\nconst PluginMetadata &QtPluginLoader::metadata() const { return metadata_; }\n\nstatic inline auto now() { return system_clock::now(); }\n\ntemplate<typename T=milliseconds>\nstatic inline auto diff(const time_point<system_clock> & tp)\n{ return duration_cast<milliseconds>(now() - tp).count(); }\n\nvoid QtPluginLoader::load()\n{\n    // Errors are intentionally not logged. Thats the responsibility of the plugin implementation.\n    // Plugins are expected to throw a localized message and print english logs using their\n    // logging category.\n\n    auto future = QtConcurrent::run([&loader=loader_, id=metadata().id]\n                                    -> unique_ptr<QTranslator> {\n        unique_ptr<QTranslator> translator;\n\n        auto tp = now();\n        if (!loader.load())\n            throw runtime_error(loader.errorString().toStdString());\n        DEBG << u\"%1: Library loaded in %2 ms (%3)\"_s\n                    .arg(id).arg(diff<>(tp)).arg(loader.fileName());\n\n        tp = now();\n        if (translator = make_unique<QTranslator>();\n            translator->load(QLocale(), id, \"_\", \":/i18n\"))\n\n            DEBG << u\"%1: Translations loaded in %2 ms (%3)\"_s\n                        .arg(id).arg(diff<>(tp)).arg(translator->filePath());\n        else\n            translator.reset();\n\n        return translator;\n    })\n    .then(this, [this](unique_ptr<QTranslator> translator) {\n        if (translator)\n        {\n            translator_ = ::move(translator);\n            // Does _not_ take ownership. Not thread-safe.\n            QCoreApplication::installTranslator(translator_.get());  // Not threadsafe\n        }\n\n        auto tp = now();\n        PluginLoader::current_loader = this;\n        auto *instance = loader_.instance();\n        if (!instance)\n            throw runtime_error(\"Plugin instance is null.\");\n        if (instance_ = dynamic_cast<PluginInstance *>(instance);\n            !instance_)\n            throw runtime_error(\"Plugin instance is not of type albert::PluginInstance.\");\n        DEBG << u\"%1: Instantiated in %2 ms\"_s\n                    .arg(metadata().id).arg(diff<>(tp));\n        emit finished({});\n    })\n    .onCanceled(this, [this] {\n        unload();\n    })\n    .onFailed(this, [](const QUnhandledException &que) {\n        if (que.exception())\n            rethrow_exception(que.exception());\n        else\n            throw runtime_error(\"QUnhandledException::exception() returned nullptr.\");\n    })\n    .onFailed(this, [this](const exception &e) {\n        unload();\n        emit finished(QString::fromStdString(e.what()));\n    })\n    .onFailed(this, [this]{\n        unload();\n        emit finished(u\"Unknown exception while loading plugin.\"_s);\n    });\n}\n\nvoid QtPluginLoader::unload()\n{\n    if (loader_.isLoaded())\n    {\n        if (!loader_.unload())\n            WARN << u\"%1: Unload failed: %2\"_s.arg(metadata_.id, loader_.errorString());\n        else\n            DEBG << u\"%1: Unloaded.\"_s.arg(metadata_.id);\n\n        instance_ = nullptr;\n    }\n\n    if (translator_)\n    {\n        QCoreApplication::removeTranslator(translator_.get());\n        translator_.reset();\n    }\n}\n\nPluginInstance *QtPluginLoader::instance() { return instance_; }\n"
  },
  {
    "path": "src/app/qtpluginloader.h",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#pragma once\n#include \"pluginloader.h\"\n#include \"pluginmetadata.h\"\n#include <QPluginLoader>\n#include <memory>\nnamespace albert { class PluginInstance; }\nclass QTranslator;\n\nclass QtPluginLoader final : public albert::PluginLoader\n{\npublic:\n\n    QtPluginLoader(const QString &path);\n    ~QtPluginLoader();\n\n    QString path() const override;\n    const albert::PluginMetadata &metadata() const override;\n    void load() override;\n    void unload() override;\n    albert::PluginInstance *instance() override;\n\nprivate:\n\n    QPluginLoader loader_;\n    albert::PluginMetadata metadata_;\n    albert::PluginInstance *instance_;\n    std::unique_ptr<QTranslator> translator_;\n\n};\n"
  },
  {
    "path": "src/app/qtpluginprovider.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"logging.h\"\n#include \"qtpluginloader.h\"\n#include \"qtpluginprovider.h\"\n#include <QCoreApplication>\n#include <QDirIterator>\nusing namespace std;\nusing namespace albert;\n\n\nQtPluginProvider::QtPluginProvider(QStringList paths)\n{\n#if defined(Q_OS_MAC)\n    paths << \"../../../../lib\";  // ./bin/albert.app/Contents/MacOS/\n#elif defined(Q_OS_UNIX)\n    paths << \"../lib\";\n#endif\n\n    QStringList install_paths;\n#if defined(Q_OS_MAC)\n    install_paths << QDir::home().filePath(\"Library/Application Support/albert/PlugIns\");\n    install_paths << QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(\"../PlugIns\");\n#elif defined(Q_OS_UNIX)\n    if (qgetenv(\"container\") == \"flatpak\")\n        install_paths << \"/app/lib/\";\n    install_paths << QDir::home().filePath(\".local/lib/\");\n    install_paths << QDir::home().filePath(\".local/lib64/\");\n    install_paths << \"/usr/local/lib/\";\n    install_paths << \"/usr/local/lib64/\";\n#if defined MULTIARCH_TUPLE\n    install_paths << \"/usr/lib/\" MULTIARCH_TUPLE;\n#endif\n    install_paths << \"/usr/lib/\";\n    install_paths << \"/usr/lib64/\";\n#endif\n    for (const QString& p : install_paths)\n        paths << QDir(p).filePath(\"albert\");\n\n    QStringList unique_canonical_paths;\n    for (const QString& p : paths)\n        if (auto pfi = QFileInfo(p); pfi.isDir())  // implicit exists()\n            unique_canonical_paths << pfi.canonicalFilePath();\n    unique_canonical_paths.removeDuplicates();\n\n    INFO << \"Searching native plugins in\" << unique_canonical_paths.join(\", \");\n    for (const auto &path : unique_canonical_paths)\n    {\n        QDirIterator dirIterator(path, QDir::Files);\n        while (dirIterator.hasNext()) {\n            try {\n                auto pl = make_unique<QtPluginLoader>(QFileInfo(dirIterator.next()).absoluteFilePath());\n                DEBG << \"Found valid native plugin\" << pl->path();\n                plugin_loaders_.emplace_back(::move(pl));\n            } catch (const runtime_error &e) {\n                DEBG << dirIterator.filePath() << e.what();\n            }\n        }\n    }\n}\n\nQtPluginProvider::~QtPluginProvider() = default;\n\nQString QtPluginProvider::id() const { return QStringLiteral(\"qtpluginprovider\"); }\n\nQString QtPluginProvider::name() const { return QStringLiteral(\"C++/Qt\"); }\n\nQString QtPluginProvider::description() const\n{\n    static const auto tr = QCoreApplication::translate(\"QtPluginProvider\", \"Loads native C++ plugins\");\n    return tr;\n}\n\nvector<PluginLoader*> QtPluginProvider::plugins()\n{\n    vector<PluginLoader*> plugins;\n    for (const auto &pl : plugin_loaders_)\n        if (pl->metadata().load_type == PluginMetadata::LoadType::User)\n            plugins.emplace_back(pl.get());\n    return plugins;\n}\n\nvector<PluginLoader*> QtPluginProvider::frontendPlugins()\n{\n    vector<PluginLoader*> frontend_plugins;\n    for (const auto &pl : plugin_loaders_)\n        if (pl->metadata().load_type == PluginMetadata::LoadType::Frontend)\n            frontend_plugins.emplace_back(pl.get());\n    return frontend_plugins;\n}\n"
  },
  {
    "path": "src/app/qtpluginprovider.h",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#pragma once\n#include \"pluginprovider.h\"\n#include <QStringList>\n#include <memory>\n#include <vector>\nnamespace albert { class PluginLoader; }\nclass QtPluginLoader;\n\nclass QtPluginProvider : public albert::PluginProvider\n{\npublic:\n\n    explicit QtPluginProvider(QStringList additional_paths);\n    ~QtPluginProvider();\n\n    // albert::PluginProvider interface\n    QString id() const override;\n    QString name() const override;\n    QString description() const override;\n    std::vector<albert::PluginLoader*> plugins() override;\n    std::vector<albert::PluginLoader*> frontendPlugins();\n\nprivate:\n\n    // on heap because vector requires to be move insertable\n    std::vector<std::unique_ptr<QtPluginLoader>> plugin_loaders_;\n\n};\n"
  },
  {
    "path": "src/app/report.cpp",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#include <QApplication>\n#include <QDir>\n#include <QFont>\n#include <QIcon>\n#include <QMetaEnum>\n#include <QProcessEnvironment>\n#include <QStringBuilder>\n#include <QStyle>\n#include <QStyleFactory>\n\nQStringList report()\n{\n    auto fn = [](const QString &label, const QString &value)\n    { return QString(\"%1: %2\").arg(label, 21).arg(value); };\n\n    QStringList sl;\n\n    // BUILD\n    sl << fn(\"Albert version\",        QApplication::applicationVersion());\n    sl << fn(\"Build date\",            __DATE__ \" \" __TIME__);\n    sl << fn(\"Qt version\",            qVersion());\n    sl << fn(\"Build ABI\",             QSysInfo::buildAbi());\n    sl << fn(\"Build architecture\",    QSysInfo::buildCpuArchitecture());\n\n    // SYSTEM\n    sl << fn(\"CPU architecture\",      QSysInfo::currentCpuArchitecture());\n    sl << fn(\"Kernel type\",           QSysInfo::kernelType());\n    sl << fn(\"Kernel version\",        QSysInfo::kernelVersion());\n    sl << fn(\"OS\",                    QSysInfo::prettyProductName());\n    sl << fn(\"OS type\",               QSysInfo::productType());\n    sl << fn(\"OS version\",            QSysInfo::productVersion());\n\n    // PLATFORM, QT CONFIG\n    sl << fn(\"Platform name\",         QGuiApplication::platformName());\n    sl << fn(\"Style name\",            QApplication::style()->objectName());\n    sl << fn(\"Available styles\",      QStyleFactory::keys().join(\", \"));\n    sl << fn(\"Icon theme\",            QIcon::themeName());\n    sl << fn(\"Font\",                  QGuiApplication::font().toString());\n    QMetaEnum metaEnum = QMetaEnum::fromType<QLocale::Language>();\n    QLocale loc;\n    sl << fn(\"Language\",              metaEnum.valueToKey(loc.language()));\n    sl << fn(\"Locale\",                loc.name());\n    sl << fn(\"Ui languages\",          loc.uiLanguages().join(\", \"));\n\n    // APP\n    sl << fn(\"Binary location\",       QApplication::applicationFilePath());\n    sl << fn(\"Working dir\",           QDir::currentPath());\n    sl << fn(\"Arguments\",             QApplication::arguments().join(\" \"));\n\n    // ENVIRONMENT\n    sl << \"ENVIRONMENT:\";\n    auto env = QProcessEnvironment::systemEnvironment();\n    for (const auto &key : env.keys())\n        sl << fn(key, env.value(key));\n\n    return sl;\n}\n\n"
  },
  {
    "path": "src/app/report.h",
    "content": "// Copyright (c) 2022-2024 Manuel Schneider\n\n#include <QStringList>\n\nQStringList report();\n"
  },
  {
    "path": "src/app/rpcserver.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"albert/app.h\"\n#include \"albert/logging.h\"\n#include \"rpcserver.h\"\n#include <QDir>\n#include <QLocalServer>\n#include <QLocalSocket>\nusing namespace albert;\nusing namespace std;\n\nstatic inline QString socketPath() { return QDir(App::cacheLocation()).filePath(\"ipc_socket\"); }\n\nclass RPCServer::Private\n{\npublic:\n    QLocalServer local_server;\n    function<QByteArray(const QByteArray&)> handler;\n\n    void onConnection()\n    {\n        QLocalSocket* socket = local_server.nextPendingConnection();\n        socket->waitForReadyRead(50);\n        if (socket->bytesAvailable())\n        {\n            if (handler)\n                socket->write(handler(socket->readAll()));\n\n        }\n        socket->flush();\n        socket->close();\n        socket->deleteLater();\n    }\n};\n\nRPCServer::RPCServer() : d(make_unique<Private>())\n{\n    auto socket_path = socketPath();\n\n    DEBG << \"Checking for a running instance…\";\n    QLocalSocket socket;\n    socket.connectToServer(socket_path);\n    if (socket.waitForConnected()) {\n        INFO << \"There is another instance of albert running.\";\n        ::exit(2);\n    } else {\n        switch (socket.error()) {\n        case QLocalSocket::ServerNotFoundError:\n            // all good. no socket.\n            break;\n        case QLocalSocket::ConnectionRefusedError:\n            // socket exists but nobody answers. probably crashed before.\n            CRIT << \"Albert has not been terminated properly. \"\n                    \"Please check your logs and report an issue.\";\n            QLocalServer::removeServer(socket_path);\n            break;\n        default:\n            // any other errors should bail out for now.\n            WARN << socket.error();\n            WARN << socket.errorString();\n            ::exit(2);\n            break;\n        }\n    }\n\n    DEBG << \"Creating local server\" << socket_path;\n    if (!d->local_server.listen(socket_path))\n        qFatal(\"Failed creating IPC server: %s\", qPrintable(d->local_server.errorString()));\n\n    QObject::connect(&d->local_server, &QLocalServer::newConnection,\n                     &d->local_server, [this]{ d->onConnection(); });\n}\n\nRPCServer::~RPCServer()\n{\n    DEBG << \"Closing local RPC server.\";\n    d->local_server.close();\n}\n\nvoid RPCServer::setMessageHandler(function<QByteArray(const QByteArray &)> h){ d->handler = h; }\n\nQByteArray RPCServer::sendMessage(const QByteArray &bytes, bool await_response)\n{\n    QLocalSocket socket;\n    socket.connectToServer(socketPath());\n    if (socket.waitForConnected(500))\n    {\n        socket.write(bytes);\n        socket.flush();\n\n        if(!await_response)\n            return {};\n\n        if (socket.waitForReadyRead(1000))\n            return socket.readAll();\n        else if (auto e = socket.error(); e == QLocalSocket::PeerClosedError)\n            return {};\n        else\n            throw runtime_error(socket.errorString().toStdString());\n    }\n    else\n        throw runtime_error(\"Failed to connect to albert.\");\n}\n"
  },
  {
    "path": "src/app/rpcserver.h",
    "content": "// Copyright (C) 2022-2025 Manuel Schneider\n\n#pragma once\n#include <functional>\n#include <memory>\nnamespace albert { class ExtensionRegistry; }\nclass QByteArray;\n\nclass RPCServer\n{\npublic:\n\n    RPCServer();\n    ~RPCServer();\n\n    void setMessageHandler(std::function<QByteArray(const QByteArray&)> handler);\n\n    static QByteArray sendMessage(const QByteArray &bytes, bool await_response = true);\n\nprivate:\n\n    class Private;\n    std::unique_ptr<Private> d;\n\n};\n"
  },
  {
    "path": "src/app/systemtrayicon.cpp",
    "content": "// Copyright (C) 2025-2025 Manuel Schneider\n\n#include \"application.h\"\n#include \"systemutil.h\"\n#include \"systemtrayicon.h\"\n#include <QCoreApplication>\n#include <QMenu>\n#include <QSettings>\n#include <QSystemTrayIcon>\nstatic const bool  DEF_SHOWTRAY = true;\nstatic const char* CFG_SHOWTRAY = \"showTray\";\nusing namespace albert;\nusing namespace std;\n\nstatic inline QString tr(const char *sourceText, const char *disambiguation = nullptr, int n = -1)\n{ return QCoreApplication::translate(\"SystemTrayIcon\", sourceText, disambiguation, n); }\n\n\nSystemTrayIcon::SystemTrayIcon(QSettings &settings)\n{\n    if (settings.value(CFG_SHOWTRAY, DEF_SHOWTRAY).toBool())\n        setEnabled(true);\n}\n\nSystemTrayIcon::~SystemTrayIcon() = default;\n\nbool SystemTrayIcon::isEnabled() const\n{\n    return tray_icon.get();\n}\n\nvoid SystemTrayIcon::setEnabled(bool enable)\n{\n    if (enable == isEnabled())\n        return;\n\n    else if (enable)\n    {\n        // menu\n\n        tray_menu = make_unique<QMenu>();\n\n        auto *action = tray_menu->addAction(tr(\"Show/Hide\"));\n        QObject::connect(action, &QAction::triggered,\n                         [] { Application::instance().toggle(); });\n\n        action = tray_menu->addAction(tr(\"Settings\"));\n        QObject::connect(action, &QAction::triggered,\n                         [] { Application::instance().showSettings(); });\n\n        action = tray_menu->addAction(tr(\"Open website\"));\n        QObject::connect(action, &QAction::triggered,\n                         [] { openUrl(\"https://albertlauncher.github.io/\"); });\n\n        tray_menu->addSeparator();\n\n        action = tray_menu->addAction(tr(\"Restart\"));\n        QObject::connect(action, &QAction::triggered,\n                         [] { Application::restart(); });\n\n        action = tray_menu->addAction(tr(\"Quit\"));\n        QObject::connect(action, &QAction::triggered,\n                         [] { Application::quit(); });\n\n        // icon\n\n        auto icon = QIcon::fromTheme(\"albert-tray\");\n        icon.setIsMask(true);\n\n        tray_icon = make_unique<QSystemTrayIcon>();\n        tray_icon->setIcon(icon);\n        tray_icon->setContextMenu(tray_menu.get());\n        tray_icon->setVisible(true);\n\n#ifndef Q_OS_MAC\n        // Some systems open menus on right click, show albert on left trigger\n        QObject::connect(tray_icon.get(), &QSystemTrayIcon::activated,\n                         tray_icon.get(), [](QSystemTrayIcon::ActivationReason reason)\n                {\n                    if(reason == QSystemTrayIcon::ActivationReason::Trigger)\n                        Application::instance().toggle();\n                });\n#endif\n    }\n    else\n    {\n        tray_icon->setVisible(false);\n        tray_icon.reset();\n        tray_menu.reset();\n    }\n\n    App::settings()->setValue(CFG_SHOWTRAY, enable);\n}\n"
  },
  {
    "path": "src/app/systemtrayicon.h",
    "content": "// Copyright (C) 2025-2025 Manuel Schneider\n\n#pragma once\n#include <memory>\nclass QSettings;\nclass QSystemTrayIcon;\nclass QMenu;\n\nclass SystemTrayIcon\n{\npublic:\n    SystemTrayIcon(QSettings &settings);\n    ~SystemTrayIcon();\n\n    bool isEnabled() const;\n    void setEnabled(bool);\n\nprivate:\n    std::unique_ptr<QSystemTrayIcon> tray_icon;\n    std::unique_ptr<QMenu> tray_menu;\n};\n"
  },
  {
    "path": "src/app/telemetry.cpp",
    "content": "// Copyright (C) 2014-2025 Manuel Schneider\n\n#include \"app.h\"\n#include \"extensionregistry.h\"\n#include \"logging.h\"\n#include \"networkutil.h\"\n#include \"pluginregistry.h\"\n#include \"telemetry.h\"\n#include \"telemetryprovider.h\"\n#include \"usagedatabase.h\"\n#include <QCryptographicHash>\n#include <QGuiApplication>\n#include <QJsonArray>\n#include <QJsonDocument>\n#include <QJsonObject>\n#include <QMessageBox>\n#include <QNetworkAccessManager>\n#include <QNetworkReply>\n#include <QSettings>\n#include <QTimeZone>\nstatic const char *CFG_LAST_REPORT = \"last_report\";\nstatic const char *CFG_TELEMETRY_ENABLED = \"telemetry\";\nusing namespace albert;\n\n\nTelemetry::Telemetry(PluginRegistry &pr, ExtensionRegistry &er):\n    plugin_registry_(pr),\n    extension_registry_(er),\n    last_report(App::state()->value(CFG_LAST_REPORT,  // Default to -24h avoid sending old data\n                               QDateTime::currentDateTime().addDays(-1)).toDateTime())\n{\n    if (auto s = App::settings(); s->contains(CFG_TELEMETRY_ENABLED))\n        enabled_ = s->value(CFG_TELEMETRY_ENABLED).toBool();\n    else\n    {\n        auto text = tr(\n            \"Albert collects data to improve the user experience. \"\n            \"Do you want to help to improve Albert by sending telemetry data?\"\n        );\n\n        auto informative_text = tr(\n            \"No tracking, profiling, sharing or commercial use. \"\n            \"The transmitted data is non-personal. \"\n            \"You can change this configuration anytime in the settings. \"\n            \"See the <a href='https://albertlauncher.github.io/privacy/'>privacy notice</a> for details.\"\n        );\n\n        using MB = QMessageBox;\n        MB mb;\n        mb.setIcon(MB::Question);\n        mb.setWindowTitle(qApp->applicationDisplayName());\n        mb.setText(text);\n        mb.setInformativeText(informative_text);\n        mb.setStandardButtons(MB::Yes|MB::No);\n        mb.setDefaultButton(MB::Yes);\n        const auto enable = MB::Yes == mb.exec();\n        enabled_ = enable;\n        App::settings()->setValue(CFG_TELEMETRY_ENABLED, enable);\n    }\n\n    connect(&timer, &QTimer::timeout, this, [this] { trySendReport(); });\n\n    timer.setInterval(60000);   // every minute\n\n    if (enabled_)\n        timer.start();\n}\n\nvoid Telemetry::trySendReport()\n{\n    auto now = QDateTime::currentDateTime();\n\n    // Skip if sent already today.\n    // At 3 AM most people are asleep. Use it as the beginning of a \"human day\".\n    if (now.addSecs(-10800).date() == last_report.addSecs(-10800).date())\n        return;\n\n    QString a = \"Zffb,!!*\\\" $## $\\\"' **!\";\n    for (auto &c : a)\n        c.unicode() = c.unicode() + 14;\n\n    QNetworkRequest request((QUrl(a)));\n    request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral(\"application/json\"));\n\n    DEBG << \"trySendReport\" << buildReportString();\n    auto *reply = network().put(request, buildReport().toJson(QJsonDocument::Compact));\n\n    connect(reply, &QNetworkReply::finished, this, [this, reply, now] {\n        reply->deleteLater();\n\n        if (reply->error() == QNetworkReply::NoError)\n        {\n            INFO << \"Successfully sent telemetry data.\";\n            last_report = now;\n            App::state()->setValue(CFG_LAST_REPORT, last_report);\n        }\n        else\n        {\n            WARN << \"Failed to send telemetry data:\";\n            WARN << reply->errorString();\n            auto json = QJsonDocument::fromJson(reply->readAll());\n            WARN << json[\"error\"].toString();\n        }\n    });\n}\n\nQJsonObject Telemetry::albertTelemetry() const\n{\n    QJsonArray enabled_plugins;\n    for (const auto &[id, plugin] : plugin_registry_.plugins())\n        if (plugin.enabled)\n            enabled_plugins.append(id);\n\n    QJsonObject activationsSinceLastReport;\n    for (const auto &[extension_id, activations] : UsageDatabase::instance().extensionActivationsSince(last_report))\n        activationsSinceLastReport.insert(extension_id, (int)activations);\n\n    QJsonObject o;\n    o.insert(\"version\", qApp->applicationVersion());\n    o.insert(\"qt_version\", qVersion());\n    o.insert(\"kernel\", QSysInfo::kernelType());\n    o.insert(\"os\", QSysInfo::prettyProductName());\n    o.insert(\"os_type\", QSysInfo::productType());\n    o.insert(\"os_version\", QSysInfo::productVersion());\n    o.insert(\"platform\", QGuiApplication::platformName());\n    o.insert(\"enabled_plugins\", enabled_plugins);\n    o.insert(\"extension_activations\", activationsSinceLastReport);\n\n    return o;\n}\n\nstatic QString machineIdentifier()\n{\n    auto bytes = QSysInfo::machineUniqueId();\n    bytes = QCryptographicHash::hash(bytes, QCryptographicHash::Sha1);\n    bytes = bytes.toHex();\n    return QString::fromUtf8(bytes).left(8);\n}\n\nstatic QString iso8601now()\n{\n    auto now = QDateTime::currentDateTime();\n    now.setTimeZone(QTimeZone::systemTimeZone());\n    return now.toString(Qt::ISODate);\n}\n\nQJsonDocument Telemetry::buildReport() const\n{\n    QJsonObject data;\n    data.insert(\"albert\", albertTelemetry());\n    if (auto *apps_plugin = App::instance().extension<detail::TelemetryProvider>(\"applications\");\n        apps_plugin)\n        data.insert(\"applications\", apps_plugin->telemetryData());\n\n    QJsonObject o;\n    o.insert(\"report\", 2);  // report version\n    o.insert(\"id\", machineIdentifier());\n    o.insert(\"time\", iso8601now());\n    o.insert(\"data\", data);\n\n    return QJsonDocument(o);\n}\n\nQString Telemetry::buildReportString() const\n{\n    return buildReport().toJson(QJsonDocument::Indented);\n}\n\nbool Telemetry::enabled() const { return enabled_; }\n\nvoid Telemetry::setEnabled(bool value)\n{\n    if (enabled_ != value)\n    {\n        enabled_ = value;\n        App::settings()->setValue(CFG_TELEMETRY_ENABLED, enabled_);\n\n        enabled_ ? timer.start() : timer.stop();\n    }\n}\n"
  },
  {
    "path": "src/app/telemetry.h",
    "content": "// Copyright (C) 2014-2024 Manuel Schneider\n\n#pragma once\n#include <QDateTime>\n#include <QObject>\n#include <QTimer>\nclass PluginRegistry;\nclass QJsonDocument;\nclass QJsonObject;\nnamespace albert { class ExtensionRegistry; }\n\nclass Telemetry : public QObject\n{\n    Q_OBJECT\n\npublic:\n    Telemetry(PluginRegistry &, albert::ExtensionRegistry &);\n\n    QJsonDocument buildReport() const;\n    QString buildReportString() const;\n\n    bool enabled() const;\n    void setEnabled(bool);\n\nprivate:\n    void trySendReport();\n    QJsonObject albertTelemetry() const;\n\n    PluginRegistry &plugin_registry_;\n    albert::ExtensionRegistry &extension_registry_;\n    QTimer timer;\n    QDateTime last_report;\n    bool enabled_;\n};\n"
  },
  {
    "path": "src/app/telemetryprovider.cpp",
    "content": "// Copyright (C) 2024-2024 Manuel Schneider\n\n#include \"telemetryprovider.h\"\n\nalbert::detail::TelemetryProvider::~TelemetryProvider() = default;  // vtable\n"
  },
  {
    "path": "src/app/triggersqueryhandler.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"app.h\"\n#include \"icon.h\"\n#include \"logging.h\"\n#include \"matcher.h\"\n#include \"queryengine.h\"\n#include \"standarditem.h\"\n#include \"triggersqueryhandler.h\"\n#include <mutex>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\nTriggersQueryHandler::TriggersQueryHandler(const QueryEngine &query_engine):\n    query_engine_(query_engine)\n{\n    QObject::connect(&query_engine, &QueryEngine::activeTriggersChanged,\n                     this, &TriggersQueryHandler::updateTriggers);\n    updateTriggers();\n}\n\nQString TriggersQueryHandler::id() const { return u\"triggers\"_s; }\n\nQString TriggersQueryHandler::name() const { return u\"Triggers\"_s; }\n\nQString TriggersQueryHandler::description() const { return tr(\"Trigger completions\"); }\n\nvoid TriggersQueryHandler::setFuzzyMatching(bool fuzzy) { fuzzy_ = fuzzy; }\n\nbool TriggersQueryHandler::supportsFuzzyMatching() const { return true; }\n\nshared_ptr<Item> TriggersQueryHandler::makeItem(const TriggerHandler &h) const\n{\n    return StandardItem::make(\n        h.id,\n        QString(h.trigger).replace(\" \", \"•\"),\n        QString(\"%1 · %2\").arg(h.name, h.description),\n        []{ return Icon::grapheme(u\"🚀\"_s); },\n        {{\n            \"set\",\n            tr(\"Set input text\"),\n            [&]{ App::instance().show(h.trigger); },\n            false\n        }},\n        h.trigger\n        );\n}\n\nvector<RankItem> TriggersQueryHandler::rankItems(QueryContext &ctx)\n{\n    Matcher matcher(ctx, {.fuzzy = fuzzy_});\n    vector<RankItem> r;\n\n    for (shared_lock l(trigger_handlers_mutex_);\n         const auto &h : trigger_handlers_)\n        if (!ctx.isValid())\n            break;\n        else if (const auto m = matcher.match(h.trigger, h.name, h.id); m)\n            r.emplace_back(makeItem(h), m);\n\n    return r;\n}\n\nvoid TriggersQueryHandler::updateTriggers()\n{\n    try {\n        vector<TriggerHandler> trigger_handlers;\n        for (const auto &[t, h] : query_engine_.activeTriggerHandlers())\n            trigger_handlers.emplace_back(h->id(), h->name(), h->description(), t);\n        lock_guard lock(trigger_handlers_mutex_);\n        trigger_handlers_ = ::move(trigger_handlers);\n    }\n    catch (...) {\n        WARN << u\"QueryHandler threw exception while updating TriggersQueryHandler.\"_s;\n    }\n}\n"
  },
  {
    "path": "src/app/triggersqueryhandler.h",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#pragma once\n#include \"globalqueryhandler.h\"\n#include <QCoreApplication>\n#include <shared_mutex>\nclass QueryEngine;\n\nclass TriggersQueryHandler : public QObject, public albert::GlobalQueryHandler\n{\n    Q_OBJECT\n\npublic:\n\n    TriggersQueryHandler(const QueryEngine &query_engine);\n    QString id() const override;\n    QString name() const override;\n    QString description() const override;\n    std::vector<albert::RankItem> rankItems(albert::QueryContext &) override;\n    void setFuzzyMatching(bool) override;\n    bool supportsFuzzyMatching() const override;\n\nprivate:\n\n    struct TriggerHandler {\n        QString id;\n        QString name;\n        QString description;\n        QString trigger;\n    };\n\n    std::shared_ptr<albert::Item> makeItem(const TriggerHandler &) const;\n    void updateTriggers();\n\n    const QueryEngine &query_engine_;\n    std::vector<TriggerHandler> trigger_handlers_;\n    std::shared_mutex trigger_handlers_mutex_;\n    std::atomic_bool fuzzy_;\n\n};\n"
  },
  {
    "path": "src/app/urlhandler.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"urlhandler.h\"\n\n// vtable goes here\nalbert::UrlHandler::~UrlHandler() = default;\n"
  },
  {
    "path": "src/common/extension.cpp",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#include \"extension.h\"\n\n// vtable\nalbert::Extension::~Extension() = default;\n"
  },
  {
    "path": "src/common/item.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"item.h\"\n#include <set>\nusing namespace albert;\n\nItem::~Item() {}\n\nQString Item::inputActionText() const { return text(); }\n\nstd::vector<Action> Item::actions() const { return {}; }\n\nvoid Item::addObserver(Item::Observer*) {}\n\nvoid Item::removeObserver(Item::Observer*) {}\n\n\nItem::Observer::~Observer() = default;\n\n\nclass detail::DynamicItem::Private\n{\npublic:\n    std::set<Item::Observer*> observers;\n};\n\ndetail::DynamicItem::DynamicItem() :\n    d(std::make_unique<detail::DynamicItem::Private>())\n{}\n\ndetail::DynamicItem::~DynamicItem() {}\n\nvoid detail::DynamicItem::dataChanged() const\n{\n    for (auto observer : d->observers)\n        observer->notify(this);\n}\n\nvoid detail::DynamicItem::addObserver(Item::Observer *o) { d->observers.insert(o); }\n\nvoid detail::DynamicItem::removeObserver(Item::Observer *o) { d->observers.erase(o); }\n\n"
  },
  {
    "path": "src/common/rankitem.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"rankitem.h\"\nusing namespace std;\n\nalbert::RankItem::RankItem(shared_ptr<Item> &&i, double s) noexcept:\n    item(::move(i)), score(s) {}\n\nalbert::RankItem::RankItem(const shared_ptr<Item> &i, double s) noexcept:\n    item(i), score(s) {}\n\nbool albert::RankItem::operator<(const RankItem &other) const\n{\n    if (score < other.score)\n        return true;\n    else if (score > other.score)\n        return false;\n    else if (const auto lt = item->text(), rt = other.item->text();\n             lt.size() > rt.size())\n        return true;\n    else if (lt.size() < rt.size())\n        return false;\n    else\n        return lt > rt;\n}\n\nbool albert::RankItem::operator>(const RankItem &other) const\n{\n    if (score > other.score)\n        return true;\n    else if (score < other.score)\n        return false;\n    else if (const auto lt = item->text(), rt = other.item->text();\n             lt.size() < rt.size())\n        return true;\n    else if (lt.size() > rt.size())\n        return false;\n    else\n        return lt < rt;\n}\n"
  },
  {
    "path": "src/config.h.in",
    "content": "// SPDX-FileCopyrightText: 2024 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\nnamespace albert\n{\nstatic const char * const ALBERT_VERSION_STRING = \"@PROJECT_VERSION@\";\nstatic const int ALBERT_VERSION_MAJOR = @PROJECT_VERSION_MAJOR@;\nstatic const int ALBERT_VERSION_MINOR = @PROJECT_VERSION_MINOR@;\nstatic const int ALBERT_VERSION_PATCH = @PROJECT_VERSION_PATCH@;\n}\n"
  },
  {
    "path": "src/frontend/frontend.cpp",
    "content": "// Copyright (c) 2024 Manuel Schneider\n\n#include \"frontend.h\"\n\n// vtable goes here\nalbert::detail::Frontend::~Frontend() = default;\n"
  },
  {
    "path": "src/frontend/session.cpp",
    "content": "// Copyright (c) 2024-2025 Manuel Schneider\n\n#include \"frontend.h\"\n#include \"query.h\"\n#include \"queryengine.h\"\n#include \"queryexecution.h\"\n#include \"session.h\"\nusing namespace albert::detail;\nusing namespace albert;\nusing namespace std;\n\nSession::Session(QueryEngine &e, Frontend &f) : engine_(e), frontend_(f)\n{\n    connect(&frontend_, &Frontend::inputChanged,\n            this, &Session::runQuery);\n    runQuery(frontend_.input());\n}\n\nSession::~Session()\n{\n    frontend_.setQuery(nullptr);\n}\n\nvoid Session::runQuery(const QString &query_string)\n{\n    if(!queries_.empty())\n        queries_.back()->execution().cancel();\n    auto &q = queries_.emplace_back(engine_.query(query_string));\n    frontend_.setQuery(q.get());\n}\n"
  },
  {
    "path": "src/frontend/session.h",
    "content": "// Copyright (c) 2024 Manuel Schneider\n\n#pragma once\n#include <QObject>\n#include <vector>\n#include <memory>\nclass QueryEngine;\nnamespace albert::detail {\nclass Query;\nclass Frontend;\n}\n\nclass Session : public QObject\n{\n    Q_OBJECT\n\npublic:\n\n    Session(QueryEngine &engine, albert::detail::Frontend &frontend);\n    ~Session();\n\nprivate:\n\n    void runQuery(const QString &query);\n\n    QueryEngine &engine_;\n    albert::detail::Frontend &frontend_;\n    std::vector<std::unique_ptr<albert::detail::Query>> queries_;\n\n};\n\n"
  },
  {
    "path": "src/icon/composedicon.cpp",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#include \"composedicon.h\"\n#include \"networkutil.h\"\n#include <QPainter>\n#include <QUrlQuery>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\ndouble ComposedIcon::default_size = 0.7;\ndouble ComposedIcon::default_pos1 = 0.0;\ndouble ComposedIcon::default_pos2 = 1.0;\n\nComposedIcon::ComposedIcon(unique_ptr<Icon> src1,\n                           unique_ptr<Icon> src2,\n                           double size1,\n                           double size2,\n                           double x1,\n                           double y1,\n                           double x2,\n                           double y2) :\n    src1_(::move(src1)),\n    src2_(::move(src2)),\n    size1_(size1),\n    size2_(size2),\n    x1_(x1),\n    y1_(y1),\n    x2_(x2),\n    y2_(y2)\n{}\n\nvoid ComposedIcon::paint(QPainter *p, const QRect &rect)\n{\n    if (isNull())\n        return;\n\n    const auto extent = min(rect.width(), rect.height());\n\n    // Add .5 to round instead of floor\n    const int extent1 = int(extent * size1_ + .5);\n    const int extent2 = int(extent * size2_ + .5);\n\n    const auto dpr = p->device()->devicePixelRatio();\n\n    const auto size1 = src1_->actualSize(QSize(extent1, extent1), dpr);\n    const auto size2 = src2_->actualSize(QSize(extent2, extent2), dpr);\n\n    const auto r1 = QRect(int((rect.width() - size1.width()) * x1_),\n                          int((rect.height() - size1.height()) * y1_),\n                          size1.width(),\n                          size1.height());\n\n    const auto r2 = QRect(int((rect.width() - size2.width()) * x2_),\n                          int((rect.height() - size2.height()) * y2_),\n                          size2.width(),\n                          size2.height());\n\n    src1_->paint(p, r1);\n    src2_->paint(p, r2);\n}\n\nbool ComposedIcon::isNull() { return !src1_ || src1_->isNull() || !src2_ || src2_->isNull(); }\n\nunique_ptr<Icon> ComposedIcon::clone() const\n{\n    return make_unique<ComposedIcon>(src1_->clone(),\n                                     src2_->clone(),\n                                     size1_,\n                                     size2_,\n                                     x1_,\n                                     y1_,\n                                     x2_,\n                                     y2_);\n}\n\nQString ComposedIcon::toUrl() const\n{\n    QString url = u\"%1:?src1=%2&src2=%3\"_s.arg(scheme(),\n                                               percentEncoded(src1_->toUrl()),\n                                               percentEncoded(src2_->toUrl()));\n    if (size1_ != default_size)\n        url += u\"&size1=\"_s + QString::number(size1_);\n    if (size2_ != default_size)\n        url += u\"&size2=\"_s + QString::number(size2_);\n    if (x1_ != default_pos1)\n        url += u\"&x1=\"_s + QString::number(x1_);\n    if (y1_ != default_pos1)\n        url += u\"&y1=\"_s + QString::number(y1_);\n    if (x2_ != default_pos2)\n        url += u\"&x2=\"_s + QString::number(x2_);\n    if (y2_ != default_pos2)\n        url += u\"&y2=\"_s + QString::number(y2_);\n    return url;\n}\n\nunique_ptr<ComposedIcon> ComposedIcon::fromUrl(const QString &url)\n{\n    QUrlQuery url_query(url.mid(scheme().size() + 2));  // \":?\"\n\n    auto src1 = iconFromUrl(percentDecoded(url_query.queryItemValue(u\"src1\"_s)));\n    if (!src1 || src1->isNull())\n        return {};\n\n    auto src2 = iconFromUrl(percentDecoded(url_query.queryItemValue(u\"src2\"_s)));\n    if (!src2 || src2->isNull())\n        return {};\n\n    const auto size1s = url_query.queryItemValue(u\"size1\"_s);\n    const auto size1 = size1s.isEmpty() ? default_size : size1s.toDouble();\n\n    const auto size2s = url_query.queryItemValue(u\"size2\"_s);\n    const auto size2 = size2s.isEmpty() ? default_size : size2s.toDouble();\n\n    const auto x1s = url_query.queryItemValue(u\"x1\"_s);\n    const auto x1 = x1s.isEmpty() ? default_pos1 : x1s.toDouble();\n\n    const auto y1s = url_query.queryItemValue(u\"y1\"_s);\n    const auto y1 = y1s.isEmpty() ? default_pos1 : y1s.toDouble();\n\n    const auto x2s = url_query.queryItemValue(u\"x2\"_s);\n    const auto x2 = x2s.isEmpty() ? default_pos2 : x2s.toDouble();\n\n    const auto y2s = url_query.queryItemValue(u\"y2\"_s);\n    const auto y2 = y2s.isEmpty() ? default_pos2 : y2s.toDouble();\n\n    return make_unique<ComposedIcon>(::move(src1), ::move(src2), size1, size2, x1, y1, x2, y2);\n}\n\nQString ComposedIcon::scheme() { return u\"comp\"_s; }\n"
  },
  {
    "path": "src/icon/composedicon.h",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"icon.h\"\n#include <QBrush>\n#include <memory>\n\nnamespace albert {\n\nclass ALBERT_EXPORT ComposedIcon : public albert::Icon\n{\npublic:\n    ComposedIcon(std::unique_ptr<Icon> src1,\n                 std::unique_ptr<Icon> src2,\n                 double size1 = default_size,\n                 double size2 = default_size,\n                 double x1 = default_pos1,\n                 double y1 = default_pos1,\n                 double x2 = default_pos2,\n                 double y2 = default_pos2);\n\n    void paint(QPainter*, const QRect&) override;\n    bool isNull() override;\n    std::unique_ptr<Icon> clone() const override;\n    QString toUrl() const override;\n\n    static std::unique_ptr<ComposedIcon> fromUrl(const QString &url);\n    static QString scheme();\n    static inline std::unique_ptr<Icon> make(auto&&... args)\n    { return std::make_unique<ComposedIcon>(std::forward<decltype(args)>(args)...); }\n\n    static double default_size;\n    static double default_pos1;\n    static double default_pos2;\n\nprivate:\n    std::shared_ptr<Icon> src1_;\n    std::shared_ptr<Icon> src2_;\n    double size1_, size2_, x1_, y1_, x2_, y2_;\n};\n\n} // namespace albert\n"
  },
  {
    "path": "src/icon/filetypeicon.cpp",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#include \"filetypeicon.h\"\n#include \"systemutil.h\"\n#include <QFileIconProvider>\nstatic QFileIconProvider qfip;\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\nFileTypeIcon::FileTypeIcon(const QString &path)\n    : QIconIcon(qfip.icon(QFileInfo(path)))\n    , path_(path)\n{}\n\nFileTypeIcon::FileTypeIcon(const filesystem::path &path)\n    : FileTypeIcon(toQString(path))\n{}\n\nunique_ptr<Icon> FileTypeIcon::clone() const { return make_unique<FileTypeIcon>(*this); }\n\nQString FileTypeIcon::toUrl() const { return u\"%1:%2\"_s.arg(scheme(), path_); }\n\nQString FileTypeIcon::scheme() { return u\"qfip\"_s; }\n\nunique_ptr<FileTypeIcon> FileTypeIcon::fromUrl(const QString &url)\n{ return make_unique<FileTypeIcon>(url.mid(scheme().size() + 1 )); }\n"
  },
  {
    "path": "src/icon/filetypeicon.h",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"qiconicon.h\"\n#include <filesystem>\n\nnamespace albert {\n\nclass ALBERT_EXPORT FileTypeIcon : public QIconIcon\n{\npublic:\n    FileTypeIcon(const QString &path);\n    FileTypeIcon(const std::filesystem::path &path);\n\n    std::unique_ptr<Icon> clone() const override;\n    QString toUrl() const override;\n\n    static std::unique_ptr<FileTypeIcon> fromUrl(const QString &url);\n    static QString scheme();\n    static inline std::unique_ptr<Icon> make(auto&& path)\n    { return std::make_unique<FileTypeIcon>(std::forward<decltype(path)>(path)); }\n\nprivate:\n    QString path_;\n};\n\n} // namespace albert\n"
  },
  {
    "path": "src/icon/graphemeicon.cpp",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#include \"graphemeicon.h\"\n#include \"networkutil.h\"\n#include <QApplication>\n#include <QPainter>\n#include <QPalette>\n#include <QUrlQuery>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\nGraphemeIcon::GraphemeIcon(const QString &grapheme, double scalar, const QBrush &color) :\n    grapheme_(grapheme),\n    scalar_(scalar),\n    color_(color)\n{}\n\nvoid GraphemeIcon::paint(QPainter *p, const QRect &rect)\n{\n    if (grapheme_.isEmpty())\n        return;\n\n    p->save();\n\n    QFont font = p->font();\n    // rough initial estimate to skip the first iterations. asc ~= 4 * desc, plus some buffer\n    font.setPixelSize(int(rect.height() * 5 / 6 ));\n    p->setFont(font);\n    auto br = p->boundingRect(rect, Qt::AlignCenter, grapheme_);\n\n    while (rect.width() < br.width() || rect.height() < br.height())\n    {\n        font.setPixelSize(font.pixelSize() - 1);\n        p->setFont(font);\n        br = p->boundingRect(rect, Qt::AlignCenter, grapheme_);\n    }\n\n    if (scalar_ != 1.0)\n    {\n        font.setPixelSize(int(font.pixelSize() * scalar_));\n        p->setFont(font);\n    }\n\n    p->setPen(QPen(color_, 1/p->device()->devicePixelRatioF()));\n    p->drawText(rect.toRectF(), Qt::AlignCenter, grapheme_);\n    // p->drawRect(p->boundingRect(rect, Qt::AlignCenter, grapheme_));\n\n    p->restore();\n\n}\n\nbool GraphemeIcon::isNull() { return grapheme_.isEmpty(); }\n\nunique_ptr<Icon> GraphemeIcon::clone() const { return make_unique<GraphemeIcon>(*this); }\n\nQString GraphemeIcon::toUrl() const\n{\n    QString url = u\"%1:?grapheme=%2\"_s.arg(scheme(), percentEncoded(grapheme_));\n    if (scalar_ != 1.0)\n        url += u\"&scalar=\"_s + QString::number(scalar_);\n    if (color_ != defaultBrush())\n        url += u\"&color=\"_s + color_.color().name(QColor::HexArgb);\n    return url;\n}\n\nunique_ptr<GraphemeIcon> GraphemeIcon::fromUrl(const QString &url)\n{\n    QUrlQuery urlquery(url.mid(scheme().size() + 2));  // \":?\"\n\n    QString text{urlquery.queryItemValue(u\"grapheme\"_s)};\n\n    bool ok;\n    double scalar{urlquery.queryItemValue(u\"scalar\"_s).toDouble(&ok)};\n    if (!ok)\n        scalar = 1.0;\n\n    QColor color(urlquery.queryItemValue(u\"color\"_s));\n\n    return make_unique<GraphemeIcon>(text, scalar, color.isValid() ? color : defaultBrush());\n}\n\nQString GraphemeIcon::scheme() { return u\"grapheme\"_s; }\n\nQBrush GraphemeIcon::defaultBrush(){ return QApplication::palette().color(QPalette::WindowText); }\n"
  },
  {
    "path": "src/icon/graphemeicon.h",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"icon.h\"\n#include <QBrush>\n#include <QString>\n\nnamespace albert {\n\nclass ALBERT_EXPORT GraphemeIcon : public albert::Icon\n{\npublic:\n    GraphemeIcon(const QString &grapheme,\n                 double scalar = 1.0,\n                 const QBrush &color = defaultBrush());\n\n    void paint(QPainter*, const QRect&) override;\n    bool isNull() override;\n    std::unique_ptr<Icon> clone() const override;\n    QString toUrl() const override;\n\n    static std::unique_ptr<GraphemeIcon> fromUrl(const QString &url);\n    static QString scheme();\n    static inline std::unique_ptr<Icon> make(auto&&... args)\n    { return std::make_unique<GraphemeIcon>(std::forward<decltype(args)>(args)...); }\n\n    static QBrush defaultBrush();\n\nprivate:\n    const QString grapheme_;\n    const double scalar_;\n    const QBrush color_;\n};\n\n} // namespace albert\n"
  },
  {
    "path": "src/icon/icon.cpp",
    "content": "// SPDX-FileCopyrightText: 2022-2026 Manuel Schneider\n\n#include \"icon.h\"\n#include \"composedicon.h\"\n#include \"filetypeicon.h\"\n#include \"graphemeicon.h\"\n#include \"iconifiedicon.h\"\n#include \"imageicon.h\"\n#include \"logging.h\"\n#include \"qiconengineadapter.h\"\n#include \"recticon.h\"\n#include \"standardicon.h\"\n#include \"themeicon.h\"\n#include <QPainter>\n#include <memory>\nusing namespace albert;\nusing namespace std;\n\nIcon::~Icon() = default;\n\nQSize Icon::actualSize(const QSize &device_independent_size, qreal)\n{\n    return device_independent_size;\n}\n\nQPixmap Icon::pixmap(const QSize &device_independent_size, qreal device_pixel_ratio)\n{\n    const auto actual_device_independent_size = actualSize(device_independent_size, device_pixel_ratio);\n\n    QPixmap pm(actual_device_independent_size * device_pixel_ratio);\n    pm.setDevicePixelRatio(device_pixel_ratio);\n    pm.fill(Qt::transparent);\n\n    QPainter p(&pm);\n    paint(&p, QRect(QPoint(0,0), actual_device_independent_size));\n\n    return pm;\n}\n\nbool Icon::isNull() { return false; }\n\nQString Icon::cacheKey() { return toUrl(); }\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nstatic inline bool checkUrlScheme(const QString &url, const QString &scheme)\n{\n    return url.size() > scheme.size() + 1  // \":\"\n           && url[scheme.size()] == u':'\n           && url.startsWith(scheme);\n}\n\nstatic unique_ptr<Icon> dispatch(const QString &url)\n{\n    if (checkUrlScheme(url, FileTypeIcon::scheme()))\n        return FileTypeIcon::fromUrl(url);\n\n    else if (checkUrlScheme(url, StandardIcon::scheme()))\n        return StandardIcon::fromUrl(url);\n\n    else if (checkUrlScheme(url, ThemeIcon::scheme()))\n        return ThemeIcon::fromUrl(url);\n\n    else if (checkUrlScheme(url, ImageIcon::fileScheme()))\n        return ImageIcon::fromUrl(url);\n\n    else if (checkUrlScheme(url, GraphemeIcon::scheme()))\n        return GraphemeIcon::fromUrl(url);\n\n    else if (checkUrlScheme(url, RectIcon::scheme()))\n        return RectIcon::fromUrl(url);\n\n    else if (checkUrlScheme(url, ComposedIcon::scheme()))\n        return ComposedIcon::fromUrl(url);\n\n    else if (checkUrlScheme(url, IconifiedIcon::scheme()))\n        return IconifiedIcon::fromUrl(url);\n\n    else if (checkUrlScheme(url, ImageIcon::qrcScheme()))\n        return make_unique<ImageIcon>(url.mid(ImageIcon::qrcScheme().size()));  // keep \":\"\n\n    else  // takes care of qresource files (:bla) as well as regular files (/foo/bar)\n        return make_unique<ImageIcon>(url);\n\n    return {};\n}\n\n//--------------------------------------------------------------------------------------------------\n\nQIcon Icon::qIcon(unique_ptr<Icon> icon) {\n    if (icon && !icon->isNull())\n        return QIcon(new QIconEngineAdapter(::move(icon)));\n    return {};\n}\n\nunique_ptr<Icon> Icon::iconFromUrl(const QString &url)\n{\n    if (auto engine = dispatch(url); engine && !engine->isNull())\n        return engine;\n    return {};\n}\n\nunique_ptr<Icon> Icon::iconFromUrls(const QStringList &urls)\n{\n    for (const auto &url : urls)\n        if (auto engine = iconFromUrl(url); engine && !engine->isNull())\n            return engine;\n\n    WARN << \"Failed getting icon for:\" << urls;\n    return {};\n}\n\n//--------------------------------------------------------------------------------------------------\n\nunique_ptr<Icon> Icon::image(const QString &path) { return make_unique<ImageIcon>(path); }\n\nunique_ptr<Icon> Icon::image(const filesystem::path &path) { return make_unique<ImageIcon>(path); }\n\n// -------------------------------------------------------------------------------------------------\n\nunique_ptr<Icon> Icon::fileType(const QString &path) { return make_unique<FileTypeIcon>(path); }\n\nunique_ptr<Icon> Icon::fileType(const filesystem::path &path)\n{ return make_unique<FileTypeIcon>(path); }\n\n// -------------------------------------------------------------------------------------------------\n\nunique_ptr<Icon> Icon::theme(const QString &icon_name) { return make_unique<ThemeIcon>(icon_name); }\n\n// -------------------------------------------------------------------------------------------------\n\nunique_ptr<Icon> Icon::standard(StandardIconType type) { return make_unique<StandardIcon>(type); }\n\n// -------------------------------------------------------------------------------------------------\n\n\nQBrush Icon::graphemeDefaultBrush() { return GraphemeIcon::defaultBrush(); }\n\nunique_ptr<Icon> Icon::grapheme(const QString &grapheme, double scalar)\n{ return make_unique<GraphemeIcon>(grapheme, scalar); }\n\nunique_ptr<Icon> Icon::grapheme(const QString &grapheme, double scalar, const QBrush &brush)\n{ return make_unique<GraphemeIcon>(grapheme, scalar, brush); }\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nconst QBrush &Icon::iconifiedDefaultBackgroundBrush()\n{ return IconifiedIcon::default_background_brush; }\n\nconst QBrush &Icon::iconifiedDefaultBorderBrush()\n{ return IconifiedIcon::default_border_brush; }\n\nunique_ptr<Icon> Icon::iconified(unique_ptr<Icon> icon,\n                                 const QBrush &background_brush,\n                                 double border_radius,\n                                 int border_width,\n                                 const QBrush &border_brush)\n{ return make_unique<IconifiedIcon>(::move(icon), background_brush, border_radius, border_width, border_brush); }\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nunique_ptr<Icon> Icon::composed(unique_ptr<Icon> icon1,\n                                unique_ptr<Icon> icon2,\n                                double size1,\n                                double size2,\n                                double x1,\n                                double y1,\n                                double x2,\n                                double y2)\n{ return make_unique<ComposedIcon>(::move(icon1), ::move(icon2), size1, size2, x1, y1, x2, y2); }\n"
  },
  {
    "path": "src/icon/iconifiedicon.cpp",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#include \"iconifiedicon.h\"\n#include \"networkutil.h\"\n#include <QPainter>\n#include <QUrlQuery>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\nstatic QRadialGradient makeBackgroundGradient(auto s, auto e)\n{\n    QRadialGradient gradient(.5, .5, .8, .5, .0);\n    gradient.setCoordinateMode(QGradient::ObjectMode);\n    gradient.setColorAt(0.0, QColor(s, s, s));\n    gradient.setColorAt(1.0, QColor(e, e, e));\n    return gradient;\n}\n\nstatic QLinearGradient makeBorderGradient(auto s, auto e)\n{\n    QLinearGradient gradient(.0, .0, .0, 1.);\n    gradient.setCoordinateMode(QGradient::ObjectMode);\n    gradient.setColorAt(0.0, QColor(s, s, s));\n    gradient.setColorAt(1.0, QColor(e, e, e));\n    return gradient;\n}\n\ndouble IconifiedIcon::default_border_radius = 1.0;\nint IconifiedIcon::default_border_width = 1;\nQBrush IconifiedIcon::default_background_brush = makeBackgroundGradient(255, 224);\nQBrush IconifiedIcon::default_border_brush = makeBorderGradient(224, 192);\n\nIconifiedIcon::IconifiedIcon(unique_ptr<Icon> icon,\n                             const QBrush &background_brush,\n                             double border_radius,\n                             int border_width,\n                             const QBrush &border_brush) :\n    src_(::move(icon)),\n    color_(background_brush),\n    border_radius_(border_radius),\n    border_width_(border_width),\n    border_brush_(border_brush)\n{}\n\nQSize IconifiedIcon::actualSize(const QSize &device_independent_size, double device_pixel_ratio)\n{\n    const auto dst_extent = min(device_independent_size.width(), device_independent_size.height());\n\n    const auto max_content_extent = dst_extent - 2 * border_width_;  // excl. border\n\n    const auto src_size = src_->actualSize({max_content_extent, max_content_extent}, device_pixel_ratio);\n\n    const auto src_extent = max(src_size.width(), src_size.height());  // excl. border\n\n    const auto final_extent = min(src_extent + 2 * border_width_, dst_extent);  // incl. border\n\n    return {final_extent, final_extent};\n}\n\nvoid IconifiedIcon::paint(QPainter *p, const QRect &rect)\n{\n    p->save();\n\n    const auto dpr = p->device()->devicePixelRatioF();\n\n    const auto size = actualSize(rect.size(), dpr);\n\n    const auto final_rect = QRect(rect.topLeft() + QPoint(rect.width() - size.width(),\n                                                          rect.height() - size.height()) / 2,\n                                  size);\n\n    QImage img(size * dpr, QImage::Format_ARGB32);\n    img.setDevicePixelRatio(dpr);\n    img.fill(Qt::transparent);\n\n    QPainter imgp(&img);\n    imgp.setRenderHint(QPainter::RenderHint::Antialiasing, true);\n\n    const QRect img_content_rect = QRect(border_width_, border_width_,\n                                         size.width() - 2 * border_width_,\n                                         size.height() - 2 * border_width_);\n\n    // Draw backgound circle\n    imgp.setPen(Qt::NoPen);\n    imgp.setBrush(color_);\n    imgp.drawRoundedRect(img_content_rect, 100 * border_radius_, 100 * border_radius_, Qt::RelativeSize);\n\n    // Draw pixmap\n    imgp.setCompositionMode(QPainter::CompositionMode_SourceAtop);\n    src_->paint(&imgp, img_content_rect);\n\n    // Draw border circle\n    if (border_width_ > 0)\n    {\n        QPen pen(border_brush_, border_width_);\n        imgp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n        imgp.setPen(pen);\n        imgp.setBrush(Qt::NoBrush);\n        // const auto m = border_width_/2 + 1/dpr; // compensate for antialiasing\n        // imgp.drawEllipse(img_rect.marginsRemoved({m, m, m, m}));\n        imgp.drawRoundedRect(img_content_rect, 100 * border_radius_, 100 * border_radius_, Qt::RelativeSize);\n    }\n\n    imgp.end();\n\n    p->drawImage(final_rect, img, img.rect());\n\n    p->restore();\n}\n\nbool IconifiedIcon::isNull() { return !src_ || src_->isNull(); }\n\nunique_ptr<Icon> IconifiedIcon::clone() const\n{\n    return make_unique<IconifiedIcon>(src_->clone(),\n                                      color_,\n                                      border_width_,\n                                      border_radius_,\n                                      border_brush_);\n}\n\nQString IconifiedIcon::toUrl() const\n{\n    QString url = u\"%1:?src=%2\"_s.arg(scheme(), percentEncoded(src_->toUrl()));\n    if (color_ != default_background_brush)\n        url += u\"&color=\"_s + color_.color().name(QColor::HexArgb);\n    if (border_width_ != default_border_width)\n        url += u\"&border_width=\"_s + QString::number(border_width_);\n    if (border_radius_ != default_border_width)\n        url += u\"&border_radius=\"_s + QString::number(border_radius_);\n    if (border_brush_ != default_border_brush)\n        url += u\"&border_color=\"_s + border_brush_.color().name(QColor::HexArgb);\n    return url;\n}\n\n\nunique_ptr<IconifiedIcon> IconifiedIcon::fromUrl(const QString &url)\n{\n    QUrlQuery url_query(url.mid(scheme().size() + 2));  // \":?\"\n\n    auto src = iconFromUrl(percentDecoded(url_query.queryItemValue(u\"src\"_s)));\n\n    QColor color(url_query.queryItemValue(u\"color\"_s));\n\n    QColor border_color(url_query.queryItemValue(u\"border_color\"_s));\n\n    bool ok;\n    int border_width{url_query.queryItemValue(u\"border_width\"_s).toInt(&ok)};\n    if (!ok)\n        border_width = default_border_width;\n\n    double border_radius{url_query.queryItemValue(u\"border_radius\"_s).toDouble(&ok)};\n    if (!ok)\n        border_radius = default_border_radius;\n\n    return make_unique<IconifiedIcon>(::move(src),\n                                      color.isValid() ? color : default_background_brush,\n                                      border_width,\n                                      border_radius,\n                                      border_color.isValid() ? border_color : default_border_brush);\n}\n\nQString IconifiedIcon::scheme() { return u\"icon\"_s; }\n"
  },
  {
    "path": "src/icon/iconifiedicon.h",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"icon.h\"\n#include <QIcon>\n#include <QBrush>\n#include <memory>\n\nnamespace albert {\n\nclass ALBERT_EXPORT IconifiedIcon : public albert::Icon\n{\npublic:\n    IconifiedIcon(std::unique_ptr<Icon> icon,\n                  const QBrush &background_brush = default_background_brush,\n                  double radius = default_border_radius,\n                  int border_width = default_border_width,\n                  const QBrush &border_brush = default_border_brush);\n\n\n    QSize actualSize(const QSize&, double) override;\n    void paint(QPainter*, const QRect&) override;\n    bool isNull() override;\n    std::unique_ptr<Icon> clone() const override;\n    QString toUrl() const override;\n\n    static std::unique_ptr<IconifiedIcon> fromUrl(const QString &url);\n    static QString scheme();\n    static inline std::unique_ptr<Icon> make(auto&&... args)\n    { return std::make_unique<IconifiedIcon>(std::forward<decltype(args)>(args)...); }\n\n    static QBrush default_background_brush;\n    static double default_border_radius;\n    static int    default_border_width;\n    static QBrush default_border_brush;\n\nprivate:\n    std::unique_ptr<Icon> src_;\n    const QBrush color_;\n    const double border_radius_;\n    const int    border_width_;\n    const QBrush border_brush_;\n};\n\n} // namespace albert\n"
  },
  {
    "path": "src/icon/imageicon.cpp",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#include \"imageicon.h\"\n#include \"systemutil.h\"\n#include <QFile>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\n// TODO use imageloader\n\nImageIcon::ImageIcon(const QString &path)\n    : QIconIcon(QFile::exists(path) ? QIcon(path) : QIcon())  // QIcon produces non null icons from non existing files\n    , path_(path)\n{}\n\nImageIcon::ImageIcon(const filesystem::path &path)\n    : ImageIcon(toQString(path))\n{}\n\nunique_ptr<Icon> ImageIcon::clone() const { return make_unique<ImageIcon>(*this); }\n\nQString ImageIcon::toUrl() const { return u\"%1:%2\"_s.arg(fileScheme(), path_); }\n\nQString ImageIcon::fileScheme() { return u\"file\"_s; }\n\nQString ImageIcon::qrcScheme() { return u\"qrc\"_s; }\n\nunique_ptr<ImageIcon> ImageIcon::fromUrl(const QString &url)\n{ return make_unique<ImageIcon>(url.mid(fileScheme().size() + 1)); }\n"
  },
  {
    "path": "src/icon/imageicon.h",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"qiconicon.h\"\n#include <filesystem>\n\nnamespace albert {\n\nclass ALBERT_EXPORT ImageIcon : public QIconIcon\n{\npublic:\n    ImageIcon(const QString &path);\n    ImageIcon(const std::filesystem::path &path);\n\n    std::unique_ptr<Icon> clone() const override;\n    QString toUrl() const override;\n\n    static std::unique_ptr<ImageIcon> fromUrl(const QString &url);\n    static QString fileScheme();\n    static QString qrcScheme();\n    static inline std::unique_ptr<Icon> make(auto&& path)\n    { return std::make_unique<ImageIcon>(std::forward<decltype(path)>(path)); }\n\nprivate:\n    QString path_;\n};\n\n} // namespace albert\n"
  },
  {
    "path": "src/icon/qiconicon.cpp",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#include \"qiconicon.h\"\nusing namespace albert;\n\nQIconIcon::QIconIcon(const QIcon &icon) : icon_(icon) {}\n\nQSize QIconIcon::actualSize(const QSize &device_independent_size, qreal device_pixel_ratio)\n{\n    //\n    // Docs: QIcon::actualSize:\n    //\n    // > Returns the actual size of the icon for the requested size, mode, and state. The result might be smaller than\n    // > requested, but never larger. The returned size is in device-independent pixels (This is relevant for high-dpi\n    // > pixmaps.)\n    //\n    // Tests show it returns device dependent sizes.\n    //\n    return icon_.actualSize(device_independent_size * device_pixel_ratio) / device_pixel_ratio;\n}\n\nQPixmap QIconIcon::pixmap(const QSize &device_independent_size, qreal device_pixel_ratio)\n{\n    return icon_.pixmap(device_independent_size, device_pixel_ratio);\n}\n\nvoid QIconIcon::paint(QPainter *painter, const QRect &rect) { icon_.paint(painter, rect); }\n\nbool QIconIcon::isNull() { return icon_.isNull(); }\n"
  },
  {
    "path": "src/icon/qiconicon.h",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"icon.h\"\n#include <QIcon>\n\nnamespace albert {\n\nclass QIconIcon : public albert::Icon\n{\npublic:\n    QIconIcon(const QIcon &icon);\n\n    QSize actualSize(const QSize&, double) override;\n    QPixmap pixmap(const QSize&, double) override;\n    void paint(QPainter*, const QRect&) override;\n    bool isNull() override;\n\nprivate:\n    QIcon icon_;\n};\n\n} // namespace albert\n"
  },
  {
    "path": "src/icon/recticon.cpp",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#include \"recticon.h\"\n#include <QPainter>\n#include <QUrlQuery>\n#include <memory>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\nRectIcon::RectIcon(const QBrush &color, double radius, int border_width, const QBrush &border_color) :\n    color_(color),\n    radius_(radius),\n    border_width_(border_width),\n    border_color_(border_color)\n{}\n\nvoid RectIcon::paint(QPainter *p, const QRect &rect)\n{\n    p->save();\n\n    p->setRenderHint(QPainter::RenderHint::Antialiasing, true);\n    p->setPen(Qt::NoPen);\n\n    const auto radius = radius_ * rect.width()/2;\n\n    if (radius == 0)\n    {\n        if (border_width_ == 0)\n        {\n            p->setBrush(color_);\n            p->drawRect(rect);\n        }\n        else\n        {\n            p->setBrush(border_color_);\n            p->drawRect(rect);\n            const auto inner_rect = rect.marginsRemoved({border_width_, border_width_, border_width_, border_width_});\n            p->setBrush(color_);\n            p->setCompositionMode(QPainter::CompositionMode_Source);\n            p->drawRect(inner_rect);\n        }\n    }\n    else\n    {\n        p->setRenderHint(QPainter::RenderHint::Antialiasing, true);\n        if (border_width_ == 0)\n        {\n            p->setBrush(color_);\n            p->drawRoundedRect(rect, radius-border_width_, radius-border_width_);\n        }\n        else\n        {\n            p->setBrush(border_color_);\n            p->drawRoundedRect(rect, radius, radius);\n            const auto inner_rect = rect.marginsRemoved({border_width_, border_width_, border_width_, border_width_});\n            p->setBrush(color_);\n            p->setCompositionMode(QPainter::CompositionMode_Source);\n            p->drawRoundedRect(inner_rect, radius-border_width_, radius-border_width_);\n\n        }\n    }\n\n    p->restore();\n}\n\nbool RectIcon::isNull() { return false; }\n\nunique_ptr<Icon> RectIcon::clone() const { return make_unique<RectIcon>(*this); }\n\nQString RectIcon::toUrl() const\n{\n    QString url = u\"%1:color=%2\"_s.arg(scheme(), color_.color().name(QColor::HexArgb));\n    if (radius_ != defaultRadius())\n        url += u\"&radius=\"_s + QString::number(radius_);\n    if (border_width_ != defaultBorderWidth())\n        url += u\"&border_width=\"_s + QString::number(border_width_);\n    if (border_color_ != defaultBorderColor())\n        url += u\"&border_color=\"_s + border_color_.color().name(QColor::HexArgb);\n    return url;\n}\n\nunique_ptr<RectIcon> RectIcon::fromUrl(const QString &url)\n{\n    QUrlQuery url_query(url.mid(scheme().size() + 2));  // \":?\"\n\n    QColor color(url_query.queryItemValue(u\"color\"_s));\n\n    QColor border_color(url_query.queryItemValue(u\"border_color\"_s));\n\n    bool ok;\n    int border_width{url_query.queryItemValue(u\"border_width\"_s).toInt(&ok)};\n    if (!ok)\n        border_width = defaultBorderWidth();\n\n    double radius{url_query.queryItemValue(u\"radius\"_s).toDouble(&ok)};\n    if (!ok)\n        radius = defaultRadius();\n\n    return make_unique<RectIcon>(color.isValid() ? color : defaultColor(),\n                                 radius,\n                                 border_width,\n                                 border_color.isValid() ? border_color : defaultBorderColor());\n}\n\nQString RectIcon::scheme() { return u\"rect\"_s; }\n\nQBrush RectIcon::defaultColor() { return QBrush(Qt::black); }\n\ndouble RectIcon::defaultRadius() { return 1.0; }\n\nint RectIcon::defaultBorderWidth() { return 0; }\n\nQBrush RectIcon::defaultBorderColor() { return QBrush(Qt::black); }\n"
  },
  {
    "path": "src/icon/recticon.h",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"icon.h\"\n#include <QBrush>\n\nnamespace albert {\n\nclass ALBERT_EXPORT RectIcon : public albert::Icon\n{\npublic:\n    RectIcon(const QBrush &color = defaultColor(),\n             double radius = defaultRadius(),\n             int border_width = defaultBorderWidth(),\n             const QBrush &border_color = defaultBorderColor());\n\n    void paint(QPainter*, const QRect&) override;\n    bool isNull() override;\n    std::unique_ptr<Icon> clone() const override;\n    QString toUrl() const override;\n\n    static std::unique_ptr<RectIcon> fromUrl(const QString &url);\n    static QString scheme();\n    static inline std::unique_ptr<Icon> make(auto&&... args)\n    { return std::make_unique<RectIcon>(std::forward<decltype(args)>(args)...); }\n\n    static QBrush defaultColor();\n    static double defaultRadius();\n    static int defaultBorderWidth();\n    static QBrush defaultBorderColor();\n\nprivate:\n    const QBrush color_;\n    const double radius_;\n    const int border_width_;\n    const QBrush border_color_;\n};\n\n} // namespace albert\n"
  },
  {
    "path": "src/icon/standardicon.cpp",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#include \"standardicon.h\"\n#include \"logging.h\"\n#include <QApplication>\n#include <QMetaEnum>\n#include <QStyle>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\n// https://doc.qt.io/qt-6/qstyle.html#StandardPixmap-enum\n\nStandardIcon::StandardIcon(StandardIconType type)\n    : QIconIcon(qApp->style()->standardIcon((QStyle::StandardPixmap)type))\n    , type_(type)\n{}\n\nunique_ptr<Icon> StandardIcon::clone() const { return make_unique<StandardIcon>(*this); }\n\nQString StandardIcon::toUrl() const\n{\n    return u\"%1:%2\"_s.arg(scheme(), QMetaEnum::fromType<QStyle::StandardPixmap>().key(type_));\n}\n\nQString StandardIcon::scheme() { return u\"qsp\"_s; }\n\nunique_ptr<StandardIcon> StandardIcon::fromUrl(const QString &url)\n{\n    const auto enum_key = url.mid(scheme().size() + 1);\n\n    const auto meta_enum = QMetaEnum::fromType<QStyle::StandardPixmap>();\n    for (int i = 0; i < meta_enum.keyCount(); ++i)\n        if (enum_key == meta_enum.key(i))\n            return make_unique<StandardIcon>(static_cast<StandardIconType>(meta_enum.value(i)));\n\n    WARN << \"No such QStyle::StandardPixmap key found:\" << enum_key;\n    return nullptr;\n}\n\n"
  },
  {
    "path": "src/icon/standardicon.h",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"qiconicon.h\"\n\nnamespace albert {\n\nclass ALBERT_EXPORT StandardIcon : public QIconIcon\n{\npublic:\n\n    StandardIcon(StandardIconType type);\n\n    std::unique_ptr<Icon> clone() const override;\n    QString toUrl() const override;\n\n    static std::unique_ptr<StandardIcon> fromUrl(const QString &url);\n    static QString scheme();\n    static inline auto make(StandardIconType type)\n    { return std::make_unique<StandardIcon>(type); }\n\nprivate:\n    StandardIconType type_;\n};\n\n} // namespace albert\n"
  },
  {
    "path": "src/icon/themeicon.cpp",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#include \"themeicon.h\"\n#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)\n#include \"iconlookup.h\"\n#endif\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\n\nThemeIcon::ThemeIcon(const QString &name)\n    : QIconIcon(\n        // https://bugreports.qt.io/browse/QTBUG-135159\n        // https://codereview.qt-project.org/c/qt/qtbase/+/634907\n#if QT_VERSION > 0x060800\n        QIcon::fromTheme(name)\n#else\n        QIcon(XDG::IconLookup::iconPath(name))\n#endif\n    )\n    , name_(name)\n{}\n\nunique_ptr<Icon> ThemeIcon::clone() const { return make_unique<ThemeIcon>(*this); }\n\nQString ThemeIcon::toUrl() const { return u\"%1:%2\"_s.arg(scheme(), name_); }\n\nQString ThemeIcon::scheme() { return u\"xdg\"_s; }\n\nunique_ptr<ThemeIcon> ThemeIcon::fromUrl(const QString &url)\n{ return make_unique<ThemeIcon>(url.mid(scheme().size() + 1 )); }\n\n"
  },
  {
    "path": "src/icon/themeicon.h",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"qiconicon.h\"\n#include <QString>\n\nnamespace albert {\n\nclass ALBERT_EXPORT ThemeIcon : public QIconIcon\n{\npublic:\n    ThemeIcon(const QString &name);\n\n    std::unique_ptr<Icon> clone() const override;\n    QString toUrl() const override;\n\n    static std::unique_ptr<ThemeIcon> fromUrl(const QString &url);\n    static QString scheme();\n    static inline std::unique_ptr<Icon> make(auto&& name)\n    { return std::make_unique<ThemeIcon>(std::forward<decltype(name)>(name)); }\n\nprivate:\n    QString name_;\n};\n\n} // namespace albert\n"
  },
  {
    "path": "src/main.cpp",
    "content": "// Copyright (C) 2024-2025 Manuel Schneider\n\n#include <iostream>\nusing namespace std;\nnamespace albert {\nextern int run(int, char **);\n}\n\nint main(int argc, char **argv)\n{\n    try {\n        return albert::run(argc, argv);\n    } catch (const exception &e) {\n        cout << e.what() << endl;\n    } catch (...) {\n        cout << \"Unknown exception in main!\" << endl;\n    }\n    return EXIT_FAILURE;\n}\n"
  },
  {
    "path": "src/platform/mac/platform.mm",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"logging.h\"\n#include \"platform.h\"\n#include <Cocoa/Cocoa.h>\n#include <Foundation/Foundation.h>\n#include <QGuiApplication>\nusing namespace std;\n#if  ! __has_feature(objc_arc)\n#error This file must be compiled with ARC.\n#endif\n\nvoid platform::initPlatform(){}\n\nvoid platform::initNativeWindow(unsigned long long wid)\n{\n    NSView *nsview = (__bridge NSView *)reinterpret_cast<void *>(wid);\n    NSWindow *ns_window = [nsview window];\n\n    /*\n     * @const NSWindowAnimationBehaviorDefault  Let AppKit infer animation behavior for this window.\n     * @const NSWindowAnimationBehaviorNone     Suppress inferred animations (don't animate).\n     * @const NSWindowAnimationBehaviorDocumentWindow\n     * @const NSWindowAnimationBehaviorUtilityWindow\n     * @const NSWindowAnimationBehaviorAlertPanel\n     */\n\n    [ns_window setAnimationBehavior: NSWindowAnimationBehaviorNone]; // no fancy fade or sth\n\n\n    /*\n     * @const NSWindowCollectionBehaviorPrimary Marks a window as primary. This collection behavior should commonly be used for document or viewer windows.\n     * @const NSWindowCollectionBehaviorAuxiliary Marks a window as auxiliary. This collection behavior should commonly be used for About or Settings windows, as well as utility panes.\n     * @const NSWindowCollectionBehaviorCanJoinAllApplications Marks a window as able to join all applications, allowing it to join other apps' sets and full screen spaces when eligible. This collection behavior should commonly be used for floating windows and system overlays.\n     *\n     * @discussion You may specify at most one of @c NSWindowCollectionBehaviorPrimary, @c NSWindowCollectionBehaviorAuxiliary, or @c NSWindowCollectionBehaviorCanJoinAllApplications. If unspecified, the window gets the default treatment determined by its other collection behaviors.\n     *\n     * @const NSWindowCollectionBehaviorDefault\n     * @const NSWindowCollectionBehaviorCanJoinAllSpaces\n     * @const NSWindowCollectionBehaviorMoveToActiveSpace\n     *\n     * @discussion You may specify at most one of \\c NSWindowCollectionBehaviorManaged, \\c NSWindowCollectionBehaviorTransient, or \\c NSWindowCollectionBehaviorStationary.  If neither is specified, the window gets the default behavior determined by its window level.\n     *\n     * @const NSWindowCollectionBehaviorManaged Participates in spaces, exposé.  Default behavior if `windowLevel == NSNormalWindowLevel`.\n     * @const NSWindowCollectionBehaviorTransient Floats in spaces, hidden by exposé.  Default behavior if `windowLevel != NSNormalWindowLevel`.\n     * @const NSWindowCollectionBehaviorStationary Unaffected by exposé.  Stays visible and stationary, like desktop window.\n     *\n     * @discussion You may specify at most one of \\c NSWindowCollectionBehaviorParticipatesInCycle or \\c NSWindowCollectionBehaviorIgnoresCycle.  If unspecified, the window gets the default behavior determined by its window level.\n     *\n     * @const NSWindowCollectionBehaviorParticipatesInCycle Default behavior if `windowLevel != NSNormalWindowLevel`.\n     * @const NSWindowCollectionBehaviorIgnoresCycle Default behavior if `windowLevel != NSNormalWindowLevel`.\n     *\n     * @discussion You may specify at most one of \\c NSWindowCollectionBehaviorFullScreenPrimary, \\c NSWindowCollectionBehaviorFullScreenAuxiliary, or \\c NSWindowCollectionBehaviorFullScreenNone.\n     *\n     * @const NSWindowCollectionBehaviorFullScreenPrimary The frontmost window with this collection behavior will be the fullscreen window.\n     * @const NSWindowCollectionBehaviorFullScreenAuxiliary Windows with this collection behavior can be shown with the fullscreen window.\n     * @const NSWindowCollectionBehaviorFullScreenNone The window can not be made fullscreen when this bit is set.\n     *\n     * @discussion You may specify at most one of \\c NSWindowCollectionBehaviorFullScreenAllowsTiling or \\c NSWindowCollectionBehaviorFullScreenDisallowsTiling, or an assertion will be raised.\n     *\n     * The default behavior is to allow any window to participate in full screen tiling, as long as it meets certain requirements, such as being resizable and not a panel or sheet. Windows which are not full screen capable can still become a secondary tile in full screen. A window can explicitly allow itself to be placed into a full screen tile by including \\c NSWindowCollectionBehaviorFullScreenAllowsTiling. Even if a window allows itself to be placed in a tile, it still may not be put in the tile if its \\c minFullScreenContentSize is too large to fit. A window can explicitly disallow itself from being placed in a full screen tile by including \\c NSWindowCollectionBehaviorFullScreenDisallowsTiling. This is useful for non-full screen capable windows to explicitly prevent themselves from being tiled. It can also be used by a full screen window to prevent any other windows from being placed in its full screen tile.\n     *\n     * @const NSWindowCollectionBehaviorFullScreenAllowsTiling This window can be a full screen tile window. It does not have to have \\c NSWindowCollectionBehaviorFullScreenPrimary set.\n     * @const NSWindowCollectionBehaviorFullScreenDisallowsTiling This window can NOT be made a full screen tile window; it still may be allowed to be a regular \\c NSWindowCollectionBehaviorFullScreenPrimary window.\n     */\n    [ns_window setCollectionBehavior: ([ns_window collectionBehavior] | NSWindowCollectionBehaviorMoveToActiveSpace | NSWindowCollectionBehaviorTransient)];\n\n    /*\n     * @const NSWindowStyleMaskBorderless\n     * @const NSWindowStyleMaskTitled\n     * @const NSWindowStyleMaskClosable\n     * @const NSWindowStyleMaskMiniaturizable\n     * @const NSWindowStyleMaskResizable\n     * @const NSWindowStyleMaskTexturedBackground  Textured window style is deprecated and should no longer be used. Specifies a window with textured background. Textured windows generally don't draw a top border line under the titlebar/toolbar. To get that line, use the \\c NSUnifiedTitleAndToolbarWindowMask mask.\n     * @const NSWindowStyleMaskUnifiedTitleAndToolbar  Specifies a window whose titlebar and toolbar have a unified look - that is, a continuous background. Under the titlebar and toolbar a horizontal separator line will appear.\n     * @const NSWindowStyleMaskFullScreen  When present, the window will appear full screen. This mask is automatically toggled when \\c -toggleFullScreen: is called.\n     * @const NSWindowStyleMaskFullSizeContentView If set, the \\c contentView will consume the full size of the window; it can be combined with other window style masks, but is only respected for windows with a titlebar. Utilizing this mask opts-in to layer-backing. Utilize the \\c contentLayoutRect or auto-layout \\c contentLayoutGuide to layout views underneath the titlebar/toolbar area.\n     * @const NSWindowStyleMaskUtilityWindow Only applicable for \\c NSPanel (or a subclass thereof).\n     * @const NSWindowStyleMaskDocModalWindow Only applicable for \\c NSPanel (or a subclass thereof).\n     * @const NSWindowStyleMaskNonactivatingPanel  Specifies that a panel that does not activate the owning application. Only applicable for \\c NSPanel (or a subclass thereof).\n     * @const NSWindowStyleMaskHUDWindow Specifies a heads up display panel.  Only applicable for \\c NSPanel (or a subclass thereof).\n     */\n    ns_window.styleMask |= NSWindowStyleMaskNonactivatingPanel;  // will get no key an not return focus without\n    ns_window.hidesOnDeactivate = false;  // makes hide on focus out work\n    [NSApp hide:nil];  // The app activates on start. undo.\n}\n\nQString platform::runAppleScript(const QString &script)\n{\n    DEBG << \"Running AppleScript:\" << script;\n\n    @autoreleasepool {\n        NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:script.toNSString()];\n        NSDictionary *errorInfo = nil;\n        NSAppleEventDescriptor *result = [appleScript executeAndReturnError:&errorInfo];\n        if (errorInfo)\n            throw runtime_error([errorInfo.description UTF8String]);\n        else\n            return QString::fromNSString([result stringValue]);\n    }\n}\n\n\n// ------------------------------------ Maybe useful trash -----------------------------------------\n\n//static void requestFullDiskAccessPermissions(){\n////    NSURL *url = [NSURL URLWithString:@\"x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles\"];\n////    if ([[NSWorkspace sharedWorkspace] openURL:url]) {\n////        NSLog(@\"Opened Security & Privacy preferences\");\n////    }\n//}\n\n\n////        // Schedule the notification.\n////        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];\n////        //        [center addNotificationRequest:request];  // 02-23-2019 don't compile\n////        [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {\n////                     if (!error) {\n////                         NSLog(@\"Local Notification succeeded\");\n////                     }\n////                     else {\n////                         NSLog(@\"Local Notification failed\");\n////                     }\n////                 }];\n\n\n//    CRIT << \"styleMask\" << ns_window.styleMask;\n//    CRIT << \"level\" << ns_window.level;\n//    CRIT << \"NSPanel\" << [ns_window isKindOfClass: [NSPanel class]];\n//    CRIT << \"hidesOnDeactivate\" << ns_window.hidesOnDeactivate;\n//    CRIT << \"hidesOnDeactivate\" << ns_window.hidesOnDeactivate;\n//    [NSApp activateIgnoringOtherApps:YES];\n//    CRIT << \"isKeyWindow\" << [ns_window isKeyWindow];\n//    CRIT << \"canBecomeKeyWindow\" << [ns_window canBecomeKeyWindow];\n//    CRIT << \"[nswindow makeKeyWindow]\";\n//    [ns_window makeKeyWindow];\n//    CRIT << \"isKeyWindow\" << [ns_window isKeyWindow];\n\n//    NSVisualEffectView *effectsView = [[NSVisualEffectView alloc] init];\n//    effectsView.blendingMode = NSVisualEffectBlendingModeBehindWindow;\n\n//void platform::show()\n//{\n//    NSWindow *ns_window = [reinterpret_cast<id>(frontend()->winId()) window];\n//    [ns_window orderFrontRegardless];\n//    [ns_window makeKeyWindow];\n////    [ns_window showWindow:nil];\n//    [ns_window makeKeyAndOrderFront:nil];\n//}\n\n//void platform::hide()\n//{\n//    NSWindow *ns_window = [reinterpret_cast<id>(frontend()->winId()) window];\n//    [ns_window resignKeyWindow];\n\n//}\n\n//void platform::resignKey(unsigned long long wid) {\n//    NSWindow *ns_window = [reinterpret_cast<id>(wid) window];\n//    /*[NSApp hide:nil];*/\n\n//    //    CRIT << QString::fromNSString( NSWorkspace.sharedWorkspace.menuBarOwningApplication.bundleIdentifier);\n//    //    [NSWorkspace.sharedWorkspace.menuBarOwningApplication activateWithOptions: NSApplicationActivateAllWindows];\n//}\n\n// Agent app\n//    [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];\n\n// Always dark mode 😎\n//[NSApp setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]];\n\n"
  },
  {
    "path": "src/platform/platform.h",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#pragma once\n#include <QString>\n\nnamespace platform\n{\n\nvoid initPlatform();\n\nvoid initNativeWindow(unsigned long long wid);\n\n/// Runs an AppleScript and returns the result. Throws runtime_error on failure.\nQString runAppleScript(const QString &script);\n\n}\n"
  },
  {
    "path": "src/platform/signalhandler.h",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n// See https://doc.qt.io/qt-6/unix-signals.html\n\n#pragma once\nclass SignalHandler\n{\npublic:\n    SignalHandler();\n    ~SignalHandler();\n};\n\n"
  },
  {
    "path": "src/platform/unix/signalhandler.cpp",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#include \"logging.h\"\n#include \"signalhandler.h\"\n#include <QCoreApplication>\n#include <QSocketNotifier>\n#include <csignal>\n#include <sys/socket.h>\n#include <unistd.h>\n\nnamespace {\nenum SocketFileDescriptor { Write, Read, Count };\nint socket_file_descriptors[SocketFileDescriptor::Count];\nQSocketNotifier *socket_notifier = nullptr;\nint handled_signals[] = { SIGTERM, SIGINT, SIGHUP, SIGPIPE };\n\n\nvoid unixSignalHandler(int signal)\n{\n    if (::write(socket_file_descriptors[SocketFileDescriptor::Write],\n                &signal, sizeof(signal)) != sizeof(signal))\n        qFatal(\"Signal handler failed to write to socket!\");\n}\n\nvoid qtSignalHandler()\n{\n    socket_notifier->setEnabled(false);\n    int signal;\n    auto bytes_read = ::read(socket_file_descriptors[SocketFileDescriptor::Read], &signal, sizeof(signal));\n    //socket_notifier->setEnabled(true);\n    if (bytes_read == sizeof(signal))\n        INFO << QString(\"Received signal %1. Quit.\").arg(signal);\n    else\n        qFatal(\"Signal socket received message of invalid size\");\n    QCoreApplication::quit();\n}\n\n}\n\nSignalHandler::SignalHandler()\n{\n    if (socket_notifier)\n       qFatal(\"Signal handler has to be unique.\");\n\n    // Create unix socket pair\n    if (::socketpair(AF_UNIX, SOCK_STREAM, 0, socket_file_descriptors))\n       qFatal(\"Couldn't create signal socketpair.\");\n\n    // Create socket notifier listening on the unix socket\n    socket_notifier = new QSocketNotifier(\n        socket_file_descriptors[SocketFileDescriptor::Read],\n        QSocketNotifier::Read\n    );\n\n    // Handle the socket notification\n    QObject::connect(socket_notifier, &QSocketNotifier::activated, qtSignalHandler);\n\n    // Install handler on signals\n    struct sigaction sigact{};\n    sigact.sa_handler = unixSignalHandler;\n    sigemptyset(&sigact.sa_mask);\n    sigact.sa_flags = SA_RESTART | SA_RESETHAND; // https://pubs.opengroup.org/onlinepubs/9699919799/functions/sigaction.html\n    for (int sig : handled_signals)\n        if (sigaction(sig, &sigact, nullptr))\n            qFatal(\"Failed installing signal handler on signal: %d\", sig);\n}\n\nSignalHandler::~SignalHandler()\n{\n    // Restore default signal handlers\n    struct sigaction sigact{};\n    sigact.sa_handler = SIG_DFL;\n    for (int sig : handled_signals)\n        if (sigaction(sig, &sigact, nullptr))\n            qFatal(\"Failed restoring default signal handler on signal: %d\", sig);\n\n    socket_notifier->disconnect();\n    delete socket_notifier;\n\n    ::close(socket_file_descriptors[SocketFileDescriptor::Read]);\n    ::close(socket_file_descriptors[SocketFileDescriptor::Write]);\n}\n"
  },
  {
    "path": "src/platform/xdg/desktopentryparser.cpp",
    "content": "// Copyright (c) 2024-2024 Manuel Schneider\n\n#include \"desktopentryparser.h\"\n#include <QFile>\n#include <albert/logging.h>\nusing namespace Qt::StringLiterals;\nusing namespace albert::detail;\nusing namespace std;\n\nDesktopEntryParser::DesktopEntryParser(const QString &path)\n{\n    if (QFile file(path); file.open(QIODevice::ReadOnly| QIODevice::Text))\n    {\n        QTextStream stream(&file);\n        QString currentGroup;\n        for (QString line=stream.readLine(); !line.isNull(); line=stream.readLine())\n        {\n            line = line.trimmed();\n\n            if (line.startsWith(u'#') || line.isEmpty())\n                continue;\n\n            if (line.startsWith(u\"[\"))\n            {\n                currentGroup = line.mid(1,line.size()-2).trimmed();\n                continue;\n            }\n\n            data[currentGroup].emplace(line.section(u'=', 0,0).trimmed(),\n                                       line.section(u'=', 1, -1).trimmed());\n        }\n        file.close();\n    }\n    else\n        throw runtime_error(u\"Failed opening file '%1': %2\"_s\n                                .arg(path, file.errorString()).toStdString());\n}\n\nQString DesktopEntryParser::getRawValue(const QString &section, const QString &key) const\n{\n    class SectionDoesNotExist : public std::out_of_range { using out_of_range::out_of_range; };\n    class KeyDoesNotExist : public std::out_of_range { using out_of_range::out_of_range; };\n\n    try {\n        auto &s = data.at(section);\n        try {\n            return s.at(key);\n        } catch (const out_of_range&) {\n            throw KeyDoesNotExist(u\"Section '%1' does not contain a key '%2'.\"_s\n                                      .arg(section, key).toStdString());\n        }\n    } catch (const out_of_range&) {\n        throw SectionDoesNotExist(u\"Desktop entry does not contain a section '%1'.\"_s\n                                      .arg(section).toStdString());\n    }\n}\n\nQString DesktopEntryParser::getEscapedValue(const QString &section, const QString &key) const\n{\n    QString result;\n\n    auto unescaped = getRawValue(section, key);\n    for (auto it = unescaped.cbegin(); it != unescaped.cend();)\n    {\n        if (*it == u'\\\\'){\n            ++it;\n            if (it == unescaped.cend())\n                break;\n            else if (*it==u's')\n                result.append(u' ');\n            else if (*it==u'n')\n                result.append(u'\\n');\n            else if (*it==u't')\n                result.append(u'\\t');\n            else if (*it==u'r')\n                result.append(u'\\r');\n            else if (*it==u'\\\\')\n                result.append(u'\\\\');\n        }\n        else\n            result.append(*it);\n        ++it;\n    }\n\n    return result;\n}\n\nQString DesktopEntryParser::getString(const QString &section, const QString &key) const\n{\n    return getEscapedValue(section, key);\n}\n\nQString DesktopEntryParser::getLocaleString(const QString &section, const QString &key)\n{\n    // https://wiki.ubuntu.com/UbuntuDevelopment/Internationalisation/Packaging#Desktop_Entries\n\n\n    // TODO: Properly fetch the localestring\n    //       (lang_COUNTRY@MODIFIER, lang_COUNTRY, lang@MODIFIER, lang, default value)\n\n    try {\n        return getEscapedValue(section, u\"%1[%2]\"_s.arg(key, locale.name()));\n    } catch (const out_of_range&) { }\n\n    try {\n        return getEscapedValue(section, u\"%1[%2]\"_s.arg(key, locale.name().left(2)));\n    } catch (const out_of_range&) { }\n\n    QString unlocalized = getEscapedValue(section, key);\n\n    try {\n        auto domain = getEscapedValue(section, u\"X-Ubuntu-Gettext-Domain\"_s);\n        // The resulting string is statically allocated and must not be modified or freed\n        // Returns msgid on lookup failure\n        // https://linux.die.net/man/3/dgettext\n        return QString::fromUtf8(dgettext(domain.toStdString().c_str(),\n                                          unlocalized.toStdString().c_str()));\n    } catch (const out_of_range&) { }\n\n    return unlocalized;\n}\n\nQString DesktopEntryParser::getIconString(const QString &section, const QString &key)\n{\n    return getEscapedValue(section, key);\n}\n\nbool DesktopEntryParser::getBoolean(const QString &section, const QString &key)\n{\n    auto raw = getRawValue(section, key);  // throws\n    if (raw == u\"true\"_s)\n        return true;\n    else if (raw == u\"false\"_s)\n        return false;\n    else\n        throw runtime_error(u\"Value for key '%1' in section '%2' is neither true nor false.\"_s\n                                .arg(key, section).toStdString());\n}\n\ndouble DesktopEntryParser::getNumeric(const QString &, const QString &)\n{\n    throw runtime_error(\"Not implemented.\");\n}\n\noptional<QStringList> DesktopEntryParser::splitExec(const QString &s) noexcept\n{\n    QStringList tokens;\n    QString token;\n    auto c = s.begin();\n\n    while (c != s.end())\n    {\n        if (*c == QChar::Space)  // separator\n        {\n            if (!token.isEmpty())\n            {\n                tokens << token;\n                token.clear();\n            }\n        }\n\n        else if (*c == u'\"')  // quote\n        {\n            ++c;\n\n            while (c != s.end())\n            {\n                if (*c == u'\"')  // quote termination\n                    break;\n\n                else if (*c == u'\\\\')  // escape\n                {\n                    ++c;\n                    if(c == s.end())\n                    {\n                        WARN << u\"Unterminated escape in %1\"_s.arg(s);\n                        return {};  // unterminated escape\n                    }\n\n                    else if (uR\"(\"`$\\)\"_s.contains(*c))\n                        token.append(*c);\n\n                    else\n                    {\n                        WARN << u\"Invalid escape '%1' at '%2': %3\"_s\n                                    .arg(*c).arg(distance(c, s.begin())).arg(s);\n                        return {};  // invalid escape\n                    }\n                }\n\n                else\n                    token.append(*c);  // regular char\n\n                ++c;\n            }\n\n            if (c == s.end())\n            {\n                WARN << u\"Unterminated escape in %1\"_s.arg(s);\n                return {};  // unterminated quote\n            }\n        }\n\n        else\n            token.append(*c);  // regular char\n\n        ++c;\n\n    }\n\n    if (!token.isEmpty())\n        tokens << token;\n\n    return tokens;\n}\n"
  },
  {
    "path": "src/platform/xdg/iconlookup.cpp",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#include <QDebug>\n#include <QDir>\n#include <QFileInfo>\n#include <QIcon>\n#include <QStandardPaths>\n#include <QString>\n#include \"themefileparser.h\"\n#include \"iconlookup.h\"\nusing namespace std;\n\nnamespace  {\n    QStringList icon_extensions = {\"png\", \"svg\", \"xpm\"};\n}\n\nQString XDG::IconLookup::iconPath(QString iconName, QSize , QString themeName)  // FIXME unused qsize\n{\n    return instance()->themeIconPath(iconName, themeName);\n}\n\nXDG::IconLookup::IconLookup()\n{\n    /*\n     * Icons and themes are looked for in a set of directories. By default,\n     * apps should look in $HOME/.icons (for backwards compatibility), in\n     * $XDG_DATA_DIRS/icons and in /usr/share/pixmaps (in that order).\n     */\n\n    QString path = QDir::home().filePath(\".icons\");\n    if (QFile::exists(path))\n        iconDirs_.append(path);\n\n    for (const QString &basedir : QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation))\n        if (QFile::exists(path = QDir(basedir).filePath(\"icons\")))\n            iconDirs_.append(path);\n\n    // Not in spec, but for tolerance\n    if (path = QStringLiteral(\"/usr/local/share/pixmaps\"); QFile::exists(path))\n        iconDirs_.append(path);\n\n    if (path = QStringLiteral(\"/usr/share/pixmaps\"); QFile::exists(path))\n        iconDirs_.append(path);\n}\n\nXDG::IconLookup *XDG::IconLookup::instance()\n{\n    static IconLookup *instance_ = nullptr;\n    if (!instance_)\n        instance_ = new IconLookup();\n    return instance_;\n}\n\nQString XDG::IconLookup::themeIconPath(QString iconName, QString themeName)\n{\n    if (iconName.isEmpty())\n        return {};\n\n    if (themeName.isEmpty())\n        themeName = QIcon::themeName();\n\n    // if we have an absolute path, just return it\n    if (iconName[0] == '/') {\n        if (QFile::exists(iconName))\n            return iconName;\n        else\n            return {};\n    }\n\n    // check if it has an extension and strip it\n    for (const QString &ext: icon_extensions)\n        if (iconName.endsWith(QString(\".\").append(ext)))\n            iconName.chop(4);\n\n    // Check cache\n    try {\n        return iconCache_.at(iconName);\n    } catch (const out_of_range &) {}\n\n    QStringList checkedThemes;\n    QString iconPath;\n\n    // Lookup themefile\n    if (!(iconPath = doRecursiveIconLookup(iconName, themeName, &checkedThemes)).isNull())\n        return iconCache_.emplace(iconName, iconPath).first->second;\n\n    // Lookup in hicolor\n    if (!checkedThemes.contains(\"hicolor\"))\n        if (!(iconPath = doRecursiveIconLookup(iconName, \"hicolor\", &checkedThemes)).isNull())\n            return iconCache_.emplace(iconName, iconPath).first->second;\n\n    // Now search unsorted\n    for (const QString &iconDir: iconDirs_)\n        for (const QString &ext: icon_extensions)\n            if (QFile(iconPath = QString(\"%1/%2.%3\").arg(iconDir, iconName, ext)).exists())\n                return iconCache_.emplace(iconName, iconPath).first->second;\n\n    // Nothing found, save though to avoid repeated expensive lookups\n    return iconCache_.emplace(iconName, QString()).first->second;\n}\n\nQString XDG::IconLookup::doRecursiveIconLookup(const QString &iconName, const QString &themeName, QStringList *checked)\n{\n    // Exlude multiple scans\n    if (checked->contains(themeName))\n        return {};\n    checked->append(themeName);\n\n    // Check if theme exists\n    QString themeFile = lookupThemeFile(themeName);\n    if (themeFile.isNull())\n        return {};\n\n    // Check if icon exists\n    QString iconPath;\n    iconPath = doIconLookup(iconName, themeFile);\n    if (!iconPath.isNull())\n        return iconPath;\n\n    // Check its parents too\n    for (const QString &parent: ThemeFileParser(themeFile).inherits()) {\n        iconPath = doRecursiveIconLookup(iconName, parent, checked);\n        if (!iconPath.isNull())\n            return iconPath;\n    }\n\n    return {};\n}\n\nQString XDG::IconLookup::doIconLookup(const QString &iconName, const QString &themeFile)\n{\n    ThemeFileParser themeFileParser(themeFile);\n    QDir themeDir = QFileInfo(themeFile).dir();\n    QString themeName = themeDir.dirName();\n\n    // Get the sizes of the dirs\n    vector<pair<QString, int>> dirsAndSizes;\n    for (const QString &subdir: themeFileParser.directories())\n        dirsAndSizes.push_back(make_pair(subdir, themeFileParser.size(subdir)));\n\n    // Sort them by size\n    sort(dirsAndSizes.begin(), dirsAndSizes.end(),\n         [](pair<QString, int> a, pair<QString, int> b) {\n             return a.second > b.second;\n         });\n\n    // Well now search for a file beginning with the greatest\n    QString filename;\n    QFile file;\n    for (const auto &dirAndSize: dirsAndSizes) {\n        for (const QString &iconDir: iconDirs_) {\n            for (const QString &ext: icon_extensions) {\n                filename = QString(\"%1/%2/%3/%4.%5\").arg(iconDir, themeName, dirAndSize.first, iconName, ext);\n                if (file.exists(filename)) {\n                    return filename;\n                }\n            }\n        }\n    }\n\n    return {};\n}\n\nQString XDG::IconLookup::lookupThemeFile(const QString &themeName)\n{\n    // Lookup themefile\n    for (const QString &iconDir: iconDirs_) {\n        QString indexFile = QString(\"%1/%2/index.theme\").arg(iconDir, themeName);\n        if (QFile(indexFile).exists())\n            return indexFile;\n    }\n    return {};\n}\n"
  },
  {
    "path": "src/platform/xdg/iconlookup.h",
    "content": "// Copyright (C) 2014-2024 Manuel Schneider\n\n#pragma once\n#include <QSize>\n#include <QStringList>\n#include <map>\n\nnamespace XDG {\n\nclass IconLookup\n{\npublic:\n\n    /**\n     * @brief iconPath Does XDG icon lookup for the given icon name\n     * @param iconName The icon name to lookup\n     * @param themeName The theme to use, use current theme if empty\n     * @return If an icon was found the path to the icon, else an empty string\n     */\n    static QString iconPath(QString iconName, QSize size = QSize(), QString themeName = QString());\n\nprivate:\n\n    IconLookup();\n    static IconLookup *instance();\n\n    QString themeIconPath(QString iconName, QString themeName = QString());\n    QString doRecursiveIconLookup(const QString &iconName, const QString &theme, QStringList *checked);\n    QString doIconLookup(const QString &iconName, const QString &themeFile);\n    QString lookupThemeFile(const QString &themeName);\n\n    QStringList iconDirs_;\n    std::map<QString, QString> iconCache_;\n};\n\n}\n"
  },
  {
    "path": "src/platform/xdg/platform.cpp",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#include \"platform.h\"\n\nvoid platform::initPlatform(){}\n\nvoid platform::initNativeWindow(unsigned long long){}\n\n\n"
  },
  {
    "path": "src/platform/xdg/themefileparser.cpp",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#include \"themefileparser.h\"\n\n\nXDG::ThemeFileParser::ThemeFileParser(const QString &iniFilePath)\n        : iniFile_(iniFilePath, QSettings::IniFormat)\n{\n}\n\nQString XDG::ThemeFileParser::path()\n{\n    return iniFile_.fileName();\n}\n\nQString XDG::ThemeFileParser::name()\n{\n    return iniFile_.value(\"Icon Theme/Name\").toString();\n}\n\nQString XDG::ThemeFileParser::comment()\n{\n    return iniFile_.value(\"Icon Theme/Comment\").toString();\n}\n\nQStringList XDG::ThemeFileParser::inherits()\n{\n    QStringList inherits = iniFile_.value(\"Icon Theme/Inherits\").toStringList();\n    if (inherits.isEmpty() && name() != \"hicolor\")\n        inherits << \"hicolor\";\n    return iniFile_.value(\"Icon Theme/Inherits\").toStringList();\n}\n\nQStringList XDG::ThemeFileParser::directories()\n{\n    return iniFile_.value(\"Icon Theme/Directories\").toStringList();\n}\n\nbool XDG::ThemeFileParser::hidden()\n{\n    return iniFile_.value(\"Icon Theme/Hidden\").toBool();\n}\n\nint XDG::ThemeFileParser::size(const QString &directory)\n{\n    iniFile_.beginGroup(directory);\n    int result = iniFile_.value(\"Size\").toInt();\n    iniFile_.endGroup();\n    return result;\n}\n\nQString XDG::ThemeFileParser::context(const QString &directory)\n{\n    iniFile_.beginGroup(directory);\n    QString result = iniFile_.value(\"Context\").toString();\n    iniFile_.endGroup();\n    return result;\n}\n\nQString XDG::ThemeFileParser::type(const QString &directory)\n{\n    iniFile_.beginGroup(directory);\n    QString result = iniFile_.contains(\"Type\") ? iniFile_.value(\"Type\").toString()\n                                               : \"Threshold\";\n    iniFile_.endGroup();\n    return result;\n}\n\nint XDG::ThemeFileParser::maxSize(const QString &directory)\n{\n    iniFile_.beginGroup(directory);\n    int result = iniFile_.contains(\"MaxSize\") ? iniFile_.value(\"MaxSize\").toInt()\n                                              : size(directory);\n    iniFile_.endGroup();\n    return result;\n}\n\nint XDG::ThemeFileParser::minSize(const QString &directory)\n{\n    iniFile_.beginGroup(directory);\n    int result = iniFile_.contains(\"MinSize\") ? iniFile_.value(\"MinSize\").toInt()\n                                              : size(directory);\n    iniFile_.endGroup();\n    return result;\n}\n\nint XDG::ThemeFileParser::threshold(const QString &directory)\n{\n    iniFile_.beginGroup(directory);\n    int result =\n            iniFile_.contains(\"Threshold\") ? iniFile_.value(\"Threshold\").toInt() : 2;\n    iniFile_.endGroup();\n    return result;\n}\n"
  },
  {
    "path": "src/platform/xdg/themefileparser.h",
    "content": "// Copyright (C) 2014-2018 Manuel Schneider\n\n#pragma once\n#include <QStringList>\n#include <QSettings>\n\nnamespace XDG {\n\nclass ThemeFileParser\n{\npublic:\n\n    ThemeFileParser(const QString &iniFile);\n\n    QString path();\n    QString name();\n    QString comment();\n    QStringList inherits();\n    QStringList directories();\n    bool hidden();\n    int size(const QString& directory);\n    QString context(const QString& directory);\n    QString type(const QString& directory);\n    int maxSize(const QString& directory);\n    int minSize(const QString& directory);\n    int threshold(const QString& directory);\n\nprivate:\n\n    QSettings iniFile_;\n\n};\n\n}\n"
  },
  {
    "path": "src/plugin/extensionregistry.cpp",
    "content": "// Copyright (c) 2022-2024 Manuel Schneider\n\n#include \"extension.h\"\n#include \"extensionregistry.h\"\n#include \"logging.h\"\nusing namespace albert;\nusing namespace std;\n\nbool ExtensionRegistry::registerExtension(Extension *e)\n{\n    auto id = e->id();\n    if (id.isEmpty())\n    {\n        CRIT << \"Registered extension id must not be empty\";\n        return false;\n    }\n\n    const auto&[it, success] = extensions_.emplace(id, e);\n    if (success)\n        emit added(e);\n    else\n        CRIT << \"Extension registered more than once:\" << e->id();\n    return success;\n}\n\nvoid ExtensionRegistry::deregisterExtension(Extension *e)\n{\n    if (extensions_.erase(e->id()))\n        emit removed(e);\n    else\n        CRIT << \"Removed extension that has not been registered before:\" << e->id();\n}\n\nconst map<QString, Extension*> &ExtensionRegistry::extensions() const { return extensions_; }\n"
  },
  {
    "path": "src/plugin/extensionregistry.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n#include <QObject>\n#include <albert/export.h>\n#include <albert/extension.h>\n#include <map>\n\nnamespace albert\n{\n\n///\n/// The common extension pool.\n///\n/// Clients can add their extensions, while services can track extensions by\n/// listening to the signals added/removed or any particular extension\n/// interface using ExtensionWatcher.\n///\n/// \\ingroup core_extension\n///\nclass ALBERT_EXPORT ExtensionRegistry : public QObject\n{\n    Q_OBJECT\n\npublic:\n\n    /// Add extension to the registry\n    bool registerExtension(Extension*);\n\n    /// Remove extension from the registry\n    void deregisterExtension(Extension*);\n\n    /// Get map of all registered extensions\n    const std::map<QString,Extension*> &extensions() const;\n\nsignals:\n\n    /// Emitted when an extension has been registered.\n    void added(albert::Extension*);\n\n    /// Emitted when an extension has been deregistered.\n    void removed(albert::Extension*);\n\nprivate:\n\n    std::map<QString,Extension*> extensions_;\n};\n\n}\n"
  },
  {
    "path": "src/plugin/plugininstance.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"albert/app.h\"\n#include \"plugininstance.h\"\n#include \"pluginloader.h\"\n#include \"pluginmetadata.h\"\n#include \"pluginregistry.h\"\n#include <QCoreApplication>\n#include <QSettings>\n#include <QStandardPaths>\n#include <filesystem>\nusing namespace albert;\nusing namespace std;\nusing filesystem::path;\n\nclass PluginInstance::Private\n{\npublic:\n    PluginLoader *loader;\n};\n\nPluginInstance::PluginInstance() :\n    d(new Private{\n        .loader = PluginLoader::current_loader,\n    })\n{}\n\nPluginInstance::~PluginInstance() = default;\n\nvoid PluginInstance::initialize() { emit initialized(); }\n\nvector<Extension*> PluginInstance::extensions() { return {}; }\n\nQWidget *PluginInstance::buildConfigWidget() { return nullptr; }\n\nfilesystem::path PluginInstance::cacheLocation() const\n{ return App::cacheLocation() / d->loader->metadata().id.toStdString(); }\n\nfilesystem::path PluginInstance::configLocation() const\n{ return App::configLocation() / d->loader->metadata().id.toStdString(); }\n\nfilesystem::path PluginInstance::dataLocation() const\n{ return App::dataLocation() / d->loader->metadata().id.toStdString(); }\n\nunique_ptr<QSettings> PluginInstance::settings() const\n{\n    auto s = App::settings();\n    s->beginGroup(d->loader->metadata().id);\n    return s;\n}\n\nunique_ptr<QSettings> PluginInstance::state() const\n{\n    auto s = App::state();\n    s->beginGroup(d->loader->metadata().id);\n    return s;\n}\n\nconst PluginLoader &PluginInstance::loader() const { return *d->loader; }\n\nvector<filesystem::path> PluginInstance::dataLocations() const\n{\n    vector<filesystem::path> data_locations;\n    const auto paths = QStandardPaths::locateAll(QStandardPaths::AppDataLocation,\n                                                 loader().metadata().id,\n                                                 QStandardPaths::LocateDirectory);\n    for (const auto &path : paths)\n        data_locations.emplace_back(path.toStdString());\n    return data_locations;\n}\n"
  },
  {
    "path": "src/plugin/pluginloader.cpp",
    "content": "// Copyright (c) 2024-2025 Manuel Schneider\n\n#include \"pluginloader.h\"\nusing namespace albert;\n\nthread_local PluginLoader *PluginLoader::current_loader;\n\nPluginLoader::~PluginLoader() = default;\n"
  },
  {
    "path": "src/plugin/pluginprovider.cpp",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#include \"pluginprovider.h\"\n\n// vtable in lib\nalbert::PluginProvider::~PluginProvider() = default;\n"
  },
  {
    "path": "src/plugin/pluginregistry.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"albert/app.h\"\n#include \"extensionregistry.h\"\n#include \"logging.h\"\n#include \"messagebox.h\"\n#include \"plugininstance.h\"\n#include \"pluginloader.h\"\n#include \"pluginmetadata.h\"\n#include \"pluginprovider.h\"\n#include \"pluginregistry.h\"\n#include \"topologicalsort.hpp\"\n#include <QCoreApplication>\n#include <QSettings>\n#include <chrono>\n#include <ranges>\nusing enum Plugin::State;\nusing enum albert::PluginMetadata::LoadType;\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std::chrono;\nusing namespace std;\n\nPluginRegistry::PluginRegistry(ExtensionRegistry &reg, bool autoload_enabled_plugins) :\n    extension_registry_(reg),\n    load_enabled_(autoload_enabled_plugins)\n{\n    connect(&extension_registry_, &ExtensionRegistry::added,\n            this, [this](Extension *e)\n            { if (auto p = dynamic_cast<PluginProvider*>(e)) onRegistered(p); });\n\n    connect(&extension_registry_, &ExtensionRegistry::removed,\n            this, [this](Extension *e)\n            { if (auto p = dynamic_cast<PluginProvider*>(e)) onDeregistered(p); });\n}\n\nPluginRegistry::~PluginRegistry()\n{\n    if (!plugin_providers_.empty())\n        WARN << \"PluginRegistry destroyed with active plugin providers\";\n\n    if (!plugins_.empty())\n        WARN << \"PluginRegistry destroyed with active plugins\";\n\n    if (!loading_plugins_.empty())\n    {\n        QEventLoop loop;\n        connect(this, &PluginRegistry::pluginStateChanged, this, [&] {\n            if (loading_plugins_.empty())\n                loop.quit();\n        });\n        loop.exec();\n    }\n}\n\nconst map<QString, Plugin> &PluginRegistry::plugins() const { return plugins_; }\n\nvoid PluginRegistry::setEnabledWithUserConfirmation(const QString id, bool enable)\n{\n    auto &plugin = plugins_.at(id);\n\n    if (plugin.enabled == enable)\n        return;\n\n    auto v = (enable ? dependencyClosure({&plugin}) : dependeeClosure({&plugin}))\n             | views::filter([&](auto p) { return p->metadata.load_type == User\n                                                  && p->id != id\n                                                  && p->enabled != enable;})\n             | views::transform([](auto p) { return p->metadata.name; });\n    QStringList names(v.begin(), v.end());  // ranges::to\n\n    if (!names.empty())\n    {\n        auto text = (enable ? tr(\"Enabling '%1' will also enable the following plugins\")\n                            : tr(\"Disabling '%1' will also disable the following plugins\"))\n                        .arg(plugin.metadata.name);\n        text.append(QString(\":\\n\\n\").append(names.join(\"\\n\")));\n\n        if (!question(text))\n            return;\n    }\n\n    setEnabled(id, enable);\n}\n\nvoid PluginRegistry::setEnabled(const QString &id, bool enable)\n{\n    auto &plugin = plugins_.at(id);\n\n    if (plugin.enabled == enable)\n        return;\n\n    for (auto p : enable ? dependencyClosure({&plugin}) : dependeeClosure({&plugin}))\n        if (p->metadata.load_type == User && p->enabled != enable)\n        {\n            const_cast<Plugin*>(p)->enabled = enable;  // safe, original is not const\n            App::settings()->setValue(QString(\"%1/enabled\").arg(p->id), enable);\n            emit pluginEnabledChanged(p->id);\n        }\n\n    // Clear state info on disabling an unloaded plugin.\n    if (!enable && plugin.state == Unloaded)\n        plugin.state_info.clear();\n\n    setLoaded(id, enable);\n}\n\nvoid PluginRegistry::setLoaded(const QString &id, bool loaded)\n{\n    if (!loading_plugins_.empty())\n        warning(QStringLiteral(\"%1: %2\")\n                    .arg(loaded ? tr(\"Failed to load plugin\") : tr(\"Failed to unload plugin\"),\n                         tr(\"Other plugins are currently being loaded.\")));\n\n    else if (loaded)\n        load({&plugins_.at(id)});\n    else\n        unload({&plugins_.at(id)});\n}\n\nstd::set<const Plugin *> PluginRegistry::dependencies(const Plugin *p) const\n{\n    auto v = p->loader.metadata().plugin_dependencies\n             | views::transform([this](const QString &id){ return &plugins_.at(id); });\n    return {v.begin(), v.end()};  // ranges::to\n}\n\nstd::set<const Plugin *> PluginRegistry::dependees(const Plugin *p) const\n{\n    auto v =  plugins_\n             | views::transform([](auto &pair){ return &pair.second; })\n             | views::filter([p](const Plugin *o){\n                   return ranges::any_of(o->metadata.plugin_dependencies,\n                                         [p](auto &id){ return id == p->id;});\n               });\n    return {v.begin(), v.end()};  // ranges::to\n}\n\nset<const Plugin *> PluginRegistry::dependencyClosure(const std::set<const Plugin*> &plugins) const\n{\n    auto D = plugins;\n    for (auto p : plugins)\n        for (auto d : dependencies(p))\n            D.merge(dependencyClosure({d}));\n    return D;\n}\n\nset<const Plugin *> PluginRegistry::dependeeClosure(const std::set<const Plugin*> &plugins) const\n{\n    auto D = plugins;\n    for (auto p : plugins)\n        for (auto d : dependees(p))\n            D.merge(dependeeClosure({d}));\n    return D;\n}\n\nvoid PluginRegistry::onRegistered(PluginProvider *pp)\n{\n    // Register plugin provider\n\n    const auto &[_, pp_reg_success] = plugin_providers_.insert(pp);\n    if (!pp_reg_success)\n        qFatal(\"Plugin provider registered twice.\");\n\n    // Make unique plugins\n\n    map<QString, PluginLoader*> unique_loaders;\n    for (auto loader : pp->plugins())\n        if (const auto &[it, succ] = unique_loaders.emplace(loader->metadata().id,\n                                                            loader);\n            !succ)\n            INFO << QString(\"Plugin '%1' at '%2' shadowed by '%3'\")\n                        .arg(it->first, loader->path(), it->second->path());\n\n    // Topo sort once to filter by valid dependencies\n\n    map<QString, set<QString>> dependency_graph;\n    for (auto &[id, loader] : unique_loaders)\n        dependency_graph.emplace(id, set<QString>{begin(loader->metadata().plugin_dependencies),\n                                                  end(loader->metadata().plugin_dependencies)});  // ranges::to\n\n    if (auto topo_result = topologicalSort(dependency_graph);\n        !topo_result.error_set.empty())\n    {\n        for (const auto &[id, deps] : topo_result.error_set)\n        {\n            WARN << \"Skipping plugin\" << id << \"because of missing or cyclic dependencies.\";\n            unique_loaders.erase(id);\n        }\n\n        WARN << \"Error set:\";\n        for (const auto &[id, deps] : topo_result.error_set)\n            for (const auto &dep : deps)\n                WARN << id << \"→\" << dep;\n    }\n\n    // Finally register the valid plugins\n\n    for (auto &[id, loader] : unique_loaders)\n    {\n        Plugin p{\n            .provider = *pp,\n            .loader = *loader,\n            .id = loader->metadata().id,\n            .metadata = loader->metadata(),\n            .enabled = App::settings()->value(QString(\"%1/enabled\").arg(id), false).toBool(),\n            .state = Unloaded,\n            .state_info = {},\n            .registered_extensions={}\n        };\n\n        if (const auto &[it, success] = plugins_.emplace(id, p);\n            success)\n            connect(loader, &PluginLoader::finished,\n                    this, [this, &p=it->second](QString info){\n                onPluginLoaderFinished(p, info);\n            });\n        else\n            CRIT << \"Logic error: Plugin already exists\" << it->first;\n    }\n\n    emit pluginsChanged();\n\n    if (load_enabled_)\n    {\n        auto v = plugins_\n                 | views::transform([](auto &p) { return &p.second; })\n                 | views::filter([pp](auto p) { return &p->provider == pp\n                                                       && p->metadata.load_type == User\n                                                       && p->enabled; });\n        load({v.begin(), v.end()});  // ranges::to\n    }\n}\n\nvoid PluginRegistry::onDeregistered(PluginProvider *pp)\n{\n    const auto filter = [pp](const auto& it){ return &it.second.provider == pp; };\n\n    // Unload plugins of this provider\n    auto v = plugins_ | views::filter(filter) | views::transform([](auto &p){ return &p.second; });\n    unload({v.begin(), v.end()});  // ranges::to\n\n    // Remove plugins of this provider\n    erase_if(plugins_, filter);\n    emit pluginsChanged();\n\n    // Remove provider\n    if (!plugin_providers_.erase(pp))\n        qFatal(\"Plugin provider was not registered onRem.\");\n}\n\nvoid PluginRegistry::load(set<const Plugin*> plugins)\n{\n    // Make dependency graph\n    map<const Plugin*, set<const Plugin*>> graph;\n    for (auto p : dependencyClosure(plugins))\n        graph.emplace(p, dependencies(p));\n\n    // Remove loaded plugins from graph\n    erase_if(graph, [](auto &p){ return p.first->state == Loaded; });\n    for (auto &[p, deps] : graph)\n        erase_if(deps, [](auto p_){ return p_->state == Loaded; });\n\n    if (graph.empty())\n        return;\n\n    loading_graph_.merge(::move(graph));\n\n    // Load initial set without dependencies\n    for (auto it = begin(loading_graph_); it != end(loading_graph_);)\n        if (it->second.empty())\n        {\n            Plugin &p = *const_cast<Plugin*>(it->first);\n\n            loading_plugins_.insert(&p);\n            it = loading_graph_.erase(it);\n\n            setPluginState(p, Loading);\n            p.loader.load();  // Async\n        }\n        else\n            ++it;\n}\n\nvoid PluginRegistry::unload(set<const Plugin*> plugins)\n{\n    // Make dependee graph\n    map<const Plugin*, set<const Plugin*>> graph;\n    for (auto p : dependeeClosure(plugins))\n        graph.emplace(p, dependees(p));\n\n    // Remove unloaded plugins from graph\n    erase_if(graph, [](auto &p){ return p.first->state == Unloaded; });\n    for (auto &[p, deps] : graph)\n        erase_if(deps, [](auto p_){ return p_->state == Unloaded; });\n\n    if (graph.empty())\n        return;\n\n    auto topo = topologicalSort(graph);\n\n    for (const Plugin *cp : topo.sorted)\n    {\n        Plugin &p = *const_cast<Plugin*>(cp);\n        for (auto *e : p.registered_extensions)\n            extension_registry_.deregisterExtension(e);\n\n        p.loader.unload();\n        setPluginState(p, Unloaded);\n    }\n}\n\nvoid PluginRegistry::onPluginLoaderFinished(Plugin &p, const QString &info)\n{\n\n    // remove from loading plugins\n    loading_plugins_.erase(&p);\n\n    // remove dependecies in load graph\n    for (auto it = begin(loading_graph_); it != end(loading_graph_);)\n        if (it->second.erase(&p) && it->second.empty())\n        {\n            Plugin &p_ = *const_cast<Plugin*>(it->first);\n\n            loading_plugins_.insert(&p_);\n            it = loading_graph_.erase(it);\n\n            setPluginState(p_, Loading);\n            p_.loader.load();  // may be async\n        }\n        else\n            ++it;\n\n    if (auto *instance = p.loader.instance();\n        !instance)\n        setPluginState(p, Unloaded, info);\n    else\n    {\n        connect(instance, &PluginInstance::initialized,\n                this, [this, &p, tp=system_clock::now()] {\n                    DEBG << u\"%1: Initialized in %2 ms\"_s\n                                .arg(p.id)\n                                .arg(duration_cast<milliseconds>(system_clock::now() - tp).count());\n\n                    try {\n                        for (p.registered_extensions = p.loader.instance()->extensions();\n                             auto *e : p.registered_extensions)\n                            extension_registry_.registerExtension(e);\n                        setPluginState(p, Loaded);\n                    }\n                    catch (const exception &e) {\n                        const auto &msg = u\"Exception in PluginInstance::extensions: %1\"_s\n                                              .arg(QString::fromUtf8(e.what()));\n                        CRIT << p.id << msg;\n                        p.loader.unload();\n                        setPluginState(p, Unloaded, msg);\n                    }\n                    catch (...) {\n                        const auto &msg = u\"Unknown exception in PluginInstance::extensions.\"_s;\n                        CRIT << p.id << msg;\n                        p.loader.unload();\n                        setPluginState(p, Unloaded, msg);\n                    }\n\n                },\n                Qt::SingleShotConnection);\n\n        try {\n            p.loader.instance()->initialize();\n        }\n        catch (const exception &e) {\n            const auto &msg = u\"Exception in PluginInstance::initialize: %1\"_s\n                                  .arg(QString::fromUtf8(e.what()));\n            CRIT << p.id << msg;\n            p.loader.unload();\n            setPluginState(p, Unloaded, msg);\n        }\n        catch (...) {\n            const auto &msg = u\"Unknown exception in PluginInstance::initialize.\"_s;\n            CRIT << p.id << msg;\n            p.loader.unload();\n            setPluginState(p, Unloaded, msg);\n        }\n    }\n}\n\nvoid PluginRegistry::setPluginState(Plugin &plugin, Plugin::State state, const QString info)\n{\n    plugin.state = state;\n    plugin.state_info = info;\n    const auto &msg = u\"%1: State changed to '%2'.\"_s;\n    switch (state)\n    {\n    case Unloaded:\n        DEBG << msg.arg(plugin.id, u\"Unloaded\"_s) << info;\n        break;\n    case Loading:\n        DEBG << msg.arg(plugin.id, u\"Loading\"_s) << info;\n        break;\n    case Loaded:\n        DEBG << msg.arg(plugin.id, u\"Loaded\"_s) << info;\n        break;\n    }\n    emit pluginStateChanged(plugin.id);\n}\n"
  },
  {
    "path": "src/plugin/pluginregistry.h",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#pragma once\n#include <QObject>\n#include <QString>\n#include <map>\n#include <set>\nnamespace albert {\nclass Extension;\nclass ExtensionRegistry;\nclass PluginInstance;\nclass PluginLoader;\nclass PluginMetadata;\nclass PluginProvider;\n}\n\n\nclass Plugin\n{\npublic:\n    const albert::PluginProvider &provider;\n    albert::PluginLoader &loader;\n    const QString &id;  // convenience reference to loader.metadata.id\n    const albert::PluginMetadata &metadata;  // convenience reference to loader.metadata\n    bool enabled;\n    enum class State {\n        Unloaded,\n        Loading,\n        Loaded\n    } state;\n    QString state_info;\n    std::vector<albert::Extension*> registered_extensions;\n};\n\n\nclass PluginRegistry : public QObject\n{\n    Q_OBJECT\n\npublic:\n\n    PluginRegistry(albert::ExtensionRegistry&, bool autoload_enabled_plugins);\n    ~PluginRegistry();\n\n    /// Get map of all registered plugins\n    const std::map<QString, Plugin> &plugins() const;\n\n    /// @throws std::out_of_range if `id` is not in \\ref plugins().\n    void setEnabledWithUserConfirmation(const QString id, bool enable);\n\n    /// Enable/Disable a plugin and its transitive dependencies/dependees.\n    /// @throws std::out_of_range if `id` is not in \\ref plugins().\n    void setEnabled(const QString &id, bool enable);\n\n    /// (Un)Load a plugin and its transitive dependencies/dependees.\n    /// @throws std::out_of_range if `id` does not exist.\n    void setLoaded(const QString &id, bool load);\n\n    std::set<const Plugin*> dependencies(const Plugin*) const;\n    std::set<const Plugin*> dependees(const Plugin*) const;\n    std::set<const Plugin*> dependencyClosure(const std::set<const Plugin*>&) const;\n    std::set<const Plugin*> dependeeClosure(const std::set<const Plugin*>&) const;\n\nprivate:\n\n    void load(std::set<const Plugin*>);\n    void unload(std::set<const Plugin *>);\n\n    void onRegistered(albert::PluginProvider*);\n    void onDeregistered(albert::PluginProvider*);\n\n    albert::ExtensionRegistry &extension_registry_;\n    std::set<albert::PluginProvider*> plugin_providers_;\n    std::map<QString, Plugin> plugins_;\n    bool load_enabled_;\n\n    std::set<const Plugin*> loading_plugins_;\n    std::map<const Plugin*, std::set<const Plugin*>> loading_graph_;\n    void onPluginLoaderFinished(Plugin &p, const QString &info);\n    void setPluginState(Plugin &plugin, Plugin::State state, const QString info = {});\n\nsignals:\n\n    void pluginsChanged();\n    void pluginEnabledChanged(const QString &id);\n    void pluginStateChanged(const QString &id);\n\n};\n"
  },
  {
    "path": "src/plugin/topologicalsort.hpp",
    "content": "// Copyright (c) 2024 Manuel Schneider\n\n#pragma once\n#include <map>\n#include <set>\n#include <vector>\n\ntemplate<class T>\nstruct TopologicalSortResult\n{\n    std::vector<T> sorted;\n    std::map<T, std::set<T>> error_set;\n};\n\ntemplate<class T>\nTopologicalSortResult<T> topologicalSort(std::map<T, std::set<T>> graph)\n{\n    // First, find a list of \"start nodes\" that have no incoming edges\n    // and insert them into a set S; at least one such node must exist\n    // in a non-empty (finite) acyclic graph. Then:\n    // L ← Empty list that will contain the sorted elements\n    // S ← Set of all nodes with no incoming edge\n    // while S is not empty do\n    //     remove a node n from S\n    //     add n to L\n    //     for each node m with an edge e from n to m do\n    //         remove edge e from the graph\n    //         if m has no other incoming edges then\n    //             insert m into S\n    // if graph has edges then\n    //     return error   (graph has at least one cycle)\n    // else\n    //     return L   (a topologically sorted order)\n\n    std::vector<T> degree_0_set;\n    for (auto it = begin(graph); it!= end(graph);)\n    {\n        if (it->second.empty())\n        {\n            degree_0_set.push_back(it->first);\n            it = graph.erase(it);\n        }\n        else\n            ++it;\n    }\n\n    std::vector<T> ordered;\n    while (!degree_0_set.empty())\n    {\n        const auto degree_0_node = degree_0_set.back();\n        degree_0_set.pop_back();\n        ordered.push_back(degree_0_node);\n\n        for (auto it = begin(graph); it!= end(graph);)\n        {\n            auto &[node, edges] = *it;\n            if (edges.erase(degree_0_node) && edges.empty())\n            {\n                degree_0_set.push_back(node);\n                it = graph.erase(it);\n            }\n            else\n                ++it;\n        }\n    }\n    return {.sorted=ordered, .error_set=graph};\n}\n\n"
  },
  {
    "path": "src/plugin.h.in",
    "content": "// SPDX-FileCopyrightText: 2024 Manuel Schneider\n// SPDX-License-Identifier: MIT\n\n#pragma once\n\n#define ALBERT_PLUGIN_IID \"org.albert.PluginInterface/@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@\"\n\n///\n/// @brief Declare a class as Qt Plugin providing an Albert plugin interface\n///\n/// For convenience also contains the Q_OBJECT macro, since it is a hard requirement anyway.\n///\n/// Sets the interface identifier to #ALBERT_PLUGIN_IID and uses the metadata file named\n/// 'metadata.json' located at CMAKE_CURRENT_SOURCE_DIR.\n///\n/// This macro has to be put into the plugin class body. There must be exactly one occurrence of\n/// this macro in the source code for a plugin. The class this macro appears on must be\n/// default-constructible and inherit \\ref albert::PluginInstance.\n///\n#define ALBERT_PLUGIN Q_OBJECT Q_PLUGIN_METADATA(IID ALBERT_PLUGIN_IID FILE \"metadata.json\")\n"
  },
  {
    "path": "src/query/asyncgeneratorqueryhandler.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"asyncgeneratorqueryhandler.h\"\n#include \"logging.h\"\n#include \"queryexecution.h\"\n#include <QCoroAsyncGenerator>\n#include <QCoroTask>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\nclass AsyncExecution final : public QueryExecution\n{\n    unique_ptr<AsyncItemGenerator> generator;\n    optional<AsyncItemGenerator::iterator> iterator;\n    QCoro::Task<> fetch_task;\n    bool active;\n\npublic:\n\n    AsyncExecution(QueryContext &ctx, AsyncItemGenerator &&gen)\n        : QueryExecution(ctx)\n        , generator(make_unique<AsyncItemGenerator>(::move(gen)))\n        , iterator(nullopt)\n        , active(false)\n    {\n        fetchMore();\n    }\n\n    ~AsyncExecution() { cancel(); }\n\n    void cancel() override\n    {\n        generator.reset();\n        if (active)\n            emit activeChanged(active = false);\n    }\n\n    bool isActive() const override { return active; }\n\n    bool canFetchMore() const override {\n        return context.isValid()\n               && (!iterator\n                   // https://github.com/qcoro/qcoro/issues/294\n                   || iterator != const_cast<AsyncItemGenerator*>(generator.get())->end());\n    }\n\n    void fetchMore() override\n    {\n        if (!active && canFetchMore())\n            fetch_task = fetchMoreTask();\n    }\n\n    QCoro::Task<> fetchMoreTask()\n    {\n        emit activeChanged(active = true);\n\n        try {\n\n            if (iterator == nullopt)\n                iterator = co_await generator->begin();\n            else\n                co_await ++(*iterator);\n\n            if (*iterator != generator->end())\n                results.add(::move(**iterator));\n\n        } catch (const exception &e) {\n            WARN << u\"AsyncGeneratorQueryHandler threw exception:\\n\"_s << e.what();\n        } catch (...) {\n            WARN << u\"AsyncGeneratorQueryHandler threw unknown exception.\"_s;\n        }\n\n        emit activeChanged(active = false);\n    }\n};\n\nAsyncGeneratorQueryHandler::~AsyncGeneratorQueryHandler() {}\n\nunique_ptr<QueryExecution> AsyncGeneratorQueryHandler::execution(QueryContext &ctx)\n{ return make_unique<AsyncExecution>(ctx, items(ctx)); }\n"
  },
  {
    "path": "src/query/fallbackhandler.cpp",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#include \"fallbackhandler.h\"\n\n// vtable goes here\nalbert::FallbackHandler::~FallbackHandler() = default;\n"
  },
  {
    "path": "src/query/generatorqueryhandler.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"generatorqueryhandler.h\"\n#include \"logging.h\"\n#include <QCoroGenerator>\n#include <QFutureWatcher>\n#include <QtConcurrentRun>\n#include <albert/queryexecution.h>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\nGeneratorQueryHandler::~GeneratorQueryHandler() {}\n\nclass GeneratorQueryHandlerExecution final : public QueryExecution\n{\n    QFutureWatcher<vector<shared_ptr<Item>>> watcher;  // implicit active flag\n    GeneratorQueryHandler &handler;\n    ItemGenerator generator;  // mutexed\n    optional<ItemGenerator::iterator> iterator;  // mutexed\n    bool active;\n    // items(), begin and operator++ are potentially long blocking operations.\n    // it had to be mutexed because canFetchMore may check the iterator in the main thread.\n    // awaiting the lock however blocks the main thread potentially long.\n    // store a simple atomic at_end flag to avoid this.\n    // now theres only generator and iterator left that are touched in the thread\n    // due to the active flag they will never run concurrently\n    // so we dont actually need to mutex them at all\n    atomic_bool at_end;\n\npublic:\n\n    GeneratorQueryHandlerExecution(QueryContext &ctx, GeneratorQueryHandler &h)\n        : QueryExecution(ctx)\n        , handler(h)\n        , iterator(nullopt)\n        , active(true)\n        , at_end(false)\n    {\n        connect(&watcher, &QFutureWatcher<void>::finished,\n                this, &GeneratorQueryHandlerExecution::onFetchFinished);\n\n        watcher.setFuture(QtConcurrent::run([this] -> vector<shared_ptr<Item>>\n        {\n            // `items()` could also be a regular function that returns a generator.\n            // This function should as well run in the thread.\n            generator = handler.items(context);\n            iterator = generator.begin();\n            if (iterator != generator.end())\n                return ::move(*iterator.value());\n            return {};\n        }));\n    }\n\n    ~GeneratorQueryHandlerExecution()\n    {\n        cancel();\n\n        // Qt 6.4 QFutureWatcher is broken.\n        // isFinished returns wrong values and waitForFinished blocks forever on finished futures.\n        // TODO(26.04): Remove workaround when dropping Qt < 6.5 support.\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n        if (!watcher.isFinished())\n#else\n        if (watcher.isRunning())\n#endif\n        {\n            DEBG << QString(\"Busy wait on query: #%1\").arg(id);\n            watcher.waitForFinished();\n        }\n    }\n\n    void cancel() override { }\n\n    bool isActive() const override { return active; }\n\n    bool canFetchMore() const override { return context.isValid() && !at_end; }\n\n    void fetchMore() override\n    {\n        if (!isActive() && canFetchMore())\n        {\n            emit activeChanged(active = true);\n            watcher.setFuture(QtConcurrent::run([this] -> vector<shared_ptr<Item>>\n            {\n                ++*iterator;\n                if (iterator != generator.end())\n                    return ::move(*iterator.value());\n                return {};\n            }));\n        }\n    }\n\n    void onFetchFinished()\n    {\n        if (context.isValid())\n            try {\n                try {\n                    auto items = watcher.future().takeResult();\n                    if (items.empty())\n                        at_end = true;\n                    else\n                        results.add(::move(items));\n                } catch (const QUnhandledException &que) {\n                    if (que.exception())\n                        rethrow_exception(que.exception());\n                    else\n                        throw runtime_error(\"QUnhandledException::exception() returned nullptr.\");\n                }\n            } catch (const exception &e) {\n                WARN << u\"GeneratorQueryHandler threw exception:\\n\"_s << e.what();\n            } catch (...) {\n                WARN << u\"GeneratorQueryHandler threw unknown exception.\"_s;\n            }\n\n        emit activeChanged(active = false);\n    }\n};\n\nunique_ptr<QueryExecution> GeneratorQueryHandler::execution(QueryContext &ctx)\n{ return make_unique<GeneratorQueryHandlerExecution>(ctx, *this); }\n\n\n// -------------------------------------------------------------------------------------------------\n// Future queryhandler implementation. Based on AsyncGeneratorQueryHandler.\n// -------------------------------------------------------------------------------------------------\n\n// // This type is required because deleting coroutines simply cleans up the stack frame and QCoro does\n// // not provide any kind of clean up facilities. When not making sure to wait for the threads to\n// // finish before the stack frame is unwound segfaults may appear due to the thread accessing already\n// // freed memory. So this class makes sure bind the lifetime of the coroutine to the lifetime of the\n// // thread.\n// template<typename T>\n// struct BlockingFutureDeleter {\n//     void operator()(QFutureWatcher<T>* watcher)\n//     {\n//         if (watcher)\n//         {\n//             if (!watcher->isFinished())\n//                 watcher->waitForFinished();\n//             watcher->~QFutureWatcher<T>();\n//         }\n//     }\n// };\n\n// AsyncItemGenerator GeneratorQueryHandler::asyncItemGenerator(Query &query)\n// {\n//     ItemGenerator sync_gen = itemGenerator(query);\n//     ItemGenerator::iterator it = sync_gen.end();\n//     unique_ptr<QFutureWatcher<void>, BlockingFutureDeleter<void>> watcher(new QFutureWatcher<void>());\n\n//     struct V\n//     {Query &query;\n//         V(Query &query) :\n//             query(query)\n//         {}\n//         ~V() { CRIT << \"Destroying asyncItemGenerator coroutine for query \" << query.string(); }\n//     } v(query);\n\n//     auto task = qCoro(watcher.get(), &QFutureWatcher<void>::finished);\n\n//     // https://github.com/qcoro/qcoro/issues/312\n//     watcher->setFuture(QtConcurrent::run([&] {\n\n\n//         CRIT << \"sync_gen.begin()\";\n\n// it = sync_gen.begin(); }));\n//     co_await task;\n\n//     while (it != sync_gen.end()) {\n//         auto items =  ::move(*it);\n//         CRIT << \"Yielding batch of size\" << items.size() << \"for query \" << query.string();\n//         co_yield ::move(items);\n\n//         // https://github.com/qcoro/qcoro/issues/312\n//         watcher->setFuture(QtConcurrent::run([&] {\n\n//         CRIT << \"it++\";\n//             ++it; }));\n//         co_await task;\n//     }\n//     CRIT << \"END asyncItemGenerator\";\n// }\n"
  },
  {
    "path": "src/query/globalquery.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"globalquery.h\"\n#include \"globalqueryexecution.h\"\n#include <ranges>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\nQString GlobalQuery::id() const { return u\"globalquery\"_s; }\n\nQString GlobalQuery::name() const { return u\"Global query\"_s; }\n\nQString GlobalQuery::description() const { return u\"Runs a bunch of global query handlers\"_s; }\n\nunique_ptr<QueryExecution> GlobalQuery::execution(QueryContext &ctx)\n{\n    // FIXME ranges::to\n    auto v = global_query_handlers | views::values;\n    return make_unique<GlobalQueryExecution>(ctx, vector<GlobalQueryHandler*>{begin(v), end(v)});\n}\n\nQString GlobalQuery::synopsis(const QString &query) const\n{ return query == u\"*\"_s ? u\"🕚\"_s : u\"\"_s; }\n"
  },
  {
    "path": "src/query/globalquery.h",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"queryhandler.h\"\n#include <QString>\n#include <map>\nnamespace albert { class GlobalQueryHandler; }\n\nclass GlobalQuery : public albert::QueryHandler\n{\npublic:\n    std::map<QString, albert::GlobalQueryHandler *> global_query_handlers;\n\nprivate:\n    QString id() const override;\n    QString name() const override;\n    QString description() const override;\n    QString synopsis(const QString &query) const override;\n    std::unique_ptr<albert::QueryExecution> execution(albert::QueryContext &context) override;\n};\n"
  },
  {
    "path": "src/query/globalqueryexecution.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"color.h\"\n#include \"globalqueryexecution.h\"\n#include \"globalqueryhandler.h\"\n#include \"logging.h\"\n#include \"rankitem.h\"\n#include \"usagescoring.h\"\n#include <QFutureWatcher>\n#include <QtConcurrentMap>\n#include <chrono>\n#include <ranges>\n#include <vector>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std::chrono;\nusing namespace std;\n\n// -------------------------------------------------------------------------------------------------\n\nclass GlobalQueryResult : public albert::RankItem\n{\npublic:\n    explicit GlobalQueryResult(albert::GlobalQueryHandler *h, const albert::RankItem &i) noexcept :\n        RankItem(i),\n        handler(h)\n    {}\n    ~GlobalQueryResult() noexcept {}\n    albert::GlobalQueryHandler *handler;\n};\n\n// -------------------------------------------------------------------------------------------------\n\nstruct MappedData {\n    GlobalQueryHandler *handler;\n    vector<RankItem> rank_items;\n    uint handling_duration;\n    uint scoring_duration;\n};\n\n//V function(T &result, const U &intermediate)\nstruct ReducedData {\n    struct Diagnostics {\n        albert::GlobalQueryHandler *handler;\n        uint handling_runtime = 0;\n        uint scoring_runtime = 0;\n        uint item_count = 0;\n    };\n    vector<Diagnostics> handler_diag;\n    vector<GlobalQueryResult> results;\n};\n\nclass GlobalQueryExecution::Private\n{\npublic:\n    Private(GlobalQueryExecution *, vector<albert::GlobalQueryHandler *>);\n\n    void addResultChunk();\n\n    GlobalQueryExecution *q;\n    const vector<albert::GlobalQueryHandler*> handlers;\n    bool active;\n\n    QFutureWatcher<ReducedData> future_watcher;\n\n    vector<GlobalQueryResult> unordered_results;\n    chrono::time_point<chrono::system_clock> start_timepoint;\n    chrono::time_point<chrono::system_clock> finish_timepoint;\n};\n\nGlobalQueryExecution::Private::Private(GlobalQueryExecution *execution,\n                                       vector<GlobalQueryHandler *> h) :\n    q(execution),\n    handlers(::move(h)),\n    active(true)\n{\n    start_timepoint = system_clock::now();\n\n    auto future = QtConcurrent::mappedReduced(\n        handlers,\n        [this](GlobalQueryHandler *handler) -> MappedData {\n            // 6.4 Still no move semantics in QtConcurrent\n            MappedData data{.handler = handler,\n                            .rank_items = {},\n                            .handling_duration = 0,\n                            .scoring_duration = 0};\n            try {\n                auto t = system_clock::now();\n                if (q->context.query().isEmpty()) // important redirection\n                    for (auto &item : handler->handleEmptyQuery()) // order ???\n                        data.rank_items.emplace_back(::move(item), 0);\n                else\n                    data.rank_items = handler->rankItems(*q);\n                data.handling_duration = duration_cast<milliseconds>(system_clock::now()-t).count();\n\n                t = system_clock::now();\n                q->usageScoring().modifyMatchScores(handler->id(), data.rank_items);\n                data.scoring_duration = duration_cast<milliseconds>(system_clock::now()-t).count();\n            }\n            catch (const exception &e) {\n                WARN << u\"GlobalQueryHandler '%1' threw exception:\\n\"_s.arg(handler->id()) << e.what();\n            }\n            catch (...) {\n                WARN << u\"GlobalQueryHandler '%1' threw unknown exception:\\n\"_s.arg(handler->id());\n            }\n\n            return data;\n        },\n        [](ReducedData &reduced, const MappedData &mapped) {\n            reduced.handler_diag.emplace_back(mapped.handler,\n                                              mapped.handling_duration,\n                                              mapped.scoring_duration,\n                                              mapped.rank_items.size());\n            reduced.results.reserve(reduced.results.size() + mapped.rank_items.size());\n            for (auto &rank_item : mapped.rank_items)\n                reduced.results.emplace_back(mapped.handler, rank_item);  // copies, but at least threaded\n        }\n    );\n\n    QObject::connect(&future_watcher, &QFutureWatcher<ReducedData>::finished, q, [this] {\n        if (q->isValid())\n        {\n            auto reduced = future_watcher.future().takeResult();\n\n            const auto total_duration = duration_cast<milliseconds>(system_clock::now() - start_timepoint).count();\n\n            static const auto header  = color::blue + u\"╭ Handling╷  Scoring╷ Count╷ Query #%1 '%2'\"_s + color::reset;\n            static const auto body    = color::blue + u\"│%1 ms│%2 ms│%3│ %4\"_s + color::reset;\n            static const auto footer  = color::blue + u\"╰%1 ms╵         ╵%2╵ TOTAL\"_s + color::reset;\n\n            DEBG << header.arg(q->id).arg(q->context.query());\n            for (const auto &diag : reduced.handler_diag)\n                DEBG << body.arg(diag.handling_runtime, 6)\n                            .arg(diag.scoring_runtime, 6)\n                            .arg(diag.item_count, 6)\n                            .arg(diag.handler->id());\n            DEBG << footer.arg(total_duration, 6).arg(reduced.results.size(), 6);\n\n            unordered_results = ::move(reduced.results);\n\n            // Required because while active fetchMore has no effect\n            addResultChunk();\n        }\n\n        emit q->activeChanged(active = false);\n    });\n\n    future_watcher.setFuture(future);\n}\n\nvoid GlobalQueryExecution::Private::addResultChunk()\n{\n    auto tp = system_clock::now();\n\n    // Partial sort the items incrementally in reverse order (for cheap \"pop_n\")\n    auto reverse_view = unordered_results | views::reverse;\n\n    auto fetch_view = reverse_view | views::take(10);\n\n    ranges::partial_sort(reverse_view, fetch_view.end(), greater{});\n\n    // FIXME ranges::to\n    auto take_view = fetch_view | views::transform([](const GlobalQueryResult &r) {\n                         return QueryResult(r.handler, ::move(r.item));\n                     });\n\n    vector<QueryResult> taken{begin(take_view), end(take_view)};\n\n    // Cheap pop_n\n    unordered_results.erase(unordered_results.end() - fetch_view.size(), unordered_results.end());\n\n    const auto duration_sort = duration_cast<milliseconds>(system_clock::now() - tp).count();\n    DEBG << u\"Fetched %1 items in %2 ms\"_s.arg(taken.size()).arg(duration_sort);\n\n    // Query::add emits model signals that may lead to fetchMore recursions.\n    // Ensure unfetched_rank_items integrity _before adding_!\n    q->results.add(::move(taken));\n}\n\n// -------------------------------------------------------------------------------------------------\n\nGlobalQueryExecution::GlobalQueryExecution(QueryContext &c, vector<GlobalQueryHandler*> h)\n    : QueryExecution(c)\n    , d(make_unique<Private>(this, ::move(h)))\n{}\n\nGlobalQueryExecution::~GlobalQueryExecution()\n{\n    cancel();\n\n    // Qt 6.4 QFutureWatcher is broken.\n    // isFinished returns wrong values and waitForFinished blocks forever on finished futures.\n    // TODO(26.04): Remove workaround when dropping Qt < 6.5 support.\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n    if (!d->future_watcher.isFinished())\n#else\n    if (d->future_watcher.isRunning())\n#endif\n    {\n        DEBG << QString(\"Busy wait on query: #%1\").arg(id);\n        d->future_watcher.waitForFinished();\n    }\n}\n\nbool GlobalQueryExecution::isValid() const { return context.isValid(); }\n\nconst QueryHandler &GlobalQueryExecution::handler() const { return context.handler(); }\n\nQString GlobalQueryExecution::query() const\n{ return context.query() == \"*\" ? QString() : context.query(); }\n\nQString GlobalQueryExecution::trigger() const { return context.trigger(); }\n\nconst albert::UsageScoring &GlobalQueryExecution::usageScoring() const\n{ return context.usageScoring(); }\n\nvoid GlobalQueryExecution::cancel()\n{\n    disconnect(&d->future_watcher, &QFutureWatcher<ReducedData>::finished, this, nullptr);\n    d->future_watcher.cancel();\n}\n\nvoid GlobalQueryExecution::fetchMore()\n{\n    if (!isActive() && canFetchMore())\n    {\n        emit activeChanged(d->active = true);\n        d->addResultChunk();\n        emit activeChanged(d->active = false);\n    }\n}\n\nbool GlobalQueryExecution::canFetchMore() const { return !d->unordered_results.empty(); }\n\nbool GlobalQueryExecution::isActive() const { return d->active; }\n\n"
  },
  {
    "path": "src/query/globalqueryexecution.h",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"querycontext.h\"\n#include \"queryexecution.h\"\n#include <memory>\nnamespace albert{ class GlobalQueryHandler; }\n\nclass GlobalQueryExecution final : public albert::QueryExecution, public albert::QueryContext\n{\npublic:\n    GlobalQueryExecution(albert::QueryContext &context,\n                         std::vector<albert::GlobalQueryHandler *> query_handlers);\n    ~GlobalQueryExecution();\n\nprivate:\n    // albert::Query\n    // Required because we want to handle '*' as empty query and for atomic valid flag\n    bool isValid() const override;\n    const albert::QueryHandler &handler() const override;\n    QString query() const override;\n    QString trigger() const override;\n    const albert::UsageScoring &usageScoring() const override;\n\n    // albert::QueryExecution\n    void cancel() override;\n    void fetchMore() override;\n    bool canFetchMore() const override;\n    bool isActive() const override;\n\n    class Private;\n    std::unique_ptr<Private> d;\n};\n"
  },
  {
    "path": "src/query/globalqueryhandler.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"globalqueryhandler.h\"\nusing namespace albert;\nusing namespace std;\n\nGlobalQueryHandler::~GlobalQueryHandler() {}\n\nvector<shared_ptr<Item>> GlobalQueryHandler::handleEmptyQuery() { return {}; }\n"
  },
  {
    "path": "src/query/query.cpp",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#include \"query.h\"\n#include \"queryexecution.h\"\n#include \"queryhandler.h\"\n#include \"queryresults.h\"\n#include \"usagescoring.h\"\n#include <memory>\n#include <vector>\nusing namespace albert::detail;\nusing namespace std;\n\nclass Query::Private\n{\npublic:\n    UsageScoring usage_scoring;\n\n    atomic_bool valid;\n    QueryHandler &handler;\n    QString trigger;\n    QString string;\n\n    QueryResults matches;\n    QueryResults fallbacks;\n\n    std::unique_ptr<QueryExecution> execution;\n};\n\nQuery::Query(UsageScoring usage_scoring,\n             vector<QueryResult> &&fallbacks,\n             QueryHandler &handler,\n             QString trigger,\n             QString string) :\n    d(new Private{.usage_scoring = ::move(usage_scoring),\n                  .valid = true,\n                  .handler = handler,\n                  .trigger = trigger,\n                  .string = string,\n                  .matches = {*this},\n                  .fallbacks = {*this},\n                  .execution = {}})\n{\n    d->fallbacks.add(::move(fallbacks));\n\n    // CRUCIAL: Instantiate execution here.\n    // Do NOT construct the exection before query instance is constructed completely.\n    // `QueryExecution`s use `Query` which has to be valid throughout their entire lifetime.\n    // Note: While creating `Private` `Query::d` is not yet assigned.\n    d->execution = handler.execution(*this);\n\n    // DEBG << QString(\"Query created. [#%1 '%2']\").arg(d->execution->id).arg(d->string);\n}\n\nQuery::~Query()\n{\n    d->valid = false;\n\n    // DEBG << QString(\"Query about to be deleted. [#%1 '%2']\").arg(d->execution->id).arg(d->string);\n\n    // If not deleted early, Query::d is under destruction while destructing Query::execution.\n    d->execution.reset();\n}\n\nconst albert::UsageScoring &Query::usageScoring() const { return d->usage_scoring; }\n\nQString Query::trigger() const { return d->trigger; }\n\nQString Query::query() const { return d->string; }\n\nalbert::QueryResults &Query::matches() { return d->execution->results; }\n\nalbert::QueryResults &Query::fallbacks() { return d->fallbacks; }\n\nalbert::QueryHandler &Query::handler() const { return d->handler; }\n\nalbert::QueryExecution &Query::execution() const { return *d->execution; }\n\nbool Query::isValid() const { return d->valid; }\n\nvoid Query::cancel()\n{\n    if (d->valid)\n    {\n        d->valid = false;\n        d->execution->cancel();\n    }\n}\n"
  },
  {
    "path": "src/query/queryengine.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"albert/app.h\"\n#include \"extensionregistry.h\"\n#include \"fallbackhandler.h\"\n#include \"globalqueryhandler.h\"\n#include \"logging.h\"\n#include \"queryengine.h\"\n#include \"queryresults.h\"\n#include \"query.h\"\n#include \"usagedatabase.h\"\n#include \"usagescoring.h\"\n#include <QCoreApplication>\n#include <QMessageBox>\n#include <QSettings>\nusing namespace Qt::StringLiterals;\nusing namespace albert;\nusing namespace std;\n\nnamespace\n{\nstatic const char*  CFG_GLOBAL_HANDLER_ENABLED = \"global_handler_enabled\";\nstatic const char*  CFG_FALLBACK_ORDER = \"fallback_order\";\nstatic const char*  CFG_FALLBACK_EXTENSION = \"extension\";\nstatic const char*  CFG_FALLBACK_ITEM = \"fallback\";\nstatic const char*  CFG_TRIGGER = \"trigger\";\nstatic const char*  CFG_FUZZY = \"fuzzy\";\nstatic const char*  CFG_MEMORY_DECAY = \"memoryDecay\";\nstatic const double DEF_MEMORY_DECAY = 0.5;\nstatic const char*  CFG_PRIO_PERFECT = \"prioritizePerfectMatch\";\nstatic const bool   DEF_PRIO_PERFECT = true;\n}\n\n\nQueryEngine::QueryEngine(ExtensionRegistry &registry)\n    : registry_(registry)\n    , usage_scoring_(0,0,{})  // Null scoring, just to not have to implement constructors\n{\n    auto s = App::settings();\n\n    auto decay = s->value(CFG_MEMORY_DECAY, DEF_MEMORY_DECAY).toDouble();\n    auto prioritize_perfect_match = s->value(CFG_PRIO_PERFECT, DEF_PRIO_PERFECT).toBool();\n    usage_scoring_ = UsageScoring(prioritize_perfect_match, decay,\n                                  make_shared<unordered_map<ItemKey, double>>\n                                  (UsageDatabase::instance().itemUsageScores(decay)));\n\n    loadFallbackOrder();\n\n    connect(&registry, &ExtensionRegistry::added, this, [this](Extension *e)\n    {\n        const auto id = e->id();\n        auto settings = App::instance().settings();\n        settings->beginGroup(id);\n\n        if (auto *h = dynamic_cast<albert::QueryHandler*>(e))\n        {\n            auto t = settings->value(CFG_TRIGGER, h->defaultTrigger()).toString();\n            auto f = settings->value(CFG_FUZZY, false).toBool();\n\n            h->setTrigger(t);\n            h->setFuzzyMatching(f);\n            trigger_handlers_.emplace(piecewise_construct,\n                                      forward_as_tuple(id),\n                                      forward_as_tuple(h, t, f));\n            emit queryHandlerAdded(h);\n\n            updateActiveTriggers();\n        }\n\n        if (auto *h = dynamic_cast<albert::GlobalQueryHandler*>(e))\n        {\n            global_handlers_.emplace(id, h);\n            if (settings->value(CFG_GLOBAL_HANDLER_ENABLED, true).toBool())\n                global_query_.global_query_handlers.emplace(id, h);\n\n            emit globalQueryHandlerAdded(h);\n        }\n\n        if (auto *h = dynamic_cast<albert::FallbackHandler*>(e))\n        {\n            fallback_handlers_.emplace(id, h);\n            emit fallbackHandlerAdded(h);\n        }\n    });\n\n    connect(&registry, &ExtensionRegistry::removed, this, [this](Extension *e)\n    {\n        const auto id = e->id();\n\n        if (const auto it = trigger_handlers_.find(id); it != trigger_handlers_.end())\n        {\n            auto h = it->second.handler;\n            trigger_handlers_.erase(it);\n            emit queryHandlerRemoved(h);\n            updateActiveTriggers();\n        }\n\n        if (const auto it = global_handlers_.find(id); it != global_handlers_.end())\n        {\n            auto h = it->second;\n            global_handlers_.erase(it);\n            global_query_.global_query_handlers.erase(id);\n            emit globalQueryHandlerRemoved(h);\n        }\n\n        if (const auto it = fallback_handlers_.find(id); it != fallback_handlers_.end())\n        {\n            auto h = it->second;\n            fallback_handlers_.erase(it);\n            emit fallbackHandlerRemoved(h);\n        }\n    });\n}\n\nvoid QueryEngine::setMemoryDecay(double v)\n{\n    if (usage_scoring_.memory_decay != v)\n    {\n        DEBG << \"memoryDecay set to\" << v;\n        App::settings()->setValue(CFG_MEMORY_DECAY, v);\n        usage_scoring_ = UsageScoring(usage_scoring_.prioritize_perfect_match, v,\n                                      make_shared<unordered_map<ItemKey, double>>\n                                      (UsageDatabase::instance().itemUsageScores(v)));\n    }\n}\n\nvoid QueryEngine::setPrioritizePerfectMatch(bool v)\n{\n    if (usage_scoring_.prioritize_perfect_match != v)\n    {\n        DEBG << \"prioritizePerfectMatch set to\" << v;\n        App::settings()->setValue(CFG_PRIO_PERFECT, v);\n        usage_scoring_ = UsageScoring(v, usage_scoring_.memory_decay, usage_scoring_.usage_scores);\n    }\n}\n\nvoid QueryEngine::storeItemActivation(const QString &query, const QString &extension,\n                                      const QString &item, const QString &action)\n{\n    UsageDatabase::instance().addActivation(query, extension, item, action);\n\n    auto scores = UsageDatabase::instance().itemUsageScores(usage_scoring_.memory_decay);\n\n    usage_scoring_ = UsageScoring(\n        usage_scoring_.prioritize_perfect_match,\n        usage_scoring_.memory_decay,\n        make_shared<unordered_map<ItemKey, double>>(::move(scores))\n    );\n}\n\nUsageScoring QueryEngine::usageScoring() const\n{\n    return usage_scoring_;\n}\n\nunique_ptr<detail::Query> QueryEngine::query(QString string)\n{\n    vector<QueryResult> fallbacks;\n    if (!string.isEmpty())\n        fallbacks = this->fallbacks(string);\n\n    QString trigger;\n    albert::QueryHandler *handler;\n    if (auto it = ranges::find_if(active_triggers_.cbegin(), active_triggers_.cend(),\n                                  [&](const auto &t){ return string.startsWith(t.first); });\n        it != active_triggers_.cend())\n    {\n        trigger = it->first;\n        handler = it->second;\n        string = string.mid(trigger.size());\n    }\n    else\n        handler = &global_query_;\n\n    auto query = unique_ptr<detail::Query>(\n        new detail::Query(usage_scoring_, ::move(fallbacks), *handler, trigger, string));\n\n    connect(&query->matches(), &QueryResults::resultActivated,\n            this, &QueryEngine::storeItemActivation);\n\n    connect(&query->fallbacks(), &QueryResults::resultActivated,\n            this, &QueryEngine::storeItemActivation);\n\n    return query;\n}\n\n//\n// Trigger handlers\n//\n\nmap<QString, QueryHandler*> QueryEngine::triggerHandlers()\n{\n    map<QString, albert::QueryHandler*> handlers;\n    for (const auto &[id, h] : trigger_handlers_)\n        handlers.emplace(id, h.handler);\n    return handlers;\n}\n\nconst map<QString, QueryHandler *> &QueryEngine::activeTriggerHandlers() const\n{ return active_triggers_; }\n\nvoid QueryEngine::updateActiveTriggers()\n{\n    active_triggers_.clear();\n    for (const auto&[id, h] : trigger_handlers_)\n        if (const auto&[it, success] = active_triggers_.emplace(h.trigger, h.handler); !success)\n            WARN << QString(\"Trigger '%1' of '%2' already registered for '%3'.\")\n                        .arg(h.trigger, id, it->second->id());\n    emit activeTriggersChanged();\n}\n\nQString QueryEngine::trigger(const QString &id) const\n{ return trigger_handlers_.at(id).trigger; }\n\nvoid QueryEngine::setTrigger(const QString &id, const QString& t)\n{\n    auto &h = trigger_handlers_.at(id);\n\n    if (h.trigger == t || !h.handler->allowTriggerRemap())\n        return;\n\n    if (t.isEmpty() || t == h.handler->defaultTrigger())\n    {\n        h.trigger = h.handler->defaultTrigger();\n        App::settings()->remove(QString(\"%1/%2\").arg(id, CFG_TRIGGER));\n    }\n    else\n    {\n        h.trigger = t;\n        App::settings()->setValue(QString(\"%1/%2\").arg(id, CFG_TRIGGER), t);\n    }\n\n    h.handler->setTrigger(h.trigger);\n    updateActiveTriggers();\n}\n\nbool QueryEngine::fuzzy(const QString &id) const\n{ return trigger_handlers_.at(id).fuzzy; }\n\nvoid QueryEngine::setFuzzy(const QString &id, bool f)\n{\n    auto &h = trigger_handlers_.at(id);\n\n    if (h.handler->supportsFuzzyMatching())\n    {\n        h.fuzzy = f;\n        App::settings()->setValue(QString(\"%1/%2\").arg(id, CFG_FUZZY), f);\n        h.handler->setFuzzyMatching(f);\n    }\n}\n\n\n//\n// Global handlers\n//\n\nmap<QString, GlobalQueryHandler*> QueryEngine::globalHandlers()\n{\n    return global_handlers_;\n    // map<QString, albert::GlobalQueryHandler*> handlers;\n    // for (const auto &[id, h] : global_handlers_)\n    //     handlers.emplace(id, h.handler);\n    // return handlers;\n}\n\nbool QueryEngine::isEnabled(const QString &id) const\n{ return global_query_.global_query_handlers.contains(id); }\n\nvoid QueryEngine::setEnabled(const QString &id, bool e)\n{\n    auto *h = global_handlers_.at(id);\n\n    if (isEnabled(id) != e)\n    {\n        App::settings()->setValue(QString(\"%1/%2\").arg(id, CFG_GLOBAL_HANDLER_ENABLED), e);\n        if (e)\n            global_query_.global_query_handlers.emplace(id, h);\n        else\n            global_query_.global_query_handlers.erase(id);\n    }\n}\n\n\n//\n// Fallback handlers\n//\n\nmap<QString, FallbackHandler*> QueryEngine::fallbackHandlers()\n{ return fallback_handlers_; }\n\nconst map<pair<QString,QString>,int> &QueryEngine::fallbackOrder() const\n{ return fallback_order_; }\n\nvoid QueryEngine::setFallbackOrder(map<pair<QString,QString>,int> order)\n{\n    fallback_order_ = order;\n    saveFallbackOrder();\n}\n\n// bool QueryEngine::isEnabled(const FallbackHandler *h) const\n// { return enabled_fallback_handlers_.contains(h->id()); }\n\n// void QueryEngine::setEnabled(FallbackHandler *h, bool e)\n// {\n//     if (isEnabled(h) == e)\n//         return;\n\n//     settings()->setValue(QString(\"%1/%2\").arg(h->id(), CFG_FHANDLER_ENABLED), e);\n\n//     if (e)\n//         enabled_fallback_handlers_.emplace(h->id(), h);\n//     else\n//         enabled_fallback_handlers_.erase(h->id());\n\n//     emit handlersChanged();\n// }\n\nvoid QueryEngine::saveFallbackOrder() const\n{\n    // Invert to ordered list\n    vector<pair<QString, QString>> o;\n    for (const auto &[pair, prio] : fallback_order_)\n        o.emplace_back(pair.first, pair.second);\n    sort(begin(o), end(o), [&](const auto &a, const auto &b)\n         { return fallback_order_.at(a) > fallback_order_.at(b); });\n\n    // Save to settings\n    auto s = App::settings();\n    s->beginWriteArray(CFG_FALLBACK_ORDER);\n    for (int i = 0; i < (int)o.size(); ++i)\n    {\n        s->setArrayIndex(i);\n        s->setValue(CFG_FALLBACK_EXTENSION, o.at(i).first);\n        s->setValue(CFG_FALLBACK_ITEM, o.at(i).second);\n    }\n    s->endArray();\n}\n\nvoid QueryEngine::loadFallbackOrder()\n{\n    // Load from settings\n    vector<pair<QString, QString>> o;\n    auto s = App::settings();\n    int size = s->beginReadArray(CFG_FALLBACK_ORDER);\n    for (int i = 0; i < size; ++i)\n    {\n        s->setArrayIndex(i);\n        o.emplace_back(s->value(CFG_FALLBACK_EXTENSION).toString(),\n                       s->value(CFG_FALLBACK_ITEM).toString());\n    }\n    s->endArray();\n\n    // Create order map\n    fallback_order_.clear();\n    uint rank = 1;\n    for (auto it = o.rbegin(); it != o.rend(); ++it, ++rank)\n        fallback_order_.emplace(*it, rank);\n}\n\nvector<QueryResult> QueryEngine::fallbacks(const QString &query)\n{\n    vector<pair<FallbackHandler*, RankItem>> fallbacks;\n\n    for (auto &[id, fallback_handler] : fallback_handlers_)\n        for (auto item : fallback_handler->fallbacks(query))\n            if (auto it = fallbackOrder().find(make_pair(id, item->id()));\n                it == fallbackOrder().end())\n                fallbacks.emplace_back(fallback_handler, RankItem(::move(item), 0));\n            else\n                fallbacks.emplace_back(fallback_handler, RankItem(::move(item), it->second));\n\n    ranges::sort(fallbacks, greater(), &decltype(fallbacks)::value_type::second);\n\n    auto view = fallbacks | views::transform([](auto &p) {\n                         return QueryResult{p.first, ::move(p.second.item)};\n                     });\n\n    return {begin(view), end(view)};\n}\n"
  },
  {
    "path": "src/query/queryengine.h",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#pragma once\n#include \"globalquery.h\"\n#include \"usagescoring.h\"\n#include <QObject>\n#include <map>\n#include <memory>\nnamespace albert {\nclass ExtensionRegistry;\nclass FallbackHandler;\nclass GlobalQueryHandler;\nclass QueryHandler;\nclass UsageScoring;\nclass QueryResult;\nnamespace detail { class Query; }\n}\n\nclass QueryEngine : public QObject\n{\n    Q_OBJECT\n\npublic:\n\n    QueryEngine(albert::ExtensionRegistry&);\n\n    std::unique_ptr<albert::detail::Query> query(QString query);\n\n    albert::UsageScoring usageScoring() const;\n    void setMemoryDecay(double);\n    void setPrioritizePerfectMatch(bool);\n    void storeItemActivation(const QString &query, const QString &extension,\n                             const QString &item, const QString &action);\n\n    std::map<QString, albert::QueryHandler*> triggerHandlers();\n    std::map<QString, albert::GlobalQueryHandler*> globalHandlers();\n    std::map<QString, albert::FallbackHandler*> fallbackHandlers();\n\n    // Trigger handlers\n    const std::map<QString, albert::QueryHandler*> &activeTriggerHandlers() const;\n    QString trigger(const QString&) const;\n    void setTrigger(const QString&, const QString&);\n    bool fuzzy(const QString&) const;\n    void setFuzzy(const QString&, bool);\n\n    // Global handlers\n    bool isEnabled(const QString&) const;\n    void setEnabled(const QString&, bool = true);\n\n    // Fallback handlers\n    const std::map<std::pair<QString, QString>, int> &fallbackOrder() const;\n    void setFallbackOrder(std::map<std::pair<QString, QString>, int>);\n\nprivate:\n\n    void updateActiveTriggers();\n    void saveFallbackOrder() const;\n    void loadFallbackOrder();\n    std::vector<albert::QueryResult> fallbacks(const QString &query);\n\n    albert::ExtensionRegistry &registry_;\n\n    struct QueryHandler {\n        albert::QueryHandler *handler;\n        QString trigger;\n        bool fuzzy;\n    };\n    std::map<QString, QueryHandler> trigger_handlers_;\n    std::map<QString, albert::QueryHandler*> active_triggers_;\n\n    GlobalQuery global_query_;\n    std::map<QString, albert::GlobalQueryHandler*> global_handlers_;\n\n    std::map<QString, albert::FallbackHandler*> fallback_handlers_;\n    std::map<std::pair<QString, QString>, int> fallback_order_;\n\n    albert::UsageScoring usage_scoring_;\n\nsignals:\n\n    void queryHandlerAdded(albert::QueryHandler*);\n    void queryHandlerRemoved(albert::QueryHandler*);\n\n    void globalQueryHandlerAdded(albert::GlobalQueryHandler*);\n    void globalQueryHandlerRemoved(albert::GlobalQueryHandler*);\n\n    void fallbackHandlerAdded(albert::FallbackHandler*);\n    void fallbackHandlerRemoved(albert::FallbackHandler*);\n\n    void activeTriggersChanged();\n\n};\n"
  },
  {
    "path": "src/query/queryexecution.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"queryexecution.h\"\nusing namespace albert;\n\nnamespace {\nuint query_execution_count = 0;\n}\n\nQueryExecution::QueryExecution(QueryContext &ctx)\n    : id(query_execution_count++)\n    , context(ctx)\n    , results(ctx)\n{}\n\n"
  },
  {
    "path": "src/query/queryhandler.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"queryhandler.h\"\n#include <QString>\nusing namespace albert;\n\nQueryHandler::~QueryHandler() {}\n\nQString QueryHandler::synopsis(const QString &) const { return {}; }\n\nQString QueryHandler::defaultTrigger() const { return id() + QChar::Space; }\n\nbool QueryHandler::allowTriggerRemap() const { return true; }\n\nvoid QueryHandler::setTrigger(const QString &) {}\n\nbool QueryHandler::supportsFuzzyMatching() const { return false; }\n\nvoid QueryHandler::setFuzzyMatching(bool) { }\n"
  },
  {
    "path": "src/query/queryresults.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"extension.h\"\n#include \"logging.h\"\n#include \"messagebox.h\"\n#include \"queryresults.h\"\nusing namespace albert;\nusing namespace std;\n\nQueryResults::QueryResults(const QueryContext &ctx) : context(ctx){}\n\nQueryResults::~QueryResults()\n{\n    for (auto &result_item : results)\n        result_item.item->removeObserver(this);\n}\n\nbool QueryResults::activate(uint item_idx, uint action_idx)\n{\n    try {\n        auto &[e, i] = results.at(item_idx);\n\n        try {\n            auto a = i->actions().at(action_idx);\n\n            INFO << QString(\"Activating action %1 > %2 > %3 (%4 > %5 > %6) \")\n                        .arg(e->id(), i->id(), a.id, e->name(), i->text(), a.text);\n\n            // Order is cumbersome here\n\n            emit resultActivated(context.query(), e->id(), i->id(), a.id);\n\n            // May delete the query, due to hide()\n            // Note to myself:\n            // - QTimer::singleShot(0, this, [a]{ a.function(); });\n            //   Disconnects on query deletion.\n\n            try {\n                a.function();  // May delete the query, due to hide()\n            } catch (const exception &exc) {\n                const auto msg = QT_TRANSLATE_NOOP(\"QueryResults\", \"Exception in action\");\n                const auto fmt = QString(\"%1:\\n\\n%2 → %3 → %4\\n\\n%5\");\n                CRIT << fmt.arg(msg, e->id(), i->id(), a.id, exc.what());\n                critical(fmt.arg(tr(msg), e->name(), i->text(), a.text, exc.what()));\n            } catch (...) {\n                const auto msg = QT_TRANSLATE_NOOP(\"QueryResults\", \"Unknown exception in action\");\n                const auto fmt = QString(\"%1:\\n\\n%2 → %3 → %4\");\n                CRIT << fmt.arg(msg, e->id(), i->id(), a.id);\n                critical(fmt.arg(tr(msg), e->name(), i->text(), a.text));\n            }\n            return a.hide_on_activation;\n        }\n        catch (const out_of_range&) {\n            WARN << \"Activated action index is invalid:\" << action_idx;\n        }\n    }\n    catch (const out_of_range&) {\n        WARN << \"Activated item index is invalid:\" << item_idx;\n    }\n    return false;\n}\n\nvoid QueryResults::notify(const Item *item)\n{\n    // O(n) but since the results are populated lazy this should be okay.\n    if (auto it = find_if(results.begin(), results.end(),\n                          [=](const auto &ri){ return ri.item.get() == item; });\n        it != results.end())\n        emit resultChanged(distance(results.begin(), it));\n}\n"
  },
  {
    "path": "src/query/rankedqueryhandler.cpp",
    "content": "// Copyright (c) 2023-2025 Manuel Schneider\n\n#include \"rankedqueryhandler.h\"\n#include \"usagescoring.h\"\n#include <QCoroGenerator>\n#include <ranges>\nusing namespace albert;\nusing namespace std;\n\nRankedQueryHandler::~RankedQueryHandler() {}\n\nItemGenerator RankedQueryHandler::items(QueryContext &ctx)\n{\n    auto rank_items = rankItems(ctx);\n    ctx.usageScoring().modifyMatchScores(id(), rank_items);\n    return lazySort(::move(rank_items));\n}\n\nItemGenerator RankedQueryHandler::lazySort(vector<RankItem> rank_items)\n{\n    while(!rank_items.empty())\n    {\n        // Partial sort the items incrementally in reverse order (for cheap \"pop_n\")\n        auto reverse_view = rank_items | views::reverse;\n        auto take_view = reverse_view | views::take(10);\n        ranges::partial_sort(reverse_view, take_view.end(), greater{});\n\n        // Yield chunk\n        auto item_view = take_view | views::transform(&RankItem::item);\n        vector<shared_ptr<Item>> item_vector {\n            make_move_iterator(begin(item_view)),\n            make_move_iterator(end(item_view))\n        };\n\n        // Cheap pop_n\n        rank_items.erase(rank_items.end() - take_view.size(),rank_items.end());\n\n        co_yield ::move(item_vector);\n    }\n}\n"
  },
  {
    "path": "src/query/usagedatabase.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"albert/app.h\"\n#include \"albert/logging.h\"\n#include \"usagedatabase.h\"\n#include <QDateTime>\n#include <QDir>\n#include <QSqlDatabase>\n#include <QSqlDriver>\n#include <QSqlError>\n#include <QSqlQuery>\nusing namespace albert;\nusing namespace std;\n\nstatic const char* db_conn_name = \"usagehistory\";\nstatic const char* db_file_name = \"albert.db\";\n\nUsageDatabase &UsageDatabase::instance()\n{\n    static UsageDatabase usage_database;\n    return usage_database;\n}\n\nUsageDatabase::UsageDatabase()\n{\n    DEBG << \"Connecting usage database…\";\n\n    if (auto db = QSqlDatabase::addDatabase(\"QSQLITE\", db_conn_name);\n        !db.isValid())\n        qFatal(\"No sqlite available\");\n\n    else if (!db.driver()->hasFeature(QSqlDriver::Transactions))\n        qFatal(\"QSqlDriver::Transactions not available.\");\n\n    else if (db.setDatabaseName(QDir(App::dataLocation()).filePath(db_file_name));\n             !db.open())\n        qFatal(\"Database: Unable to establish connection: %s\", qPrintable(db.lastError().text()));\n\n    DEBG << \"Initializing usage database…\";\n\n    QSqlQuery sql(QSqlDatabase::database(db_conn_name));\n    sql.exec(\"CREATE TABLE IF NOT EXISTS activation ( \"\n             \"    timestamp INTEGER DEFAULT CURRENT_TIMESTAMP, \"\n             \"    query TEXT, \"\n             \"    extension_id, \"\n             \"    item_id TEXT, \"\n             \"    action_id TEXT \"\n             \"); \");\n    if (!sql.isActive())\n        qFatal(\"Unable to create table 'activation': %s\", sql.lastError().text().toUtf8().constData());\n}\n\nmap<QString, uint> UsageDatabase::extensionActivationsSince(const QDateTime &datetime) const\n{\n    QSqlQuery sql(QSqlDatabase::database(db_conn_name));\n    sql.exec(QString(\"SELECT extension_id, COUNT(extension_id) \"\n                     \"FROM activation \"\n                     \"WHERE timestamp > '%1' \"\n                     \"GROUP BY extension_id\").arg(datetime.toString(\"yyyy-MM-dd hh:mm:ss\")));\n\n    if (!sql.isActive())\n        qFatal(\"SQL ERROR: %s %s\", qPrintable(sql.executedQuery()), qPrintable(sql.lastError().text()));\n\n    map<QString, uint> activations;\n    while (sql.next())\n        activations.emplace(sql.value(0).toString(), sql.value(1).toUInt());\n\n    return activations;\n}\n\nvoid UsageDatabase::addActivation(const QString &q, const QString &e, const QString &i, const QString &a) const\n{\n    DEBG << \"Storing activation…\";\n\n    QSqlQuery sql(QSqlDatabase::database(db_conn_name));\n    sql.prepare(\"INSERT INTO activation (query, extension_id, item_id, action_id) \"\n                \"VALUES (:query, :extension_id, :item_id, :action_id);\");\n    sql.bindValue(\":query\", q);\n    sql.bindValue(\":extension_id\", e);\n    sql.bindValue(\":item_id\", i);\n    sql.bindValue(\":action_id\", a);\n    if (!sql.exec())\n        qFatal(\"SQL ERROR: %s %s\", qPrintable(sql.executedQuery()), qPrintable(sql.lastError().text()));\n}\n\nstd::unordered_map<ItemKey, double> UsageDatabase::itemUsageScores(double memory_decay) const\n{\n    DEBG << \"Fetching usage scores…\";\n\n    struct Activation\n    {\n        QString query;\n        QString extension_id;\n        QString item_id;\n        QString action_id;\n    };\n    vector<Activation> activations;\n\n    // Get activations\n    QSqlQuery sql(QSqlDatabase::database(db_conn_name));\n    sql.exec(\"SELECT query, extension_id, item_id, action_id FROM activation WHERE item_id<>''\");\n    if (!sql.isActive())\n        qFatal(\"SQL ERROR: %s %s\", qPrintable(sql.executedQuery()), qPrintable(sql.lastError().text()));\n    while (sql.next())\n        activations.emplace_back(sql.value(0).toString(), sql.value(1).toString(),\n                                 sql.value(2).toString(), sql.value(3).toString());\n\n    // Compute usage weights\n    unordered_map<ItemKey, double> usage_weights;\n    for (int i = 0, k = (int)activations.size(); i < (int)activations.size(); ++i, --k)\n    {\n        auto activation = activations[i];\n        double weight = pow(memory_decay, k);\n        if (const auto &[it, success] = usage_weights.emplace(\n                std::piecewise_construct,\n                std::forward_as_tuple(activation.extension_id, activation.item_id),\n                std::forward_as_tuple(weight));\n            !success)\n            it->second += weight;\n    }\n\n    // Invert the list. Results in ordered by rank map\n    map<double, vector<ItemKey>> weight_items;\n    for (const auto &[ids, weight] : usage_weights)\n        weight_items[weight].emplace_back(ids);\n\n    // Distribute scores linearly over the interval preserving the order\n    unordered_map<ItemKey, double> usage_scores;\n    double rank = 0.0;\n    for (const auto &[weight, vids] : weight_items)\n    {\n        double score = rank / weight_items.size();\n        for (const auto &ids : vids)\n            usage_scores.emplace(ids, score);\n        rank += 1.0;\n    }\n\n    return usage_scores;\n}\n\nvoid UsageDatabase::clearActivations() const\n{\n    DEBG << \"Clearing usage database…\";\n\n    QSqlQuery sql(QSqlDatabase::database(db_conn_name));\n    sql.exec(\"TRUNCATE TABLE activation;\");\n}\n"
  },
  {
    "path": "src/query/usagedatabase.h",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"usagescoring.h\"  // ItemKey\n#include <QString>\n#include <map>\nclass QDateTime;\n\n//\n// Direct database access.\n// Use in main thread only!\n//\nclass UsageDatabase\n{\npublic:\n\n    static UsageDatabase &instance();\n\n    std::map<QString, uint> extensionActivationsSince(const QDateTime &query) const;\n\n    std::unordered_map<albert::ItemKey, double> itemUsageScores(double memory_decay) const;\n\n    void addActivation(const QString &query,\n                       const QString &extension,\n                       const QString &item,\n                       const QString &action) const;\n\n    void clearActivations() const;\n\nprivate:\n\n    UsageDatabase();\n\n};\n"
  },
  {
    "path": "src/query/usagescoring.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"logging.h\"\n#include \"rankitem.h\"\n#include \"usagescoring.h\"\nusing namespace albert;\nusing namespace std;\n\ndouble UsageScoring::modifiedMatchScore(const ItemKey &key, double match_score) const\n{\n    const auto &it = usage_scores->find(key);\n\n    if (match_score == 1.0 && prioritize_perfect_match)\n    {\n        if (it != usage_scores->end())\n            match_score = 2.0 + it->second;\n        else\n            match_score = 2.0;\n    }\n    else if (it != usage_scores->end())\n        match_score = 1.0 + it->second;\n    // else score remains unmodified\n\n    return match_score;\n}\n\nvoid UsageScoring::modifyMatchScores(const QString &extension_id, vector<RankItem> &rank_items) const\n{\n    ItemKey key{extension_id, {}}; // avoid execessive key creation\n    for (auto &rank_item : rank_items)\n    {\n        try {\n            key.item_id = rank_item.item->id();\n        } catch (const std::exception &e) {\n            WARN << QString(\"Item in extension '%1' threw exception in id(): %2\")\n                        .arg(extension_id, e.what());\n            continue;\n        } catch (...) {\n            WARN << QString(\"Item in extension '%1' threw unknown exception in id()\").arg(extension_id);\n            continue;\n        }\n        rank_item.score = modifiedMatchScore(key, rank_item.score);\n    }\n}\n"
  },
  {
    "path": "src/settings/pluginswidget/pluginsmodel.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"logging.h\"\n#include \"pluginloader.h\"\n#include \"pluginmetadata.h\"\n#include \"pluginregistry.h\"\n#include \"pluginsmodel.h\"\n#include <QApplication>\n#include <QPalette>\n#include <QStyle>\n#include <ranges>\nusing enum Plugin::State;\nusing enum albert::PluginMetadata::LoadType;\nusing namespace Qt;\nusing namespace albert;\nusing namespace std;\n\n\nPluginsModel::PluginsModel(PluginRegistry &plugin_registry, QObject *parent):\n    QAbstractListModel(parent),\n    plugin_registry_(plugin_registry)\n{\n    for (auto &[_, plugin] : plugin_registry.plugins())\n        plugins.emplace_back(&plugin);\n\n    connect(&plugin_registry_, &PluginRegistry::pluginsChanged,\n            this, [this]\n            {\n                auto v = plugin_registry_.plugins()\n                         | views::transform([](auto &p){ return &p.second; });\n                vector<const Plugin*> vec(v.begin(), v.end());  // ranges::to\n                ranges::sort(vec, less<>{}, [](auto p){ return p->id; });\n                beginResetModel();\n                plugins = std::move(vec);\n                endResetModel();\n            });\n\n    connect(&plugin_registry, &PluginRegistry::pluginEnabledChanged,\n            this, [this](const QString &id)\n            {\n                if (auto it = ranges::find(plugins, id, [](auto p){ return p->id; });\n                    it != plugins.end())\n                {\n                    auto index = this->index(distance(begin(plugins), it));\n                    emit dataChanged(index, index, {CheckStateRole});\n                }\n                else\n                    WARN << \"enabledChanged called for a plugin not in model: \" << id;\n            });\n\n    connect(&plugin_registry, &PluginRegistry::pluginStateChanged,\n            this, [this](const QString &id)\n            {\n                if (auto it = ranges::find(plugins, id, [](auto p){ return p->id; });\n                    it != plugins.end())\n                {\n                    auto index = this->index(distance(begin(plugins), it));\n                    emit dataChanged(index, index, {DecorationRole,\n                                                    CheckStateRole,\n                                                    ForegroundRole,\n                                                    ToolTipRole});\n                }\n                else\n                    WARN << \"stateChanged called for a plugin not in model: \" << id;\n            });\n}\n\nint PluginsModel::rowCount(const QModelIndex &) const\n{ return static_cast<int>(plugins.size()); }\n\nint PluginsModel::columnCount(const QModelIndex &) const\n{ return 1; }\n\nQVariant PluginsModel::data(const QModelIndex &index, int role) const\n{\n    if (!index.isValid())\n        return {};\n\n    switch (const auto &p = *plugins[index.row()];\n            role)\n    {\n    case CheckStateRole:\n        if (p.metadata.load_type == User)\n        {\n            if (p.state == Loading)\n                return PartiallyChecked;\n            else\n                return p.enabled ? Checked : Unchecked;\n        }\n        break;\n\n    case DecorationRole:\n        if (p.state == Unloaded && !p.state_info.isNull())\n            return QApplication::style()->standardIcon(QStyle::SP_MessageBoxCritical);\n        break;\n\n    case DisplayRole:\n        return p.metadata.name;\n\n    case ForegroundRole:\n        if (p.state != Loaded)\n            return qApp->palette().color(QPalette::PlaceholderText);\n        break;\n\n    case ToolTipRole:\n        return p.state_info;\n\n    case UserRole:\n        return p.id;\n\n    }\n    return {};\n}\n\nbool PluginsModel::setData(const QModelIndex &index, const QVariant &value, int role)\n{\n    if (index.isValid() && index.column() == 0 && role == CheckStateRole)\n    {\n        try\n        {\n            if (auto &p = *plugins[index.row()];\n                p.metadata.load_type == User)\n            {\n                if (value == Checked)\n                    plugin_registry_.setEnabledWithUserConfirmation(p.id, true);\n                else if (value == Unchecked)\n                    plugin_registry_.setEnabledWithUserConfirmation(p.id, false);\n            }\n        }\n        catch (out_of_range &e){}\n    }\n    return false;\n}\n\nItemFlags PluginsModel::flags(const QModelIndex &idx) const\n{\n    if (idx.isValid())\n    {\n        switch (auto &p = *plugins[idx.row()];\n                p.state)\n        {\n        case Loading:\n            return ItemNeverHasChildren | ItemIsSelectable | ItemIsEnabled;\n        case Loaded:\n        case Unloaded:\n            return ItemNeverHasChildren | ItemIsSelectable | ItemIsEnabled | ItemIsUserCheckable;\n        }\n    }\n    return NoItemFlags;\n}\n"
  },
  {
    "path": "src/settings/pluginswidget/pluginsmodel.h",
    "content": "// Copyright (c) 2022-2024 Manuel Schneider\n\n#pragma once\n#include <QAbstractListModel>\nclass PluginRegistry;\nclass Plugin;\n\nclass PluginsModel: public QAbstractListModel\n{\npublic:\n    explicit PluginsModel(PluginRegistry &plugin_registry, QObject *parent = nullptr);\n\n    int rowCount(const QModelIndex& = {}) const override;\n    int columnCount(const QModelIndex&) const override;\n    QVariant data(const QModelIndex &idx, int role) const override;\n    bool setData(const QModelIndex &idx, const QVariant&, int role) override;\n    Qt::ItemFlags flags(const QModelIndex &idx) const override;\n\nprivate:\n    PluginRegistry &plugin_registry_;\n    std::vector<const Plugin*> plugins;\n};\n"
  },
  {
    "path": "src/settings/pluginswidget/pluginssortproxymodel.cpp",
    "content": "// Copyright (c) 2024-2024 Manuel Schneider\n\n#include \"albert/app.h\"\n#include \"pluginssortproxymodel.h\"\n#include <QSettings>\nusing namespace albert;\nconst char* CFG_SORT_MODE = \"show_enabled_plugins_first\";\nconst bool  DEF_SORT_MODE = true;\n\n\nPluginsSortProxyModel::PluginsSortProxyModel(QObject *parent) : QSortFilterProxyModel(parent)\n{\n    show_enabled_first_ = App::settings()->value(CFG_SORT_MODE, DEF_SORT_MODE).toBool();\n}\n\nbool PluginsSortProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const\n{\n    if (show_enabled_first_)\n    {\n        auto l = left.data(Qt::CheckStateRole).toInt();\n        auto r = right.data(Qt::CheckStateRole).toInt();\n        if (l != r)\n            return l > r;\n    }\n    return left.data(Qt::DisplayRole).toString() < right.data(Qt::DisplayRole).toString();\n}\n\nbool PluginsSortProxyModel::showEnabledFirst() const { return show_enabled_first_; }\n\nvoid PluginsSortProxyModel::setShowEnabledFirst(bool value)\n{\n    if (value != show_enabled_first_)\n    {\n        show_enabled_first_ = value;\n        App::settings()->setValue(CFG_SORT_MODE, show_enabled_first_);\n        invalidate();\n        sort(0);\n    }\n}\n\n"
  },
  {
    "path": "src/settings/pluginswidget/pluginssortproxymodel.h",
    "content": "// Copyright (c) 2024-2024 Manuel Schneider\n\n#pragma once\n#include <QSortFilterProxyModel>\n\nclass PluginsSortProxyModel : public QSortFilterProxyModel\n{\n    bool show_enabled_first_;\n\npublic:\n    PluginsSortProxyModel(QObject *parent = nullptr);\n\n    bool showEnabledFirst() const;\n    void setShowEnabledFirst(bool);\n\n    bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;\n};\n\n"
  },
  {
    "path": "src/settings/pluginswidget/pluginswidget.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"messagebox.h\"\n#include \"pluginloader.h\"\n#include \"pluginmetadata.h\"\n#include \"pluginregistry.h\"\n#include \"pluginsmodel.h\"\n#include \"pluginssortproxymodel.h\"\n#include \"pluginswidget.h\"\n#include \"pluginwidget.h\"\n#include <QApplication>\n#include <QHBoxLayout>\n#include <QLabel>\n#include <QListView>\n#include <QMenu>\n#include <QScrollArea>\nusing enum Plugin::State;\nusing enum albert::PluginMetadata::LoadType;\nusing namespace albert;\nusing namespace std;\n\nPluginsWidget::PluginsWidget(PluginRegistry &plugin_registry):\n    plugin_registry_(plugin_registry),\n    model_(new PluginsModel(plugin_registry, this)),\n    proxy_model_(new PluginsSortProxyModel(this))\n{\n    // Plugins list\n\n    plugins_list_view_ = new QListView(this);\n    plugins_list_view_->setModel(proxy_model_);\n    proxy_model_->setSourceModel(model_);\n    proxy_model_->setDynamicSortFilter(true);\n    proxy_model_->sort(0);\n\n    plugins_list_view_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);\n    plugins_list_view_->setEditTriggers(QAbstractItemView::NoEditTriggers);\n    plugins_list_view_->setProperty(\"showDropIndicator\", QVariant(false));\n    plugins_list_view_->setUniformItemSizes(true);\n\n    // https://invent.kde.org/plasma/breeze/-/merge_requests/520\n    // https://bugs.kde.org/show_bug.cgi?id=508437\n    // plugins_list_view_->viewport()->setAutoFillBackground(false);  // otherwise draws over border\n    plugins_list_view_->setProperty(\"_breeze_force_frame\", true);\n    // #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)\n    //     // Some styles on linux have bigger icons than rows\n    //     auto rh = plugins_list_view_->sizeHintForRow(0);  // this requires a model\n    //     plugins_list_view_->setIconSize(QSize(rh, rh));\n    // #endif\n\n    updatePluginListWidth();\n    connect(proxy_model_, &PluginsModel::modelReset,\n            this, &PluginsWidget::updatePluginListWidth);\n\n    plugins_list_view_->setContextMenuPolicy(Qt::CustomContextMenu);\n    connect(plugins_list_view_, &QListView::customContextMenuRequested,\n            this, &PluginsWidget::showContextMenu);\n\n\n    // Plugin config widget area\n\n    config_widget_scroll_area_ = new QScrollArea(this);\n    config_widget_scroll_area_->setFrameShape(QFrame::StyledPanel);\n    config_widget_scroll_area_->setFrameShadow(QFrame::Sunken);\n    config_widget_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);\n    config_widget_scroll_area_->setWidgetResizable(true);\n    config_widget_scroll_area_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignTop);\n    setPlaceholderWidget();\n    connect(plugins_list_view_->selectionModel(), &QItemSelectionModel::currentChanged,\n            this, [this](const QModelIndex &current, const QModelIndex &){\n        try {\n            const auto &p = plugin_registry_.plugins().at(current.data(Qt::UserRole).toString());\n            config_widget_scroll_area_->setWidget(new PluginWidget(plugin_registry_, p));  // takes ownership\n        }\n        catch (const out_of_range &) {\n            setPlaceholderWidget();\n        }\n    });\n\n    // Layout\n\n    auto *l = new QHBoxLayout(this);\n    l->addWidget(plugins_list_view_);\n    l->addWidget(config_widget_scroll_area_);\n    l->setContentsMargins(6, 6, 6, 6);\n    l->setSpacing(6);\n}\n\nPluginsWidget::~PluginsWidget() = default;\n\nvoid PluginsWidget::tryShowPluginSettings(QString plugin_id)\n{\n    for (auto row = 0; row < proxy_model_->rowCount(); ++row)\n    {\n        if (auto index = proxy_model_->index(row, 0);\n            index.data(Qt::UserRole).toString() == plugin_id)\n        {\n            plugins_list_view_->setCurrentIndex(index);\n            plugins_list_view_->setFocus();\n            return;\n        }\n    }\n}\n\nvoid PluginsWidget::showContextMenu(const QPoint &pos)\n{\n    QMenu menu;\n\n    if (auto index = proxy_model_->mapToSource(plugins_list_view_->currentIndex()); index.isValid())\n    {\n        try {\n            auto &p = plugin_registry_.plugins().at(index.data(Qt::UserRole).toString());\n            auto id = p.id;\n\n            if (p.metadata.load_type == User)\n            {\n                auto *a = new QAction(&menu);\n                a->setText(p.enabled ? tr(\"Disable\") : tr(\"Enable\"));\n                connect(a, &QAction::triggered,\n                        this, [=, this] { plugin_registry_.setEnabledWithUserConfirmation(id, !p.enabled); });\n                menu.addAction(a);\n\n                if (p.state == Loaded)\n                {\n                    a = new QAction(&menu);\n                    a->setText(tr(\"Unload\"));\n                    connect(a, &QAction::triggered,\n                            this, [=, this] { plugin_registry_.setLoaded(id, false); });\n                    menu.addAction(a);\n                }\n\n                if (p.state == Unloaded)\n                {\n                    a = new QAction(&menu);\n                    a->setText(tr(\"Load\"));\n                    connect(a, &QAction::triggered,\n                            this, [=, this] { plugin_registry_.setLoaded(id, true); });\n                    menu.addAction(a);\n                }\n\n                menu.addSeparator();\n            }\n        }\n        catch (const out_of_range &) { }\n    }\n\n    auto *a = new QAction(&menu);\n    a->setText(tr(\"Enabled first\"));\n    a->setCheckable(true);\n    a->setChecked(proxy_model_->showEnabledFirst());\n    connect(a, &QAction::toggled, proxy_model_, &PluginsSortProxyModel::setShowEnabledFirst);\n    menu.addAction(a);\n\n    menu.exec(mapToGlobal(pos));\n}\n\nvoid PluginsWidget::setPlaceholderWidget()\n{\n    auto contrib = \"https://albertlauncher.github.io/contributing/\";\n\n    auto t = tr(\"<p>Plugins are a community effort,\"\n                \"<br>built by awesome people like you.</p>\"\n                \"<p><a href='%1'>Join our community</a>\"\n                \"<br>and help make Albert thrive.</p>\"\n                \"<br>\")  // move text slightly up, looks more balanced\n                 .arg(contrib);\n\n    auto *lbl = new QLabel(t);\n    lbl->setAlignment(Qt::AlignCenter);\n    lbl->setOpenExternalLinks(true);\n    config_widget_scroll_area_->setWidget(lbl);  // takes ownership\n}\n\nvoid PluginsWidget::updatePluginListWidth()\n{\n    plugins_list_view_->setMaximumWidth(plugins_list_view_->sizeHintForColumn(0)\n                                      + qApp->style()->pixelMetric(QStyle::PM_ScrollBarExtent));\n}\n"
  },
  {
    "path": "src/settings/pluginswidget/pluginswidget.h",
    "content": "// Copyright (c) 2022-2024 Manuel Schneider\n\n#pragma once\n#include <QWidget>\nclass PluginRegistry;\nclass PluginsModel;\nclass PluginsSortProxyModel;\nclass QListView;\nclass QScrollArea;\n\nclass PluginsWidget final : public QWidget\n{\n    Q_OBJECT\n\npublic:\n    PluginsWidget(PluginRegistry&);\n    ~PluginsWidget();\n    void tryShowPluginSettings(QString);\n\nprivate:\n    void setPlaceholderWidget();\n    void showContextMenu(const QPoint &pos);\n    void updatePluginListWidth();\n\n    PluginRegistry &plugin_registry_;\n\n    PluginsModel *model_;\n    PluginsSortProxyModel *proxy_model_;\n\n    QListView *plugins_list_view_;\n    QScrollArea *config_widget_scroll_area_;\n};\n"
  },
  {
    "path": "src/settings/pluginswidget/pluginwidget.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"plugininstance.h\"\n#include \"pluginloader.h\"\n#include \"pluginmetadata.h\"\n#include \"pluginprovider.h\"\n#include \"pluginregistry.h\"\n#include \"pluginwidget.h\"\n#include <QBoxLayout>\n#include <QLabel>\n#include <QLocale>\n#include <ranges>\nusing enum Plugin::State;\nusing namespace albert;\nusing namespace std;\n\n\nPluginWidget::PluginWidget(const PluginRegistry &r, const Plugin &p):\n    plugin_registry(r), plugin(p)\n{\n    layout = new QVBoxLayout;\n    layout->setContentsMargins(6, 6, 6, 6);\n    layout->addWidget(createPluginPageHeader());\n    layout->addWidget(body = createPluginPageBody(), 1);  // Placeholder, Strech 1\n    layout->addStretch();  // Strech 0\n    layout->addWidget(createPluginPageFooter());\n    setLayout(layout);\n\n    connect(&plugin_registry, &PluginRegistry::pluginStateChanged,\n            this, &PluginWidget::onPluginStateChanged);\n}\n\nPluginWidget::~PluginWidget() = default;\n\nQWidget *PluginWidget::createPluginPageHeader() const\n{\n    auto *w = new QWidget;\n    auto *l = new QVBoxLayout(w);\n\n    auto *t = new QLabel(plugin.metadata.version.startsWith(\"0.\") ? plugin.metadata.name + \" ⚠️🚧👷\"\n                                                                  : plugin.metadata.name);\n    auto f = w->font();\n    f.setPointSize(f.pointSize() + 2);\n    t->setFont(f);\n    t->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);\n\n    auto *d = new QLabel(plugin.metadata.description);\n    f = w->font();\n    f.setPointSize(f.pointSize() - 2);\n    d->setForegroundRole(QPalette::PlaceholderText);\n    d->setFont(f);\n\n    l->setContentsMargins({});\n    l->setSpacing(2);\n    l->addWidget(t);\n    l->addWidget(d);\n\n    return w;\n}\n\nQWidget *PluginWidget::createPluginPageBody() const\n{\n    if (auto *inst = plugin.loader.instance(); inst)\n    {\n        if (auto *cw = inst->buildConfigWidget(); cw)\n        {\n            if (auto *cwl = cw->layout(); cwl)\n                cwl->setContentsMargins(0,0,0,0);\n            return cw;\n        }\n    }\n\n    else if (const auto info = plugin.state_info;\n             !info.isEmpty())\n    {\n        auto *lbl = new QLabel(info);\n        lbl->setWordWrap(true);\n        lbl->setAlignment(Qt::AlignTop);\n        return lbl;\n    }\n\n    return new QWidget;  // Empty placeholder\n}\n\nQWidget *PluginWidget::createPluginPageFooter() const\n{\n    QStringList meta;\n\n    if (!plugin.metadata.readme_url.isEmpty())\n        meta << QString(\"<a href=\\\"%1\\\">README</a>\").arg(plugin.metadata.readme_url);\n\n    // Id, version\n    meta << QString(\"<a href=\\\"%1\\\">%2 v%3</a>\")\n                .arg(plugin.metadata.url, plugin.metadata.id, plugin.metadata.version);\n\n    // License\n    meta << tr(\"License: %1\").arg(plugin.metadata.license);\n\n    // Authors\n    QStringList authors;\n    for (const auto &a : plugin.metadata.authors)\n        if (a.startsWith(QStringLiteral(\"@\")))\n            authors << QStringLiteral(\"<a href=\\\"https://github.com/%1\\\">%2</a>\")\n                           .arg(a.mid(1), a);\n        else\n            authors << a;\n\n    meta << tr(\"Authors: %1\", nullptr, authors.size()).arg(authors.join(\", \"));\n\n    // Maintainers\n    QStringList maintainers;\n    for (const auto &m : plugin.metadata.maintainers)\n        if (m.startsWith(QStringLiteral(\"@\")))\n            maintainers << QStringLiteral(\"<a href=\\\"https://github.com/%1\\\">%2</a>\")\n                           .arg(m.mid(1), m);\n        else\n            maintainers << m;\n\n    meta << tr(\"Maintainers: %1\", nullptr, authors.size())\n                //: Placeholder for empty maintainers\n                .arg(maintainers.isEmpty() ? QStringLiteral(\"<b>%1</b>\").arg(tr(\"Wanted!\"))\n                                           : maintainers.join(\", \"));\n\n    // Dependencies\n    if (const auto &list = plugin_registry.dependencies(&plugin);\n        !list.empty())\n    {\n        auto names = list | views::transform([](const auto &p){ return p->metadata.name; });\n        meta << tr(\"Required plugins: %1\", nullptr, names.size())\n                    .arg(QStringList(names.begin(), names.end()).join(\", \"));  // ranges::to\n    }\n\n    // Dependees\n    if (const auto &list = plugin_registry.dependees(&plugin);\n        !list.empty())\n    {\n        auto names = list | views::transform([](const auto &p){ return p->metadata.name; });\n        meta << tr(\"Required by plugins: %1\", nullptr, names.size())\n                    .arg(QStringList(names.begin(), names.end()).join(\", \"));  // ranges::to\n    }\n\n    // Required executables, if any\n    if (const auto &list = plugin.metadata.binary_dependencies; !list.isEmpty())\n        meta << tr(\"Required executables: %1\", nullptr, list.size()).arg(list.join(\", \"));\n\n    // Required libraries, if any\n    if (const auto &list = plugin.metadata.runtime_dependencies; !list.isEmpty())\n        meta << tr(\"Required libraries: %1\", nullptr, list.size()).arg(list.join(\", \"));\n\n    // Translations\n    if (const auto &list = plugin.metadata.translations; !list.empty())\n    {\n        QStringList displayList;\n        for (const auto &lang : list)\n        {\n            auto split = lang.split(\" \");\n            auto language = QLocale(split[0]).nativeLanguageName();\n            displayList << QString(\"%1 %2\").arg(language, split[1]);\n        }\n        meta << tr(\"Translations: %1\").arg(displayList.join(\", \"));\n    }\n\n    // Provider\n    meta << tr(\"%1, Interface: %2\").arg(plugin.provider.name(), plugin.metadata.iid);\n\n    // Path\n    meta << plugin.loader.path();\n\n    // Credits if any\n    if (const auto &list = plugin.metadata.third_party_credits; !list.isEmpty())\n        meta << tr(\"Credits: %1\").arg(list.join(\", \"));\n\n    auto *l = new QLabel(meta.join(\"<br>\"));\n    auto font = l->font();\n    font.setPointSize(font.pointSize() - 4);\n    l->setForegroundRole(QPalette::PlaceholderText);\n    l->setFont(font);\n    l->setOpenExternalLinks(true);\n    l->setWordWrap(true);\n    return l;\n}\n\nvoid PluginWidget::onPluginStateChanged(const QString &id)\n{\n    QWidget *new_body;\n    if (plugin.id == id && plugin.state == Loaded)\n        new_body = createPluginPageBody();\n    else\n        new_body = new QWidget;\n\n    auto layout_item = layout->replaceWidget(body, new_body, Qt::FindDirectChildrenOnly);\n    Q_ASSERT(layout_item != nullptr);\n\n    // Do _not_ delete later\n    delete layout_item;\n    delete body;\n\n    body = new_body;\n}\n"
  },
  {
    "path": "src/settings/pluginswidget/pluginwidget.h",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#pragma once\n#include <QWidget>\nclass Plugin;\nclass PluginRegistry;\nclass QVBoxLayout;\n\nclass PluginWidget final : public QWidget\n{\n    Q_OBJECT\n\npublic:\n    PluginWidget(const PluginRegistry &, const Plugin &);\n    ~PluginWidget();\n    void onPluginStateChanged(const QString &id);\n\nprivate:\n    QWidget *createPluginPageHeader() const;\n    QWidget *createPluginPageBody() const;\n    QWidget *createPluginPageFooter() const;\n\n    const PluginRegistry &plugin_registry;\n    const Plugin &plugin;\n    QVBoxLayout *layout;\n    QWidget *body;\n};\n"
  },
  {
    "path": "src/settings/querywidget/fallbacksmodel.cpp",
    "content": "// Copyright (c) 2022-2024 Manuel Schneider\n\n#include \"fallbackhandler.h\"\n#include \"fallbacksmodel.h\"\n#include \"icon.h\"\n#include \"queryengine.h\"\n#include <QCoreApplication>\n#include <QHeaderView>\n#include <QIODevice>\n#include <QMimeData>\nusing namespace albert::detail;\nusing namespace albert;\nusing namespace std;\n\nnamespace {\nenum class Column {\n    Name,\n    Description,\n};\nstatic int column_count = 2;\n}\n\nFallbacksModel::FallbacksModel(QueryEngine &e, QObject *p) : QAbstractTableModel(p), engine(e)\n{\n    connect(&e, &QueryEngine::fallbackHandlerAdded, this, &FallbacksModel::updateFallbackList);\n    connect(&e, &QueryEngine::fallbackHandlerRemoved, this, &FallbacksModel::updateFallbackList);\n    updateFallbackList();\n}\n\nvoid FallbacksModel::updateFallbackList()\n{\n    beginResetModel();\n\n    fallbacks.clear();\n\n    for (const auto &[hid, h] : engine.fallbackHandlers())\n        for (auto f : h->fallbacks(\"…\"))\n            fallbacks.emplace_back(h, f);\n\n    auto order = engine.fallbackOrder();\n\n    ::sort(begin(fallbacks), end(fallbacks), [&](const auto &a, const auto &b)\n           { return order[make_pair(a.first->id(), a.second->id())] > order[make_pair(b.first->id(), b.second->id())]; });\n\n    endResetModel();\n}\n\nint FallbacksModel::rowCount(const QModelIndex &) const\n{ return (int)fallbacks.size(); }\n\nint FallbacksModel::columnCount(const QModelIndex &) const\n{ return column_count; }\n\nQVariant FallbacksModel::data(const QModelIndex &index, int role) const\n{\n    if (!index.isValid())\n        return {};\n\n    auto &[h, i] = fallbacks[index.row()];\n    auto c = (Column)index.column();\n\n    if (c == Column::Name)\n    {\n        if (role == Qt::DecorationRole)\n            try {\n                return icon_cache.at(i->id());\n            } catch (const out_of_range &) {\n                return icon_cache[i->id()] = Icon::qIcon(i->icon());\n            }\n\n        else if (role == Qt::DisplayRole)\n            return i->text();\n\n        else if (role == Qt::ToolTipRole)\n            return QString(\"%1 - %2\").arg(h->id(), i->id());\n    }\n\n    else if (c == Column::Description)\n    {\n        if (role == Qt::DisplayRole)\n            return i->subtext();\n    }\n\n    return {};\n}\n\nQVariant FallbacksModel::headerData(int section, Qt::Orientation orientation, int role) const\n{\n    if (role == Qt::DisplayRole)\n        switch ((Column) section) {\n        case Column::Name:        return tr(\"Name\");\n        case Column::Description: return tr(\"Description\");\n        }\n    else if (role == Qt::ToolTipRole)\n        switch ((Column) section) {\n        case Column::Name:        return headerData(section, orientation, Qt::DisplayRole);\n        case Column::Description: return headerData(section, orientation, Qt::DisplayRole);\n        }\n    return {};\n}\n\nQt::ItemFlags FallbacksModel::flags(const QModelIndex &index) const\n{\n    if (index.isValid())\n        switch ((Column) index.column())\n        {\n        case Column::Name:\n        case Column::Description:\n            return Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable;\n        }\n    else\n        return QAbstractTableModel::flags(index) | Qt::ItemIsDropEnabled;\n    return {};\n}\n\nQt::DropActions FallbacksModel::supportedDropActions() const { return Qt::MoveAction; }\n\nbool FallbacksModel::dropMimeData(const QMimeData *data, Qt::DropAction, int dstRow, int, const QModelIndex &)\n{\n    QByteArray encoded = data->data(\"application/x-qabstractitemmodeldatalist\");\n    QDataStream stream(&encoded, QIODevice::ReadOnly);\n    int srcRow = 0;\n    if (!stream.atEnd())\n        stream >> srcRow;\n    return moveRows(QModelIndex(), srcRow, 1, QModelIndex(), dstRow);\n}\n\nbool FallbacksModel::moveRows(const QModelIndex &srcParent, int srcRow, int cnt, const QModelIndex &dstParent, int dstRow)\n{\n    if (srcRow < 0 || cnt < 1 || dstRow < 0)\n        return false;\n\n    // Exclude noop, segfaults\n    if (srcRow <= dstRow && dstRow <= srcRow + cnt)\n        return false;\n\n    beginMoveRows(srcParent, srcRow, srcRow + cnt - 1, dstParent, dstRow);\n    if (dstRow < srcRow)\n        std::rotate(fallbacks.begin() + dstRow, fallbacks.begin() + srcRow, fallbacks.begin() + srcRow + cnt);\n    else\n        std::rotate(fallbacks.begin() + srcRow,fallbacks.begin() + srcRow + cnt, fallbacks.begin() + dstRow + cnt - 1);\n    endMoveRows();\n\n    map<pair<QString,QString>,int> newOrder;\n    int rank = 1;\n    for (auto it = fallbacks.rbegin(); it != fallbacks.rend(); ++it, ++rank)\n        newOrder[make_pair(it->first->id(), it->second->id())] = rank;\n    engine.setFallbackOrder(newOrder);\n\n    return true;\n}\n"
  },
  {
    "path": "src/settings/querywidget/fallbacksmodel.h",
    "content": "// Copyright (c) 2022-2024 Manuel Schneider\n\n#pragma once\n#include <QAbstractTableModel>\n#include <vector>\nclass QIcon;\nclass QueryEngine;\nnamespace albert {\nclass FallbackHandler;\nclass Item;\n}\n\nclass FallbacksModel : public QAbstractTableModel\n{\n    Q_OBJECT\n\npublic:\n\n    FallbacksModel(QueryEngine &qe, QObject *parent);\n\n    void updateFallbackList();\n\nprivate:\n\n    int rowCount(const QModelIndex &) const override;\n    int columnCount(const QModelIndex &) const override;\n    QVariant data(const QModelIndex &idx, int role) const override;\n    QVariant headerData(int section, Qt::Orientation orientation, int role) const override;\n    Qt::ItemFlags flags(const QModelIndex &idx) const override;\n    Qt::DropActions supportedDropActions() const override;\n    bool dropMimeData(const QMimeData *data, Qt::DropAction action,\n                      int dstRow, int dstC, const QModelIndex &parent) override;\n    bool moveRows(const QModelIndex &srcParent, int srcRow, int count,\n                  const QModelIndex &dstParent, int dstRow) override;\n\n\n    QueryEngine &engine;\n    std::vector<std::pair<albert::FallbackHandler*, std::shared_ptr<albert::Item>>> fallbacks;\n    mutable std::map<QString, QIcon> icon_cache;\n\n};\n"
  },
  {
    "path": "src/settings/querywidget/queryhandlermodel.cpp",
    "content": "// Copyright (c) 2022-2024 Manuel Schneider\n\n#include \"globalqueryhandler.h\"\n#include \"queryengine.h\"\n#include \"queryhandlermodel.h\"\n#include <QCoreApplication>\n#include <QHeaderView>\n#include <QMessageBox>\n#include <set>\nusing namespace albert;\nusing namespace std;\n\nnamespace {\nenum class Column { Name, Trigger, Global, Fuzzy };\nstatic int column_count = 4;\n}\n\n\nQueryHandlerModel::QueryHandlerModel(QueryEngine &qe, QObject *parent)\n    : QAbstractTableModel(parent), engine(qe)\n{\n    connect(&engine, &QueryEngine::queryHandlerAdded, this, &QueryHandlerModel::updateHandlers);\n    connect(&engine, &QueryEngine::queryHandlerRemoved, this, &QueryHandlerModel::updateHandlers);\n    updateHandlers();\n}\n\nvoid QueryHandlerModel::updateHandlers()\n{\n    beginResetModel();\n\n    handlers_.clear();\n    for (auto &[id, h] : engine.triggerHandlers())\n        handlers_.emplace_back(h);\n\n    ::sort(begin(handlers_), end(handlers_),\n           [&](auto *a, auto *b){ return a->name() < b->name(); });\n\n    endResetModel();\n}\n\nint QueryHandlerModel::rowCount(const QModelIndex&) const\n{ return handlers_.size(); }\n\nint QueryHandlerModel::columnCount(const QModelIndex&) const\n{ return column_count; }\n\nQVariant QueryHandlerModel::data(const QModelIndex &idx, int role) const\n{\n    const auto *h = handlers_[idx.row()];\n\n    if (idx.column() == (int) Column::Name)\n    {\n        if (role == Qt::DisplayRole)\n            return h->name();\n\n        else if (role == Qt::ToolTipRole)\n            return h->description();\n    }\n\n    else if (idx.column() == (int) Column::Trigger)\n    {\n        auto t = engine.trigger(h->id());\n\n        if (role == Qt::DisplayRole)\n            return t.replace(\" \", \"•\");\n\n        else if (role == Qt::EditRole)\n            return t;\n\n        else if (role == Qt::ToolTipRole)\n        {\n            if (!h->allowTriggerRemap())\n                return tr(\"This extension does not allow trigger remapping.\");\n            else if (auto it = engine.activeTriggerHandlers().find(t);\n                     it != engine.activeTriggerHandlers().end() && it->second != h)\n                return tr(\"Trigger '%1' is reserved for '%2'.\")\n                    .arg(t, it->second->name());\n        }\n\n        else if (role == Qt::ForegroundRole)\n        {\n            if (auto it = engine.activeTriggerHandlers().find(t);\n                it == engine.activeTriggerHandlers().end() || it->second != h)\n                return QColor(Qt::red);\n            else if (!h->allowTriggerRemap())\n                return QColor(Qt::gray);\n        }\n    }\n\n    else if (idx.column() == (int) Column::Global)\n    {\n        if (auto *gh = dynamic_cast<const GlobalQueryHandler*>(h); gh)\n        {\n            if (role == Qt::CheckStateRole)\n                return engine.isEnabled(gh->id()) ? Qt::Checked : Qt::Unchecked;\n\n            else if (role == Qt::ToolTipRole)\n                return tr(\"Enable global query handling.\");\n        }\n    }\n\n    else if (idx.column() == (int) Column::Fuzzy)\n    {\n        if (h->supportsFuzzyMatching())\n        {\n            if (role == Qt::CheckStateRole)\n                return engine.fuzzy(h->id()) ? Qt::Checked : Qt::Unchecked;\n\n            else if (role == Qt::ToolTipRole)\n                return tr(\"Enable fuzzy matching.\");\n        }\n    }\n\n    return {};\n}\n\nbool QueryHandlerModel::setData(const QModelIndex &idx, const QVariant &value, int role)\n{\n    auto *h = handlers_[idx.row()];\n\n    if (idx.column() == (int) Column::Trigger)\n    {\n        if (role == Qt::EditRole)\n        {\n            if (const auto it = engine.activeTriggerHandlers().find(value.toString());\n                it != engine.activeTriggerHandlers().end() && it->second != h)\n                QMessageBox::warning(nullptr, qApp->applicationName(),\n                                     tr(\"Trigger '%1' is reserved for '%2'.\")\n                                         .arg(value.toString(), it->second->name()));\n            else\n            {\n                engine.setTrigger(h->id(), value.toString());\n                return true;\n            }\n        }\n    }\n\n    else if (idx.column() == (int) Column::Global)\n    {\n        if (auto *gh = dynamic_cast<GlobalQueryHandler*>(h); gh)\n        {\n            if (role == Qt::CheckStateRole)\n            {\n                engine.setEnabled(gh->id(), value == Qt::Checked);\n                return true;\n            }\n        }\n    }\n\n    else if (idx.column() == (int) Column::Fuzzy)\n    {\n        if (role == Qt::CheckStateRole) {\n            engine.setFuzzy(h->id(), value == Qt::Checked);\n            return true;\n        }\n    }\n\n    return false;\n}\n\nQVariant QueryHandlerModel::headerData(int section, Qt::Orientation orientation, int role) const\n{\n    if (role == Qt::DisplayRole)\n        switch ((Column) section) {\n        case Column::Name: return tr(\"Extension\");\n        case Column::Trigger: return tr(\"Trigger\");\n        case Column::Global: return tr(\"G\", \"short Global\");\n        case Column::Fuzzy: return tr(\"F\", \"short Fuzzy\");\n        }\n    else if (role == Qt::ToolTipRole)\n        switch ((Column) section) {\n        case Column::Name: return headerData(section, orientation, Qt::DisplayRole);\n        case Column::Trigger: return tr(\"The trigger of the handler. Spaces are visualized by •.\");\n        case Column::Global: return tr(\"Global query handling.\");\n        case Column::Fuzzy: return tr(\"Fuzzy matching.\");\n        }\n    return {};\n}\n\nQt::ItemFlags QueryHandlerModel::flags(const QModelIndex &idx) const\n{\n    auto *h = handlers_[idx.row()];\n\n    switch ((Column) idx.column()) {\n    case Column::Name:\n        return Qt::NoItemFlags;\n    case Column::Trigger:\n        return h->allowTriggerRemap() ? Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEditable : Qt::NoItemFlags;\n    case Column::Global:\n        return dynamic_cast<GlobalQueryHandler*>(h) ? Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable : Qt::NoItemFlags;\n    case Column::Fuzzy:\n        return h->supportsFuzzyMatching() ? Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable : Qt::NoItemFlags;\n    }\n    return {};\n}\n"
  },
  {
    "path": "src/settings/querywidget/queryhandlermodel.h",
    "content": "// Copyright (c) 2022-2024 Manuel Schneider\n\n#pragma once\n#include <QAbstractTableModel>\nclass QueryEngine;\nnamespace albert { class QueryHandler; }\n\nclass QueryHandlerModel : public QAbstractTableModel\n{\n    Q_OBJECT\n\npublic:\n\n    explicit QueryHandlerModel(QueryEngine &qe, QObject *parent);\n\nprivate:\n\n    void updateHandlers();\n\n    int rowCount(const QModelIndex &) const override;\n    int columnCount(const QModelIndex &) const override;\n    QVariant data(const QModelIndex &idx, int role) const override;\n    bool setData(const QModelIndex &idx, const QVariant &value, int role) override;\n    QVariant headerData(int section, Qt::Orientation orientation, int role) const override;\n    Qt::ItemFlags flags(const QModelIndex &idx) const override;\n\n    QueryEngine &engine;\n    std::vector<albert::QueryHandler*> handlers_;\n\n};\n"
  },
  {
    "path": "src/settings/querywidget/querywidget.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"fallbacksmodel.h\"\n#include \"queryengine.h\"\n#include \"queryhandlermodel.h\"\n#include \"querywidget.h\"\n#include \"usagescoring.h\"\n#include <QHeaderView>\n\nQueryWidget::QueryWidget(QueryEngine &query_engine) : query_engine_(query_engine)\n{\n    ui.setupUi(this);\n\n    auto usage_scoring = query_engine_.usageScoring();\n\n    ui.slider_decay->setValue((int)(usage_scoring.memory_decay * 100));\n\n    connect(ui.slider_decay, &QSlider::sliderReleased, this,\n            [this]{ query_engine_.setMemoryDecay((double)ui.slider_decay->value()/100.0); });\n\n    ui.checkBox_prioritizePerfectMatch->setChecked(usage_scoring.prioritize_perfect_match);\n\n    connect(ui.checkBox_prioritizePerfectMatch, &QCheckBox::toggled, this,\n            [this](bool val){ query_engine_.setPrioritizePerfectMatch(val); });\n\n    ui.tableView_queryHandlers->setModel(new QueryHandlerModel(query_engine_, this)); // Takes ownership\n    ui.tableView_fallbackOrder->setModel(fallbacks_model_ = new FallbacksModel(query_engine_, this)); // Takes ownership\n\n    for (auto *tv : {ui.tableView_queryHandlers, ui.tableView_fallbackOrder})\n    {\n        tv->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);  // Requires a model!\n        tv->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);  // Requires a model!\n        tv->horizontalHeader()->setSectionsClickable(false);\n    }\n\n    // Fix for some styles on linux setting a minimum secion size\n    ui.tableView_queryHandlers->horizontalHeader()->setMinimumSectionSize(0);\n\n    // Width adjust\n    auto updateWidth = [&]{\n        int width = 0;\n        for (int c = 0; c < ui.tableView_queryHandlers->model()->columnCount(); ++c)\n            width += ui.tableView_queryHandlers->horizontalHeader()->sectionSize(c);\n        width += + qApp->style()->pixelMetric(QStyle::PM_ScrollBarExtent);\n        ui.tableView_queryHandlers->setFixedWidth(width + 2);  // 2: Frame spacing?\n    };\n    connect(&query_engine_, &QueryEngine::queryHandlerAdded, this, updateWidth);\n    connect(&query_engine_, &QueryEngine::queryHandlerRemoved, this, updateWidth);\n    updateWidth();\n}\n\nvoid QueryWidget::showEvent(QShowEvent*)\n{\n    // This is a workaround such that FallbackHandlers dont need a signal\n    // for the change of fallbacks. This is a bit dirty, but a cheap solution\n    // maintenance and performance wise. The alternative would be to have a\n    // signal in FallbackHandler and connect change the fallbacksmodel to\n    // listen to it. That would be a lot of overhead for a very simple thing.\n    fallbacks_model_->updateFallbackList();\n}\n"
  },
  {
    "path": "src/settings/querywidget/querywidget.h",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#pragma once\n#include \"ui_querywidget.h\"\n#include <QWidget>\nclass QueryEngine;\nclass FallbacksModel;\n\nclass QueryWidget : public QWidget\n{\npublic:\n\n    QueryWidget(QueryEngine&);\n\nprivate:\n\n    void showEvent(QShowEvent *event) override;\n\n    Ui::QueryWidget ui;\n    QueryEngine &query_engine_;\n    FallbacksModel *fallbacks_model_;\n\n};\n"
  },
  {
    "path": "src/settings/querywidget/querywidget.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>QueryWidget</class>\n <widget class=\"QWidget\" name=\"QueryWidget\">\n  <layout class=\"QGridLayout\" name=\"gridLayout\">\n   <property name=\"leftMargin\">\n    <number>6</number>\n   </property>\n   <property name=\"topMargin\">\n    <number>6</number>\n   </property>\n   <property name=\"rightMargin\">\n    <number>6</number>\n   </property>\n   <property name=\"bottomMargin\">\n    <number>6</number>\n   </property>\n   <property name=\"spacing\">\n    <number>6</number>\n   </property>\n   <item row=\"1\" column=\"1\">\n    <widget class=\"QGroupBox\" name=\"groupBox_fallbackOrder\">\n     <property name=\"title\">\n      <string>Fallback order</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n      <property name=\"leftMargin\">\n       <number>0</number>\n      </property>\n      <property name=\"topMargin\">\n       <number>0</number>\n      </property>\n      <property name=\"rightMargin\">\n       <number>0</number>\n      </property>\n      <property name=\"bottomMargin\">\n       <number>0</number>\n      </property>\n      <item>\n       <widget class=\"QTableView\" name=\"tableView_fallbackOrder\">\n        <property name=\"dragDropMode\">\n         <enum>QAbstractItemView::DragDropMode::InternalMove</enum>\n        </property>\n        <property name=\"selectionMode\">\n         <enum>QAbstractItemView::SelectionMode::ContiguousSelection</enum>\n        </property>\n        <property name=\"selectionBehavior\">\n         <enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>\n        </property>\n        <property name=\"showGrid\">\n         <bool>false</bool>\n        </property>\n        <property name=\"wordWrap\">\n         <bool>false</bool>\n        </property>\n        <attribute name=\"horizontalHeaderStretchLastSection\">\n         <bool>true</bool>\n        </attribute>\n        <attribute name=\"verticalHeaderVisible\">\n         <bool>false</bool>\n        </attribute>\n       </widget>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item row=\"0\" column=\"1\">\n    <layout class=\"QFormLayout\" name=\"formLayout\">\n     <item row=\"0\" column=\"0\">\n      <widget class=\"QLabel\" name=\"label_decay\">\n       <property name=\"text\">\n        <string>Sort preference</string>\n       </property>\n       <property name=\"buddy\">\n        <cstring>slider_decay</cstring>\n       </property>\n      </widget>\n     </item>\n     <item row=\"0\" column=\"1\">\n      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_decay\">\n       <item>\n        <widget class=\"QLabel\" name=\"label_mru\">\n         <property name=\"toolTip\">\n          <string>Prefer most recently used results.</string>\n         </property>\n         <property name=\"text\">\n          <string>MRU</string>\n         </property>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QSlider\" name=\"slider_decay\">\n         <property name=\"toolTip\">\n          <string>This preference determines how past activations influence the ranking of the results of a query.\nUsing MFU all activations get the same weight and the cumulated score of a results is the count of its usages.\nUsing MRU the sum of weights of past activations can not exceed those of newer ones.\nRecommended is a value in between, such that the results you use often are preferred while still allowing Albert to adapt to changes in your usage habits.</string>\n         </property>\n         <property name=\"minimum\">\n          <number>50</number>\n         </property>\n         <property name=\"maximum\">\n          <number>100</number>\n         </property>\n         <property name=\"singleStep\">\n          <number>5</number>\n         </property>\n         <property name=\"pageStep\">\n          <number>5</number>\n         </property>\n         <property name=\"orientation\">\n          <enum>Qt::Orientation::Horizontal</enum>\n         </property>\n         <property name=\"tickPosition\">\n          <enum>QSlider::TickPosition::TicksBelow</enum>\n         </property>\n         <property name=\"tickInterval\">\n          <number>5</number>\n         </property>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QLabel\" name=\"label_mfu\">\n         <property name=\"toolTip\">\n          <string>Prefer most frequently used results.</string>\n         </property>\n         <property name=\"text\">\n          <string>MFU</string>\n         </property>\n        </widget>\n       </item>\n      </layout>\n     </item>\n     <item row=\"1\" column=\"1\">\n      <widget class=\"QCheckBox\" name=\"checkBox_prioritizePerfectMatch\">\n       <property name=\"toolTip\">\n        <string>Prioritize perfect matches even if they have no usage history and therefore would have a lower rank.</string>\n       </property>\n       <property name=\"text\">\n        <string/>\n       </property>\n      </widget>\n     </item>\n     <item row=\"1\" column=\"0\">\n      <widget class=\"QLabel\" name=\"label_prioritizePerfectMatch\">\n       <property name=\"text\">\n        <string>Prioritize perfect matches</string>\n       </property>\n       <property name=\"buddy\">\n        <cstring>checkBox_prioritizePerfectMatch</cstring>\n       </property>\n      </widget>\n     </item>\n    </layout>\n   </item>\n   <item row=\"0\" column=\"0\" rowspan=\"2\">\n    <widget class=\"QGroupBox\" name=\"groupBox_queryHandlers\">\n     <property name=\"title\">\n      <string>Query handlers</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n      <property name=\"leftMargin\">\n       <number>0</number>\n      </property>\n      <property name=\"topMargin\">\n       <number>0</number>\n      </property>\n      <property name=\"rightMargin\">\n       <number>0</number>\n      </property>\n      <property name=\"bottomMargin\">\n       <number>0</number>\n      </property>\n      <item>\n       <widget class=\"QTableView\" name=\"tableView_queryHandlers\">\n        <property name=\"sizePolicy\">\n         <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Expanding\">\n          <horstretch>0</horstretch>\n          <verstretch>0</verstretch>\n         </sizepolicy>\n        </property>\n        <property name=\"selectionMode\">\n         <enum>QAbstractItemView::SelectionMode::SingleSelection</enum>\n        </property>\n        <property name=\"showGrid\">\n         <bool>false</bool>\n        </property>\n        <property name=\"wordWrap\">\n         <bool>false</bool>\n        </property>\n        <attribute name=\"verticalHeaderVisible\">\n         <bool>false</bool>\n        </attribute>\n       </widget>\n      </item>\n     </layout>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <tabstops>\n  <tabstop>tableView_queryHandlers</tabstop>\n  <tabstop>slider_decay</tabstop>\n  <tabstop>checkBox_prioritizePerfectMatch</tabstop>\n  <tabstop>tableView_fallbackOrder</tabstop>\n </tabstops>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "src/settings/settingswindow.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"application.h\"\n#include \"frontend.h\"\n#include \"messagebox.h\"\n#include \"pathmanager.h\"\n#include \"pluginswidget.h\"\n#include \"querywidget.h\"\n#include \"settingswindow.h\"\n#include \"systemtrayicon.h\"\n#include \"systemutil.h\"\n#include \"telemetry.h\"\n#include <QDialog>\n#include <QHotkey>\n#include <QKeyEvent>\nusing namespace albert;\nusing namespace std;\n\nconst auto privacy_notice_url = \"https://albertlauncher.github.io/privacy/\";\n\n\nclass QHotKeyDialog : public QDialog\n{\npublic:\n\n    QHotKeyDialog(QWidget *parent) : QDialog(parent)\n    {\n        setWindowTitle(SettingsWindow::tr(\"Set hotkey\"));\n        setLayout(new QVBoxLayout);\n        layout()->addWidget(&label);\n        label.setText(SettingsWindow::tr(\"Press a key combination\"));\n        setWindowModality(Qt::WindowModal);\n    }\n\n    bool event(QEvent *event) override\n    {\n        if (event->type() == QEvent::KeyPress){\n            auto *keyEvent = static_cast<QKeyEvent*>(event);\n\n            if (Qt::Key_Shift <= keyEvent->key() && keyEvent->key() <= Qt::Key_ScrollLock)\n                return false; // Filter mod keys\n\n            if (keyEvent->modifiers() == Qt::NoModifier)\n            {\n                if (keyEvent->key() == Qt::Key_Escape)\n                    reject();\n                else if (keyEvent->key() == Qt::Key_Backspace)\n                    accept();\n                return true;\n            }\n\n            if (auto hk = make_unique<QHotkey>(keyEvent->keyCombination());\n                hk->setRegistered(true))\n            {\n                label.setText(hk->shortcut().toString(QKeySequence::NativeText));\n                hotkey = ::move(hk);\n                accept();\n            }\n        }\n        return QDialog::event(event);\n    }\n\n    QLabel label;\n    std::unique_ptr<QHotkey> hotkey;\n};\n\n\nSettingsWindow::SettingsWindow(Application &a):\n    app(a),\n    ui(),\n    small_text_fmt(R\"(<span style=\"font-size:9pt; color:#808080;\">%1</span>)\")\n{\n    ui.setupUi(this);\n    setAttribute(Qt::WA_DeleteOnClose);\n\n    init_tab_general_hotkey();\n    init_tab_general_trayIcon();\n    init_tab_general_frontends();\n    init_tab_general_path();\n    init_tab_general_telemetry();\n    init_tab_general_about();\n\n    ui.tabs->insertTab(ui.tabs->count(), app.frontend()->createFrontendConfigWidget(), tr(\"&Window\"));\n    ui.tabs->insertTab(ui.tabs->count(), plugin_widget = new PluginsWidget(app.pluginRegistry()), tr(\"&Plugins\"));\n    ui.tabs->insertTab(ui.tabs->count(), new QueryWidget(app.queryEngine()), tr(\"&Query\"));\n\n    auto *screen = QGuiApplication::screenAt(QCursor::pos());\n    if (!screen)\n        screen = QGuiApplication::primaryScreen();\n\n    auto geometry = screen->geometry();\n    move(geometry.center().x() - frameSize().width()/2,\n         geometry.top() + geometry.height() / 5);\n}\n\nSettingsWindow::~SettingsWindow() = default;\n\nvoid SettingsWindow::init_tab_general_hotkey()\n{\n    if (QHotkey::isPlatformSupported())\n    {\n        if (const auto &hk = app.hotkey(); hk)\n            ui.pushButton_hotkey->setText(hk->shortcut().toString(QKeySequence::NativeText));\n        else\n            ui.pushButton_hotkey->setText(tr(\"Not set\"));\n\n        connect(ui.pushButton_hotkey, &QPushButton::clicked, this, [this]{\n            QHotKeyDialog dialog(this);\n            if(dialog.exec() == QDialog::Accepted)\n            {\n                if (dialog.hotkey)\n                {\n                    app.setHotkey(::move(dialog.hotkey));\n                    ui.pushButton_hotkey->\n                        setText(app.hotkey()->shortcut().toString(QKeySequence::NativeText));\n                }\n                else\n                {\n                    app.setHotkey(nullptr);\n                    ui.pushButton_hotkey->setText(tr(\"Not set\"));\n                }\n            }\n        });\n    }\n    else\n    {\n        ui.label_hotkey->setEnabled(false);\n        ui.pushButton_hotkey->setText(tr(\"Not supported\"));\n        connect(ui.pushButton_hotkey, &QPushButton::clicked, this, []{\n            openUrl(\"https://albertlauncher.github.io/faq/#wayland\");\n        });\n    }\n}\n\nvoid SettingsWindow::init_tab_general_trayIcon()\n{\n    ui.checkBox_showTray->setChecked(app.systemTrayIcon().isEnabled());\n    connect(ui.checkBox_showTray, &QCheckBox::toggled,\n            this, [this](bool enable) { app.systemTrayIcon().setEnabled(enable); });\n}\n\nvoid SettingsWindow::init_tab_general_frontends()\n{\n    // Populate frontend checkbox\n    for (const auto &name : app.availableFrontends())\n    {\n        ui.comboBox_frontend->addItem(name);\n        if (name == app.currentFrontend())\n            ui.comboBox_frontend->setCurrentIndex(ui.comboBox_frontend->count()-1);\n    }\n    connect(ui.comboBox_frontend, &QComboBox::currentIndexChanged, this,\n            [this](int index){ app.setFrontend(index); });\n}\n\nvoid SettingsWindow::init_tab_general_path()\n{\n    const auto &additional = app.pathManager().additionalPathEntries();\n    const auto &original  = app.pathManager().originalPathEntries();\n\n    auto *le = ui.lineEdit_additional_path_entries;\n    le->setPlaceholderText(original.join(\":\"));\n    le->setText(additional.join(\":\"));\n    le->setToolTip((additional + original).join(\":\"));\n\n    connect(le, &QLineEdit::editingFinished,\n            this, [this, le] {\n                const auto new_add = le->text().split(\":\");\n\n                if (new_add == app.pathManager().additionalPathEntries())\n                    return;\n\n                app.pathManager().setAdditionalPathEntries(new_add);\n                le->setToolTip((new_add + app.pathManager().originalPathEntries()).join(\":\"));\n\n                if (question(tr(\"For the changes to take effect, Albert has to be restarted. \"\n                                \"Do you want to restart Albert now?\")))\n                    App::restart();\n            });\n}\n\nvoid SettingsWindow::init_tab_general_telemetry()\n{\n    ui.checkBox_telemetry->setToolTip(app.telemetry().buildReportString());\n    ui.checkBox_telemetry->setIcon(style()->standardPixmap(QStyle::SP_MessageBoxQuestion));\n    ui.checkBox_telemetry->setChecked(app.telemetry().enabled());\n\n    connect(ui.checkBox_telemetry, &QCheckBox::toggled, this, [this](bool checked){\n        app.telemetry().setEnabled(checked);\n        ui.checkBox_telemetry->setToolTip(app.telemetry().buildReportString());\n    });\n\n    ui.label_telemetry->setText(QString(\"[%1](%2)\")\n                                    .arg(ui.label_telemetry->text(), privacy_notice_url));\n}\n\nvoid SettingsWindow::init_tab_general_about()\n{\n    ui.label_app->setText(QString(\"<b>%1 v%2</b>\")\n                          .arg(qApp->applicationDisplayName(),\n                               qApp->applicationVersion()));\n\n    QStringList links;\n    links << QStringLiteral(\"[Telegram](https://telegram.me/albert_launcher_community)\");\n    links << QStringLiteral(\"[Discord](https://discord.com/invite/t8G2EkvRZh)\");\n    links << QStringLiteral(\"[News](https://albertlauncher.github.io/news/)\");\n    links << QStringLiteral(\"[GitHub](https://github.com/albertlauncher)\");\n    links << QStringLiteral(\"[Donate](https://albertlauncher.github.io/donation/)\");\n    ui.label_links->setText(links.join(\" · \"));\n\n    QStringList credits;\n    credits << ui.label_credits->text();\n    credits << \"QHotkey - Felix Barz  (BSD-3-Clause)\";\n    ui.label_credits->setText(small_text_fmt.arg(credits.join(\"<br>\")));\n}\n\nvoid SettingsWindow::bringToFront(const QString &plugin)\n{\n    show();\n    raise();\n    activateWindow();\n    if (!plugin.isNull()){\n        plugin_widget->tryShowPluginSettings(plugin);\n        ui.tabs->setCurrentWidget(plugin_widget);\n    }\n}\n\nvoid SettingsWindow::keyPressEvent(QKeyEvent *event)\n{\n    if (event->modifiers() == Qt::NoModifier){\n\n        if (event->key() == Qt::Key_Escape)\n            close();\n\n    } else if (event->modifiers() == Qt::ControlModifier){\n\n        if(event->key() == Qt::Key_W)\n            close();\n\n        // Tab navi by number\n        else if((int)Qt::Key_1 <= event->key() && event->key() < (int)Qt::Key_1 + ui.tabs->count())\n            ui.tabs->setCurrentIndex((int)event->key() - (int)Qt::Key_1);\n    }\n\n    QWidget::keyPressEvent(event);\n}\n"
  },
  {
    "path": "src/settings/settingswindow.h",
    "content": "// Copyright (c) 2014-2024 Manuel Schneider\n\n#pragma once\n#include \"ui_settingswindow.h\"\n#include <QWidget>\nclass Application;\nclass PluginsWidget;\n\nclass SettingsWindow final : public QWidget\n{\n    Q_OBJECT\n\npublic:\n\n    SettingsWindow(Application &app);\n    ~SettingsWindow();\n\n    void bringToFront(const QString & = {});\n\nprivate:\n\n    void init_tab_general_hotkey();\n    void init_tab_general_frontends();\n    void init_tab_general_path();\n    void init_tab_general_trayIcon();\n    void init_tab_general_telemetry();\n    void init_tab_general_about();\n    void keyPressEvent(QKeyEvent * event) override;\n\n    Application &app;\n    Ui::SettingsWindow ui;\n    PluginsWidget *plugin_widget;\n    QString small_text_fmt;\n};\n"
  },
  {
    "path": "src/settings/settingswindow.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>SettingsWindow</class>\n <widget class=\"QWidget\" name=\"SettingsWindow\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>800</width>\n    <height>600</height>\n   </rect>\n  </property>\n  <property name=\"minimumSize\">\n   <size>\n    <width>640</width>\n    <height>480</height>\n   </size>\n  </property>\n  <property name=\"windowTitle\">\n   <string>Settings</string>\n  </property>\n  <layout class=\"QGridLayout\" name=\"gridLayout\">\n   <property name=\"leftMargin\">\n    <number>8</number>\n   </property>\n   <property name=\"topMargin\">\n    <number>8</number>\n   </property>\n   <property name=\"rightMargin\">\n    <number>8</number>\n   </property>\n   <property name=\"bottomMargin\">\n    <number>8</number>\n   </property>\n   <item row=\"3\" column=\"0\">\n    <widget class=\"QTabWidget\" name=\"tabs\">\n     <property name=\"currentIndex\">\n      <number>0</number>\n     </property>\n     <widget class=\"QWidget\" name=\"tab_general\">\n      <attribute name=\"title\">\n       <string>General</string>\n      </attribute>\n      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_2\" stretch=\"1,1\">\n       <item>\n        <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\" stretch=\"1,0,1\">\n         <item>\n          <spacer name=\"verticalSpacer_3\">\n           <property name=\"orientation\">\n            <enum>Qt::Orientation::Vertical</enum>\n           </property>\n           <property name=\"sizeHint\" stdset=\"0\">\n            <size>\n             <width>0</width>\n             <height>0</height>\n            </size>\n           </property>\n          </spacer>\n         </item>\n         <item>\n          <layout class=\"QFormLayout\" name=\"formLayout_general\">\n           <property name=\"formAlignment\">\n            <set>Qt::AlignmentFlag::AlignCenter</set>\n           </property>\n           <item row=\"0\" column=\"0\">\n            <widget class=\"QLabel\" name=\"label_hotkey\">\n             <property name=\"text\">\n              <string>Hotkey</string>\n             </property>\n             <property name=\"buddy\">\n              <cstring>pushButton_hotkey</cstring>\n             </property>\n            </widget>\n           </item>\n           <item row=\"0\" column=\"1\">\n            <widget class=\"QPushButton\" name=\"pushButton_hotkey\">\n             <property name=\"sizePolicy\">\n              <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Fixed\">\n               <horstretch>0</horstretch>\n               <verstretch>0</verstretch>\n              </sizepolicy>\n             </property>\n             <property name=\"text\">\n              <string notr=\"true\"/>\n             </property>\n            </widget>\n           </item>\n           <item row=\"1\" column=\"0\">\n            <widget class=\"QLabel\" name=\"label_frontend\">\n             <property name=\"text\">\n              <string>Frontend</string>\n             </property>\n             <property name=\"buddy\">\n              <cstring>comboBox_frontend</cstring>\n             </property>\n            </widget>\n           </item>\n           <item row=\"1\" column=\"1\">\n            <widget class=\"QComboBox\" name=\"comboBox_frontend\">\n             <property name=\"sizePolicy\">\n              <sizepolicy hsizetype=\"Maximum\" vsizetype=\"Fixed\">\n               <horstretch>0</horstretch>\n               <verstretch>0</verstretch>\n              </sizepolicy>\n             </property>\n            </widget>\n           </item>\n           <item row=\"2\" column=\"0\">\n            <widget class=\"QLabel\" name=\"label_path\">\n             <property name=\"text\">\n              <string>Addtional PATH entries</string>\n             </property>\n             <property name=\"buddy\">\n              <cstring>lineEdit_additional_path_entries</cstring>\n             </property>\n            </widget>\n           </item>\n           <item row=\"3\" column=\"0\">\n            <widget class=\"QLabel\" name=\"label_show_tray\">\n             <property name=\"text\">\n              <string>Show tray icon</string>\n             </property>\n             <property name=\"buddy\">\n              <cstring>checkBox_showTray</cstring>\n             </property>\n            </widget>\n           </item>\n           <item row=\"3\" column=\"1\">\n            <widget class=\"QCheckBox\" name=\"checkBox_showTray\">\n             <property name=\"sizePolicy\">\n              <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Fixed\">\n               <horstretch>0</horstretch>\n               <verstretch>0</verstretch>\n              </sizepolicy>\n             </property>\n             <property name=\"text\">\n              <string/>\n             </property>\n            </widget>\n           </item>\n           <item row=\"4\" column=\"0\">\n            <widget class=\"QLabel\" name=\"label_telemetry\">\n             <property name=\"text\">\n              <string>Telemetry</string>\n             </property>\n             <property name=\"textFormat\">\n              <enum>Qt::TextFormat::MarkdownText</enum>\n             </property>\n             <property name=\"openExternalLinks\">\n              <bool>true</bool>\n             </property>\n             <property name=\"buddy\">\n              <cstring>checkBox_telemetry</cstring>\n             </property>\n            </widget>\n           </item>\n           <item row=\"4\" column=\"1\">\n            <widget class=\"QCheckBox\" name=\"checkBox_telemetry\">\n             <property name=\"sizePolicy\">\n              <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Fixed\">\n               <horstretch>0</horstretch>\n               <verstretch>0</verstretch>\n              </sizepolicy>\n             </property>\n             <property name=\"text\">\n              <string/>\n             </property>\n            </widget>\n           </item>\n           <item row=\"2\" column=\"1\">\n            <widget class=\"QLineEdit\" name=\"lineEdit_additional_path_entries\"/>\n           </item>\n          </layout>\n         </item>\n         <item>\n          <spacer name=\"verticalSpacer_5\">\n           <property name=\"orientation\">\n            <enum>Qt::Orientation::Vertical</enum>\n           </property>\n           <property name=\"sizeHint\" stdset=\"0\">\n            <size>\n             <width>0</width>\n             <height>0</height>\n            </size>\n           </property>\n          </spacer>\n         </item>\n        </layout>\n       </item>\n       <item>\n        <layout class=\"QVBoxLayout\" name=\"verticalLayout\" stretch=\"1,0,1\">\n         <item>\n          <spacer name=\"verticalSpacer_2\">\n           <property name=\"orientation\">\n            <enum>Qt::Orientation::Vertical</enum>\n           </property>\n           <property name=\"sizeHint\" stdset=\"0\">\n            <size>\n             <width>0</width>\n             <height>0</height>\n            </size>\n           </property>\n          </spacer>\n         </item>\n         <item>\n          <layout class=\"QVBoxLayout\" name=\"verticalLayout_4\">\n           <item>\n            <layout class=\"QHBoxLayout\" name=\"horizontalLayout\">\n             <item>\n              <spacer name=\"horizontalSpacer_2\">\n               <property name=\"orientation\">\n                <enum>Qt::Orientation::Horizontal</enum>\n               </property>\n               <property name=\"sizeHint\" stdset=\"0\">\n                <size>\n                 <width>0</width>\n                 <height>0</height>\n                </size>\n               </property>\n              </spacer>\n             </item>\n             <item>\n              <widget class=\"QLabel\" name=\"label\">\n               <property name=\"sizePolicy\">\n                <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Fixed\">\n                 <horstretch>0</horstretch>\n                 <verstretch>0</verstretch>\n                </sizepolicy>\n               </property>\n               <property name=\"maximumSize\">\n                <size>\n                 <width>64</width>\n                 <height>64</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string/>\n               </property>\n               <property name=\"pixmap\">\n                <pixmap resource=\"../../resources/resources.qrc\">:/icons/fallback/scalable/albert.svg</pixmap>\n               </property>\n               <property name=\"scaledContents\">\n                <bool>true</bool>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <spacer name=\"horizontalSpacer\">\n               <property name=\"orientation\">\n                <enum>Qt::Orientation::Horizontal</enum>\n               </property>\n               <property name=\"sizeHint\" stdset=\"0\">\n                <size>\n                 <width>0</width>\n                 <height>0</height>\n                </size>\n               </property>\n              </spacer>\n             </item>\n            </layout>\n           </item>\n           <item>\n            <widget class=\"QLabel\" name=\"label_app\">\n             <property name=\"alignment\">\n              <set>Qt::AlignmentFlag::AlignCenter</set>\n             </property>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QLabel\" name=\"label_links\">\n             <property name=\"text\">\n              <string/>\n             </property>\n             <property name=\"textFormat\">\n              <enum>Qt::TextFormat::MarkdownText</enum>\n             </property>\n             <property name=\"alignment\">\n              <set>Qt::AlignmentFlag::AlignCenter</set>\n             </property>\n             <property name=\"wordWrap\">\n              <bool>true</bool>\n             </property>\n             <property name=\"openExternalLinks\">\n              <bool>true</bool>\n             </property>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QLabel\" name=\"label_credits\">\n             <property name=\"text\">\n              <string>Credits:</string>\n             </property>\n             <property name=\"textFormat\">\n              <enum>Qt::TextFormat::RichText</enum>\n             </property>\n             <property name=\"alignment\">\n              <set>Qt::AlignmentFlag::AlignCenter</set>\n             </property>\n            </widget>\n           </item>\n          </layout>\n         </item>\n         <item>\n          <spacer name=\"verticalSpacer\">\n           <property name=\"orientation\">\n            <enum>Qt::Orientation::Vertical</enum>\n           </property>\n           <property name=\"sizeHint\" stdset=\"0\">\n            <size>\n             <width>0</width>\n             <height>0</height>\n            </size>\n           </property>\n          </spacer>\n         </item>\n        </layout>\n       </item>\n      </layout>\n     </widget>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <tabstops>\n  <tabstop>pushButton_hotkey</tabstop>\n  <tabstop>comboBox_frontend</tabstop>\n  <tabstop>lineEdit_additional_path_entries</tabstop>\n  <tabstop>checkBox_showTray</tabstop>\n  <tabstop>checkBox_telemetry</tabstop>\n  <tabstop>tabs</tabstop>\n </tabstops>\n <resources>\n  <include location=\"../../resources/resources.qrc\"/>\n </resources>\n <connections/>\n</ui>\n"
  },
  {
    "path": "src/util/color.h",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#pragma once\n\nnamespace color\n{\ninline constexpr const char* black  = \"\\x1b[30m\";\ninline constexpr const char* red    = \"\\x1b[31m\";\ninline constexpr const char* green  = \"\\x1b[32m\";\ninline constexpr const char* yellow = \"\\x1b[33m\";\ninline constexpr const char* blue   = \"\\x1b[34m\";\ninline constexpr const char* purple = \"\\x1b[35m\";\ninline constexpr const char* cyan   = \"\\x1b[36m\";\ninline constexpr const char* reset  = \"\\x1b[0m\";\n}\n"
  },
  {
    "path": "src/util/download.cpp",
    "content": "// Copyright (c) 2025-2025 Manuel Schneider\n\n#include \"download.h\"\n#include \"logging.h\"\n#include \"networkutil.h\"\n#include <QCoreApplication>\n#include <QFileInfo>\n#include <QNetworkAccessManager>\n#include <QNetworkReply>\n#include <QNetworkRequest>\n#include <QSaveFile>\n#include <QUrl>\n#include <QDir>\n#include <map>\n#include <memory>\n#include <mutex>\n#include <QTimer>\nusing namespace albert;\nusing namespace std;\n\nclass Download::Private\n{\npublic:\n    const QUrl url;\n    const QString path;\n    QNetworkReply *reply;\n    QString error;\n    mutex instance_mutex;\n};\n\nDownload::Download(const QUrl &_url, const QString &_path, QObject *_parent) :\n    QObject(_parent),\n    d(make_unique<Private>(_url, _path))\n{\n}\n\nDownload::~Download() = default;\n\nconst QUrl &Download::url() { return d->url; }  // threadsafe, const.\n\nconst QString &Download::path() { return d->path; }  // threadsafe, const.\n\nbool Download::isActive()\n{\n    lock_guard<mutex> lock(d->instance_mutex);\n    return d->reply;\n}\n\nconst QString &Download::error()\n{\n    lock_guard<mutex> lock(d->instance_mutex);\n    return d->error;\n}\n\nvoid Download::start()\n{\n    if (d->reply)\n    {\n        WARN << \"Download already in progress:\" << d->url.toString();\n        return;\n    }\n\n    QMetaObject::invokeMethod(qApp, [this] // Invoke on the main thread\n    {\n        DEBG << \"Start download from\" << d->url.toString();\n\n        lock_guard<mutex> lock(d->instance_mutex);\n        d->reply = network().get(QNetworkRequest(d->url));\n        d->reply->setParent(this);\n\n        connect(d->reply, &QNetworkReply::finished, this, [this]\n        {\n            d->instance_mutex.lock();\n\n            if (d->reply->error() == QNetworkReply::NoError)\n            {\n                QFileInfo info(d->path);\n\n                if (info.exists())\n                    d->error = \"File already exists.\";\n\n                else if (auto dir = info.dir();\n                         !dir.mkpath(\".\"))\n                    d->error = \"Cannot create parent directory.\";\n\n                if (QSaveFile file(d->path); file.open(QIODevice::WriteOnly))\n                {\n                    file.write(d->reply->readAll());\n                    if (auto success = file.commit(); !success)\n                    {\n                        if (d->error = file.errorString(); d->error.isEmpty())\n                            d->error = \"Failed to write file. Error unknown.\";\n                    }\n                }\n                else\n                {\n                    if (d->error = file.errorString(); d->error.isEmpty())\n                        d->error = \"Failed to open file. Error unknown.\";\n                }\n            }\n            else\n            {\n                if (d->error = d->reply->errorString(); d->error.isEmpty())\n                    d->error = \"Failed to download file. Unkown network error.\";\n            }\n\n            if (d->error.isNull())\n                DEBG << \"Download successful:\" << d->url.toString() << d->path;\n            else\n                DEBG << \"Download failed:\" << d->error << d->url.toString() << d->path;\n\n            d->reply->deleteLater();\n            d->reply = nullptr;\n\n            d->instance_mutex.unlock();\n\n            // QTimer::singleShot(1s, this, [this]{ emit finished(); });\n            emit finished();\n\n        });\n\n    }, Qt::QueuedConnection);\n}\n\nshared_ptr<Download> Download::unique(const QUrl &url, const QString &path)\n{\n    static mutex static_mutex;\n    static map<pair<QUrl, QString>, shared_ptr<Download>> active_downloads;\n\n    const auto key = make_pair(url, path);\n    lock_guard<mutex> lock(static_mutex);\n\n    if (auto it = active_downloads.find(key); it == active_downloads.end())\n    {\n        auto sd = shared_ptr<Download>(new Download(url, path),\n                                       [](Download *dl) { dl->deleteLater(); });\n\n        sd->moveToThread(qApp->thread());\n\n        active_downloads.emplace(key, sd);\n\n        QObject::connect(sd.get(), &Download::finished, [key]\n        {\n            lock_guard<mutex> inner_lock(static_mutex);\n            active_downloads.erase(key);\n        });\n\n        sd->start();\n\n        return sd;\n    }\n    else\n    {\n        return it->second;\n    }\n}\n"
  },
  {
    "path": "src/util/extensionplugin.cpp",
    "content": "// Copyright (c) 2024-2025 Manuel Schneider\n\n#include \"extensionplugin.h\"\n#include \"pluginloader.h\"\n#include \"pluginmetadata.h\"\nusing namespace albert;\nusing namespace std;\n\nQString ExtensionPlugin::id() const\n{ return loader().metadata().id; }\n\nQString ExtensionPlugin::name() const\n{ return loader().metadata().name; }\n\nQString ExtensionPlugin::description() const\n{ return loader().metadata().description; }\n\nvector<Extension*> ExtensionPlugin::extensions()\n{ return { this }; }\n"
  },
  {
    "path": "src/util/indexitem.cpp",
    "content": "// Copyright (c) 2021-2024 Manuel Schneider\n\n#include \"indexitem.h\"\nusing namespace albert;\nusing namespace std;\n\nIndexItem::IndexItem(shared_ptr<Item> i, QString s):\n    item(::move(i)), string(::move(s)){}\n"
  },
  {
    "path": "src/util/indexqueryhandler.cpp",
    "content": "// Copyright (c) 2023-2024 Manuel Schneider\n\n#include \"indexqueryhandler.h\"\n#include \"itemindex.h\"\n#include \"querycontext.h\"\n#include <memory>\n#include <mutex>\n#include <shared_mutex>\nusing namespace albert;\nusing namespace std;\n\nclass IndexQueryHandler::Private\n{\npublic:\n    unique_ptr<ItemIndex> index;\n    std::shared_mutex index_mutex;\n};\n\nIndexQueryHandler::IndexQueryHandler() : d(new Private()) {}\n\nIndexQueryHandler::~IndexQueryHandler() = default;\n\nvoid IndexQueryHandler::setIndexItems(vector<IndexItem> &&index_items)\n{\n    scoped_lock l(d->index_mutex);\n    if (d->index)\n        d->index->setItems(::move(index_items));\n}\n\nvector<RankItem> IndexQueryHandler::rankItems(QueryContext &ctx)\n{\n    shared_lock l(d->index_mutex);\n    if (d->index)\n        return d->index->search(ctx.query(), [&ctx] { return ctx.isValid(); });\n    return {};\n}\n\nbool IndexQueryHandler::supportsFuzzyMatching() const { return true; }\n\nvoid IndexQueryHandler::setFuzzyMatching(bool fuzzy)\n{\n    d->index_mutex.lock();\n    if (!d->index  // lazy index init\n        || d->index->config().fuzzy != fuzzy)\n    {\n        d->index = make_unique<ItemIndex>(MatchConfig{.fuzzy = fuzzy});\n        d->index_mutex.unlock();\n        updateIndexItems();\n    }\n    else\n        d->index_mutex.unlock();\n}\n"
  },
  {
    "path": "src/util/inputhistory.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"app.h\"\n#include \"inputhistory.h\"\n#include \"logging.h\"\n#include <QDir>\n#include <QFile>\n#include <QJsonArray>\n#include <QJsonDocument>\n#include <QStringList>\n#include <ranges>\nusing namespace albert::detail;\nusing namespace albert;\nusing namespace std;\n\nclass InputHistory::Private\n{\npublic:\n    QString file_path;\n    QStringList history;\n    qsizetype current;\n    uint max = -1;  // -1: max unsigned int\n};\n\n\nInputHistory::InputHistory(const QString &path):\n    d(make_unique<Private>())\n{\n    if (path.isEmpty())\n        d->file_path = QDir(App::instance().dataLocation()).filePath(\"albert.history\");\n    else\n        d->file_path = path;\n\n    if (QFile f(d->file_path); f.open(QIODevice::ReadOnly))\n    {\n        const auto doc = QJsonDocument::fromJson(f.readAll());\n\n        if (doc.isArray())\n        {\n            const auto a = doc.array();\n            d->history.reserve(a.size());\n            for (const auto v : a | views::reverse)\n                d->history << v.toString();\n        }\n        f.close();\n    }\n\n    resetIterator();\n}\n\nInputHistory::~InputHistory()\n{\n    if (QFile f(d->file_path); f.open(QIODevice::WriteOnly))\n    {\n        QJsonArray array;\n        for (const auto& line : d->history | views::reverse)\n            array.append(line);\n\n        const QJsonDocument doc(array);\n        f.write(doc.toJson(QJsonDocument::Indented));\n        f.close();\n    }\n    else\n        WARN << \"Writing history file failed:\" << d->file_path;\n}\n\nvoid InputHistory::add(const QString& s)\n{\n    if (!s.isEmpty())\n    {\n        d->history.prepend(s);\n        d->history.removeDuplicates();\n        if (d->history.size() > d->max)\n            d->history.resize(d->max);\n    }\n    resetIterator();\n}\n\nQString InputHistory::next(const QString &substring)\n{\n    while(true)\n    {\n        if (d->current == d->history.size() - 1)  // at end\n            return {};\n\n        if (const auto current_string = d->history.at(++d->current);\n            current_string != substring  // skip if equals search string\n            && current_string.contains(substring, Qt::CaseInsensitive))  // skip if no match\n            return current_string;\n    }\n}\n\nQString InputHistory::prev(const QString &substring)\n{\n    while(true)\n    {\n        if (d->current == 0)  // prev on first item: reset.\n            d->current = -1;\n\n        if (d->current == -1)  // at end\n            return {};\n\n        if (const auto current_string = d->history.at(--d->current);  // has been decremented above\n            current_string != substring  // skip if equals search string\n            && current_string.contains(substring, Qt::CaseInsensitive))  // skip if no match\n            return current_string;\n    }\n}\n\nvoid InputHistory::resetIterator() { d->current = -1; }\n\nvoid InputHistory::clear()\n{\n    d->history.clear();\n    resetIterator();\n}\n\nuint InputHistory::limit() const { return d->max; }\n\nvoid InputHistory::setLimit(uint v)\n{\n    if (v != d->max)\n    {\n        d->max = v;\n\n        if (d->history.size() > d->max)\n            d->history.resize(d->max);\n    }\n}\n"
  },
  {
    "path": "src/util/itemindex.cpp",
    "content": "// Copyright (c) 2021-2024 Manuel Schneider\n\n#include \"item.h\"\n#include \"itemindex.h\"\n#include \"levenshtein.h\"\n#include \"logging.h\"\n#include \"querypreprocessing.h\"\n#include <QRegularExpression>\n#include <algorithm>\n#include <map>\n#include <mutex>\n#include <shared_mutex>\n#include <unordered_map>\n#include <utility>\nusing namespace albert;\nusing namespace std;\n\nnamespace\n{\n\nusing Index = uint32_t;\nusing Position = uint16_t;\nstatic const uint N = 2;\n\n\nstruct StringIndexItem\n{\n    uint32_t item_index;\n    uint16_t max_match_len;\n};\n\n\nstruct Location\n{\n    Index index;\n    Position position;\n};\n\n\nstruct WordIndexItem\n{\n    QString word;\n    vector<Location> occurrences;\n    // TODO: heres the place to add a term_frequency weighting approach\n};\n\n\nstruct WordMatch\n{\n    const WordIndexItem &word_index_item;\n    uint match_length;\n};\n\n\nstruct StringMatch\n{\n    Index index;\n    Position position;\n    uint16_t match_len;\n};\n\n\nstruct IndexData\n{\n    ///\n    /// The flat random access index of unique items.\n    ///\n    vector<shared_ptr<albert::Item>> items;\n\n    ///\n    /// The string index (Inverted item index).\n    ///\n    /// Multiple strings can point to items.\n    /// Technically the string itself is not needed/stored.\n    /// The information is kept in the word index though.\n    ///\n    /// s_idx > (i_idx, mml)\n    ///\n    vector<StringIndexItem> strings;\n\n    ///\n    /// The word index (inverted string index).\n    ///\n    /// Holds pointers to word occurrences.\n    ///\n    /// w_idx > (word, [ (s_idx, w_pos) ] )\n    ///\n    vector<WordIndexItem> words;\n\n    ///\n    /// The nGram index.\n    ///\n    unordered_map<QString, vector<Location>> ngrams;\n};\n\n}\n\nclass ItemIndex::Private\n{\npublic:\n    MatchConfig config;\n    mutable shared_mutex mutex;\n    IndexData index;\n\n    vector<QString> ngrams_for_word(const QString &word)const;\n    vector<WordMatch> getWordMatches(const QString &word,\n                                     const function<bool()> &stop_requested) const;\n    vector<StringMatch> getStringMatches(const QString &word,\n                                         const function<bool()> &stop_requested) const;\n};\n\nvector<QString> ItemIndex::Private::ngrams_for_word(const QString &word) const\n{\n    vector<QString> ngrams;\n    ngrams.reserve(word.size());\n    auto padded = QString(\"%1%2\").arg(QString(N - 1, ' '), word);\n    for (int i = 0; i < word.size(); ++i){\n        QString ngram{padded.mid(i, N)};\n        ngram.shrink_to_fit();\n        ngrams.emplace_back(ngram);\n    }\n    return ngrams;\n}\n\nvector<WordMatch> ItemIndex::Private::getWordMatches(const QString &word,\n                                                     const function<bool()> &stop_requested) const\n{\n    vector<WordMatch> matches;\n    const uint word_length = word.length();\n\n    // Get range of perfect prefix match words\n    const auto &[eq_begin, eq_end] =\n            equal_range(\n                index.words.cbegin(), index.words.cend(), WordIndexItem{word, {}},\n                [l=word_length](const WordIndexItem &a, const WordIndexItem &b)\n                { return QStringView{a.word}.left(l) < QStringView{b.word}.left(l); }\n            );\n\n    // Store perfect prefix match words\n    for (auto it = eq_begin; it != eq_end; ++it)\n        matches.emplace_back(*it, word_length);\n\n    // Get the (fuzzy) prefix matches\n    if (config.fuzzy)\n    {\n        // Exclusion range for already collected prefix matches\n        Index exclude_begin = eq_begin - index.words.begin();  // Ignore interval. closed begin [\n        Index exclude_end = eq_end - index.words.begin();  // Ignore interval. open end )\n\n        auto ngrams = ngrams_for_word(word);\n\n        // Get the words referenced by each nGram\n        unordered_map<Index, uint> word_match_counts;\n        for (const QString &n_gram : ngrams)\n        {\n            if (!stop_requested)\n                return {};\n\n            // Get the ngram occurrences\n            if (auto it = index.ngrams.find(n_gram); it != index.ngrams.end())\n            {\n                // Iterate all ngram occurrences\n                for (const auto &ngram_occurrences : it->second)\n                {\n                    // Excluding the existing perfect matches\n                    if (exclude_begin <= ngram_occurrences.index\n                        && ngram_occurrences.index < exclude_end)\n                        continue;\n\n                    // count the ngrams where position < word_length\n                    if (ngram_occurrences.position < static_cast<Position>(word_length))\n                        ++word_match_counts[ngram_occurrences.index];\n                }\n            }\n        }\n\n        // First do a cheap preselection by mathematical bound.\n        // Then compute the edit distance to filter matches.\n        // If there are less than |word_length|-δ*n matching qGrams it is no\n        // match. If the common qGrams are less than |word|-δ*q this implies\n        // that there are more errors than δ.\n\n        Levenshtein levenshtein;\n        uint allowed_errors = word_length / 4;  // hardcoded 25% tolerance\n        uint minimum_match_count = word_length - allowed_errors * N;\n\n        for (const auto &[word_idx, ngram_count]: word_match_counts)\n        {\n            if (!stop_requested)\n                return {};\n\n            if (ngram_count < minimum_match_count)\n                continue;\n\n            if (auto edit_distance =\n                    levenshtein.computePrefixEditDistanceWithLimit(\n                        word, index.words[word_idx].word, allowed_errors);\n                    edit_distance > allowed_errors)\n                continue;\n            else\n                matches.emplace_back(index.words[word_idx], word_length-edit_distance);\n        }\n    }\n\n    return matches;\n}\n\nvector<StringMatch> ItemIndex::Private::getStringMatches(const QString &word,\n                                                         const function<bool()> &stop_requested) const\n{\n    vector<StringMatch> string_matches;\n\n    for (const auto &word_match : getWordMatches(word, stop_requested))\n        for (const auto &occurrence : word_match.word_index_item.occurrences)\n            string_matches.emplace_back(occurrence.index, occurrence.position, word_match.match_length);\n\n    sort(string_matches.begin(), string_matches.end(),\n         [](auto &l, auto &r){ return l.index < r.index; });\n\n    return string_matches;\n}\n\n\nItemIndex::ItemIndex(MatchConfig config)\n    : d(new Private{.config = ::move(config), .mutex = {}, .index = {}}) {}\n\nItemIndex &ItemIndex::operator=(ItemIndex &&) = default;\n\nItemIndex::ItemIndex(ItemIndex &&) = default;\n\nItemIndex::~ItemIndex() = default;\n\nconst MatchConfig &ItemIndex::config() { return d->config; }\n\nvoid ItemIndex::setItems(vector<IndexItem> &&index_items)\n{\n    IndexData new_index;\n\n    unordered_map<albert::Item*,Index> item_indices_;  // implicit unique\n    map<QString,WordIndexItem> word_index_;  // implicit lexicographical order\n\n    for (auto &[item, string] : index_items)\n    {\n        const auto words = preprocessQuery(string, d->config);\n        if (words.empty())\n        {\n            WARN << QString(\"Skipping index entry '%1'. Tokenization of '%2' yields empty set.\")\n                        .arg(item->id(), string);\n            continue;\n        }\n\n        // Try to add the item to the temporary item index map (ensures uniqueness)\n        // Assume it is going to be added to the end\n        const auto &[it, emplaced] = item_indices_.emplace(item.get(), (Index)new_index.items.size());\n\n        // If item does not exist, move it into the index.\n        if (emplaced)\n            new_index.items.emplace_back(::move(item));\n\n        // Add string to item mapping.\n        auto &string_index_item = new_index.strings.emplace_back(it->second, 0);\n\n        // Iterate the words\n        for (Position p = 0; p < (Position)words.size(); ++p)\n        {\n            // Add word to string mapping.\n            word_index_[words[p]].occurrences.emplace_back(new_index.strings.size() - 1, p);\n\n            // Store the maximal match length for scoring\n            string_index_item.max_match_len += words[p].size();\n        }\n    }\n\n    new_index.items.shrink_to_fit();\n    new_index.strings.shrink_to_fit();\n\n    // Build the random access word index\n    for (auto &[word, word_index_item] : word_index_)\n    {\n        word_index_item.word = word;\n        word_index_item.word.shrink_to_fit();\n        word_index_item.occurrences.shrink_to_fit();\n        new_index.words.emplace_back(::move(word_index_item));\n    }\n    new_index.words.shrink_to_fit();\n\n    if (d->config.fuzzy)\n    {\n        // Build n_gram_index\n        for (Index word_index = 0; word_index < (Index)new_index.words.size(); ++word_index)\n        {\n            auto ngrams = d->ngrams_for_word(new_index.words[word_index].word);\n            for (Position pos = 0 ; pos < (Position)ngrams.size(); ++pos)\n                new_index.ngrams[ngrams[pos]].emplace_back(word_index, pos);\n        }\n    }\n    for (auto &[_, word_refs] : new_index.ngrams)\n        word_refs.shrink_to_fit();\n\n    unique_lock lock(d->mutex);\n    d->index = new_index;\n}\n\nvector<albert::RankItem> ItemIndex::search(const QString &string,\n                                           const function<bool()> &stop_requested) const\n{\n    vector<RankItem> result;\n    const auto words = preprocessQuery(string, d->config);\n    shared_lock lock(d->mutex);\n\n    if (words.empty())\n    {\n        if (string.isEmpty())\n        {\n            // Return all items\n            result.reserve(d->index.items.size());\n            for (const auto &item : d->index.items)\n                result.emplace_back(item, 0.0f);\n            return result;\n        }\n    }\n    else\n    {\n        unordered_map<Index, double> result_map;\n        vector<StringMatch> string_matches = d->getStringMatches(words[0], stop_requested);\n\n        // In case of multiple words intersect. Todo: user chooses strategy\n        for (int w = 1; w < words.size(); ++w)\n        {\n            if (!stop_requested || string_matches.empty())\n                return {};\n\n            vector<StringMatch> other_string_matches = d->getStringMatches(words[w], stop_requested);\n\n            if (other_string_matches.empty())\n                return {};\n\n            vector<StringMatch> new_string_matches;\n            for (auto lit = string_matches.cbegin(); lit != string_matches.cend();)\n            {\n                // Build a range of upcoming left_matches with same index\n                auto elit = lit;\n                while(elit != string_matches.cend() && lit->index==elit->index)\n                    ++elit;\n\n                // Get the range of equal string matches on the right side\n                const auto &[eq_begin, eq_end] =\n                        equal_range(other_string_matches.cbegin(), other_string_matches.cend(),\n                                    *lit, [](auto &l, auto &r) { return l.index < r.index; });\n\n                // If no match on the right side continue with next leftmatch\n                if (eq_begin == eq_end){\n                    lit = elit;\n                    continue;\n                }\n\n                // Intersect and aggregate match lengths\n                for (;lit != elit; ++lit)\n                    for (auto rit = eq_begin; rit != eq_end; ++rit)\n                        if (lit->position < rit->position)  // Sequence check\n                            new_string_matches.emplace_back(rit->index, rit->position,\n                                                            rit->match_len + lit->match_len);\n            }\n\n            string_matches = ::move(new_string_matches);\n        }\n\n        // Build the list of matched items with their highest scoring match\n        for (const auto &match : string_matches)\n        {\n            double score = (double)match.match_len / d->index.strings[match.index].max_match_len;\n\n            const auto &[it, success] =\n                    result_map.emplace(d->index.strings[match.index].item_index, score);\n\n            // Update score if exists and is less\n            if (!success && it->second < score)\n                it->second = score;\n        }\n\n        // Convert results to return type\n        result.reserve(result_map.size());\n        for (const auto &[item_idx, score] : result_map)\n            result.emplace_back(d->index.items[item_idx], score);\n\n    }\n    return result;\n}\n"
  },
  {
    "path": "src/util/itemindex.h",
    "content": "// SPDX-FileCopyrightText: 2024 Manuel Schneider\n\n#pragma once\n#include <QString>\n#include <albert/export.h>\n#include <albert/indexitem.h>\n#include <albert/matchconfig.h>\n#include <albert/rankitem.h>\n#include <memory>\n#include <vector>\n\n///\n/// A fuzzy search index for items.\n///\nclass ALBERT_EXPORT ItemIndex final\n{\npublic:\n\n    ItemIndex(albert::MatchConfig config = {});\n    ItemIndex(ItemIndex &&);\n    ItemIndex& operator=(ItemIndex &&);\n    ~ItemIndex();\n\n    /// The index config\n    const albert::MatchConfig &config();\n\n    /// Set the items to be indexed.\n    /// @param items The items to be indexed.\n    void setItems(std::vector<albert::IndexItem> &&items);\n\n    /// Search the index for a string.\n    /// @param string The string to search for.\n    /// @param isValid A flag used to cancel the search.\n    /// @return A list of scored items.\n    std::vector<albert::RankItem> search(const QString &string,\n                                         const std::function<bool()> &stop_requested) const;\n\nprivate:\n\n    class Private;\n    std::unique_ptr<Private> d;\n\n};\n"
  },
  {
    "path": "src/util/levenshtein.cpp",
    "content": "// Copyright (c) 2021-2024 Manuel Schneider\n\n#include \"levenshtein.h\"\n#include <algorithm>\n#include <iostream>\n#include <limits>\nusing namespace std;\n\nstatic constexpr uint8_t max_edit_distance = numeric_limits<uint8_t>().max();\n\nuint Levenshtein::computePrefixEditDistanceWithLimit(const QString &prefix, const QString &string, uint k)\n{\n    if (k == 0)\n        return string.startsWith(prefix) ? 0 : 1;\n\n    if (prefix.size() > string.size()+k)\n        return k+1;\n\n    uint rows = prefix.size() + 1;\n    uint cols = min(prefix.size() + (qsizetype)k + 1, string.size() + 1);\n\n    expand_matrix_if_necessary(rows, cols);\n\n    //  Example distance d=2\n    //        a   b   _   e   f   g   h   i   j\n    //  ┌───┬───────────────────────────┬───────┐\n    //  │ 0 │ 1   2   3   4   5   6   7 │ 8   9 │\n    //  ├───┼───────────────────────────┼───────┤\n    //a │ 1 │(0)  1   2                 │       │\n    //  │   │                           │       d  r-1<d\n    //b │ 2 │ 1  (0)  1   2             │       │\n    //  │   ├───────────────────────────┼───d───┤\n    //c │ 3 │ 2   1  (1)  2   3         │       │\n    //  │   │                           │       │\n    //d │ 4 │     2  (2)  2   3   4     │       │\n    //  │   │                           │       │\n    //e │ 5 │         3  (2)  3   4   5 │       │\n    //  │   ├───────────────────────────┼───d───┤\n    //f │ 6 │             3  (2)  3   4 │ 5     │\n    //  │   │                           │       d\n    //g │ 7 │                 3  (2)  3 │ 4   5 │\n    //  └───┴───────────────────────────┴───────┘\n\n    uint8_t edit_distance{0};\n    for (uint r=1; r < rows; ++r) {\n        edit_distance = max_edit_distance;\n\n        if (r>k)\n            edit_distance = min(\n                edit_distance,\n                cell(r, r - k) = min({cell(r - 1, r - k - 1) + (prefix[r-1] == string[r - k-1] ? 0u : 1u),\n                                      cell(r - 1, r - k) + 1u}));\n\n        uint end = min(cols, r+k);\n        for (uint c = (uint)max(1,1+(int)r-(int)k); c < end; ++c)\n            edit_distance = min(\n                edit_distance,\n                cell(r,c) = min({cell(r - 1, c - 1) + (prefix[r - 1] == string[c - 1] ? 0u : 1u),\n                                 cell(r - 1, c) + 1u, cell(r, c - 1) + 1u})\n            );\n\n        if (r<cols-k)\n            edit_distance = min(\n                edit_distance,\n                cell(r, r + k) = min({cell(r - 1, r + k - 1) + (prefix[r-1] == string[r + k-1] ? 0u : 1u),\n                                      cell(r, r + k - 1) + 1u})\n            );\n        if (edit_distance > k)\n            return edit_distance;\n    }\n\n//    cout << \"k\" << k << endl;\n//    print_matrix(prefix, string);\n//    print_matrix_view(prefix, string, rows, cols);\n\n    return edit_distance;\n}\n\n\nconst uint8_t &Levenshtein::cell(uint r, uint c) const\n{\n    return matrix[(r)*matrix_cols + c];\n}\n\nuint8_t &Levenshtein::cell(uint r, uint c)\n{\n    return matrix[(r)*matrix_cols + c];\n}\n\n\nvoid Levenshtein::expand_matrix_if_necessary(uint rows, uint cols)\n{\n    // if space needed expand and init matrix\n    if (matrix_rows < rows || matrix_cols < cols){\n        matrix_rows = max(matrix_rows, rows);\n        matrix_cols = max(matrix_cols, cols);\n        matrix.resize(matrix_rows*matrix_cols);\n        for (uint r = 0; r < matrix_rows; ++r)\n            cell(r, 0) = r;\n        for (uint c = 0; c < matrix_cols; ++c)\n            cell(0, c) = c;\n    }\n}\n\nvoid Levenshtein::print_matrix_view(const QString &prefix, const QString &string, uint rows, uint cols) const\n{\n    cout << qPrintable(prefix) << endl;\n    cout << qPrintable(string) << endl;\n    cout << \"   \" ;\n    for (int r = 0; r < string.size(); ++r)\n        cout << \" \" << qPrintable(string)[r];\n    cout  << endl;\n    for (uint r = 0; r < rows; ++r){\n        cout << qPrintable(QString(\" %1\").arg(prefix))[r];\n        for (uint c = 0; c < cols; ++c){\n            cout << \" \" << (int)cell(r,c);\n        }\n        cout << '\\n';\n    }\n}\n\nvoid Levenshtein::print_matrix(const QString &prefix, const QString &string) const\n{\n    cout << qPrintable(prefix) << endl;\n    cout << qPrintable(string) << endl;\n    cout << \"   \" ;\n    for (int r = 0; r < string.size(); ++r)\n        cout << \" \" << qPrintable(string)[r];\n    cout  << endl;\n    for (uint r = 0; r < matrix_rows; ++r){\n        cout << qPrintable(QString(\" %1\").arg(prefix))[r];\n        for (uint c = 0; c < matrix_cols; ++c){\n            cout << \" \" << (int)cell(r,c);\n        }\n        cout << endl;\n    }\n}\n/// Returns true if delta is not exceeded\nbool Levenshtein::checkPrefixEditDistance_Legacy(const QString &prefix, const QString &str, uint delta)\n{\n    uint row_count = prefix.size() + 1;\n    uint col_count = min(prefix.size() + (qsizetype)delta + 1, str.size() + 1);\n\n    uint* table = new uint[row_count * col_count];\n\n    // Initialize left and top row.\n    for (uint r = 0; r < row_count; ++r) { table[r * col_count + 0] = r; }\n    for (uint c = 0; c < col_count; ++c) { table[c] = c; }\n\n    // Now fill the matrix. TODO column-first algo break if <= delta\n    for (uint r = 1; r < row_count; ++r)\n        for (uint c = 1; c < col_count; ++c) // TODO c<=r?\n            table[r * col_count + c] =\n                    min({table[(r - 1) * col_count + c - 1] + (prefix[r - 1] == str[c - 1] ? 0 : 1),  // substitution\n                         table[r * col_count + c - 1] + 1,  // deletion\n                         table[(r - 1) * col_count + c] + 1});  // insertion\n\n    // Check the last row if there is an entry <= delta.\n    bool result = false;\n    for (uint j = 0; j < col_count; ++j) {\n        if (table[(row_count - 1) * col_count + j] <= delta) {\n            result = true;\n            break;\n        }\n    }\n\n    delete[] table;\n    return result;\n}\n"
  },
  {
    "path": "src/util/levenshtein.h",
    "content": "// Copyright (c) 2021-2024 Manuel Schneider\n\n#pragma once\n#include <QString>\n#include <vector>\n\n// Fast allocation-avoiding Levenshtein distance\n// See https://doi.org/10.1137/S0097539794264810\nclass Levenshtein\n{\npublic:\n    /// Fast computation of Levenshtein distance from prefix to string up to a max of max_delta\n    /// @note Requires prefix.size < str.size. No bounds are checked!\n    /// @return The error count up to max_delta. If there are more errors, always returns max_delta+1.\n    uint computePrefixEditDistanceWithLimit(const QString &prefix, const QString &string, uint k);\n    static bool checkPrefixEditDistance_Legacy(const QString &prefix, const QString &str, uint delta);\n\nprivate:\n    inline uint8_t &cell(uint r, uint c) ;\n    inline const uint8_t &cell(uint r, uint c) const;\n    void expand_matrix_if_necessary(uint rows, uint cols);\n    void print_matrix(const QString &prefix, const QString &string) const;\n    void print_matrix_view(const QString &prefix, const QString &string, uint rows, uint cols) const;\n\n    std::vector<uint8_t> matrix;\n    uint matrix_rows = 0;\n    uint matrix_cols = 0;\n\n};\n"
  },
  {
    "path": "src/util/matcher.cpp",
    "content": "// SPDX-FileCopyrightText: 2024 Manuel Schneider\n\n#include \"levenshtein.h\"\n#include \"matchconfig.h\"\n#include \"matcher.h\"\n#include \"querypreprocessing.h\"\n#include <QRegularExpression>\n#include <QStringList>\nusing namespace albert;\nusing namespace std;\n\nclass Matcher::Private\n{\npublic:\n\n    MatchConfig config;\n    const QString string;\n    mutable Levenshtein levenshtein;\n    QStringList tokens;\n\n    Match match(const QString &s) const\n    {\n        // Empty query is a 0 score (epsilon) match\n        if (string.isEmpty())\n            return {0.};\n\n        // Do not match strings containing only separators\n        if (tokens.isEmpty())\n            return {-1.};\n\n        const auto other_tokens = preprocessQuery(s, config);\n\n        double matched_chars = 0;\n        double total_chars = 0;\n\n        auto it = tokens.begin();\n        auto oit = other_tokens.begin();\n\n        while (it != tokens.end() && oit != other_tokens.end())\n        {\n            // if the query word is longer it cant be a prefix\n            if ((it->size() <= oit->size()))\n            {\n                // check if the query word is a prefix of the matched word\n                if(config.fuzzy)\n                {\n                    uint allowed_errors = it->size() / 4; // hardcoded 25% tolerance\n                    auto edit_distance = levenshtein.computePrefixEditDistanceWithLimit(\n                                *it, *oit, allowed_errors);\n                    if (edit_distance <= allowed_errors)\n                        // Accumulate matched chars and move to the next matcher word\n                        matched_chars += it++->size() - edit_distance;\n                }\n                else  // non fuzzy\n                {\n                    if (oit->startsWith(*it))\n                        // Accumulate matched chars and move to the next matcher word\n                        matched_chars += it++->size();\n                }\n            }\n\n            total_chars += oit->size();\n            ++oit;  // move to the next matched word\n        }\n\n        // Count chars of the left other_tokens (if any)\n        while (oit != other_tokens.end())\n            total_chars += oit++->size();\n\n        // if all matcher words have been consumed this is a match\n        if (it == tokens.end())\n            return {matched_chars/total_chars};\n\n        return {-1.};\n    }\n};\n\nMatcher::Matcher(const QString &query, MatchConfig config):\n    d(new Private{\n      .config = config,\n      .string = query,\n      .levenshtein = {},\n      .tokens = preprocessQuery(query, config)\n    })\n{}\n\nMatcher::Matcher(Matcher &&o) = default;\n\nMatcher::~Matcher() = default;\n\nMatcher &Matcher::operator=(Matcher &&o) = default;\n\nMatch Matcher::match(const QString &s) const { return d->match(s); }\n"
  },
  {
    "path": "src/util/messagebox.cpp",
    "content": "// Copyright (c) 2025-2025 Manuel Schneider\n\n#include \"application.h\"\n#include \"frontend.h\"\n#include \"messagebox.h\"\n#include <QGuiApplication>\n#include <QMessageBox>\nusing namespace albert;\n\ninline static QWidget *mainWindow() { return QWidget::find(Application::instance().frontend()->winId()); }\n\nbool albert::question(const QString &text, QWidget *parent)\n{\n    return QMessageBox::question(parent ? parent : mainWindow(),\n                                 qApp->applicationDisplayName(),\n                                 text)\n           == QMessageBox::Yes;\n}\n\nvoid albert::information(const QString &text, QWidget *parent)\n{\n    QMessageBox::information(parent ? parent : mainWindow(),\n                             qApp->applicationDisplayName(),\n                             text);\n}\n\nvoid albert::warning(const QString &text, QWidget *parent)\n{\n    QMessageBox::warning(parent ? parent : mainWindow(),\n                         qApp->applicationDisplayName(),\n                         text);\n}\n\nvoid albert::critical(const QString &text, QWidget *parent)\n{\n    QMessageBox::critical(parent ? parent : mainWindow(),\n                          qApp->applicationDisplayName(),\n                          text);\n}\n"
  },
  {
    "path": "src/util/networkutil.cpp",
    "content": "// Copyright (c) 2025-2025 Manuel Schneider\n\n#include \"networkutil.h\"\n#include <QDateTime>\n#include <QEventLoop>\n#include <QNetworkAccessManager>\n#include <QNetworkReply>\n#include <QUrl>\n\nQNetworkAccessManager &albert::network()\n{\n    static thread_local QNetworkAccessManager network_manager;\n    return network_manager;\n}\n\nQNetworkReply *albert::await(QNetworkReply *reply)\n{\n    if (reply->isFinished())\n        return reply;\n    else\n    {\n        QEventLoop loop;\n        QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);\n        loop.exec();\n        return reply;\n    }\n}\n\nQString albert::percentEncoded(const QString &string)\n{ return QString::fromUtf8(QUrl::toPercentEncoding(string)); }\n\nQString albert::percentDecoded(const QString &string)\n{ return QUrl::fromPercentEncoding(string.toUtf8()); }\n"
  },
  {
    "path": "src/util/notification.cpp",
    "content": "// Copyright (c) 2024 Manuel Schneider\n\n#include \"notification.h\"\n#include <QNotification>\nusing namespace albert;\nusing namespace std;\n\nclass Notification::Private\n{\npublic:\n    QNotification notification;\n};\n\nNotification::Notification(const QString &title, const QString &text, QObject *parent)\n    : QObject(parent), d(new Private{{title, text}})\n{\n    connect(&d->notification, &QNotification::activated,\n            this, &Notification::activated);\n}\n\nNotification::~Notification() = default;\n\nconst QString &Notification::title() const\n{\n    return d->notification.title();\n}\n\nvoid Notification::setTitle(const QString &title)\n{\n    d->notification.setTitle(title);\n}\n\nconst QString &Notification::text() const\n{\n    return d->notification.text();\n}\n\nvoid Notification::setText(const QString &text)\n{\n    d->notification.setText(text);\n}\n\nvoid Notification::send()\n{\n    d->notification.send();\n}\n\nvoid Notification::dismiss()\n{\n    d->notification.dismiss();\n}\n"
  },
  {
    "path": "src/util/oauth.cpp",
    "content": "// Copyright (c) 2025-2025 Manuel Schneider\n\n#include \"systemutil.h\"\n#include \"logging.h\"\n#include \"networkutil.h\"\n#include \"oauth.h\"\n#include <QByteArray>\n#include <QCryptographicHash>\n#include <QJsonDocument>\n#include <QJsonObject>\n#include <QNetworkAccessManager>\n#include <QNetworkReply>\n#include <QNetworkRequest>\n#include <QRandomGenerator>\n#include <QTimer>\n#include <QUrl>\n#include <QUrlQuery>\nusing enum albert::OAuth2::State;\nusing namespace albert;\nusing namespace std;\n\nstatic QString generateRandomString(int length) {\n    static const QString chars = QStringLiteral(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n                                                \"abcdefghijklmnopqrstuvwxyz\"\n                                                \"0123456789\");\n    QString result;\n    result.reserve(length);\n    for (int i = 0; i < length; ++i) {\n        int idx = QRandomGenerator::global()->bounded(chars.size());\n        result.append(chars[idx]);\n    }\n    return result;\n}\n\nstatic QString generateCodeChallenge(const QString &code_verifier) {\n    QByteArray hash = QCryptographicHash::hash(code_verifier.toUtf8(), QCryptographicHash::Sha256);\n    QByteArray b64 = hash.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);\n    return QString::fromUtf8(b64);\n}\n\n// -------------------------------------------------------------------------------------------------\n\nclass OAuth2::Private\n{\npublic:\n    OAuth2 *q;\n\n    QString client_id;\n    QString client_secret;\n    QString scope;\n    QString token_url;\n\n    // Stage 1 Grant permissions\n\n    QString auth_url;\n    QString redirect_uri;\n    QString code_verifier;\n    QString state_string;\n    bool pkce = true;\n\n    // Stage 2 Authorize access\n\n    QString refresh_token;\n    QString access_token;\n    QDateTime token_expiration;\n    QTimer token_refresh_timer;\n\n    QString error;\n\n    void requestAuthorization(); // 4.1.1\n    void handleAutorizationResponse(const QUrl &callback_url); // 4.1.2\n    void requestAccessToken(const QString &code); // 4.1.3\n    void handleAccessTokenResponse(QNetworkReply *reply); // 4.1.4\n\n    void refreshAccessToken();\n    void parseTokenReply(QNetworkReply *reply);\n};\n\nvoid OAuth2::Private::requestAuthorization()\n{\n    // Authorization Request - https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1\n    //\n    // response_type\n    //         REQUIRED.  Value MUST be set to \"code\".\n    // client_id\n    //         REQUIRED.  The client identifier as described in Section 2.2.\n    // redirect_uri\n    //         OPTIONAL.  As described in Section 3.1.2.\n    // scope\n    //         OPTIONAL.  The scope of the access request as described by\n    //         Section 3.3.\n    // state\n    //         RECOMMENDED.  An opaque value used by the client to maintain\n    //         state between the request and callback.  The authorization\n    //         server includes this value when redirecting the user-agent back\n    //         to the client.  The parameter SHOULD be used for preventing\n    //         cross-site request forgery as described in Section 10.12.\n\n    state_string = generateRandomString(8);\n\n    QUrlQuery query;\n    query.addQueryItem(\"response_type\", \"code\");\n    query.addQueryItem(\"client_id\", client_id);\n    query.addQueryItem(\"scope\", scope);\n    query.addQueryItem(\"redirect_uri\", redirect_uri);\n    query.addQueryItem(\"state\", state_string);\n    if (pkce)\n    {\n        code_verifier = generateRandomString(64);\n        auto code_challenge = generateCodeChallenge(code_verifier);\n        query.addQueryItem(\"code_challenge_method\", \"S256\");\n        query.addQueryItem(\"code_challenge\", code_challenge);\n    }\n\n    QUrl url(auth_url);\n    url.setQuery(query);\n    open(url);\n\n    emit q->stateChanged(Awaiting);\n}\n\nvoid OAuth2::Private::handleAutorizationResponse(const QUrl &callback_url)\n{\n    // Authorization Response\n    //\n    // SUCCESS - https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2\n    //\n    // code\n    //          REQUIRED.  The authorization code generated by the\n    //          authorization server.  The authorization code MUST expire\n    //          shortly after it is issued to mitigate the risk of leaks.  A\n    //          maximum authorization code lifetime of 10 minutes is\n    //          RECOMMENDED.  The client MUST NOT use the authorization code\n    //          more than once.  If an authorization code is used more than\n    //          once, the authorization server MUST deny the request and SHOULD\n    //          revoke (when possible) all tokens previously issued based on\n    //          that authorization code.  The authorization code is bound to\n    //          the client identifier and redirection URI.\n    // state\n    //          REQUIRED if the \"state\" parameter was present in the client\n    //          authorization request.  The exact value received from the\n    //          client.\n    //\n    // ERROR - https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1\n    //\n    // error\n    //          REQUIRED.  A single ASCII [USASCII] error code from the\n    //          following:\n    //\n    //      invalid_request\n    //               The request is missing a required parameter, includes an\n    //               invalid parameter value, includes a parameter more than\n    //               once, or is otherwise malformed.\n    //      unauthorized_client\n    //               The client is not authorized to request an authorization\n    //               code using this method.\n    //      access_denied\n    //               The resource owner or authorization server denied the\n    //               request.\n    //      unsupported_response_type\n    //               The authorization server does not support obtaining an\n    //               authorization code using this method.\n    //      invalid_scope\n    //               The requested scope is invalid, unknown, or malformed.\n    //      server_error\n    //               The authorization server encountered an unexpected\n    //               condition that prevented it from fulfilling the request.\n    //               (This error code is needed because a 500 Internal Server\n    //               Error HTTP status code cannot be returned to the client\n    //               via an HTTP redirect.)\n    //      temporarily_unavailable\n    //               The authorization server is currently unable to handle\n    //               the request due to a temporary overloading or maintenance\n    //               of the server.  (This error code is needed because a 503\n    //               Service Unavailable HTTP status code cannot be returned\n    //               to the client via an HTTP redirect.)\n    //\n    // error_description\n    //          OPTIONAL.  Human-readable ASCII [USASCII] text providing\n    //          additional information, used to assist the client developer in\n    //          understanding the error that occurred.\n    //          Values for the \"error_description\" parameter MUST NOT include\n    //          characters outside the set %x20-21 / %x23-5B / %x5D-7E.\n    // error_uri\n    //          OPTIONAL.  A URI identifying a human-readable web page with\n    //          information about the error, used to provide the client\n    //          developer with additional information about the error.\n    //          Values for the \"error_uri\" parameter MUST conform to the\n    //          URI-reference syntax and thus MUST NOT include characters\n    //          outside the set %x21 / %x23-5B / %x5D-7E.\n    // state\n    //          REQUIRED if a \"state\" parameter was present in the client\n    //          authorization request.  The exact value received from the\n    //          client.\n\n    QUrlQuery url_query(callback_url.query());\n\n    if (state_string.isEmpty() || url_query.queryItemValue(\"state\") != state_string)\n    {\n        WARN << \"Received unexpected authorization response.\";\n        return;\n    }\n\n    state_string.clear();\n\n    if (url_query.hasQueryItem(\"code\"))\n        requestAccessToken(url_query.queryItemValue(\"code\"));\n\n    else if (url_query.hasQueryItem(\"error\"))\n    {\n        error = url_query.queryItemValue(\"error\");\n        if (url_query.hasQueryItem(\"error_description\"))\n            error += \": \" + url_query.queryItemValue(\"error_description\");\n        if (url_query.hasQueryItem(\"error_uri\"))\n            error += \" (\" + url_query.queryItemValue(\"error_uri\") + \")\";\n        emit q->stateChanged(NotAuthorized);\n    }\n\n    else\n    {\n        error = \"Neither 'code' nor 'error' set authorization response.\";\n        emit q->stateChanged(NotAuthorized);\n    }\n}\n\nvoid OAuth2::Private::requestAccessToken(const QString & code)\n{\n    // Access Token Request - https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3\n    //\n    // grant_type\n    //       REQUIRED.  Value MUST be set to \"authorization_code\".\n    // code\n    //       REQUIRED.  The authorization code received from the\n    //       authorization server.\n    // redirect_uri\n    //       REQUIRED, if the \"redirect_uri\" parameter was included in the\n    //       authorization request as described in Section 4.1.1, and their\n    //       values MUST be identical.\n    // client_id\n    //       REQUIRED, if the client is not authenticating with the\n    //       authorization server as described in Section 3.2.1.\n    QUrlQuery params;\n    params.addQueryItem(\"grant_type\", \"authorization_code\");\n    params.addQueryItem(\"code\", code);\n    params.addQueryItem(\"redirect_uri\", redirect_uri);\n    QNetworkRequest request(token_url);\n    request.setHeader(QNetworkRequest::ContentTypeHeader, \"application/x-www-form-urlencoded\");\n    request.setRawHeader(\"Accept\", \"application/json\");\n    if (pkce)\n    {\n        params.addQueryItem(\"client_id\", client_id);\n        params.addQueryItem(\"code_verifier\", code_verifier);\n    }\n    else\n    {\n        const auto base64 = QString(\"%1:%2\").arg(client_id, client_secret).toUtf8().toBase64();\n        request.setRawHeader(QByteArray(\"Authorization\"), QString(\"Basic %1\").arg(base64).toUtf8());\n    }\n\n    QNetworkReply *reply = network().post(request, params.toString(QUrl::FullyEncoded).toUtf8());\n\n    QObject::connect(reply, &QNetworkReply::finished, q, [this, reply] {\n        handleAccessTokenResponse(reply);\n        reply->deleteLater();\n    });\n}\n\nvoid OAuth2::Private::handleAccessTokenResponse(QNetworkReply *reply)\n{\n    // Access Token Response - https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4\n    //\n    // SUCCESS - https://datatracker.ietf.org/doc/html/rfc6749#section-5.1\n    //\n    // {\n    //     access_token: 'BQBLuPRYBQ...BP8stIv5xr-Iwaf4l8eg',\n    //     token_type: 'Bearer',\n    //     expires_in: 3600,\n    //     refresh_token: 'AQAQfyEFmJJuCvAFh...cG_m-2KTgNDaDMQqjrOa3',\n    //     scope: 'user-read-email user-read-private'\n    // }\n    //\n    // access_token\n    //       REQUIRED.  The access token issued by the authorization server.\n    // token_type\n    //       REQUIRED.  The type of the token issued as described in\n    //       Section 7.1.  Value is case insensitive.\n    // expires_in\n    //       RECOMMENDED.  The lifetime in seconds of the access token.  For\n    //       example, the value \"3600\" denotes that the access token will\n    //       expire in one hour from the time the response was generated.\n    //       If omitted, the authorization server SHOULD provide the\n    //       expiration time via other means or document the default value.\n    // refresh_token\n    //       OPTIONAL.  The refresh token, which can be used to obtain new\n    //       access tokens using the same authorization grant as described\n    //       in Section 6.\n    // scope\n    //       OPTIONAL, if identical to the scope requested by the client;\n    //       otherwise, REQUIRED.  The scope of the access token as\n    //       described by Section 3.3.\n    //\n    // ERROR - https://datatracker.ietf.org/doc/html/rfc6749#section-5.2\n    //\n    // {\n    //     \"error\":\"incorrect_client_credentials\",\n    //     \"error_description\":\"The client_id and/or client_secret passed are incorrect.\",\n    //     \"error_uri\":\"https:…\",\n    //     \"token_type\":null\n    // }\n    //\n    // error\n    //       REQUIRED.  A single ASCII [USASCII] error code from the\n    //       following:\n    //       invalid_request\n    //             The request is missing a required parameter, includes an\n    //             unsupported parameter value (other than grant type),\n    //             repeats a parameter, includes multiple credentials,\n    //             utilizes more than one mechanism for authenticating the\n    //             client, or is otherwise malformed.\n    //       invalid_client\n    //             Client authentication failed (e.g., unknown client, no\n    //             client authentication included, or unsupported\n    //             authentication method).  The authorization server MAY\n    //             return an HTTP 401 (Unauthorized) status code to indicate\n    //             which HTTP authentication schemes are supported.  If the\n    //             client attempted to authenticate via the \"Authorization\"\n    //             request header field, the authorization server MUST\n    //             respond with an HTTP 401 (Unauthorized) status code and\n    //             include the \"WWW-Authenticate\" response header field\n    //             matching the authentication scheme used by the client.\n    //       invalid_grant\n    //             The provided authorization grant (e.g., authorization\n    //             code, resource owner credentials) or refresh token is\n    //             invalid, expired, revoked, does not match the redirection\n    //             URI used in the authorization request, or was issued to\n    //             another client.\n    //       unauthorized_client\n    //             The authenticated client is not authorized to use this\n    //             authorization grant type.\n    //       unsupported_grant_type\n    //             The authorization grant type is not supported by the\n    //             authorization server.\n    //       invalid_scope\n    //             The requested scope is invalid, unknown, malformed, or\n    //             exceeds the scope granted by the resource owner.\n    //       Values for the \"error\" parameter MUST NOT include characters\n    //       outside the set %x20-21 / %x23-5B / %x5D-7E.\n    // error_description\n    //       OPTIONAL.  Human-readable ASCII [USASCII] text providing\n    //       additional information, used to assist the client developer in\n    //       understanding the error that occurred.\n    //       Values for the \"error_description\" parameter MUST NOT include\n    //       characters outside the set %x20-21 / %x23-5B / %x5D-7E.\n    // error_uri\n    //       OPTIONAL.  A URI identifying a human-readable web page with\n    //       information about the error, used to provide the client\n    //       developer with additional information about the error.\n    //       Values for the \"error_uri\" parameter MUST conform to the\n    //       URI-reference syntax and thus MUST NOT include characters\n    //       outside the set %x21 / %x23-5B / %x5D-7E.\n\n\n    error.clear();\n    QJsonParseError parseError;\n\n    if ((int)reply->error() == 302)  // Grant invalid/revoked\n    {\n        refresh_token.clear();\n\n        error = QString(\"%1 (%2) - %3 %4\")\n                    .arg((int)reply->error())\n                    .arg(reply->error())\n                    .arg(reply->errorString(), reply->readAll());\n    }\n\n    else if (reply->error() != QNetworkReply::NoError)\n    {\n        error = QString(\"%1 (%2) - %3 %4\")\n                    .arg((int)reply->error())\n                    .arg(reply->error())\n                    .arg(reply->errorString(), reply->readAll());\n    }\n\n    else if (auto doc = QJsonDocument::fromJson(reply->readAll(), &parseError);\n             parseError.error != QJsonParseError::NoError)\n        error = QString(\"Failed parsing response: \") + parseError.errorString();\n\n    else if (auto obj = doc.object();\n             obj.contains(\"error\"))\n    {\n        error = obj[\"error\"].toString();\n        if (auto desc = obj[\"error_description\"].toString(); !desc.isEmpty())\n            error += \": \" + desc;\n        if (auto url = obj[\"error_uri\"].toString(); !url.isEmpty())\n            error += \" (\" + url + \")\";\n    }\n\n    else if (obj.contains(\"access_token\") && obj.contains(\"token_type\"))\n    {\n        // {\n        //     access_token: 'BQBLuPRYBQ...BP8stIv5xr-Iwaf4l8eg',\n        //     token_type: 'Bearer',\n        //     expires_in: 3600,\n        //     refresh_token: 'AQAQfyEFmJJuCvAFh...cG_m-2KTgNDaDMQqjrOa3',\n        //     scope: 'user-read-email user-read-private'\n        // }\n        //\n        // access_token\n        //       REQUIRED.  The access token issued by the authorization server.\n        // token_type\n        //       REQUIRED.  The type of the token issued as described in\n        //       Section 7.1.  Value is case insensitive.\n        // expires_in\n        //       RECOMMENDED.  The lifetime in seconds of the access token.  For\n        //       example, the value \"3600\" denotes that the access token will\n        //       expire in one hour from the time the response was generated.\n        //       If omitted, the authorization server SHOULD provide the\n        //       expiration time via other means or document the default value.\n        // refresh_token\n        //       OPTIONAL.  The refresh token, which can be used to obtain new\n        //       access tokens using the same authorization grant as described\n        //       in Section 6.\n        // scope\n        //       OPTIONAL, if identical to the scope requested by the client;\n        //       otherwise, REQUIRED.  The scope of the access token as\n        //       described by Section 3.3.\n\n        if (const auto type = obj[\"token_type\"].toString();\n            QString::compare(type, QStringLiteral(\"bearer\"), Qt::CaseInsensitive))\n            error = QString(\"Unsupported token type: %1.\").arg(type);\n        else\n        {\n            q->setTokens(obj[\"access_token\"].toString(),\n                         obj[\"refresh_token\"].toString(),\n                         QDateTime::currentDateTime().addSecs(obj[\"expires_in\"].toInt()));\n\n            emit q->stateChanged(Granted);\n\n            return;  // success\n        }\n    }\n\n    else\n        error = \"Neither 'error' nor 'access_token' and 'token_type' in access token response.\";\n\n    error = QString(\"Access token request failed: %1\").arg(error);\n\n\n\n    access_token.clear();\n    refresh_token.clear();\n    token_expiration = {};\n    emit q->tokensChanged();\n    emit q->stateChanged(NotAuthorized);\n}\n\nvoid OAuth2::Private::refreshAccessToken()\n{\n    // Refreshing an Access Token - https://datatracker.ietf.org/doc/html/rfc6749#section-6\n    //\n    // grant_type\n    //       REQUIRED.  Value MUST be set to \"refresh_token\".\n    // refresh_token\n    //       REQUIRED.  The refresh token issued to the client.\n    // scope\n    //       OPTIONAL.  The scope of the access request as described by\n    //       Section 3.3.  The requested scope MUST NOT include any scope\n    //       not originally granted by the resource owner, and if omitted is\n    //       treated as equal to the scope originally granted by the\n    //       resource owner.\n\n    QUrlQuery params;\n    params.addQueryItem(\"grant_type\", \"refresh_token\");\n    params.addQueryItem(\"refresh_token\", refresh_token);\n    QNetworkRequest request(token_url);\n    request.setHeader(QNetworkRequest::ContentTypeHeader, \"application/x-www-form-urlencoded\");\n    if (pkce)\n        params.addQueryItem(\"client_id\", client_id);\n    else\n    {\n        const auto base64 = QString(\"%1:%2\").arg(client_id, client_secret).toUtf8().toBase64();\n        request.setRawHeader(QByteArray(\"Authorization\"), QString(\"Basic %1\").arg(base64).toUtf8());\n    }\n\n    QNetworkReply *reply = network().post(request, params.toString(QUrl::FullyEncoded).toUtf8());\n    QObject::connect(reply, &QNetworkReply::finished, q, [this, reply] {\n        // If valid and authorized, the authorization server issues an access\n        // token as described in Section 5.1.  If the request failed\n        // verification or is invalid, the authorization server returns an error\n        // response as described in Section 5.2.\n        handleAccessTokenResponse(reply);\n        reply->deleteLater();\n    });\n}\n\n// -------------------------------------------------------------------------------------------------\n\nOAuth2::OAuth2() : d(make_unique<Private>(this))\n{\n    connect(&d->token_refresh_timer, &QTimer::timeout, this, &OAuth2::updateTokens);\n    d->token_refresh_timer.setSingleShot(true);\n}\n\nOAuth2::~OAuth2() {}\n\nvoid OAuth2::requestAccess() { d->requestAuthorization(); }\n\nvoid OAuth2::handleCallback(const QUrl &callback_url)\n{ d->handleAutorizationResponse(callback_url); }\n\nvoid OAuth2::updateTokens() { d->refreshAccessToken(); }\n\nconst QString &OAuth2::clientId() const { return d->client_id; }\n\nvoid OAuth2::setClientId(const QString &v)\n{\n    if (v != d->client_id)\n    {\n        d->client_id = v;\n        emit clientIdChanged(v);\n    }\n}\n\nconst QString &OAuth2::clientSecret() const { return d->client_secret; }\n\nvoid OAuth2::setClientSecret(const QString &v)\n{\n    if (v != d->client_secret)\n    {\n        d->client_secret = v;\n        emit clientSecretChanged(v);\n    }\n}\n\nconst QString &OAuth2::scope() const { return d->scope; }\n\nvoid OAuth2::setScope(const QString &v)\n{\n    if (v != d->scope)\n    {\n        d->scope = v;\n        emit scopeChanged(v);\n    }\n}\n\nconst QString &OAuth2::authUrl() const { return d->auth_url; }\n\nvoid OAuth2::setAuthUrl(const QString &v)\n{\n    if (v != d->auth_url)\n    {\n        d->auth_url = v;\n        emit authUrlChanged(v);\n    }\n}\n\nconst QString &OAuth2::redirectUri() const { return d->redirect_uri; }\n\nvoid OAuth2::setRedirectUri(const QString &v)\n{\n    if (v != d->redirect_uri)\n    {\n        d->redirect_uri = v;\n        emit redirectUriChanged(v);\n    }\n}\n\nbool OAuth2::isPkceEnabled() const { return d->pkce; }\n\nvoid OAuth2::setPkceEnabled(bool v)\n{\n    if (v != d->pkce)\n    {\n        d->pkce = v;\n    }\n}\n\nconst QString &OAuth2::tokenUrl() const { return d->token_url; }\n\nvoid OAuth2::setTokenUrl(const QString &v)\n{\n    if (v != d->token_url)\n    {\n        d->token_url = v;\n        emit tokenUrlChanged(v);\n    }\n}\n\nconst QString &OAuth2::accessToken() const { return d->access_token; }\n\nconst QString &OAuth2::refreshToken() const { return d->refresh_token; }\n\nconst QDateTime &OAuth2::tokenExpiration() const { return d->token_expiration; }\n\nvoid OAuth2::setTokens(const QString &access_token,\n                       const QString &refresh_token,\n                       const QDateTime &expiration)\n{\n    const auto state_before = state();\n\n    d->access_token = access_token;\n    d->refresh_token = refresh_token;\n    d->token_refresh_timer.stop();\n    d->token_expiration = expiration;\n    if (!refresh_token.isEmpty())\n    {\n        if (expiration.isNull())\n        {\n            WARN << \"Got 'refresh_token' but no valid expiration. Refreshing immediately.\";\n            d->refreshAccessToken();\n        }\n        else\n        {\n            if (const auto expires_in = QDateTime::currentDateTime().secsTo(expiration);\n                expires_in > 0)\n                d->token_refresh_timer.start((expires_in - 30) * 1000);\n            else\n                d->refreshAccessToken();\n        }\n    }\n\n    emit tokensChanged();\n\n    const auto state_after = state();\n    if (state_before != state_after)\n        emit stateChanged(state_after);\n}\n\nconst QString &OAuth2::error() const { return d->error; }\n\nOAuth2::State OAuth2::state() const\n{\n    using enum State;\n    if (!d->access_token.isEmpty())\n        return Granted;\n    else if (d->state_string.isEmpty())\n        return NotAuthorized;\n    else\n        return Awaiting;\n}\n"
  },
  {
    "path": "src/util/oauthconfigwidget.cpp",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n\n#include \"oauth.h\"\n#include \"oauthconfigwidget.h\"\n#include \"widgetsutil.h\"\n#include <QCoreApplication>\n#include <QFormLayout>\n#include <QLabel>\n#include <QLineEdit>\n#include <QPushButton>\n#include <QString>\nusing namespace std;\nusing namespace albert;\n\nclass OAuthConfigWidget::Private\n{\npublic:\n    OAuthConfigWidget *q;\n    OAuth2 &oauth;\n    QFormLayout *formLayout;\n    QLabel *label_client_id;\n    QLabel *label_client_secret;\n    QLabel *label_auth;\n    QLabel *label_auth_state;\n    QLineEdit *lineEdit_client_id;\n    QLineEdit *lineEdit_client_secret;\n    QPushButton *pushButton_auth;\n\n    Private(OAuthConfigWidget *_q, OAuth2 &_oauth):\n        q(_q), oauth(_oauth)\n    {\n        formLayout = new QFormLayout;\n        label_client_id = new QLabel;\n        label_client_secret = new QLabel;\n        label_auth = new QLabel;\n        label_auth_state = new QLabel;\n        lineEdit_client_id = new QLineEdit;\n        lineEdit_client_secret = new QLineEdit;\n        pushButton_auth = new QPushButton;\n\n        label_client_id->setAlignment(Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter);\n        label_client_secret->setAlignment(Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter);\n\n        QSizePolicy sizePolicy(QSizePolicy::Policy::Fixed, QSizePolicy::Policy::Fixed);\n        sizePolicy.setHorizontalStretch(0);\n        sizePolicy.setVerticalStretch(0);\n        sizePolicy.setHeightForWidth(pushButton_auth->sizePolicy().hasHeightForWidth());\n        pushButton_auth->setSizePolicy(sizePolicy);\n\n        formLayout->addRow(label_client_id, lineEdit_client_id);\n        if (!oauth.isPkceEnabled())\n            formLayout->addRow(label_client_secret, lineEdit_client_secret);\n        formLayout->addRow(label_auth, label_auth_state);\n        formLayout->addRow(nullptr, pushButton_auth);\n\n        label_client_id->setText(QCoreApplication::translate(\"OAuthConfigWidget\", \"Client identifier\", nullptr));\n        label_client_secret->setText(QCoreApplication::translate(\"OAuthConfigWidget\", \"Client secret\", nullptr));\n        label_auth->setText(QCoreApplication::translate(\"OAuthConfigWidget\", \"Authorization\", nullptr));\n        label_auth_state->setWordWrap(true);\n        pushButton_auth->setText(QCoreApplication::translate(\"OAuthConfigWidget\", \"Request\", \"action\"));\n\n        updateGrantState();\n\n        connect(&oauth, &OAuth2::stateChanged,\n                q, [this]{ updateGrantState(); });\n\n        connect(pushButton_auth, &QPushButton::clicked,\n                &oauth, &OAuth2::requestAccess);\n\n        bindWidget(lineEdit_client_id,\n                   &oauth,\n                   &OAuth2::clientId,\n                   &OAuth2::setClientId,\n                   &OAuth2::clientIdChanged);\n\n        bindWidget(lineEdit_client_secret,\n                   &oauth,\n                   &OAuth2::clientSecret,\n                   &OAuth2::setClientSecret,\n                   &OAuth2::clientSecretChanged);\n\n        q->setLayout(formLayout);\n    }\n\n    void updateGrantState()\n    {\n        using enum OAuth2::State;\n        switch (oauth.state())\n        {\n        case Awaiting:\n            label_auth_state->setText(QCoreApplication::translate(\"OAuthConfigWidget\",\n                                                                  \"Awaiting authorization…\", nullptr));\n            pushButton_auth->show();\n            break;\n        case NotAuthorized:\n            if (const auto e = oauth.error(); e.isEmpty())\n                label_auth_state->setText(QCoreApplication::translate(\"OAuthConfigWidget\",\n                                                                      \"Not authorized.\", nullptr));\n            else\n                label_auth_state->setText(e);\n            pushButton_auth->show();\n            break;\n        case Granted:\n            label_auth_state->setText(QStringLiteral(\"<font color=\\\"green\\\">%1</font>\")\n                                          .arg(QCoreApplication::translate(\"OAuthConfigWidget\",\n                                                                           \"Granted\", nullptr)));\n            // pushButton_auth->hide();\n            q->window()->show();\n            q->window()->raise();\n            q->window()->activateWindow();\n        default:\n            break;\n        }\n    }\n};\n\nOAuthConfigWidget::OAuthConfigWidget(OAuth2 &_oauth):\n    QWidget(nullptr),\n    d(make_unique<Private>(this, _oauth)) {}\n\nOAuthConfigWidget::~OAuthConfigWidget() {}\n"
  },
  {
    "path": "src/util/qiconengineadapter.cpp",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#include \"qiconengineadapter.h\"\n#include \"icon.h\"\nclass QPainter;\nusing namespace std;\n\nQIconEngineAdapter::QIconEngineAdapter(unique_ptr<albert::Icon> icon) :\n    icon_(::move(icon))\n{}\n\nQIconEngineAdapter::~QIconEngineAdapter() {}\n\nQSize QIconEngineAdapter::actualSize(const QSize &device_dependent_size, QIcon::Mode, QIcon::State)\n{\n    return device_dependent_size;\n}\n\nQPixmap QIconEngineAdapter::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state)\n{\n    return scaledPixmap(size, mode, state, 1.0);\n}\n\nQPixmap QIconEngineAdapter::scaledPixmap(const QSize &device_independent_size, QIcon::Mode, QIcon::State, qreal scale)\n{\n    return icon_->pixmap(device_independent_size, scale);\n}\n\nvoid QIconEngineAdapter::paint(QPainter *painter, const QRect &rect, QIcon::Mode, QIcon::State)\n{\n    icon_->paint(painter, rect);\n}\n\nQString QIconEngineAdapter::iconName() { return icon_->toUrl(); }\n\nQIconEngine* QIconEngineAdapter::clone() const { return new QIconEngineAdapter(icon_->clone()); }\n\nbool QIconEngineAdapter::isNull() { return icon_->isNull(); }\n"
  },
  {
    "path": "src/util/qiconengineadapter.h",
    "content": "// SPDX-FileCopyrightText: 2022-2025 Manuel Schneider\n\n#pragma once\n#include <QIconEngine>\n#include <memory>\nnamespace albert { class Icon; }\n\nclass QIconEngineAdapter : public QIconEngine\n{\n    std::unique_ptr<albert::Icon> icon_;\npublic:\n    QIconEngineAdapter(std::unique_ptr<albert::Icon> icon);\n    ~QIconEngineAdapter() override;\n\n    QSize actualSize(const QSize &device_dependent_size, QIcon::Mode, QIcon::State) override;\n    QPixmap pixmap(const QSize &device_dependent_size, QIcon::Mode, QIcon::State) override;\n    QPixmap scaledPixmap(const QSize &device_independent_size, QIcon::Mode, QIcon::State, qreal scale) override;\n    QIconEngine* clone() const override;\n    QString iconName() override;\n    void paint(QPainter *painter, const QRect &rect, QIcon::Mode, QIcon::State) override;\n    bool isNull() override;\n};\n"
  },
  {
    "path": "src/util/querypreprocessing.cpp",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n\n#include \"matchconfig.h\"\n#include \"querypreprocessing.h\"\n#include <QRegularExpression>\n#include <QTextBoundaryFinder>\nusing namespace albert;\n\nQStringList preprocessQuery(QString s, const MatchConfig &config)\n{\n    if (config.ignore_diacritics)\n    {\n        static const QRegularExpression re(\n            \"([\"\n            R\"(\\x{0300}-\\x{036f})\"  // diacritical marks\n            \"\\u00AD\"  // Soft hyphen\n            \"])\");\n        s = s.normalized(QString::NormalizationForm_D).remove(re);\n    }\n    else\n    {\n        // Remove soft hyphens\n        s.remove(QChar(0x00AD));\n    }\n\n    if (config.ignore_case)\n        s = s.toLower();\n\n    QTextBoundaryFinder finder(QTextBoundaryFinder::Word, s);\n    QStringList tokens;\n    qsizetype begin = 0;\n    for (qsizetype end = finder.toNextBoundary();\n         end != -1;\n         end = finder.toNextBoundary())\n    {\n        if (begin != end)\n        {\n            // Only add non-space tokens\n            // Assumes that the tokes that either are all spaces or non-spaces\n            if(!s[begin].isSpace())\n                tokens << s.mid(begin, end - begin);\n            begin = end;\n        }\n    }\n\n    if (config.ignore_word_order)\n        tokens.sort();\n\n    return tokens;\n}\n\nQStringList preprocessLegacy(QString s)\n{\n    static const QRegularExpression legacy_regex =\n        QRegularExpression(QStringLiteral(R\"(([\\s\\\\/\\-\\[\\](){}#!?<>\"'=+*.:,;_]+))\"));\n    s.remove(QChar(0x00AD));  // Soft hyphen\n    return s.split(legacy_regex, Qt::SkipEmptyParts);\n}\n"
  },
  {
    "path": "src/util/querypreprocessing.h",
    "content": "// SPDX-FileCopyrightText: 2025 Manuel Schneider\n\n#include <QString>\nnamespace albert { class MatchConfig; }\n\nQStringList preprocessQuery(QString s, const albert::MatchConfig &config);\n\nQStringList preprocessQueryLegacy(QString s);\n\n"
  },
  {
    "path": "src/util/ratelimiter.cpp",
    "content": "// Copyright (c) 2025-2025 Manuel Schneider\n\n#include \"ratelimiter.h\"\n#include <QPointer>\n#include <QThread>\n#include <QTimer>\n#include <algorithm>\n#include <chrono>\n#include <mutex>\n#include <queue>\nusing namespace albert::detail;\nusing namespace albert;\nusing namespace std::chrono;\nusing namespace std;\n\nclass detail::Acquire::Private\n{\npublic:\n    Acquire *q;\n    atomic_bool is_granted_ = false;\n\n    void grant()\n    {\n        is_granted_ = true;\n        emit q->granted();\n    }\n\n};\n\nAcquire::Acquire() : d(make_unique<detail::Acquire::Private>(this, false)) {}\n\nAcquire::~Acquire() {}\n\nbool Acquire::isGranted() { return d->is_granted_; }\n\nbool Acquire::await(std::function<bool()> stop_requested)\n{\n    while (true)\n    {\n        if (stop_requested())\n            return false;\n        else if (d->is_granted_)\n            return true;\n        else\n            QThread::msleep(10);\n    }\n}\n\n// -------------------------------------------------------------------------------------------------\n\n\nclass detail::RateLimiterPrivate\n{\npublic:\n    RateLimiter *q;\n    milliseconds delay;\n    time_point<system_clock> last_grant;\n    queue<QPointer<Acquire>> wait_queue;\n    mutex mtx;\n\n    inline uint remaining()\n    {\n        auto ms = duration_cast<milliseconds>(last_grant + delay - system_clock::now()).count();\n        return static_cast<uint>(max<decltype(ms)>(0, ms));\n    }\n\n    inline void grantNext()\n    {\n        lock_guard lock(mtx);\n        while (!wait_queue.empty())\n        {\n            auto *acquire = wait_queue.front().get();\n            wait_queue.pop();\n\n            if (acquire)\n            {\n                last_grant = system_clock::now();\n                acquire->d->grant();\n                if (!wait_queue.empty())\n                    QTimer::singleShot(delay, q, [this] { grantNext(); });\n                return;\n            }\n        }\n    }\n\n    inline void queueAcquire(Acquire *acquire)\n    {\n        lock_guard lock(mtx);\n        wait_queue.push(acquire);\n        if (wait_queue.size() == 1) // trigger\n            QTimer::singleShot(remaining(), q, [this] { grantNext(); });\n    }\n};\n\nRateLimiter::RateLimiter(uint ms):\n    d(make_unique<RateLimiterPrivate>(this, milliseconds(ms)))\n{}\n\nRateLimiter::~RateLimiter() {}\n\nvoid RateLimiter::setDelay(uint delay)\n{\n    lock_guard lock(d->mtx);\n    d->delay = milliseconds(delay);\n}\n\nuint RateLimiter::delay() const\n{\n    lock_guard lock(d->mtx);\n    return d->delay.count();\n}\n\nunique_ptr<Acquire> RateLimiter::acquire()\n{\n    auto acquire = make_unique<Acquire>();\n    d->queueAcquire(acquire.get());\n    return acquire;\n}\n"
  },
  {
    "path": "src/util/standarditem.cpp",
    "content": "// Copyright (c) 2022-2025 Manuel Schneider\n\n#include \"icon.h\"\n#include \"standarditem.h\"\nusing namespace albert;\nusing namespace std;\n\nStandardItem::~StandardItem() {}\n\nvoid StandardItem::setId(QString id) { id_ = ::move(id); }\n\nvoid StandardItem::setText(QString text) { text_ = ::move(text); }\n\nvoid StandardItem::setSubtext(QString subtext) { subtext_ = ::move(subtext); }\n\nvoid StandardItem::setIconFactory(function<unique_ptr<Icon>()> icon_factory) { icon_factory_ = ::move(icon_factory); }\n\nstd::function<std::unique_ptr<Icon>()> StandardItem::iconFactory() { return icon_factory_; }\n\nvoid StandardItem::setInputActionText(QString t) { input_action_text_ = ::move(t); }\n\nvoid StandardItem::setActions(vector<Action> actions) { actions_ = ::move(actions); }\n\nQString StandardItem::id() const { return id_; }\n\nQString StandardItem::text() const { return text_; }\n\nQString StandardItem::subtext() const { return subtext_; }\n\nstd::unique_ptr<Icon> StandardItem::icon() const\n{\n    if (icon_factory_)\n        if (auto icon = icon_factory_(); icon)\n            return icon;\n    return {};\n}\n\nQString StandardItem::inputActionText() const { return input_action_text_.isNull() ? text_ : input_action_text_; }\n\nvector<Action> StandardItem::actions() const { return actions_; }\n"
  },
  {
    "path": "src/util/systemutil.cpp",
    "content": "// Copyright (c) 2025-2025 Manuel Schneider\n\n#include \"logging.h\"\n#include \"systemutil.h\"\n#include \"messagebox.h\"\n#include <QClipboard>\n#include <QDesktopServices>\n#include <QDir>\n#include <QGuiApplication>\n#include <QProcess>\n#include <QStandardPaths>\n#include <QUrl>\nusing namespace albert;\nusing namespace std;\n\nvoid albert::openUrl(const QString &url)\n{\n    if (QUrl qurl(url); qurl.isValid())\n        open(QUrl(url));\n    else\n        WARN << \"Invalid URL\" << url << qurl.errorString();\n}\n\nvoid albert::open(const QUrl &url)\n{\n    DEBG << QString(\"Open URL '%1'\").arg(url.toString());\n\n    if (qApp->platformName() == \"wayland\")\n        runDetachedProcess({\"xdg-open\", url.toString()});\n    else if (!QDesktopServices::openUrl(url))\n        WARN << \"Failed to open URL\" << url;\n}\n\nvoid albert::open(const QString &path) { open(QUrl::fromLocalFile(path)); }\n\nvoid albert::open(const filesystem::path &path) { open(QString::fromLocal8Bit(path.native())); }\n\n\nvoid albert::setClipboardText(const QString &text)\n{\n    QGuiApplication::clipboard()->setText(text, QClipboard::Clipboard);\n    QGuiApplication::clipboard()->setText(text, QClipboard::Selection);\n}\n\nstatic bool checkPasteSupport()\n{\n#if defined Q_OS_MACOS\n    return !QStandardPaths::findExecutable(\"osascript\").isEmpty();\n#elif defined(Q_OS_UNIX)\n    bool have_paste_support = !QStandardPaths::findExecutable(\"xdotool\").isEmpty();\n    if(!have_paste_support)\n        WARN << \"xdotool is not available. No paste support.\";\n    else if(qgetenv(\"XDG_SESSION_TYPE\") != \"x11\")\n        WARN << \"xdotool is available but but session type is not x11. \"\n                \"Paste will work for X11 windows only.\";\n    return have_paste_support;\n#endif\n}\n\nbool albert::havePasteSupport()\n{\n    static bool have_paste_support = checkPasteSupport();\n    return have_paste_support;\n}\n\nvoid albert::setClipboardTextAndPaste(const QString &text)\n{\n    setClipboardText(text);\n    if (!havePasteSupport())\n    {\n        auto t = \"Received a request to paste, although the feature is not supported. \"\n                 \"Looks like the plugin did not check for feature support before. \"\n                 \"Please report this issue.\";\n        WARN << t;\n        warning(t);\n        return;\n    }\n\n#if defined(Q_OS_MACOS)\n    runDetachedProcess({\n        \"osascript\", \"-e\",\n        R\"(tell application \"System Events\" to keystroke \"v\" using command down)\"\n    });\n#elif defined(Q_OS_UNIX)\n    QCoreApplication::processEvents(); // ??\n    auto *proc = new QProcess;\n    proc->start(\"sh\" , {\"-c\", \"sleep 0.1 && xdotool key ctrl+v\"});\n    QObject::connect(proc, &QProcess::finished, proc, [proc](int exitCode, QProcess::ExitStatus exitStatus){\n        if (exitStatus != QProcess::ExitStatus::NormalExit || exitCode != EXIT_SUCCESS)\n        {\n            WARN << QString(\"Paste failed (%1).\").arg(exitCode);\n            if (auto out = proc->readAllStandardOutput(); out.isEmpty())\n                WARN << out;\n            if (auto err = proc->readAllStandardError(); err.isEmpty())\n                WARN << err;\n        }\n        proc->deleteLater();\n    });\n#elif defined(Q_OS_WIN)\n    qFatal(\"Paste not implemented on windows.\");\n#endif\n}\n\nlong long albert::runDetachedProcess(const QStringList &commandline, const QString &working_dir)\n{\n    qint64 pid = 0;\n    if (!commandline.empty())\n    {\n        auto wd = working_dir.isEmpty() ? QDir::homePath() : working_dir;\n        if (QProcess::startDetached(commandline[0], commandline.mid(1), wd, &pid))\n            INFO << QString(\"Detached process started successfully. (WD: %1, PID: %2, CMD: %3\")\n                        .arg(wd).arg(pid).arg(QDebug::toString(commandline));\n        else\n            WARN << \"Starting detached process failed.\" << commandline;\n    } else\n        WARN << \"runDetachedProcess: commandline must not be empty!\";\n    return pid;\n}\n\nlong long albert::runDetachedProcess(const QStringList &commandline)\n{ return runDetachedProcess(commandline, {}); }\n\nQString albert::toQString(const filesystem::path &path)\n{\n#ifdef Q_OS_WIN\n    return QString::fromStdWString(path.native());\n#else\n    return QString::fromStdString(path.native());\n#endif\n}\n\n#ifdef Q_OS_MAC\n#include \"platform.h\"\nQString albert::runAppleScript(const QString &script)\n{\n    return platform::runAppleScript(script);\n}\n#endif\n"
  },
  {
    "path": "test/test.cpp",
    "content": "// Copyright (c) 2024-2025 Manuel Schneider\n\n#include \"app.h\"\n#include \"extensionplugin.h\"\n#include \"extensionregistry.h\"\n#include \"icon.h\"\n#include \"inputhistory.h\"\n#include \"itemindex.h\"\n#include \"levenshtein.h\"\n#include \"matcher.h\"\n#include \"plugininstance.h\"\n#include \"pluginloader.h\"\n#include \"pluginmetadata.h\"\n#include \"pluginprovider.h\"\n#include \"pluginregistry.h\"\n#include \"querypreprocessing.h\"\n#include \"standarditem.h\"\n#include \"test.h\"\n#include \"topologicalsort.hpp\"\n#include <QSettings>\n#include <QTemporaryFile>\n#include <QTimer>\n#include <set>\n#include <unistd.h>\nusing namespace albert;\nusing namespace std::chrono;\nusing namespace std;\n\nQTEST_GUILESS_MAIN(AlbertTests)\n\nvoid AlbertTests::topological_sort_linear()\n{\n    auto result = topologicalSort(map<int, set<int>>{{1, {2}}, {2, {3}}, {3, {}}});\n    auto expect = vector<int>{3, 2, 1};\n    QCOMPARE(result.sorted, expect);\n    QVERIFY(result.error_set.empty());\n}\n\nvoid AlbertTests::topological_sort_diamond()\n{\n    auto result = topologicalSort(map<int, set<int>>{{1, {}}, {2, {1}}, {3, {1}}, {4, {2, 3}}});\n    // auto expect = vector<int>{1,2,3,4};  // or …\n    auto expect = vector<int>{1, 3, 2, 4};\n    QCOMPARE(result.sorted, expect);\n    QVERIFY(result.error_set.empty());\n}\n\nvoid AlbertTests::topological_sort_cycle()\n{\n    auto result = topologicalSort(map<int, set<int>>{{1, {2}}, {2, {1}}});\n    auto expect = map<int, set<int>>{{1, {2}}, {2, {1}}};\n    QVERIFY(result.sorted.empty());\n    QCOMPARE(result.error_set, expect);\n}\n\nvoid AlbertTests::topological_sort_not_existing_node()\n{\n    auto result = topologicalSort(map<int, set<int>>{{1, {2}}});\n    auto expect = map<int, set<int>>{{1, {2}}};\n    QVERIFY(result.sorted.empty());\n    QCOMPARE(result.error_set, expect);\n}\n\nvoid AlbertTests::plugin_registry()\n{\n    using enum Plugin::State;\n\n    struct PluginInstanceMock : public ExtensionPlugin{};\n\n    struct PluginLoaderMock : public PluginLoader\n    {\n        QString path_;\n        albert::PluginMetadata metadata_;\n        unique_ptr<PluginInstanceMock> instance_;\n\n        PluginLoaderMock(const QString &id):\n            path_(\"/mock/plugin/\" + id),\n            metadata_{\n                .id=id,\n                .version=\"1.0.0\",\n                .name=id,\n                .description=id + \" description\"\n            }\n        {}\n        ~PluginLoaderMock() {}\n\n        QString path() const noexcept override { return path_; }\n        const albert::PluginMetadata &metadata() const noexcept override { return metadata_; }\n        albert::PluginInstance *instance() noexcept override { return instance_.get(); }\n    };\n\n    struct SyncPluginLoaderMock : public PluginLoaderMock\n    {\n        using PluginLoaderMock::PluginLoaderMock;\n\n        void load() noexcept override\n        {\n            PluginLoader::current_loader = this;\n            instance_ = make_unique<PluginInstanceMock>();\n            emit finished({});\n        }\n        void unload() noexcept override\n        {\n            instance_.reset();\n        }\n    };\n\n    struct AsyncPluginLoaderMock : public SyncPluginLoaderMock\n    {\n        using SyncPluginLoaderMock::SyncPluginLoaderMock;\n        void load() noexcept override\n        {\n            QTimer::singleShot(10, this,  [this](){ SyncPluginLoaderMock::load(); });\n        }\n    };\n\n    struct PluginProviderMock : public PluginProvider\n    {\n        vector<PluginLoader *> loaders_;\n        PluginProviderMock(vector<PluginLoader*> p) : loaders_(p){}\n\n        QString id() const noexcept override { return \"testpluginprovider\"; }\n        QString name() const noexcept override { return \"Mock Plugin Provider\"; }\n        QString description() const noexcept override { return \"Mock Plugin Provider Description\"; }\n\n        vector<PluginLoader *> plugins() override { return loaders_; }\n    };\n\n    // Diamond\n    auto *loader0 = new AsyncPluginLoaderMock{\"testplugin0\"};\n    auto *loader1 = new AsyncPluginLoaderMock{\"testplugin1\"};\n    auto *loader2 = new AsyncPluginLoaderMock{\"testplugin2\"};\n    auto *loader3 = new AsyncPluginLoaderMock{\"testplugin3\"};\n    loader0->metadata_.plugin_dependencies = {\"testplugin1\", \"testplugin2\"};\n    loader1->metadata_.plugin_dependencies = {\"testplugin3\"};\n    loader2->metadata_.plugin_dependencies = {\"testplugin3\"};\n\n    const auto loaders = vector<PluginLoader*>{loader0, loader1, loader2, loader3};\n\n    for (const auto &loader : loaders)\n        App::settings()->setValue(QString(\"%1/enabled\").arg(loader->metadata().id), false);\n\n    PluginProviderMock provider(loaders);\n    ExtensionRegistry ext_reg;\n    PluginRegistry plu_reg(ext_reg, true);\n    ext_reg.registerExtension(&provider);\n\n    QVERIFY(ext_reg.extensions().contains(\"testpluginprovider\"));\n    QCOMPARE(plu_reg.plugins().size(), 4);\n\n    QVERIFY(plu_reg.plugins().contains(\"testplugin0\"));\n    QVERIFY(plu_reg.plugins().contains(\"testplugin1\"));\n    QVERIFY(plu_reg.plugins().contains(\"testplugin2\"));\n    QVERIFY(plu_reg.plugins().contains(\"testplugin3\"));\n\n    const auto p0 = &plu_reg.plugins().at(\"testplugin0\");\n    const auto p1 = &plu_reg.plugins().at(\"testplugin1\");\n    const auto p2 = &plu_reg.plugins().at(\"testplugin2\");\n    const auto p3 = &plu_reg.plugins().at(\"testplugin3\");\n\n    QCOMPARE(plu_reg.dependencies(p0).size(), 2);\n    QCOMPARE(plu_reg.dependencies(p1).size(), 1);\n    QCOMPARE(plu_reg.dependencies(p2).size(), 1);\n    QCOMPARE(plu_reg.dependencies(p3).size(), 0);\n    QVERIFY(plu_reg.dependencies(p0).contains(p1));\n    QVERIFY(plu_reg.dependencies(p0).contains(p2));\n    QVERIFY(plu_reg.dependencies(p1).contains(p3));\n    QVERIFY(plu_reg.dependencies(p2).contains(p3));\n\n    QCOMPARE(plu_reg.dependees(p0).size(), 0);\n    QCOMPARE(plu_reg.dependees(p1).size(), 1);\n    QCOMPARE(plu_reg.dependees(p2).size(), 1);\n    QCOMPARE(plu_reg.dependees(p3).size(), 2);\n    QVERIFY(plu_reg.dependees(p1).contains(p0));\n    QVERIFY(plu_reg.dependees(p2).contains(p0));\n    QVERIFY(plu_reg.dependees(p3).contains(p1));\n    QVERIFY(plu_reg.dependees(p3).contains(p2));\n\n    QCOMPARE(plu_reg.dependencyClosure({p0}).size(), 4);\n    QCOMPARE(plu_reg.dependencyClosure({p1}).size(), 2);\n    QCOMPARE(plu_reg.dependencyClosure({p2}).size(), 2);\n    QCOMPARE(plu_reg.dependencyClosure({p3}).size(), 1);\n    QCOMPARE(plu_reg.dependencyClosure({p1,p2}).size(), 3);\n\n    QCOMPARE(plu_reg.dependeeClosure({p0}).size(), 1);\n    QCOMPARE(plu_reg.dependeeClosure({p1}).size(), 2);\n    QCOMPARE(plu_reg.dependeeClosure({p2}).size(), 2);\n    QCOMPARE(plu_reg.dependeeClosure({p3}).size(), 4);\n    QCOMPARE(plu_reg.dependeeClosure({p1,p2}).size(), 3);\n\n    QCOMPARE(p0->enabled, false);\n    QCOMPARE(p1->enabled, false);\n    QCOMPARE(p2->enabled, false);\n    QCOMPARE(p3->enabled, false);\n    QCOMPARE(p0->state, Unloaded);\n    QCOMPARE(p1->state, Unloaded);\n    QCOMPARE(p2->state, Unloaded);\n    QCOMPARE(p3->state, Unloaded);\n    QVERIFY(!ext_reg.extensions().contains(\"testplugin0\"));\n    QVERIFY(!ext_reg.extensions().contains(\"testplugin1\"));\n    QVERIFY(!ext_reg.extensions().contains(\"testplugin2\"));\n    QVERIFY(!ext_reg.extensions().contains(\"testplugin3\"));\n\n    plu_reg.setEnabled(\"testplugin1\", true);\n    QTest::qWait(100);\n\n    QCOMPARE(p0->enabled, false);\n    QCOMPARE(p1->enabled, true);\n    QCOMPARE(p2->enabled, false);\n    QCOMPARE(p3->enabled, true);\n    QCOMPARE(p0->state, Unloaded);\n    QCOMPARE(p1->state, Loaded);\n    QCOMPARE(p2->state, Unloaded);\n    QCOMPARE(p3->state, Loaded);\n    QVERIFY(!ext_reg.extensions().contains(\"testplugin0\"));\n    QVERIFY(ext_reg.extensions().contains(\"testplugin1\"));\n    QVERIFY(!ext_reg.extensions().contains(\"testplugin2\"));\n    QVERIFY(ext_reg.extensions().contains(\"testplugin3\"));\n\n    plu_reg.setEnabled(\"testplugin0\", true);\n    QTest::qWait(100);\n\n    QCOMPARE(p0->enabled, true);\n    QCOMPARE(p1->enabled, true);\n    QCOMPARE(p2->enabled, true);\n    QCOMPARE(p3->enabled, true);\n    QCOMPARE(p0->state, Loaded);\n    QCOMPARE(p1->state, Loaded);\n    QCOMPARE(p2->state, Loaded);\n    QCOMPARE(p3->state, Loaded);\n    QVERIFY(ext_reg.extensions().contains(\"testplugin0\"));\n    QVERIFY(ext_reg.extensions().contains(\"testplugin1\"));\n    QVERIFY(ext_reg.extensions().contains(\"testplugin2\"));\n    QVERIFY(ext_reg.extensions().contains(\"testplugin3\"));\n\n    plu_reg.setEnabled(\"testplugin1\", false);\n    QTest::qWait(100);\n\n    QCOMPARE(p0->enabled, false);\n    QCOMPARE(p1->enabled, false);\n    QCOMPARE(p2->enabled, true);\n    QCOMPARE(p3->enabled, true);\n    QCOMPARE(p0->state, Unloaded);\n    QCOMPARE(p1->state, Unloaded);\n    QCOMPARE(p2->state, Loaded);\n    QCOMPARE(p3->state, Loaded);\n    QVERIFY(!ext_reg.extensions().contains(\"testplugin0\"));\n    QVERIFY(!ext_reg.extensions().contains(\"testplugin1\"));\n    QVERIFY(ext_reg.extensions().contains(\"testplugin2\"));\n    QVERIFY(ext_reg.extensions().contains(\"testplugin3\"));\n\n    plu_reg.setEnabled(\"testplugin3\", false);\n    QTest::qWait(100);\n\n    QCOMPARE(p0->enabled, false);\n    QCOMPARE(p1->enabled, false);\n    QCOMPARE(p2->enabled, false);\n    QCOMPARE(p3->enabled, false);\n    QCOMPARE(p0->state, Unloaded);\n    QCOMPARE(p1->state, Unloaded);\n    QCOMPARE(p2->state, Unloaded);\n    QCOMPARE(p3->state, Unloaded);\n    QVERIFY(!ext_reg.extensions().contains(\"testplugin0\"));\n    QVERIFY(!ext_reg.extensions().contains(\"testplugin1\"));\n    QVERIFY(!ext_reg.extensions().contains(\"testplugin2\"));\n    QVERIFY(!ext_reg.extensions().contains(\"testplugin3\"));\n\n    ext_reg.deregisterExtension(&provider);\n\n    loader0->metadata_.plugin_dependencies = {\"testplugin1\"};  // invalid, 1 is invalid\n    loader1->metadata_.plugin_dependencies = {\"testplugin3\"};  // invalid, 3 is invalid\n    loader2->metadata_.plugin_dependencies = {\"testplugin3\"};  // invalid, circle\n    loader3->metadata_.plugin_dependencies = {\"testplugin2\"};  // invalid, circle\n\n    ext_reg.registerExtension(&provider);\n\n    QCOMPARE(plu_reg.plugins().size(), 0);\n\n    ext_reg.deregisterExtension(&provider);\n\n    loader0->metadata_.plugin_dependencies = {};\n    loader1->metadata_.plugin_dependencies = {\"testplugin3\"};  // invalid, 3 is invalid\n    loader2->metadata_.plugin_dependencies = {\"testplugin3\"};  // invalid, circle\n    loader3->metadata_.plugin_dependencies = {\"testplugin2\"};  // invalid, circle\n\n    ext_reg.registerExtension(&provider);\n\n    QCOMPARE(plu_reg.plugins().size(), 1);\n    QVERIFY(plu_reg.plugins().contains(\"testplugin0\"));\n}\n\nvoid AlbertTests::levenshtein_fast_levenshtein_threshold()\n{\n    Levenshtein l;\n\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"0123456789\", 0) == 0);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"0123456789\", 1) == 0);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"0123456789\", 2) == 0);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"0123456789\", 3) == 0);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"0123456789\", 4) == 0);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"0123456789\", 5) == 0);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"0123456789\", 6) == 0);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"0123456789\", 7) == 0);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"0123456789\", 8) == 0);\n\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"-123456789\", 0) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"-123456789\", 1) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"-123456789\", 2) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"-123456789\", 3) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"-123456789\", 4) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"-123456789\", 5) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"-123456789\", 6) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"-123456789\", 7) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"-123456789\", 8) == 1);\n\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--23456789\", 0) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--23456789\", 1) == 2);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--23456789\", 2) == 2);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--23456789\", 3) == 2);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--23456789\", 4) == 2);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--23456789\", 5) == 2);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--23456789\", 6) == 2);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--23456789\", 7) == 2);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--23456789\", 8) == 2);\n\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--234-6789\", 0) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--234-6789\", 1) == 2);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--234-6789\", 2) == 3);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--234-6789\", 3) == 3);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--234-6789\", 4) == 3);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--234-6789\", 5) == 3);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--234-6789\", 6) == 3);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--234-6789\", 7) == 3);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"0123456789\", \"--234-6789\", 8) == 3);\n}\n\nvoid AlbertTests::levenshtein_fuzzy_substitution()\n{\n    Levenshtein l;\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"test\", \"_est____\", 1) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"test\", \"__st____\", 1) == 2);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"test\", \"_est____\", 2) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"test\", \"__st____\", 2) == 2);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"test\", \"___t____\", 2) == 3);\n}\n\nvoid AlbertTests::levenshtein_fuzzy_deletion()\n{\n    Levenshtein l;\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"test\", \"ttest____\", 1) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"test\", \"tttest____\", 1) == 2);\n}\n\nvoid AlbertTests::levenshtein_fuzzy_insertion()\n{\n    Levenshtein l;\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"test\", \"est____\", 1) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"test\", \"st____\", 1) == 2);\n}\n\nvoid AlbertTests::levenshtein_shorter_prefix()\n{\n    Levenshtein l;\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"abc\", \"abc\", 1) == 0);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"abc\", \"ab\", 1) == 1);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"abc\", \"a\", 1) == 2);\n    QVERIFY(l.computePrefixEditDistanceWithLimit(\"abc\", \"\", 1) == 2);\n}\n\n// void AlbertTests::bench_tokenizer()\n// {\n//     MatchConfig c;\n//     static const auto test_split_string =\n//         QString(uR\"(String_containing, can't 1,11 2.22 한글날 金沢 我爱你。)\");\n\n//     QBENCHMARK { Q_UNUSED(preprocessQueryLegacy(test_split_string)); }\n//     QBENCHMARK { Q_UNUSED(preprocessQuery(test_split_string, c)); }\n//     QBENCHMARK { Q_UNUSED(preprocessQuery(test_split_string, MatchConfig{.ignore_diacritics=false})); }\n\n//     static const auto test_split_string1 =\n//         QString(uR\"(金沢金沢金沢金沢金沢金沢金沢金沢金沢金沢金沢金沢金沢金沢金沢金沢金沢金沢)\");\n\n//     QBENCHMARK { Q_UNUSED(preprocessQueryLegacy(test_split_string1)); }\n//     QBENCHMARK { Q_UNUSED(preprocessQuery(test_split_string1, c)); }\n\n//     static const auto test_split_string2 =\n//         QString(uR\"(aaa bbb ccc aaa bbb ccc aaa bbb ccc)\");\n\n//     QBENCHMARK { Q_UNUSED(preprocessQueryLegacy(test_split_string2)); }\n//     QBENCHMARK { Q_UNUSED(preprocessQuery(test_split_string2, c)); }\n// }\n\nvoid AlbertTests::match_config()\n{\n    MatchConfig c;  // F, !C, !O, !D\n\n    // Case sensitivity\n    QCOMPARE(preprocessQuery(\"A\", MatchConfig{.ignore_case=true}), QStringList({\"a\"}));\n    QCOMPARE(preprocessQuery(\"A\", MatchConfig{.ignore_case=false}), QStringList({\"A\"}));\n\n    // Word order\n    QCOMPARE(preprocessQuery(\"b a\", MatchConfig{.ignore_word_order=true}), QStringList({\"a\", \"b\"}));\n    QCOMPARE(preprocessQuery(\"b a\", MatchConfig{.ignore_word_order=false}), QStringList({\"b\", \"a\"}));\n\n    // Normalization\n    QCOMPARE(preprocessQuery(\"àáâãāa̅ăȧäåa̋ǎa̍a̎ȁa̐ȃa̒a̓a̔a̕a̖a̗a̘a̙a̚a̛a̜a̝a̞a̟a̠a̡a̢ạa̤ḁ\"\n                             \"a̦a̧ąa̩a̪a̫a̬a̭a̮a̯a̰a̱a̲a̳a̴a̵a̶a̷a̸a̹a̺a̻a̼a̽a̾a̿àáa͂a̓ä́aͅa͆a͇a͈a͉a͊\"\n                             \"a͋a͌a͍a͎a͏a͐a͑a͒a͓a͔a͕a͖a͗a͘a͙a͚a͛a͜a͝a͞a͟a͠a͡a͢aͣaͤaͥaͦaͧaͨaͩaͪaͫaͬaͭaͮaͯ\", c),\n             QStringList({\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n                          \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n                          \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}));\n\n    // ICU Break Tokenization\n\n    // Korean should not be split\n    QCOMPARE(preprocessQuery(\"한글날\", c).size(), 1);\n\n    // Japanese should be split by grapheme\n    QCOMPARE(preprocessQuery(\"金沢\", c).size(), 2);\n\n    // Chinese should be split by grapheme\n    QCOMPARE(preprocessQuery(\"我爱你。\", c).size(), 4);\n\n    QCOMPARE(preprocessQuery(\"a!b\", c).size(), 3);\n\n    QCOMPARE(preprocessQuery(\"22.04\", c).size(), 1);\n}\n\nvoid AlbertTests::match_conversion()\n{\n    auto m = Match(-1);\n    QCOMPARE((bool)m, false);\n    QCOMPARE(m.isMatch(), false);\n    QCOMPARE(m.isEmptyMatch(), false);\n    QCOMPARE(m.isExactMatch(), false);\n\n    m = Match(0);\n    QCOMPARE((bool)m, true);\n    QCOMPARE(m.isMatch(), true);\n    QCOMPARE(m.isEmptyMatch(), true);\n    QCOMPARE(m.isExactMatch(), false);\n\n    m = Match(1);\n    QCOMPARE((bool)m, true);\n    QCOMPARE(m.isMatch(), true);\n    QCOMPARE(m.isEmptyMatch(), false);\n    QCOMPARE(m.isExactMatch(), true);\n}\n\nvoid AlbertTests::matcher_empty()\n{\n    Matcher m(\"\");\n    QVERIFY(qFuzzyCompare(m.match(\"a\").score(), .0));\n    QVERIFY(qFuzzyCompare(m.match(\"a b\").score(), .0));\n    QCOMPARE(m.match(\"a\").isMatch(), true);\n    QCOMPARE(m.match(\"a b\").isMatch(), true);\n    QCOMPARE(m.match(\"a\").isEmptyMatch(), true);\n    QCOMPARE(m.match(\"a b\").isEmptyMatch(), true);\n    QCOMPARE(m.match(\"a\").isExactMatch(), false);\n    QCOMPARE(m.match(\"a b\").isExactMatch(), false);\n}\n\nvoid AlbertTests::matcher_single()\n{\n    Matcher m(\"a\");\n    QVERIFY(qFuzzyCompare(m.match(\"a\").score(), 1.0));\n    QVERIFY(qFuzzyCompare(m.match(\"a b\").score(), 1.0 / 2));\n    QCOMPARE(m.match(\"a\").isMatch(), true);\n    QCOMPARE(m.match(\"a b\").isMatch(), true);\n    QCOMPARE(m.match(\"a\").isEmptyMatch(), false);\n    QCOMPARE(m.match(\"a b\").isEmptyMatch(), false);\n    QCOMPARE(m.match(\"a\").isExactMatch(), true);\n    QCOMPARE(m.match(\"a b\").isExactMatch(), false);\n}\n\nvoid AlbertTests::matcher_multiple()\n{\n    Matcher m(\"a b\");\n    QCOMPARE(m.match(\"a\").isMatch(), false);\n    QCOMPARE(m.match(\"b\").isMatch(), false);\n    QCOMPARE(m.match(\"a b\").isMatch(), true);\n    QCOMPARE(m.match(\"b a\").isMatch(), true);\n    QCOMPARE(m.match(\"a b c\").isMatch(), true);\n    QCOMPARE(m.match(\"a c b\").isMatch(), true);\n    QCOMPARE(m.match(\"b a c\").isMatch(), true);\n    QCOMPARE(m.match(\"c a b\").isMatch(), true);\n    QCOMPARE(m.match(\"b c a\").isMatch(), true);\n    QCOMPARE(m.match(\"c b a\").isMatch(), true);\n    QVERIFY(qFuzzyCompare(m.match(\"a b c\").score(), 2.0 / 3));\n    QVERIFY(qFuzzyCompare(m.match(\"a c b\").score(), 2.0 / 3));\n    QVERIFY(qFuzzyCompare(m.match(\"b a c\").score(), 2.0 / 3));\n    QVERIFY(qFuzzyCompare(m.match(\"c a b\").score(), 2.0 / 3));\n    QVERIFY(qFuzzyCompare(m.match(\"b c a\").score(), 2.0 / 3));\n    QVERIFY(qFuzzyCompare(m.match(\"c b a\").score(), 2.0 / 3));\n    QVERIFY(qFuzzyCompare(m.match(\"a b\").score(), 1.0));\n    QVERIFY(qFuzzyCompare(m.match(\"b a\").score(), 1.0));\n}\n\nvoid AlbertTests::matcher_multiple_ordered()\n{\n    Matcher m(\"a b\", {.ignore_word_order = false});\n    QCOMPARE(m.match(\"a\").isMatch(), false);\n    QCOMPARE(m.match(\"b\").isMatch(), false);\n    QCOMPARE(m.match(\"a b\").isMatch(), true);\n    QCOMPARE(m.match(\"b a\").isMatch(), false);\n    QCOMPARE(m.match(\"a b c\").isMatch(), true);\n    QCOMPARE(m.match(\"a c b\").isMatch(), true);\n    QCOMPARE(m.match(\"b a c\").isMatch(), false);\n    QCOMPARE(m.match(\"c a b\").isMatch(), true);\n    QCOMPARE(m.match(\"b c a\").isMatch(), false);\n    QCOMPARE(m.match(\"c b a\").isMatch(), false);\n}\n\nvoid AlbertTests::matcher_diacritics()\n{\n    Matcher m(\"é\");\n    QCOMPARE(m.match(\"e\").isMatch(), true);\n    QCOMPARE(m.match(\"é\").isMatch(), true);\n    Matcher m2(\"e\");\n    QCOMPARE(m2.match(\"e\").isMatch(), true);\n    QCOMPARE(m2.match(\"é\").isMatch(), true);\n}\n\nvoid AlbertTests::matcher_fuzzy()\n{\n    QString abc{\"abcdefghijklmnopqrstuvwxyz\"};\n\n    MatchConfig c{.fuzzy = true};\n\n    QVERIFY(Matcher(\"abcd\", c).match(abc));\n    QVERIFY(Matcher(\"abc_\", c).match(abc));\n    QVERIFY(!Matcher(\"ab__\", c).match(abc));\n    QVERIFY(Matcher(\"abcdefgh\", c).match(abc));\n    QVERIFY(Matcher(\"abcdefg_\", c).match(abc));\n    QVERIFY(Matcher(\"abcde_g_\", c).match(abc));\n    QVERIFY(!Matcher(\"abc_e_g_\", c).match(abc));\n}\n\nvoid AlbertTests::matcher_case()\n{\n    auto m = Matcher(\"A\", {.ignore_case = true});\n    QVERIFY(m.match(\"A\"));\n    QVERIFY(m.match(\"a\"));\n\n    m = Matcher(\"A\", {.ignore_case = false});\n    QVERIFY(m.match(\"A\"));\n    QVERIFY(!m.match(\"a\"));\n\n    m = Matcher(\"a\", {.ignore_case = true});\n    QVERIFY(m.match(\"A\"));\n    QVERIFY(m.match(\"a\"));\n\n    m = Matcher(\"a\", {.ignore_case = false});\n    QVERIFY(!m.match(\"A\"));\n    QVERIFY(m.match(\"a\"));\n}\n\nvoid AlbertTests::matcher_score()\n{\n    auto m = Matcher(\"a\");\n\n    QCOMPARE(m.match(\"a\").score(), 1./1.);\n    QCOMPARE(m.match(\"a ab\").score(), 1./3.);\n    QCOMPARE(m.match(\"a ab abc\").score(), 1./6.);\n\n    // variadic\n    QCOMPARE(m.match(\"a\", \"a ab\", \"a ab abc\").score(), 1./1.);\n    QCOMPARE(m.match(\"a ab\", \"a ab abc\").score(), 1./3.);\n\n    // range\n    vector<QString> strings{\"a\", \"a ab\", \"a ab abc\"};\n    QCOMPARE(m.match(strings).score(), 1./1.);\n\n    m = Matcher(\"ab\");\n\n    QCOMPARE(m.match(\"a\").score(), -1);\n    QCOMPARE(m.match(\"a ab\").score(), 2./3.);\n    QCOMPARE(m.match(\"a ab abc\").score(), 2./6.);\n\n    m = Matcher(\"abcc\", {.fuzzy = true});\n\n    QCOMPARE(m.match(\"ab--\").score(), -1);\n    QCOMPARE(m.match(\"abcc\").score(), 1.);\n    QCOMPARE(m.match(\"abcd\").score(), 3./4.);\n}\n\nstatic auto indexMatch(const QStringList &item_strings,\n                       const QString &search_string,\n                       const MatchConfig &config = {})\n{\n    ItemIndex index(config);\n\n    vector<IndexItem> index_items;\n    for (auto &string : item_strings)\n        index_items.emplace_back(StandardItem::make(string, {}, {}, {}),\n                                 string);\n\n    index.setItems(::move(index_items));\n\n    return index.search(search_string, [] { return true; });\n};\n\nvoid AlbertTests::index_empty()\n{\n    auto m = indexMatch({\"a\",\"A\"}, \"\");\n    QVERIFY(m.size() == 2);\n    QVERIFY(m[0].score == 0.);\n    QVERIFY(m[1].score == 0.);\n}\n\nstatic const QStringList abc_perm\n{\n    \"a b c\",\n    \"a c b\",\n    \"b a c\",\n    \"c ä b\",\n    \"b c a\",\n    \"c b ã\"\n};\n\nvoid AlbertTests::index_multiple()\n{\n    QVERIFY(indexMatch(abc_perm, \"a\").size() == 6);\n    QVERIFY(indexMatch(abc_perm, \"a b\").size() == 6);\n    QVERIFY(indexMatch(abc_perm, \"a b c\").size() == 6);\n}\n\nvoid AlbertTests::index_multiple_ordered()\n{\n    QVERIFY(indexMatch(abc_perm, \"a\", {.ignore_word_order = false}).size() == 6);\n    QVERIFY(indexMatch(abc_perm, \"a b\", {.ignore_word_order = false}).size() == 3);\n    QVERIFY(indexMatch(abc_perm, \"a b c\", {.ignore_word_order = false}).size() == 1);\n}\n\nvoid AlbertTests::index_diacritics()\n{\n    QVERIFY(indexMatch(abc_perm, \"a\", {.ignore_diacritics = false}).size() == 4);\n    QVERIFY(indexMatch(abc_perm, \"a b\", {.ignore_diacritics = false}).size() == 4);\n    QVERIFY(indexMatch(abc_perm, \"a b c\", {.ignore_diacritics = false}).size() == 4);\n    QVERIFY(indexMatch(abc_perm, \"b\", {.ignore_diacritics = false}).size() == 6);\n}\n\nvoid AlbertTests::index_fuzzy()\n{\n    QStringList abc{\"abcdefghijklmnopqrstuvwxyz\"};\n\n    MatchConfig c{.fuzzy = true};\n\n    QVERIFY(indexMatch(abc, \"abcd\", c).size() == 1);\n    QVERIFY(indexMatch(abc, \"abc_\", c).size() == 1);\n    QVERIFY(indexMatch(abc, \"ab__\", c).size() == 0);\n    QVERIFY(indexMatch(abc, \"abcdefgh\", c).size() == 1);\n    QVERIFY(indexMatch(abc, \"abcdefg_\", c).size() == 1);\n    QVERIFY(indexMatch(abc, \"abcde_g_\", c).size() == 1);\n    QVERIFY(indexMatch(abc, \"abc_e_g_\", c).size() == 0);\n}\n\nvoid AlbertTests::index_case()\n{\n    auto m = indexMatch({\"a\",\"A\"}, \"a\", {});\n    QVERIFY(m.size() == 2);\n    QVERIFY(m[0].score == 1.);\n    QVERIFY(m[1].score == 1.);\n    m = indexMatch({\"a\",\"A\"}, \"a\", {.ignore_case = false});\n    QVERIFY(m.size() == 1);\n    QVERIFY(m[0].score == 1.);\n}\n\nvoid AlbertTests::index_score()\n{\n    auto m = indexMatch({\"a\",\"ab\",\"abc\"}, \"a\",  {.fuzzy = true});\n\n    QVERIFY(m.size() == 3);\n    sort(m.begin(), m.end(), [](auto &a, auto &b){ return a.item->id() < b.item->id(); });\n    QVERIFY(qFuzzyCompare(m[0].score, 1.0));\n    QVERIFY(qFuzzyCompare(m[1].score, 1.0/2.0));\n    QVERIFY(qFuzzyCompare(m[2].score, 1.0/3.0));\n\n    m = indexMatch({\"abcd\",\"abcb\"}, \"abcc\", {.fuzzy = true});\n\n    QVERIFY(m.size() == 2);\n    QVERIFY(qFuzzyCompare(m[0].score, 3./4.));\n    QVERIFY(qFuzzyCompare(m[1].score, 3./4.));\n}\n\nvoid AlbertTests::input_history()\n{\n    // Create a temporary file\n    QTemporaryFile t;\n    QVERIFY(t.open());\n    t.close();\n\n    detail::InputHistory h(t.fileName());\n\n    h.add(\"a\");\n    h.add(\"b\");\n    h.add(\"c\");\n\n    // Full iteration\n    QCOMPARE(h.prev(), \"\");\n    QCOMPARE(h.prev(), \"\");\n    QCOMPARE(h.next(), \"c\");\n    QCOMPARE(h.next(), \"b\");\n    QCOMPARE(h.next(), \"a\");\n    QCOMPARE(h.next(), \"\");\n    QCOMPARE(h.next(), \"\");\n    QCOMPARE(h.prev(), \"b\");\n    QCOMPARE(h.prev(), \"c\");\n    QCOMPARE(h.prev(), \"\");\n    QCOMPARE(h.prev(), \"\");\n\n    // Reset\n    h.resetIterator();\n    QCOMPARE(h.prev(), \"\");\n    QCOMPARE(h.prev(), \"\");\n    QCOMPARE(h.next(), \"c\");\n    h.resetIterator();\n    QCOMPARE(h.prev(), \"\");\n    QCOMPARE(h.prev(), \"\");\n    QCOMPARE(h.next(), \"c\");\n    h.resetIterator();\n\n    // Direction change\n    QCOMPARE(h.prev(), \"\");\n    QCOMPARE(h.next(), \"c\");\n    QCOMPARE(h.next(), \"b\");\n    QCOMPARE(h.prev(), \"c\");\n    QCOMPARE(h.prev(), \"\");\n\n    // Clear\n    h.clear();\n    QCOMPARE(h.prev(), \"\");\n    QCOMPARE(h.prev(), \"\");\n    QCOMPARE(h.next(), \"\");\n    QCOMPARE(h.next(), \"\");\n\n    h.add(\"abc\");\n    h.add(\"def\");\n    h.add(\"ghj\");\n\n    // search\n    QCOMPARE(h.prev(\"hj\"), \"\");\n    QCOMPARE(h.prev(\"hj\"), \"\");\n    QCOMPARE(h.next(\"hj\"), \"ghj\");\n    QCOMPARE(h.next(\"hj\"), \"\");\n    QCOMPARE(h.prev(\"hj\"), \"ghj\");\n    QCOMPARE(h.prev(\"hj\"), \"\");\n    QCOMPARE(h.prev(\"hj\"), \"\");\n\n\n}\n\n// // -------------------------------------------------------------------------------------------------\n\n// static string gen_random(const int len) {\n//     static const char alphanum[] =\n//             \"0123456789\"\n//             \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n//             \"abcdefghijklmnopqrstuvwxyz\";\n//     string tmp_s;\n//     tmp_s.reserve(len);\n\n//     for (int i = 0; i < len; ++i) {\n//         tmp_s += alphanum[rand() % (sizeof(alphanum) - 1)];\n//     }\n\n//     return tmp_s;\n// }\n\n// static void levenshtein_compare_benchmarks_and_check_results(const vector<QString> &strings, uint k)\n// {\n//     Levenshtein l;\n//     vector<bool> results_old;\n//     vector<bool> results_new;\n\n//     results_old.reserve(strings.size());\n//     auto start = system_clock::now();\n//     auto i = strings.cbegin();\n//     auto j = strings.crbegin();\n//     for (; i != strings.cend(); ++i, ++j)\n//         results_old.push_back(l.checkPrefixEditDistance_Legacy(*i, *j, k));\n//     long duration_old = duration_cast<microseconds>(system_clock::now()-start).count();\n\n//     results_new.reserve(strings.size());\n//     start = system_clock::now();\n//     i = strings.cbegin();\n//     j = strings.crbegin();\n//     for (; i != strings.cend(); ++i, ++j)\n//         results_new.push_back(l.computePrefixEditDistanceWithLimit(*i, *j, k) <= k);\n//     long duration_new = duration_cast<microseconds>(system_clock::now()-start).count();\n\n//     cout << \"Levensthein old: \"\n//          << setw(12)\n//          << duration_old\n//          << \" µs. New: \"\n//          << setw(12)\n//          << duration_new\n//          << \" µs. Improvement: \"\n//          << (double)duration_old/duration_new\n//          << endl;\n\n//     QVERIFY(results_old == results_new);\n// }\n\n// void AlbertTests::benchmark_comparison_vanilla_vs_fast_levenshtein()\n// {\n//     int test_count = 100000;\n\n//     srand((unsigned)time(NULL) * getpid());\n\n//     vector<QString> strings(test_count);\n//     auto lens = {4,8,16,24};\n//     auto divisor=4;\n//     cout << \"Randoms\"<<endl;\n//     for (int len : lens){\n//         int k = floor(len/divisor);\n//         cout << \"len: \"<< setw(2)<<len<<\". k: \"<<k<<\" \";\n//         for (auto &string : strings)\n//             string = QString::fromStdString(gen_random(len));\n//         levenshtein_compare_benchmarks_and_check_results(strings, k);\n//     }\n\n//     cout << \"Equals\"<<endl;\n//     for (int len : lens) {\n//         int k = floor(len/divisor);\n//         cout << \"len: \"<< setw(2)<<len<<\". k: \"<<k<<\" \";\n//         for (auto &string : strings)\n//             string = QString(len, 'a');\n//         levenshtein_compare_benchmarks_and_check_results(strings, k);\n//     }\n\n//     cout << \"Halfhalf equal random\"<<endl;\n//     for (int len : lens) {\n//         int k = floor(len/divisor);\n//         cout << \"len: \"<< setw(2)<<len<<\". k: \"<<k<<\" \";\n//         for (auto &string : strings)\n//             string = QString(\"%1%2\").arg(QString(len/2, 'a'), QString::fromStdString(gen_random(len/2)));\n//         levenshtein_compare_benchmarks_and_check_results(strings, k);\n//     }\n// }\n\n\n// // -------------------------------------------------------------------------------------------------\n\n// template<typename S>\n// static void benchmark_hash(const S &s)\n// {\n//     size_t h = 0;\n//     auto hf = std::hash<S>();\n//     QBENCHMARK {\n//         for (int i = 0; i < 1'000'000; ++i)\n//             h |= hf(s);\n//     }\n//     qDebug() << h;\n// }\n\n// QStringList strings = {\n//     \"0123456789\",\n//     \"abcdefghijklmnopqrstuvwxyz\",\n//     \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZÜÖÄ,.-;:_+*#@<>)\"\n// };\n\n// void AlbertTests::benchmark_hash_string()\n// {\n//     for (auto &s : strings)\n//         benchmark_hash(s.toStdString());\n// }\n\n// void AlbertTests::benchmark_hash_string_view()\n// {\n//     for (auto &s : strings)\n//         benchmark_hash(string_view(s.toStdString()));\n// }\n\n// void AlbertTests::benchmark_hash_u16string()\n// {\n//     for (auto &s : strings)\n//         benchmark_hash(s.toStdU16String());\n// }\n\n// void AlbertTests::benchmark_hash_u16string_view()\n// {\n//     for (auto &s : strings)\n//         benchmark_hash(u16string_view(s.toStdU16String()));\n// }\n\n// void AlbertTests::benchmark_hash_qstring()\n// {\n//     for (auto &s : strings)\n//         benchmark_hash(s);\n// }\n\n// void AlbertTests::benchmark_hash_qstring_view()\n// {\n//     for (auto &s : strings)\n//         benchmark_hash(QStringView(s));\n// }\n\n\n// // -------------------------------------------------------------------------------------------------\n\n// static std::hash<std::string> s_hash;\n// static std::hash<std::u8string> u8_hash;\n// static std::hash<std::u16string> u16_hash;\n// static std::hash<QString> q_hash;\n\n// namespace std {\n// template <>\n// struct hash<std::pair<QString, QString>>\n// {\n//     // https://stackoverflow.com/questions/17016175/c-unordered-map-using-a-custom-class-type-as-the-key#comment39936543_17017281\n//     inline size_t operator()(const std::pair<QString, QString>& k) const noexcept\n//     { return (qHash(k.first) ^ (qHash(k.second) << 1)); }\n// };\n\n// template <>\n// struct hash<std::pair<string, string>>\n// {\n//     // https://stackoverflow.com/questions/17016175/c-unordered-map-using-a-custom-class-type-as-the-key#comment39936543_17017281\n//     inline size_t operator()(const std::pair<string, string>& k) const noexcept\n//     { return (s_hash(k.first) ^ (s_hash(k.second)<< 1)); }\n// };\n\n// template <>\n// struct hash<std::pair<u8string, u8string>>\n// {\n//     // https://stackoverflow.com/questions/17016175/c-unordered-map-using-a-custom-class-type-as-the-key#comment39936543_17017281\n//     inline size_t operator()(const std::pair<u8string, u8string>& k) const noexcept\n//     { return (u8_hash(k.first) ^ (u8_hash(k.second)<< 1)); }\n// };\n\n// template <>\n// struct hash<std::pair<u16string, u16string>>\n// {\n//     // https://stackoverflow.com/questions/17016175/c-unordered-map-using-a-custom-class-type-as-the-key#comment39936543_17017281\n//     inline size_t operator()(const std::pair<u16string, u16string>& k) const noexcept\n//     { return (u16_hash(k.first) ^ (u16_hash(k.second)<< 1)); }\n// };\n// }\n\n// template<typename S>\n// static void benchmark_hash_pair(const S &s)\n// {\n//     auto p = make_pair(s, s);\n//     auto h = std::hash<std::pair<S, S>>();\n//     QBENCHMARK {\n//         for (int i = 0; i < 1'000'000; ++i)\n//             h(p);\n//     }\n// }\n\n\n// void AlbertTests::benchmark_hash_pair_qstring()\n// { benchmark_hash_pair(QString(\"abcdefghijklmnopqrstuvwxyz\")); }\n\n// void AlbertTests::benchmark_hash_pair_string()\n// { benchmark_hash_pair(u8\"abcdefghijklmnopqrstuvwxyz\"s); }\n\n// void AlbertTests::benchmark_hash_pair_u16string()\n// { benchmark_hash_pair(u\"abcdefghijklmnopqrstuvwxyz\"s); }\n\n\n// // -------------------------------------------------------------------------------------------------\n\n// #include <iostream>\n// #include <string>\n// #include <vector>\n// #include <random>\n// #include \"timeit.h\"\n\n\n// std::string generateRandomWord(size_t length) {\n//     const std::string charset = \"abcdefghijklmnopqrstuvwxyz\";\n//     std::string result;\n//     std::default_random_engine generator(std::random_device{}());\n//     std::uniform_int_distribution<size_t> distribution(0, charset.size() - 1);\n\n//     for (size_t i = 0; i < length; ++i) {\n//         result += charset[distribution(generator)];\n//     }\n//     return result;\n// }\n\n\n// double generateRandomDouble(double min, double max) {\n//     std::default_random_engine generator(std::random_device{}());\n//     std::uniform_real_distribution<double> distribution(min, max);\n//     return distribution(generator);\n// }\n\n// template<\n//     template<\n//         typename ...\n//         > typename C,\n//     typename KD\n//     >\n// struct Benchmark\n// {\n//     const char * n;\n//     C<pair<KD, KD>, double> c;\n\n//     Benchmark(const char *name, vector<tuple<KD, KD, double>> &data)\n//         : n(name)\n//     {\n//         for (const auto &[k1, k2, d] : data)\n//             c.emplace(pair<KD,KD>{k1, k2}, d);\n//     }\n\n//     template<typename K>\n//     auto & run(const char *name, vector<tuple<K, K>> &lookup_strings)\n//     {\n//         for (int i = 0; i < 5; ++i) {\n//             TimeIt t(QString(\"%1 %2\").arg(n, name));\n\n//             if constexpr (std::is_same<KD, K>::value)\n//                 for (const auto &[k1, k2] : lookup_strings)\n//                     c.contains({k1, k2});\n\n//             else if constexpr (is_same<KD, QString>::value && is_same<K, string>::value)\n//                 for (const auto &[k1, k2] : lookup_strings)\n//                     c.contains({QString::fromStdString(k1), QString::fromStdString(k2)});\n\n\n//             else if constexpr (is_same<KD, string>::value && is_same<K, QString>::value)\n//                 for (const auto &[k1, k2] : lookup_strings)\n//                     c.contains({k1.toStdString(), k2.toStdString()});\n\n//             else if constexpr (is_same<KD, QString>::value && is_same<K, string>::value)\n//                 for (const auto &[k1, k2] : lookup_strings)\n//                     c.contains({QString::fromStdString(k1), QString::fromStdString(k2)});\n\n\n//             else if constexpr (is_same<KD, u16string>::value && is_same<K, QString>::value)\n//             {\n//                 u16string u1, u2;\n//                 for (const auto &[k1, k2] : lookup_strings)\n//                     c.contains({u16string(reinterpret_cast<const char16_t*>(k1.utf16()), k1.size()),\n//                                 u16string(reinterpret_cast<const char16_t*>(k2.utf16()), k2.size())});\n//             }\n\n//             else if constexpr (is_same<KD, QString>::value && is_same<K, u16string>::value)\n//                 for (const auto &[k1, k2] : lookup_strings)\n//                     c.contains({QString::fromStdU16String(k1), QString::fromStdU16String(k2)});\n\n//             else\n//                 std::cout << \"run_ is not available for this type.\\n\";\n//         }\n//         return *this;\n//     }\n// };\n\n\n// void AlbertTests::benchmark_maps()\n// {\n//     vector<tuple<string, string, double>> cdata;\n//     for (size_t i = 0; i < 10'000; ++i)\n//         cdata.emplace_back(generateRandomWord(10), generateRandomWord(10), generateRandomDouble(0, 1));\n\n//     vector<tuple<QString, QString, double>> qdata;\n//     for (auto & [k1, k2, d] : cdata)\n//         qdata.emplace_back(QString::fromStdString(k1), QString::fromStdString(k2), d);\n\n//     vector<tuple<u16string, u16string, double>> u16data;\n//     for (auto & [k1, k2, d] : qdata)\n//         u16data.emplace_back(k1.toStdU16String(), k2.toStdU16String(), d);\n\n\n//     vector<tuple<string, string>> clookup_strings;\n//     for (size_t i = 0; i < 1'000'000; ++i)\n//         clookup_strings.emplace_back(generateRandomWord(10), generateRandomWord(10));\n\n//     vector<tuple<QString, QString>> qlookup_strings;\n//     for (auto & [k1, k2] : clookup_strings)\n//         qlookup_strings.emplace_back(QString::fromStdString(k1), QString::fromStdString(k2));\n\n\n\n//     vector<tuple<u16string, u16string>> u16lookup_strings;\n//     for (auto & [k1, k2] : qlookup_strings)\n//         u16lookup_strings.emplace_back(k1.toStdU16String(), k2.toStdU16String());\n\n\n\n//     Benchmark<QHash, QString>(\"QHash QString\", qdata)\n//         .run(\"QString\", qlookup_strings)\n//         .run(\"string\", clookup_strings);\n\n//     Benchmark<QHash, string>(\"QHash string\", cdata)\n//         .run(\"QString\", qlookup_strings)\n//         .run(\"string\", clookup_strings);\n\n//     Benchmark<unordered_map, QString>(\"unordered_map QString\", qdata)\n//         .run(\"QString\", qlookup_strings)\n//         .run(\"string\", clookup_strings);\n\n//     Benchmark<unordered_map, string>(\"unordered_map string\", cdata)\n//         .run(\"QString\", qlookup_strings)\n//         .run(\"string\", clookup_strings);\n\n//     Benchmark<QHash, u16string>(\"QHash u16string\", u16data)\n//         .run(\"QString\", qlookup_strings)\n//         .run(\"u16string\", u16lookup_strings);\n\n//     Benchmark<unordered_map, u16string>(\"unordered_map u16string\", u16data)\n//         .run(\"QString\", qlookup_strings)\n//         .run(\"u16string\", u16lookup_strings);\n// }\n\n"
  },
  {
    "path": "test/test.h",
    "content": "// Copyright (c) 2024 Manuel Schneider\n\n#include <QCoreApplication>\n#include <QTest>\n\nclass AlbertTests : public QObject\n{\n    Q_OBJECT\n\nprivate slots:\n\n    void topological_sort_linear();\n    void topological_sort_diamond();\n    void topological_sort_cycle();\n    void topological_sort_not_existing_node();\n\n    void plugin_registry();\n\n    void levenshtein_fast_levenshtein_threshold();\n    void levenshtein_fuzzy_substitution();\n    void levenshtein_fuzzy_deletion();\n    void levenshtein_fuzzy_insertion();\n    void levenshtein_shorter_prefix();\n\n    // void bench_tokenizer();\n    void match_config();\n    void match_conversion();\n\n    void matcher_empty();\n    void matcher_single();\n    void matcher_multiple();\n    void matcher_multiple_ordered();\n    void matcher_diacritics();\n    void matcher_fuzzy();\n    void matcher_case();\n    void matcher_score();\n\n\n    void index_empty();\n    void index_multiple();\n    void index_multiple_ordered();\n    void index_diacritics();\n    void index_fuzzy();\n    void index_case();\n    void index_score();\n\n    void input_history();\n\n    // void benchmark_comparison_vanilla_vs_fast_levenshtein();\n\n    // void benchmark_hash_qstring();\n    // void benchmark_hash_qstring_view();\n    // void benchmark_hash_string();\n    // void benchmark_hash_string_view();\n    // void benchmark_hash_u16string();\n    // void benchmark_hash_u16string_view();\n\n    // void benchmark_hash_pair_qstring();\n    // void benchmark_hash_pair_string();\n    // void benchmark_hash_pair_u16string();\n\n    // void benchmark_maps();\n\n};\n"
  }
]