[
  {
    "path": ".clang-format",
    "content": "---\nBasedOnStyle: LLVM\nIndentWidth: 4\nColumnLimit: 120\n---\nLanguage: Cpp\nDerivePointerAlignment: false\nPointerAlignment: Left\nAccessModifierOffset: -4\nAlignAfterOpenBracket: DontAlign\nAllowShortEnumsOnASingleLine: false\nAllowShortFunctionsOnASingleLine: Inline\nAllowShortLambdasOnASingleLine: None\nBinPackArguments: true\nBreakBeforeBraces: Attach\nBreakConstructorInitializers: BeforeComma\nCpp11BracedListStyle: false\nEmptyLineAfterAccessModifier: Never\nEmptyLineBeforeAccessModifier: Always\nIndentAccessModifiers: false\nIndentCaseLabels: false\nInsertNewlineAtEOF: true\nSeparateDefinitionBlocks: Always\nWrapNamespaceBodyWithEmptyLines: Always\n...\n"
  },
  {
    "path": ".envrc",
    "content": "if has nix; then\n    use flake\nfi\n\nshopt -s globstar\nwatch_file assets/cpp/**/*.cpp\nwatch_file assets/cpp/**/*.hpp\nwatch_file plugin/**/*.cpp\nwatch_file plugin/**/*.hpp\nwatch_file **/CMakeLists.txt\n\ncmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_COMPILER=clazy -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DDISTRIBUTOR=direnv\ncmake --build build\nexport CAELESTIA_LIB_DIR=\"$PWD/build/lib\"\nexport QML2_IMPORT_PATH=\"$PWD/build/qml:${QML2_IMPORT_PATH:-}\"\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contributing\n\nThere are only a few rules:\n- Follow the commit convention as follows:\n  - The name of the commit should be `module: change`\n  - Try to be consistent with the module names; you can look at existing commits for the module names I use\n  - If there is more than one change, the change in the commit name should be the most impactful change\n  - Put other changes in the description\n- Format your code\n  - I use the vscode qml extension with default arguments to format the code, however you do not have to use it\n  - Just try to follow the code style of the rest of the code and ensure that there is:\n    - no trailing whitespace on any lines\n    - a single space between operators\n- No AI slop allowed\n  - AI readme/docs slop = instant block\n- PLEASE TEST YOUR PRS\n  - I can't believe I have to put this here, but please test your PRs before submitting them\n  - Your PR must not break anything currently existing, or specify in the description if it does\n- PR descriptions should be descriptive\n  - Please explain what the PR does and how to use it in your PR description\n  - Also include any breaking changes and/or side effects of the PR\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: soramanew\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: soramane\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: soramane\nthanks_dev: # Replace with a single thanks.dev username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yml",
    "content": "name: Feature request\ndescription: Suggest a new feature\nlabels: [\"enhancement\"]\ntype: \"Feature\"\ntitle: \"[FEATURE] \"\nbody:\n    - type: markdown\n      attributes:\n          value: \"NOTE: Please write in **English**.\"\n\n    - type: textarea\n      attributes:\n          label: \"What would you like to be added?\"\n          description: \"Can be a suggestion for an existing feature. You can suggest a widget, minor user interaction changes.. whatever.\"\n\n    - type: textarea\n      attributes:\n          label: \"How will it help?\"\n          description: \"It's helpful to include examples (like in your use case).\"\n\n    - type: textarea\n      attributes:\n          label: \"Extra info\"\n          description: \"If you want a new widget, a pic of the inspiration (if available) would be awesome.\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/issue.yml",
    "content": "name: Issue\ndescription: Report an issue with the dots\nlabels: [\"bug\"]\ntype: \"Bug\"\ntitle: \"[BUG] \"\nbody:\n    - type: markdown\n      attributes:\n          value: \"**Welcome to submit a new issue!**\\n- It takes only 3 steps, so please be patient :)\\n- Tip: If your issue is not a feature request and is not an issue with the dots (e.g. \\\"how do I use X feature\\\"), please use [Discussions](https://github.com/caelestia-dots/shell/discussions) instead.\"\n    - type: checkboxes\n      attributes:\n          label: \"Step 1. Before you submit\"\n          description: \"Hint: The 2nd and 3rd checkbox is **not** forcely required as you may have failed to do so.\"\n          options:\n              - label: I have read the above instructions and am sure that this is supposed to be posted here.\n                required: true\n              - label: I've successfully updated to the latest versions following the [updating guide](https://github.com/caelestia-dots/caelestia?tab=readme-ov-file#updating).\n                required: false # Not required cuz user may have failed to do so\n              - label: I've successfully updated the system packages to the latest.\n                required: false # Not required cuz user may have failed to do so\n              - label: I've ticked the checkboxes without reading their contents\n                required: false # Obviously\n\n    - type: textarea\n      attributes:\n          label: \"Step 2. Version info\"\n          description: \"Run `caelestia -v` and paste the result below.\"\n          value: \"<details><summary>Version info</summary>\\n\\n```\\n<!-- Run `caelestia -v` and paste the result here! -->\\n```\\n\\n</details>\"\n      validations:\n          required: true\n\n    - type: markdown\n      attributes:\n          value: |\n              **Tips for the following Step 3**\n              1. Use `LANG=C LC_ALL=C` to get the output of a command in English, eg. `LANG=C LC_ALL=C date` displays time in English.\n              2. If it throws errors, **PLEASE**, attach logs and describe in detail if possible.\n                 - Something happened to the shell (bar, dashboard, etc)? Run `caelestia shell -l` WITHOUT exiting the shell for logs.\n                 - Installation failed? Run installation again for logs.\n                 - You may use more code blocks when needed.\n              3. In case you are confused, the `<details>`, `<summary>`, `</summary>`, `</details>` are HTML tags for folding the logs (typically very long) inside. Please do not touch them (unless you know what you are doing).\n              4. If the logs are suuuuuuper long, consider using an online pastebin service instead.\n\n    - type: textarea\n      attributes:\n          label: \"Step 3. Describe the issue\"\n          value: \"\\n<!-- Firstly describe your issue here! -->\\n\\n<details><summary>Logs</summary>\\n\\n```\\n<!-- Put your log content here!-->\\n```\\n\\n</details>\"\n      validations:\n          required: true\n\n    - type: checkboxes\n      attributes:\n          label: Reminder\n          options:\n              - label: I agree that it's usually impossible for others to help me without my logs.\n                required: true\n"
  },
  {
    "path": ".github/workflows/check-format.yml",
    "content": "name: Check formatting\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\njobs:\n  check-format:\n    runs-on: ubuntu-latest\n\n    container:\n      image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Check QML format\n        shell: fish {0}\n        run: |\n          for file in (string match -v 'build/*' **.qml)\n            /usr/lib/qt6/bin/qmlformat $file | diff -u $file - || exit 1\n          end\n          python3 scripts/qml-lint-conventions.py\n\n      - name: Check C++ format\n        shell: fish {0}\n        run: |\n          for file in (string match -v 'build/*' **.cpp **.hpp)\n            clang-format $file | diff -u $file - || exit 1\n          end\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Create release\n\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  build-and-release:\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: write\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Create packages\n        run: |\n          mkdir -p release\n          rsync -av \\\n            --exclude='release' \\\n            --exclude='.*' \\\n            --exclude='nix' \\\n            --exclude='flake.lock' \\\n            --exclude='flake.nix' \\\n            . release\n          tar -czf caelestia-shell-${{ github.ref_name }}.tar.gz release\n          cp caelestia-shell-${{ github.ref_name }}.tar.gz caelestia-shell-latest.tar.gz\n\n      - name: Create release\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            caelestia-shell-${{ github.ref_name }}.tar.gz\n            caelestia-shell-latest.tar.gz\n          generate_release_notes: true\n"
  },
  {
    "path": ".github/workflows/update-flake-inputs.yml",
    "content": "name: Update flake inputs\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 0 * * 0\"\n\njobs:\n  update-flake:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Generate app token\n        id: app-token\n        uses: actions/create-github-app-token@v3\n        with:\n          app-id: ${{ secrets.APP_ID }}\n          private-key: ${{ secrets.APP_PRIVATE_KEY }}\n\n      - uses: actions/checkout@v6\n        with:\n          token: ${{ steps.app-token.outputs.token }}\n          persist-credentials: false\n\n      - name: Install Nix\n        uses: nixbuild/nix-quick-install-action@v31\n        with:\n          nix_conf: |\n            keep-env-derivations = true\n            keep-outputs = true\n\n      - name: Restore and save Nix store\n        uses: nix-community/cache-nix-action@v6\n        with:\n          # restore and save a cache using this key\n          primary-key: nix-${{ hashFiles('**/*.nix', '**/flake.lock') }}\n          # if there's no cache hit, restore a cache by this prefix\n          restore-prefixes-first-match: nix-\n          # collect garbage until the Nix store size (in bytes) is at most this number\n          # before trying to save a new cache\n          # 1G = 1073741824\n          gc-max-store-size-linux: 1G\n          # do purge caches\n          purge: true\n          # purge all versions of the cache\n          purge-prefixes: nix-\n          # created more than this number of seconds ago\n          purge-created: 0\n          # or, last accessed more than this number of seconds ago\n          # relative to the start of the `Post Restore and save Nix store` phase\n          purge-last-accessed: 0\n          # except any version with the key that is the same as the `primary-key`\n          purge-primary-key: never\n\n      - name: Update flake inputs\n        run: nix flake update\n\n      - name: Attempt to build flake\n        run: nix build\n\n      - name: Test on Sway\n        env:\n          XDG_RUNTIME_DIR: /home/runner/runtime\n          WLR_BACKENDS: headless\n          WLR_LIBINPUT_NO_DEVICES: 1\n          WAYLAND_DISPLAY: wayland-1\n        run: |\n          mkdir $XDG_RUNTIME_DIR\n          chown $USER $XDG_RUNTIME_DIR\n          chmod 0700 $XDG_RUNTIME_DIR\n\n          nix profile install 'nixpkgs#sway'\n          sway &\n          sleep 3  # Give Sway some time to start\n          result/bin/caelestia-shell -d\n          sleep 3  # Give the shell some time to start (and die)\n          pgrep .quickshell-wra  # Fail job if shell died\n\n          result/bin/caelestia-shell kill\n          killall sway  # Screw using IPC\n\n      - name: Check for changes\n        id: check\n        run: echo modified=$(git diff --exit-code flake.lock &>/dev/null && echo 'false' || echo 'true') >> $GITHUB_OUTPUT\n\n      - name: Commit and push changes\n        if: steps.check.outputs.modified == 'true'\n        uses: EndBug/add-and-commit@v9\n        with:\n          github_token: ${{ steps.app-token.outputs.token }}\n          add: flake.lock\n          default_author: github_actions\n          message: \"[CI] chore: update flake\"\n"
  },
  {
    "path": ".github/workflows/update-image.yml",
    "content": "name: Update Docker CI image\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 0 * * 0\"\n\npermissions:\n  packages: write\n\njobs:\n  build-image:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: docker/login-action@v4\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Write Dockerfile\n        run: |\n          cat > /tmp/Dockerfile <<EOF\n          FROM archlinux:latest\n          RUN pacman -Syu --needed --noconfirm sudo base-devel cmake ninja fish git clang qt6-declarative python && \\\n              useradd -m builder && \\\n              echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \\\n              sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \\\n              cd /home/builder/yay-bin && \\\n              sudo -u builder makepkg -si --noconfirm && \\\n              sudo -u builder yay -S --noconfirm quickshell-git && \\\n              sudo -u builder yay -Yc --noconfirm && \\\n              pacman -Rns --noconfirm yay-bin && \\\n              sed -i '/builder ALL=(ALL) NOPASSWD:ALL/d' /etc/sudoers && \\\n              userdel -r builder && \\\n              pacman -Scc --noconfirm\n          EOF\n\n      - name: Build and push\n        uses: docker/build-push-action@v7\n        with:\n          context: .\n          file: /tmp/Dockerfile\n          push: true\n          tags: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest\n"
  },
  {
    "path": ".gitignore",
    "content": ".direnv\n/result\n/.qmlls.ini\nbuild/\n.cache/\nlogs"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"editor.defaultFormatter\": \"theqtcompany.qt-qml\",\n    \"[cpp]\": {\n        \"editor.defaultFormatter\": \"llvm-vs-code-extensions.vscode-clangd\"\n    }\n}\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.19)\n\nif(NOT DEFINED VERSION)\n    execute_process(COMMAND git describe --tags --abbrev=0\n        WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"\n        OUTPUT_VARIABLE VERSION\n        OUTPUT_STRIP_TRAILING_WHITESPACE\n    )\n\n    if(\"${VERSION}\" STREQUAL \"\")\n        message(FATAL_ERROR \"VERSION is not set and failed to get from git\")\n    endif()\nendif()\n\nif(NOT DEFINED GIT_REVISION)\n    execute_process(COMMAND git rev-parse HEAD\n        WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"\n        OUTPUT_VARIABLE GIT_REVISION\n        OUTPUT_STRIP_TRAILING_WHITESPACE\n    )\n\n    if(\"${GIT_REVISION}\" STREQUAL \"\")\n        message(FATAL_ERROR \"GIT_REVISION is not set and failed to get from git\")\n    endif()\nendif()\n\nstring(REGEX REPLACE \"^v\" \"\" VERSION \"${VERSION}\")\n\nproject(caelestia-shell VERSION ${VERSION} LANGUAGES CXX)\n\nset(CMAKE_CXX_STANDARD 20)\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\nset(CMAKE_CXX_EXTENSIONS OFF)\nset(CMAKE_RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/lib\")\n\nset(DISTRIBUTOR \"Unset\" CACHE STRING \"Distributor\")\nset(ENABLE_MODULES \"extras;plugin;shell\" CACHE STRING \"Modules to build/install\")\nset(INSTALL_LIBDIR \"usr/lib/caelestia\" CACHE STRING \"Library install dir\")\nset(INSTALL_QMLDIR \"usr/lib/qt6/qml\" CACHE STRING \"QML install dir\")\nset(INSTALL_QSCONFDIR \"etc/xdg/quickshell/caelestia\" CACHE STRING \"Quickshell config install dir\")\n\nadd_compile_options(\n    -Wall -Wextra -Wpedantic -Wshadow -Wconversion\n    -Wold-style-cast -Wnull-dereference -Wdouble-promotion\n    -Wformat=2 -Wfloat-equal -Woverloaded-virtual\n    -Wsign-conversion -Wredundant-decls -Wswitch\n    -Wunreachable-code\n)\n\nif(CMAKE_CXX_COMPILER_ID MATCHES \"Clang\")\n    add_compile_options(-Wunused-lambda-capture)\nendif()\n\nif(\"extras\" IN_LIST ENABLE_MODULES)\n    add_subdirectory(extras)\nendif()\n\nif(\"plugin\" IN_LIST ENABLE_MODULES)\n    add_subdirectory(plugin)\nendif()\n\nif(\"shell\" IN_LIST ENABLE_MODULES)\n    foreach(dir assets components config modules services utils)\n        install(DIRECTORY ${dir} DESTINATION \"${INSTALL_QSCONFDIR}\")\n    endforeach()\n    install(FILES shell.qml LICENSE DESTINATION \"${INSTALL_QSCONFDIR}\")\nendif()\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=center>caelestia-shell</h1>\n\n<div align=center>\n\n![GitHub last commit](https://img.shields.io/github/last-commit/caelestia-dots/shell?style=for-the-badge&labelColor=101418&color=9ccbfb)\n![GitHub Repo stars](https://img.shields.io/github/stars/caelestia-dots/shell?style=for-the-badge&labelColor=101418&color=b9c8da)\n![GitHub repo size](https://img.shields.io/github/repo-size/caelestia-dots/shell?style=for-the-badge&labelColor=101418&color=d3bfe6)\n[![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Fsoramane)](https://ko-fi.com/soramane)\n[![Discord invite](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscordapp.com%2Fapi%2Finvites%2FBGDCFCmMBk%3Fwith_counts%3Dtrue&query=approximate_member_count&style=for-the-badge&logo=discord&logoColor=ffffff&label=discord&labelColor=101418&color=96f1f1&link=https%3A%2F%2Fdiscord.gg%2FBGDCFCmMBk)](https://discord.gg/BGDCFCmMBk)\n\n</div>\n\nhttps://github.com/user-attachments/assets/0840f496-575c-4ca6-83a8-87bb01a85c5f\n\n## Components\n\n-   Widgets: [`Quickshell`](https://quickshell.outfoxxed.me)\n-   Window manager: [`Hyprland`](https://hyprland.org)\n-   Dots: [`caelestia`](https://github.com/caelestia-dots)\n\n## Installation\n\n> [!NOTE]\n> This repo is for the desktop shell of the caelestia dots. If you want installation instructions\n> for the entire dots, head to [the main repo](https://github.com/caelestia-dots/caelestia) instead.\n\n### Arch linux\n\n> [!NOTE]\n> If you want to make your own changes/tweaks to the shell do NOT edit the files installed by the AUR\n> package. Instead, follow the instructions in the [manual installation section](#manual-installation).\n\nThe shell is available from the AUR as `caelestia-shell`. You can install it with an AUR helper\nlike [`yay`](https://github.com/Jguer/yay) or manually downloading the PKGBUILD and running `makepkg -si`.\n\nA package following the latest commit also exists as `caelestia-shell-git`. This is bleeding edge\nand likely to be unstable/have bugs. Regular users are recommended to use the stable package\n(`caelestia-shell`).\n\n### Nix\n\nYou can run the shell directly via `nix run`:\n\n```sh\nnix run github:caelestia-dots/shell\n```\n\nOr add it to your system configuration:\n\n```nix\n{\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n\n    caelestia-shell = {\n      url = \"github:caelestia-dots/shell\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n  };\n}\n```\n\nThe package is available as `caelestia-shell.packages.<system>.default`, which can be added to your\n`environment.systemPackages`, `users.users.<username>.packages`, `home.packages` if using home-manager,\nor a devshell. The shell can then be run via `caelestia-shell`.\n\n> [!TIP]\n> The default package does not have the CLI enabled by default, which is required for full funcionality.\n> To enable the CLI, use the `with-cli` package.\n\nFor home-manager, you can also use the Caelestia's home manager module (explained in [configuring](https://github.com/caelestia-dots/shell?tab=readme-ov-file#home-manager-module)) that installs and configures the shell and the CLI.\n\n### Manual installation\n\nDependencies:\n\n-   [`caelestia-cli`](https://github.com/caelestia-dots/cli)\n-   [`quickshell-git`](https://quickshell.outfoxxed.me) - this has to be the git version, not the latest tagged version\n-   [`ddcutil`](https://github.com/rockowitz/ddcutil)\n-   [`brightnessctl`](https://github.com/Hummer12007/brightnessctl)\n-   [`app2unit`](https://github.com/Vladimir-csp/app2unit)\n-   [`libcava`](https://github.com/LukashonakV/cava)\n-   [`networkmanager`](https://networkmanager.dev)\n-   [`lm-sensors`](https://github.com/lm-sensors/lm-sensors)\n-   [`fish`](https://github.com/fish-shell/fish-shell)\n-   [`aubio`](https://github.com/aubio/aubio)\n-   [`libpipewire`](https://pipewire.org)\n-   `glibc`\n-   `qt6-declarative`\n-   `gcc-libs`\n-   [`material-symbols`](https://fonts.google.com/icons)\n-   [`caskaydia-cove-nerd`](https://www.nerdfonts.com/font-downloads)\n-   [`swappy`](https://github.com/jtheoof/swappy)\n-   [`libqalculate`](https://github.com/Qalculate/libqalculate)\n-   [`bash`](https://www.gnu.org/software/bash)\n-   `qt6-base`\n-   `qt6-declarative`\n\nBuild dependencies:\n\n-   [`cmake`](https://cmake.org)\n-   [`ninja`](https://github.com/ninja-build/ninja)\n\nTo install the shell manually, install all dependencies and clone this repo to `$XDG_CONFIG_HOME/quickshell/caelestia`.\nThen simply build and install using `cmake`.\n\n```sh\ncd $XDG_CONFIG_HOME/quickshell\ngit clone https://github.com/caelestia-dots/shell.git caelestia\n\ncd caelestia\ncmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/\ncmake --build build\nsudo cmake --install build\n```\n\n> [!TIP]\n> You can customise the installation location via the `cmake` flags `INSTALL_LIBDIR`, `INSTALL_QMLDIR` and\n> `INSTALL_QSCONFDIR` for the libraries (the beat detector), QML plugin and Quickshell config directories\n> respectively. If changing the library directory, remember to set the `CAELESTIA_LIB_DIR` environment\n> variable to the custom directory when launching the shell.\n>\n> e.g. installing to `~/.config/quickshell/caelestia` for easy local changes:\n>\n> ```sh\n> mkdir -p ~/.config/quickshell/caelestia\n> cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/ -DINSTALL_QSCONFDIR=~/.config/quickshell/caelestia\n> cmake --build build\n> sudo cmake --install build\n> sudo chown -R $USER ~/.config/quickshell/caelestia\n> ```\n\n## Usage\n\nThe shell can be started via the `caelestia shell -d` command or `qs -c caelestia`.\nIf the entire caelestia dots are installed, the shell will be autostarted on login\nvia an `exec-once` in the hyprland config.\n\n### Shortcuts/IPC\n\nAll keybinds are accessible via Hyprland [global shortcuts](https://wiki.hyprland.org/Configuring/Binds/#dbus-global-shortcuts).\nIf using the entire caelestia dots, the keybinds are already configured for you.\nOtherwise, [this file](https://github.com/caelestia-dots/caelestia/blob/main/hypr/hyprland/keybinds.conf#L1-L39)\ncontains an example on how to use global shortcuts.\n\nAll IPC commands can be accessed via `caelestia shell ...`. For example\n\n```sh\ncaelestia shell mpris getActive trackTitle\n```\n\nThe list of IPC commands can be shown via `caelestia shell -s`:\n\n```\n$ caelestia shell -s\ntarget drawers\n  function toggle(drawer: string): void\n  function list(): string\ntarget notifs\n  function clear(): void\ntarget lock\n  function lock(): void\n  function unlock(): void\n  function isLocked(): bool\ntarget mpris\n  function playPause(): void\n  function getActive(prop: string): string\n  function next(): void\n  function stop(): void\n  function play(): void\n  function list(): string\n  function pause(): void\n  function previous(): void\ntarget picker\n  function openFreeze(): void\n  function open(): void\ntarget wallpaper\n  function set(path: string): void\n  function get(): string\n  function list(): string\n```\n\n### PFP/Wallpapers\n\nThe profile picture for the dashboard is read from the file `~/.face`, so to set\nit you can copy your image to there or set it via the dashboard.\n\nThe wallpapers for the wallpaper switcher are read from `~/Pictures/Wallpapers`\nby default. To change it, change the wallpapers path in `~/.config/caelestia/shell.json`.\n\nTo set the wallpaper, you can use the command `caelestia wallpaper`. Use `caelestia wallpaper -h` for more info about\nthe command.\n\n## Updating\n\nIf installed via the AUR package, simply update your system (e.g. using `yay`).\n\nIf installed manually, you can update by running `git pull` in `$XDG_CONFIG_HOME/quickshell/caelestia`.\n\n```sh\ncd $XDG_CONFIG_HOME/quickshell/caelestia\ngit pull\n```\n\n## Configuring\n\nAll configuration options should be put in `~/.config/caelestia/shell.json`. This file is _not_ created by\ndefault, you must create it manually.\n\n### Example configuration\n\n> [!NOTE]\n> The example configuration only includes recommended configuration options. For more advanced customisation\n> such as modifying the size of individual items or changing constants in the code, there are some other\n> options which can be found in the source files in the `config` directory.\n\n<details><summary>Example</summary>\n\n```json\n{\n    \"appearance\": {\n        \"mediaGifSpeedAdjustment\": 300,\n        \"sessionGifSpeed\": 0.7,\n        \"anim\": {\n            \"durations\": {\n                \"scale\": 1\n            }\n        },\n        \"font\": {\n            \"family\": {\n                \"clock\": \"Rubik\",\n                \"material\": \"Material Symbols Rounded\",\n                \"mono\": \"CaskaydiaCove NF\",\n                \"sans\": \"Rubik\"\n            },\n            \"size\": {\n                \"scale\": 1\n            }\n        },\n        \"padding\": {\n            \"scale\": 1\n        },\n        \"rounding\": {\n            \"scale\": 1\n        },\n        \"spacing\": {\n            \"scale\": 1\n        },\n        \"transparency\": {\n            \"enabled\": false,\n            \"base\": 0.85,\n            \"layers\": 0.4\n        }\n    },\n    \"general\": {\n        \"logo\": \"caelestia\",\n        \"apps\": {\n            \"terminal\": [\"foot\"],\n            \"audio\": [\"pavucontrol\"],\n            \"playback\": [\"mpv\"],\n            \"explorer\": [\"thunar\"]\n        },\n        \"battery\": {\n            \"warnLevels\": [\n                {\n                    \"level\": 20,\n                    \"title\": \"Low battery\",\n                    \"message\": \"You might want to plug in a charger\",\n                    \"icon\": \"battery_android_frame_2\"\n                },\n                {\n                    \"level\": 10,\n                    \"title\": \"Did you see the previous message?\",\n                    \"message\": \"You should probably plug in a charger <b>now</b>\",\n                    \"icon\": \"battery_android_frame_1\"\n                },\n                {\n                    \"level\": 5,\n                    \"title\": \"Critical battery level\",\n                    \"message\": \"PLUG THE CHARGER RIGHT NOW!!\",\n                    \"icon\": \"battery_android_alert\",\n                    \"critical\": true\n                }\n            ],\n            \"criticalLevel\": 3\n        },\n        \"idle\": {\n            \"lockBeforeSleep\": true,\n            \"inhibitWhenAudio\": true,\n            \"timeouts\": [\n                {\n                    \"timeout\": 180,\n                    \"idleAction\": \"lock\"\n                },\n                {\n                    \"timeout\": 300,\n                    \"idleAction\": \"dpms off\",\n                    \"returnAction\": \"dpms on\"\n                },\n                {\n                    \"timeout\": 600,\n                    \"idleAction\": [\"systemctl\", \"suspend-then-hibernate\"]\n                }\n            ]\n        }\n    },\n    \"background\": {\n        \"desktopClock\": {\n            \"enabled\": false,\n            \"scale\": 1.0,\n            \"position\": \"bottom-right\",\n            \"shadow\": {\n                \"enabled\": true,\n                \"opacity\": 0.7,\n                \"blur\": 0.4\n            },\n            \"background\": {\n                \"enabled\": false,\n                \"opacity\": 0.7,\n                \"blur\": true\n            },\n            \"invertColors\": false\n        },\n        \"enabled\": true,\n        \"visualiser\": {\n            \"blur\": false,\n            \"enabled\": false,\n            \"autoHide\": true,\n            \"rounding\": 1,\n            \"spacing\": 1\n        }\n    },\n    \"bar\": {\n        \"activeWindow\": {\n            \"compact\": false,\n            \"inverted\": false,\n            \"showOnHover\": true\n        },\n        \"clock\": {\n            \"background\": false,\n            \"showDate\": false,\n            \"showIcon\": true\n        },\n        \"dragThreshold\": 20,\n        \"entries\": [\n            {\n                \"id\": \"logo\",\n                \"enabled\": true\n            },\n            {\n                \"id\": \"workspaces\",\n                \"enabled\": true\n            },\n            {\n                \"id\": \"spacer\",\n                \"enabled\": true\n            },\n            {\n                \"id\": \"activeWindow\",\n                \"enabled\": true\n            },\n            {\n                \"id\": \"spacer\",\n                \"enabled\": true\n            },\n            {\n                \"id\": \"tray\",\n                \"enabled\": true\n            },\n            {\n                \"id\": \"clock\",\n                \"enabled\": true\n            },\n            {\n                \"id\": \"statusIcons\",\n                \"enabled\": true\n            },\n            {\n                \"id\": \"power\",\n                \"enabled\": true\n            }\n        ],\n        \"persistent\": true,\n        \"popouts\": {\n            \"activeWindow\": true,\n            \"statusIcons\": true,\n            \"tray\": true\n        },\n        \"scrollActions\": {\n            \"brightness\": true,\n            \"workspaces\": true,\n            \"volume\": true\n        },\n        \"showOnHover\": true,\n        \"status\": {\n            \"showAudio\": false,\n            \"showBattery\": true,\n            \"showBluetooth\": true,\n            \"showKbLayout\": false,\n            \"showMicrophone\": false,\n            \"showNetwork\": true,\n            \"showWifi\": true,\n            \"showLockStatus\": true\n        },\n        \"tray\": {\n            \"background\": false,\n            \"compact\": false,\n            \"iconSubs\": [],\n            \"recolour\": false\n        },\n        \"workspaces\": {\n            \"activeIndicator\": true,\n            \"activeLabel\": \"󰮯\",\n            \"activeTrail\": false,\n            \"label\": \"  \",\n            \"occupiedBg\": false,\n            \"occupiedLabel\": \"󰮯\",\n            \"perMonitorWorkspaces\": true,\n            \"showWindows\": true,\n            \"shown\": 5,\n            \"specialWorkspaceIcons\": [\n                {\n                    \"name\": \"steam\",\n                    \"icon\": \"sports_esports\"\n                }\n            ],\n            \"windowIcons\": [\n                {\n                    \"regex\": \"steam(_app_(default|[0-9]+))?\",\n                    \"icon\": \"sports_esports\"\n                }\n            ]\n        },\n        \"excludedScreens\": [\"\"],\n        \"activeWindow\": {\n            \"inverted\": false\n        }\n    },\n    \"border\": {\n        \"rounding\": 25,\n        \"thickness\": 10\n    },\n    \"dashboard\": {\n        \"enabled\": true,\n        \"dragThreshold\": 50,\n        \"mediaUpdateInterval\": 500,\n        \"showOnHover\": true\n    },\n    \"launcher\": {\n        \"actionPrefix\": \">\",\n        \"actions\": [\n            {\n                \"name\": \"Calculator\",\n                \"icon\": \"calculate\",\n                \"description\": \"Do simple math equations (powered by Qalc)\",\n                \"command\": [\"autocomplete\", \"calc\"],\n                \"enabled\": true,\n                \"dangerous\": false\n            },\n            {\n                \"name\": \"Scheme\",\n                \"icon\": \"palette\",\n                \"description\": \"Change the current colour scheme\",\n                \"command\": [\"autocomplete\", \"scheme\"],\n                \"enabled\": true,\n                \"dangerous\": false\n            },\n            {\n                \"name\": \"Wallpaper\",\n                \"icon\": \"image\",\n                \"description\": \"Change the current wallpaper\",\n                \"command\": [\"autocomplete\", \"wallpaper\"],\n                \"enabled\": true,\n                \"dangerous\": false\n            },\n            {\n                \"name\": \"Variant\",\n                \"icon\": \"colors\",\n                \"description\": \"Change the current scheme variant\",\n                \"command\": [\"autocomplete\", \"variant\"],\n                \"enabled\": true,\n                \"dangerous\": false\n            },\n            {\n                \"name\": \"Transparency\",\n                \"icon\": \"opacity\",\n                \"description\": \"Change shell transparency\",\n                \"command\": [\"autocomplete\", \"transparency\"],\n                \"enabled\": false,\n                \"dangerous\": false\n            },\n            {\n                \"name\": \"Random\",\n                \"icon\": \"casino\",\n                \"description\": \"Switch to a random wallpaper\",\n                \"command\": [\"caelestia\", \"wallpaper\", \"-r\"],\n                \"enabled\": true,\n                \"dangerous\": false\n            },\n            {\n                \"name\": \"Light\",\n                \"icon\": \"light_mode\",\n                \"description\": \"Change the scheme to light mode\",\n                \"command\": [\"setMode\", \"light\"],\n                \"enabled\": true,\n                \"dangerous\": false\n            },\n            {\n                \"name\": \"Dark\",\n                \"icon\": \"dark_mode\",\n                \"description\": \"Change the scheme to dark mode\",\n                \"command\": [\"setMode\", \"dark\"],\n                \"enabled\": true,\n                \"dangerous\": false\n            },\n            {\n                \"name\": \"Shutdown\",\n                \"icon\": \"power_settings_new\",\n                \"description\": \"Shutdown the system\",\n                \"command\": [\"systemctl\", \"poweroff\"],\n                \"enabled\": true,\n                \"dangerous\": true\n            },\n            {\n                \"name\": \"Reboot\",\n                \"icon\": \"cached\",\n                \"description\": \"Reboot the system\",\n                \"command\": [\"systemctl\", \"reboot\"],\n                \"enabled\": true,\n                \"dangerous\": true\n            },\n            {\n                \"name\": \"Logout\",\n                \"icon\": \"exit_to_app\",\n                \"description\": \"Log out of the current session\",\n                \"command\": [\"loginctl\", \"terminate-user\", \"\"],\n                \"enabled\": true,\n                \"dangerous\": true\n            },\n            {\n                \"name\": \"Lock\",\n                \"icon\": \"lock\",\n                \"description\": \"Lock the current session\",\n                \"command\": [\"loginctl\", \"lock-session\"],\n                \"enabled\": true,\n                \"dangerous\": false\n            },\n            {\n                \"name\": \"Sleep\",\n                \"icon\": \"bedtime\",\n                \"description\": \"Suspend then hibernate\",\n                \"command\": [\"systemctl\", \"suspend-then-hibernate\"],\n                \"enabled\": true,\n                \"dangerous\": false\n            },\n            {\n                \"name\": \"Settings\",\n                \"icon\": \"settings\",\n                \"description\": \"Configure the shell\",\n                \"command\": [\"caelestia\", \"shell\", \"controlCenter\", \"open\"],\n                \"enabled\": true,\n                \"dangerous\": false\n            }\n        ],\n        \"dragThreshold\": 50,\n        \"vimKeybinds\": false,\n        \"enableDangerousActions\": false,\n        \"maxShown\": 7,\n        \"maxWallpapers\": 9,\n        \"specialPrefix\": \"@\",\n        \"useFuzzy\": {\n            \"apps\": false,\n            \"actions\": false,\n            \"schemes\": false,\n            \"variants\": false,\n            \"wallpapers\": false\n        },\n        \"showOnHover\": false,\n        \"favouriteApps\": [],\n        \"hiddenApps\": []\n    },\n    \"lock\": {\n        \"recolourLogo\": false,\n        \"hideNotifs\": false\n    },\n    \"notifs\": {\n        \"actionOnClick\": false,\n        \"clearThreshold\": 0.3,\n        \"defaultExpireTimeout\": 5000,\n        \"expandThreshold\": 20,\n        \"openExpanded\": false,\n        \"expire\": false\n    },\n    \"osd\": {\n        \"enabled\": true,\n        \"enableBrightness\": true,\n        \"enableMicrophone\": false,\n        \"hideDelay\": 2000\n    },\n    \"paths\": {\n        \"mediaGif\": \"root:/assets/bongocat.gif\",\n        \"sessionGif\": \"root:/assets/kurukuru.gif\",\n        \"wallpaperDir\": \"~/Pictures/Wallpapers\",\n        \"lyricsDir\": \"~/Music/lyrics\"\n    },\n    \"services\": {\n        \"audioIncrement\": 0.1,\n        \"brightnessIncrement\": 0.1,\n        \"maxVolume\": 1.0,\n        \"defaultPlayer\": \"Spotify\",\n        \"gpuType\": \"\",\n        \"playerAliases\": [{ \"from\": \"com.github.th_ch.youtube_music\", \"to\": \"YT Music\" }],\n        \"weatherLocation\": \"\",\n        \"useFahrenheit\": false,\n        \"useFahrenheitPerformance\": false,\n        \"useTwelveHourClock\": false,\n        \"smartScheme\": true,\n        \"visualiserBars\": 45\n    },\n    \"session\": {\n        \"dragThreshold\": 30,\n        \"enabled\": true,\n        \"vimKeybinds\": false,\n        \"icons\": {\n            \"logout\": \"logout\",\n            \"shutdown\": \"power_settings_new\",\n            \"hibernate\": \"downloading\",\n            \"reboot\": \"cached\"\n        },\n        \"commands\": {\n            \"logout\": [\"loginctl\", \"terminate-user\", \"\"],\n            \"shutdown\": [\"systemctl\", \"poweroff\"],\n            \"hibernate\": [\"systemctl\", \"hibernate\"],\n            \"reboot\": [\"systemctl\", \"reboot\"]\n        }\n    },\n    \"sidebar\": {\n        \"dragThreshold\": 80,\n        \"enabled\": true\n    },\n    \"utilities\": {\n        \"enabled\": true,\n        \"maxToasts\": 4,\n        \"toasts\": {\n            \"audioInputChanged\": true,\n            \"audioOutputChanged\": true,\n            \"capsLockChanged\": true,\n            \"chargingChanged\": true,\n            \"configLoaded\": true,\n            \"dndChanged\": true,\n            \"gameModeChanged\": true,\n            \"kbLayoutChanged\": true,\n            \"kbLimit\": true,\n            \"numLockChanged\": true,\n            \"vpnChanged\": true,\n            \"nowPlaying\": false\n        },\n        \"vpn\": {\n            \"enabled\": true,\n            \"provider\": [\n                {\n                    \"name\": \"wireguard\",\n                    \"interface\": \"your-connection-name\",\n                    \"displayName\": \"Wireguard (Your VPN)\",\n                    \"enabled\": false\n                }\n            ]\n        },\n        \"quickToggles\": [\n            {\n                \"id\": \"wifi\",\n                \"enabled\": true\n            },\n            {\n                \"id\": \"bluetooth\",\n                \"enabled\": true\n            },\n            {\n                \"id\": \"mic\",\n                \"enabled\": true\n            },\n            {\n                \"enabled\": true,\n                \"id\": \"settings\"\n            },\n            {\n                \"id\": \"gameMode\",\n                \"enabled\": true\n            },\n            {\n                \"id\": \"dnd\",\n                \"enabled\": true\n            },\n            {\n                \"id\": \"vpn\",\n                \"enabled\": true\n            }\n        ]\n    }\n}\n```\n\n</details>\n\n### Home Manager Module\n\nFor NixOS users, a home manager module is also available.\n\n<details><summary><code>home.nix</code></summary>\n\n```nix\nprograms.caelestia = {\n  enable = true;\n  systemd = {\n    enable = false; # if you prefer starting from your compositor\n    target = \"graphical-session.target\";\n    environment = [];\n  };\n  settings = {\n    bar.status = {\n      showBattery = false;\n    };\n    paths.wallpaperDir = \"~/Images\";\n  };\n  cli = {\n    enable = true; # Also add caelestia-cli to path\n    settings = {\n      theme.enableGtk = false;\n    };\n  };\n};\n```\n\nThe module automatically adds Caelestia shell to the path with **full functionality**. The CLI is not required, however you have the option to enable and configure it.\n\n</details>\n\n## FAQ\n\n### Need help or support?\n\nYou can join the community Discord server for assistance and discussion:\nhttps://discord.gg/BGDCFCmMBk\n\n### My screen is flickering, help pls!\n\nTry disabling VRR in the hyprland config. You can do this by adding the following to `~/.config/caelestia/hypr-user.conf`:\n\n```conf\nmisc {\n    vrr = 0\n}\n```\n\n### I want to make my own changes to the hyprland config!\n\nYou can add your custom hyprland configs to `~/.config/caelestia/hypr-user.conf`.\n\n### I want to make my own changes to other stuff!\n\nSee the [manual installation](https://github.com/caelestia-dots/shell?tab=readme-ov-file#manual-installation) section\nfor the corresponding repo.\n\n### I want to disable XXX feature!\n\nPlease read the [configuring](https://github.com/caelestia-dots/shell?tab=readme-ov-file#configuring) section in the readme.\nIf there is no corresponding option, make feature request.\n\n### How do I make my colour scheme change with my wallpaper?\n\nSet a wallpaper via the launcher or `caelestia wallpaper` and set the scheme to the dynamic scheme via the launcher\nor `caelestia scheme set`. e.g.\n\n```sh\ncaelestia wallpaper -f <path/to/file>\ncaelestia scheme set -n dynamic\n```\n\n### My wallpapers aren't showing up in the launcher!\n\nThe launcher pulls wallpapers from `~/Pictures/Wallpapers` by default. You can change this in the config. Additionally,\nthe launcher only shows an odd number of wallpapers at one time. If you only have 2 wallpapers, consider getting more\n(or just putting one).\n\n## Credits\n\nThanks to the Hyprland discord community (especially the homies in #rice-discussion) for all the help and suggestions\nfor improving these dots!\n\nA special thanks to [@outfoxxed](https://github.com/outfoxxed) for making Quickshell and the effort put into fixing issues\nand implementing various feature requests.\n\nAnother special thanks to [@end_4](https://github.com/end-4) for his [config](https://github.com/end-4/dots-hyprland)\nwhich helped me a lot with learning how to use Quickshell.\n\nFinally another thank you to all the configs I took inspiration from (only one for now):\n\n-   [Axenide/Ax-Shell](https://github.com/Axenide/Ax-Shell)\n\n## Stonks 📈\n\n<a href=\"https://www.star-history.com/#caelestia-dots/shell&Date\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=caelestia-dots/shell&type=Date&theme=dark\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=caelestia-dots/shell&type=Date\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=caelestia-dots/shell&type=Date\" />\n </picture>\n</a>\n"
  },
  {
    "path": "assets/pam.d/fprint",
    "content": "#%PAM-1.0\n\nauth    required    pam_fprintd.so  max-tries=1\n"
  },
  {
    "path": "assets/pam.d/passwd",
    "content": "#%PAM-1.0\n\nauth    required                    pam_faillock.so     preauth\nauth    [success=1 default=bad]     pam_unix.so         nullok\nauth    [default=die]               pam_faillock.so     authfail\nauth    required                    pam_faillock.so     authsucc\n"
  },
  {
    "path": "assets/shaders/fade.frag",
    "content": "#version 440\n\nlayout(location = 0) in vec2 qt_TexCoord0;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(std140, binding = 0) uniform buf {\n    mat4 qt_Matrix;\n    float qt_Opacity;\n    float fadeMargin;\n};\n\nlayout(binding = 1) uniform sampler2D source;\n\nvoid main() {\n    vec4 tex = texture(source, qt_TexCoord0);\n    float factor = 1.0;\n    float margin = 0.1;\n\n    if (qt_TexCoord0.y < margin) {\n        factor = qt_TexCoord0.y / margin;\n    } else if (qt_TexCoord0.y > (1.0 - margin)) {\n        factor = (1.0 - qt_TexCoord0.y) / margin;\n    }\n\n    fragColor = tex * factor * qt_Opacity;\n}\n"
  },
  {
    "path": "assets/shaders/opacitymask.frag",
    "content": "#version 440\n\nlayout(location = 0) in vec2 qt_TexCoord0;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(std140, binding = 0) uniform buf {\n    // qt_Matrix and qt_Opacity must always be both present\n    // if the built-in vertex shader is used.\n    mat4 qt_Matrix;\n    float qt_Opacity;\n};\n\nlayout(binding = 1) uniform sampler2D source;\nlayout(binding = 2) uniform sampler2D maskSource;\n\nvoid main()\n{\n    fragColor = texture(source, qt_TexCoord0.st) * (texture(maskSource, qt_TexCoord0.st).a) * qt_Opacity;\n}\n"
  },
  {
    "path": "assets/wrap_term_launch.sh",
    "content": "#!/usr/bin/env sh\n\ncat ~/.local/state/caelestia/sequences.txt 2>/dev/null\n\nexec \"$@\"\n"
  },
  {
    "path": "components/Anim.qml",
    "content": "import qs.config\nimport QtQuick\n\nNumberAnimation {\n    duration: Appearance.anim.durations.normal\n    easing.type: Easing.BezierSpline\n    easing.bezierCurve: Appearance.anim.curves.standard\n}\n"
  },
  {
    "path": "components/CAnim.qml",
    "content": "import qs.config\nimport QtQuick\n\nColorAnimation {\n    duration: Appearance.anim.durations.normal\n    easing.type: Easing.BezierSpline\n    easing.bezierCurve: Appearance.anim.curves.standard\n}\n"
  },
  {
    "path": "components/ConnectionHeader.qml",
    "content": "import qs.components\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property string icon\n    required property string title\n\n    spacing: Appearance.spacing.normal\n    Layout.alignment: Qt.AlignHCenter\n\n    MaterialIcon {\n        Layout.alignment: Qt.AlignHCenter\n        animate: true\n        text: root.icon\n        font.pointSize: Appearance.font.size.extraLarge * 3\n        font.bold: true\n    }\n\n    StyledText {\n        Layout.alignment: Qt.AlignHCenter\n        animate: true\n        text: root.title\n        font.pointSize: Appearance.font.size.large\n        font.bold: true\n    }\n}\n"
  },
  {
    "path": "components/ConnectionInfoSection.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property var deviceDetails\n\n    spacing: Appearance.spacing.small / 2\n\n    StyledText {\n        text: qsTr(\"IP Address\")\n    }\n\n    StyledText {\n        text: root.deviceDetails?.ipAddress || qsTr(\"Not available\")\n        color: Colours.palette.m3outline\n        font.pointSize: Appearance.font.size.small\n    }\n\n    StyledText {\n        Layout.topMargin: Appearance.spacing.normal\n        text: qsTr(\"Subnet Mask\")\n    }\n\n    StyledText {\n        text: root.deviceDetails?.subnet || qsTr(\"Not available\")\n        color: Colours.palette.m3outline\n        font.pointSize: Appearance.font.size.small\n    }\n\n    StyledText {\n        Layout.topMargin: Appearance.spacing.normal\n        text: qsTr(\"Gateway\")\n    }\n\n    StyledText {\n        text: root.deviceDetails?.gateway || qsTr(\"Not available\")\n        color: Colours.palette.m3outline\n        font.pointSize: Appearance.font.size.small\n    }\n\n    StyledText {\n        Layout.topMargin: Appearance.spacing.normal\n        text: qsTr(\"DNS Servers\")\n    }\n\n    StyledText {\n        text: (root.deviceDetails && root.deviceDetails.dns && root.deviceDetails.dns.length > 0) ? root.deviceDetails.dns.join(\", \") : qsTr(\"Not available\")\n        color: Colours.palette.m3outline\n        font.pointSize: Appearance.font.size.small\n        wrapMode: Text.Wrap\n        Layout.maximumWidth: parent.width\n    }\n}\n"
  },
  {
    "path": "components/DashboardState.qml",
    "content": "import Quickshell\n\nPersistentProperties {\n    property int currentTab\n    property date currentDate: new Date()\n}\n"
  },
  {
    "path": "components/DrawerVisibilities.qml",
    "content": "import Quickshell\n\nPersistentProperties {\n    property bool bar\n    property bool osd\n    property bool session\n    property bool launcher\n    property bool dashboard\n    property bool utilities\n    property bool sidebar\n}\n"
  },
  {
    "path": "components/Logo.qml",
    "content": "import QtQuick\nimport QtQuick.Shapes\nimport qs.services\n\nItem {\n    id: root\n\n    readonly property real designWidth: 128\n    readonly property real designHeight: 90.38\n\n    property color topColour: Colours.palette.m3primary\n    property color bottomColour: Colours.palette.m3onSurface\n\n    implicitWidth: designWidth\n    implicitHeight: designHeight\n\n    Shape {\n        anchors.centerIn: parent\n        width: root.designWidth\n        height: root.designHeight\n        scale: Math.min(root.width / width, root.height / height)\n        transformOrigin: Item.Center\n        preferredRendererType: Shape.CurveRenderer\n\n        ShapePath {\n            fillColor: root.topColour\n            strokeColor: \"transparent\"\n\n            PathSvg {\n                path: \"m42.56,42.96c-7.76,1.6-16.36,4.22-22.44,6.22-.49.16-.88-.44-.53-.82,5.37-5.85,9.66-13.3,9.66-13.3,8.66-14.67,22.97-23.51,39.85-21.14,6.47.91,12.33,3.38,17.26,6.98.99.72,1.14,2.14.31,3.04-.4.44-.95.67-1.51.67-.34,0-.69-.09-1-.26-3.21-1.84-6.82-2.69-10.71-3.24-13.1-1.84-25.41,4.75-31.06,15.83-.94,1.84-.61,3.81.45,5.21.22.3.07.72-.29.8Z\"\n            }\n        }\n\n        ShapePath {\n            fillColor: root.bottomColour\n            strokeColor: \"transparent\"\n\n            PathSvg {\n                path: \"m103.02,51.8c-.65.11-1.26-.37-1.28-1.03-.06-1.96.15-3.89-.2-5.78-.28-1.48-1.66-2.5-3.16-2.34h-.05c-6.53.73-24.63,3.1-48,9.32-6.89,1.83-9.83,10-5.67,15.79,4.62,6.44,11.84,10.93,20.41,12.13,11.82,1.66,22.99-3.36,29.21-12.65.54-.81,1.54-1.17,2.47-.86.91.3,1.47,1.15,1.47,2.04,0,.33-.08.66-.24.98-7.23,14.21-22.91,22.95-39.59,20.6-7.84-1.1-14.8-4.5-20.28-9.43,0,0,0,0-.02-.01-7.28-5.14-14.7-9.99-27.24-11.98-18.82-2.98-9.53-8.75.46-13.78,7.36-3.13,25.17-7.9,36.24-10.73.16-.03.31-.06.47-.1,1.52-.4,3.2-.83,5.02-1.29,1.06-.26,1.93-.48,2.58-.64.09-.02.18-.04.26-.06.31-.08.56-.14.73-.18.03,0,.06-.01.08-.02.03,0,.05-.01.07-.02.02,0,.04,0,.06-.01.01,0,.03,0,.04-.01,0,0,.02,0,.03,0,.01,0,.02,0,.02,0,10.62-2.58,24.63-5.62,37.74-7.34,1.02-.13,2.03-.26,3.03-.37,7.49-.87,14.58-1.26,20.42-.81,25.43,1.95-4.71,16.77-15.12,18.61Z\"\n            }\n        }\n\n        ShapePath {\n            fillColor: root.topColour\n            strokeColor: \"transparent\"\n\n            PathSvg {\n                path: \"m98.12.06c-.29,2.08-1.72,8.42-8.36,9.19-.09,0-.09.13,0,.14,6.64.78,8.07,7.11,8.36,9.19.01.08.13.08.14,0,.29-2.08,1.72-8.42,8.36-9.19.09,0,.09-.13,0-.14-6.64-.78-8.07-7.11-8.36-9.19-.01-.08-.13-.08-.14,0Z\"\n            }\n        }\n\n        ShapePath {\n            fillColor: root.topColour\n            strokeColor: \"transparent\"\n\n            PathSvg {\n                path: \"m113.36,15.5c-.22,1.29-1.08,4.35-4.38,4.87-.08.01-.08.13,0,.14,3.3.52,4.17,3.58,4.38,4.87.01.08.13.08.14,0,.22-1.29,1.08-4.35,4.38-4.87.08-.01.08-.13,0-.14-3.3-.52-4.17-3.58-4.38-4.87-.01-.08-.13-.08-.14,0Z\"\n            }\n        }\n\n        ShapePath {\n            fillColor: root.topColour\n            strokeColor: \"transparent\"\n\n            PathSvg {\n                path: \"m112.69,65.22c-.19,1.01-.86,3.15-3.2,3.57-.08.01-.08.13,0,.14,2.34.42,3.01,2.56,3.2,3.57.01.08.13.08.14,0,.19-1.01.86-3.15,3.2-3.57.08-.01.08-.13,0-.14-2.34-.42-3.01-2.56-3.2-3.57-.01-.08-.13-.08-.14,0Z\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "components/MaterialIcon.qml",
    "content": "import qs.services\nimport qs.config\n\nStyledText {\n    property real fill\n    property int grade: Colours.light ? 0 : -25\n\n    font.family: Appearance.font.family.material\n    font.pointSize: Appearance.font.size.larger\n    font.variableAxes: ({\n            FILL: fill.toFixed(1),\n            GRAD: grade,\n            opsz: fontInfo.pixelSize,\n            wght: fontInfo.weight\n        })\n}\n"
  },
  {
    "path": "components/PropertyRow.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property string label\n    required property string value\n    property bool showTopMargin: false\n\n    spacing: Appearance.spacing.small / 2\n\n    StyledText {\n        Layout.topMargin: root.showTopMargin ? Appearance.spacing.normal : 0\n        text: root.label\n    }\n\n    StyledText {\n        text: root.value\n        color: Colours.palette.m3outline\n        font.pointSize: Appearance.font.size.small\n    }\n}\n"
  },
  {
    "path": "components/SectionContainer.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    default property alias content: contentColumn.data\n    property real contentSpacing: Appearance.spacing.larger\n    property bool alignTop: false\n\n    Layout.fillWidth: true\n    implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2\n\n    radius: Appearance.rounding.normal\n    color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : Colours.palette.m3surfaceContainerHigh\n\n    ColumnLayout {\n        id: contentColumn\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.top: root.alignTop ? parent.top : undefined\n        anchors.verticalCenter: root.alignTop ? undefined : parent.verticalCenter\n        anchors.margins: Appearance.padding.large\n\n        spacing: root.contentSpacing\n    }\n}\n"
  },
  {
    "path": "components/SectionHeader.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property string title\n    property string description: \"\"\n\n    spacing: 0\n\n    StyledText {\n        Layout.topMargin: Appearance.spacing.large\n        text: root.title\n        font.pointSize: Appearance.font.size.larger\n        font.weight: 500\n    }\n\n    StyledText {\n        visible: root.description !== \"\"\n        text: root.description\n        color: Colours.palette.m3outline\n    }\n}\n"
  },
  {
    "path": "components/StateLayer.qml",
    "content": "import qs.services\nimport qs.config\nimport QtQuick\n\nMouseArea {\n    id: root\n\n    property bool disabled\n    property bool showHoverBackground: true\n    property color color: Colours.palette.m3onSurface\n    // Pick up radius from parent if it has one (parent can be anything with a radius property)\n    property real radius: parent?.radius ?? 0 // qmllint disable missing-property\n    property alias rect: hoverLayer\n\n    function onClicked(): void {\n    }\n\n    anchors.fill: parent\n\n    enabled: !disabled\n    cursorShape: disabled ? undefined : Qt.PointingHandCursor\n    hoverEnabled: true\n\n    onPressed: event => {\n        if (disabled)\n            return;\n\n        rippleAnim.x = event.x;\n        rippleAnim.y = event.y;\n\n        const dist = (ox, oy) => ox * ox + oy * oy;\n        rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y), dist(event.x, height - event.y), dist(width - event.x, event.y), dist(width - event.x, height - event.y)));\n\n        rippleAnim.restart();\n    }\n\n    onClicked: event => !disabled && onClicked(event)\n\n    SequentialAnimation {\n        id: rippleAnim\n\n        property real x\n        property real y\n        property real radius\n\n        PropertyAction {\n            target: ripple\n            property: \"x\"\n            value: rippleAnim.x\n        }\n        PropertyAction {\n            target: ripple\n            property: \"y\"\n            value: rippleAnim.y\n        }\n        PropertyAction {\n            target: ripple\n            property: \"opacity\"\n            value: 0.08\n        }\n        Anim {\n            target: ripple\n            properties: \"implicitWidth,implicitHeight\"\n            from: 0\n            to: rippleAnim.radius * 2\n            easing.bezierCurve: Appearance.anim.curves.standardDecel\n        }\n        Anim {\n            target: ripple\n            property: \"opacity\"\n            to: 0\n        }\n    }\n\n    StyledClippingRect {\n        id: hoverLayer\n\n        anchors.fill: parent\n\n        color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.12 : (root.showHoverBackground && root.containsMouse) ? 0.08 : 0)\n        radius: root.radius\n\n        StyledRect {\n            id: ripple\n\n            radius: Appearance.rounding.full\n            color: root.color\n            opacity: 0\n\n            transform: Translate {\n                x: -ripple.width / 2\n                y: -ripple.height / 2\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "components/StyledClippingRect.qml",
    "content": "import Quickshell.Widgets\nimport QtQuick\n\nClippingRectangle {\n    id: root\n\n    color: \"transparent\"\n\n    Behavior on color {\n        CAnim {}\n    }\n}\n"
  },
  {
    "path": "components/StyledRect.qml",
    "content": "import QtQuick\n\nRectangle {\n    id: root\n\n    color: \"transparent\"\n\n    Behavior on color {\n        CAnim {}\n    }\n}\n"
  },
  {
    "path": "components/StyledText.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.services\nimport qs.config\nimport QtQuick\n\nText {\n    id: root\n\n    property bool animate: false\n    property string animateProp: \"scale\"\n    property real animateFrom: 0\n    property real animateTo: 1\n    property int animateDuration: Appearance.anim.durations.normal\n\n    renderType: Text.NativeRendering\n    textFormat: Text.PlainText\n    color: Colours.palette.m3onSurface\n    font.family: Appearance.font.family.sans\n    font.pointSize: Appearance.font.size.smaller\n\n    Behavior on color {\n        CAnim {}\n    }\n\n    Behavior on text {\n        enabled: root.animate\n\n        SequentialAnimation {\n            Anim {\n                to: root.animateFrom\n                easing.bezierCurve: Appearance.anim.curves.standardAccel\n            }\n            PropertyAction {}\n            Anim {\n                to: root.animateTo\n                easing.bezierCurve: Appearance.anim.curves.standardDecel\n            }\n        }\n    }\n\n    component Anim: NumberAnimation {\n        target: root\n        property: root.animateProp\n        duration: root.animateDuration / 2\n        easing.type: Easing.BezierSpline\n    }\n}\n"
  },
  {
    "path": "components/containers/StyledFlickable.qml",
    "content": "import \"..\"\nimport QtQuick\n\nFlickable {\n    id: root\n\n    maximumFlickVelocity: 3000\n\n    rebound: Transition {\n        Anim {\n            properties: \"x,y\"\n        }\n    }\n}\n"
  },
  {
    "path": "components/containers/StyledListView.qml",
    "content": "import \"..\"\nimport QtQuick\n\nListView {\n    id: root\n\n    maximumFlickVelocity: 3000\n\n    rebound: Transition {\n        Anim {\n            properties: \"x,y\"\n        }\n    }\n}\n"
  },
  {
    "path": "components/containers/StyledWindow.qml",
    "content": "import Quickshell\nimport Quickshell.Wayland\n\nPanelWindow {\n    required property string name\n\n    WlrLayershell.namespace: `caelestia-${name}`\n    color: \"transparent\"\n}\n"
  },
  {
    "path": "components/controls/CircularIndicator.qml",
    "content": "import \"..\"\nimport qs.services\nimport qs.config\nimport Caelestia.Internal\nimport QtQuick\nimport QtQuick.Templates\n\nBusyIndicator {\n    id: root\n\n    enum AnimType {\n        Advance = 0,\n        Retreat\n    }\n\n    enum AnimState {\n        Stopped,\n        Running,\n        Completing\n    }\n\n    property real implicitSize: Appearance.font.size.normal * 3\n    property real strokeWidth: Appearance.padding.small * 0.8\n    property color fgColour: Colours.palette.m3primary\n    property color bgColour: Colours.palette.m3secondaryContainer\n\n    property alias type: manager.indeterminateAnimationType\n    readonly property alias progress: manager.progress\n\n    property real internalStrokeWidth: strokeWidth\n    property int animState\n\n    padding: 0\n    implicitWidth: implicitSize\n    implicitHeight: implicitSize\n\n    Component.onCompleted: {\n        if (running) {\n            running = false;\n            running = true;\n        }\n    }\n\n    onRunningChanged: {\n        if (running) {\n            manager.completeEndProgress = 0;\n            animState = CircularIndicator.Running;\n        } else {\n            if (animState == CircularIndicator.Running)\n                animState = CircularIndicator.Completing;\n        }\n    }\n\n    states: State {\n        name: \"stopped\"\n        when: !root.running\n\n        PropertyChanges {\n            root.opacity: 0\n            root.internalStrokeWidth: root.strokeWidth / 3\n        }\n    }\n\n    transitions: Transition {\n        Anim {\n            properties: \"opacity,internalStrokeWidth\"\n            duration: manager.completeEndDuration * Appearance.anim.durations.scale\n        }\n    }\n\n    contentItem: CircularProgress {\n        anchors.fill: parent\n        strokeWidth: root.internalStrokeWidth\n        fgColour: root.fgColour\n        bgColour: root.bgColour\n        padding: root.padding\n        rotation: manager.rotation\n        startAngle: manager.startFraction * 360\n        value: manager.endFraction - manager.startFraction\n    }\n\n    CircularIndicatorManager {\n        id: manager\n    }\n\n    NumberAnimation {\n        running: root.animState !== CircularIndicator.Stopped\n        loops: Animation.Infinite\n        target: manager\n        property: \"progress\"\n        from: 0\n        to: 1\n        duration: manager.duration * Appearance.anim.durations.scale\n    }\n\n    NumberAnimation {\n        running: root.animState === CircularIndicator.Completing\n        target: manager\n        property: \"completeEndProgress\"\n        from: 0\n        to: 1\n        duration: manager.completeEndDuration * Appearance.anim.durations.scale\n        onFinished: {\n            if (root.animState === CircularIndicator.Completing)\n                root.animState = CircularIndicator.Stopped;\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/CircularProgress.qml",
    "content": "import \"..\"\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Shapes\n\nShape {\n    id: root\n\n    property real value\n    property int startAngle: -90\n    property int strokeWidth: Appearance.padding.smaller\n    property int padding: 0\n    property int spacing: Appearance.spacing.small\n    property color fgColour: Colours.palette.m3primary\n    property color bgColour: Colours.palette.m3secondaryContainer\n\n    readonly property real size: Math.min(width, height)\n    readonly property real arcRadius: (size - padding - strokeWidth) / 2\n    readonly property real vValue: value || 1 / 360\n    readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI)\n\n    preferredRendererType: Shape.CurveRenderer\n    asynchronous: true\n\n    ShapePath {\n        fillColor: \"transparent\"\n        strokeColor: root.bgColour\n        strokeWidth: root.strokeWidth\n        capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap\n\n        PathAngleArc {\n            startAngle: root.startAngle + 360 * root.vValue + root.gapAngle\n            sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2)\n            radiusX: root.arcRadius\n            radiusY: root.arcRadius\n            centerX: root.size / 2\n            centerY: root.size / 2\n        }\n\n        Behavior on strokeColor {\n            CAnim {\n                duration: Appearance.anim.durations.large\n            }\n        }\n    }\n\n    ShapePath {\n        fillColor: \"transparent\"\n        strokeColor: root.fgColour\n        strokeWidth: root.strokeWidth\n        capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap\n\n        PathAngleArc {\n            startAngle: root.startAngle\n            sweepAngle: 360 * root.vValue\n            radiusX: root.arcRadius\n            radiusY: root.arcRadius\n            centerX: root.size / 2\n            centerY: root.size / 2\n        }\n\n        Behavior on strokeColor {\n            CAnim {\n                duration: Appearance.anim.durations.large\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/CollapsibleSection.qml",
    "content": "import \"..\"\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property string title\n    property string description: \"\"\n    property bool expanded: false\n    property bool showBackground: false\n    property bool nested: false\n\n    default property alias content: contentColumn.data\n\n    signal toggleRequested\n\n    spacing: Appearance.spacing.small\n    Layout.fillWidth: true\n\n    Item {\n        id: sectionHeaderItem\n\n        Layout.fillWidth: true\n        Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48)\n\n        RowLayout {\n            id: titleRow\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.leftMargin: Appearance.padding.normal\n            anchors.rightMargin: Appearance.padding.normal\n            spacing: Appearance.spacing.normal\n\n            StyledText {\n                text: root.title\n                font.pointSize: Appearance.font.size.larger\n                font.weight: 500\n            }\n\n            Item {\n                Layout.fillWidth: true\n            }\n\n            MaterialIcon {\n                text: \"expand_more\"\n                rotation: root.expanded ? 180 : 0\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.normal\n\n                Behavior on rotation {\n                    Anim {\n                        duration: Appearance.anim.durations.small\n                        easing.bezierCurve: Appearance.anim.curves.standard\n                    }\n                }\n            }\n        }\n\n        StateLayer {\n            function onClicked(): void {\n                root.toggleRequested();\n                root.expanded = !root.expanded;\n            }\n\n            anchors.fill: parent\n            color: Colours.palette.m3onSurface\n            radius: Appearance.rounding.normal\n            showHoverBackground: false\n        }\n    }\n\n    Item {\n        id: contentWrapper\n\n        Layout.fillWidth: true\n        Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0\n        clip: true\n\n        Behavior on Layout.preferredHeight {\n            Anim {\n                easing.bezierCurve: Appearance.anim.curves.standard\n            }\n        }\n\n        StyledRect {\n            id: backgroundRect\n\n            anchors.fill: parent\n            radius: Appearance.rounding.normal\n            color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer)\n            opacity: root.showBackground && root.expanded ? 1.0 : 0.0\n            visible: root.showBackground\n\n            Behavior on opacity {\n                Anim {\n                    easing.bezierCurve: Appearance.anim.curves.standard\n                }\n            }\n        }\n\n        ColumnLayout {\n            id: contentColumn\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            y: Appearance.spacing.small\n            anchors.leftMargin: Appearance.padding.normal\n            anchors.rightMargin: Appearance.padding.normal\n            anchors.bottomMargin: Appearance.spacing.small\n            spacing: Appearance.spacing.small\n            opacity: root.expanded ? 1.0 : 0.0\n\n            Behavior on opacity {\n                Anim {\n                    easing.bezierCurve: Appearance.anim.curves.standard\n                }\n            }\n\n            StyledText {\n                id: descriptionText\n\n                Layout.fillWidth: true\n                Layout.topMargin: root.description !== \"\" ? Appearance.spacing.smaller : 0\n                Layout.bottomMargin: root.description !== \"\" ? Appearance.spacing.small : 0\n                visible: root.description !== \"\"\n                text: root.description\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.small\n                wrapMode: Text.Wrap\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/CustomMouseArea.qml",
    "content": "import QtQuick\n\nMouseArea {\n    property int scrollAccumulatedY: 0\n\n    function onWheel(event: WheelEvent): void {\n    }\n\n    onWheel: event => {\n        // Update accumulated scroll\n        if (Math.sign(event.angleDelta.y) !== Math.sign(scrollAccumulatedY))\n            scrollAccumulatedY = 0;\n        scrollAccumulatedY += event.angleDelta.y;\n\n        // Trigger handler and reset if above threshold\n        if (Math.abs(scrollAccumulatedY) >= 120) {\n            onWheel(event);\n            scrollAccumulatedY = 0;\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/CustomSpinBox.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nRowLayout {\n    id: root\n\n    property real value\n    property real max: Infinity\n    property real min: -Infinity\n    property real step: 1\n    property alias repeatRate: timer.interval\n\n    property bool isEditing: false\n    property string displayText: root.value.toString()\n\n    signal valueModified(value: real)\n\n    spacing: Appearance.spacing.small\n\n    onValueChanged: {\n        if (!root.isEditing) {\n            root.displayText = root.value.toString();\n        }\n    }\n\n    StyledTextField {\n        id: textField\n\n        inputMethodHints: Qt.ImhFormattedNumbersOnly\n        text: root.isEditing ? text : root.displayText\n        validator: DoubleValidator {\n            bottom: root.min\n            top: root.max\n            decimals: root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0\n        }\n        onActiveFocusChanged: {\n            if (activeFocus) {\n                root.isEditing = true;\n            } else {\n                root.isEditing = false;\n                root.displayText = root.value.toString();\n            }\n        }\n        onAccepted: {\n            const numValue = parseFloat(text);\n            if (!isNaN(numValue)) {\n                const clampedValue = Math.max(root.min, Math.min(root.max, numValue));\n                root.value = clampedValue;\n                root.displayText = clampedValue.toString();\n                root.valueModified(clampedValue);\n            } else {\n                text = root.displayText;\n            }\n            root.isEditing = false;\n        }\n        onEditingFinished: {\n            if (text !== root.displayText) {\n                const numValue = parseFloat(text);\n                if (!isNaN(numValue)) {\n                    const clampedValue = Math.max(root.min, Math.min(root.max, numValue));\n                    root.value = clampedValue;\n                    root.displayText = clampedValue.toString();\n                    root.valueModified(clampedValue);\n                } else {\n                    text = root.displayText;\n                }\n            }\n            root.isEditing = false;\n        }\n\n        padding: Appearance.padding.small\n        leftPadding: Appearance.padding.normal\n        rightPadding: Appearance.padding.normal\n\n        background: StyledRect {\n            implicitWidth: 100\n            radius: Appearance.rounding.small\n            color: Colours.tPalette.m3surfaceContainerHigh\n        }\n    }\n\n    StyledRect {\n        radius: Appearance.rounding.small\n        color: Colours.palette.m3primary\n\n        implicitWidth: implicitHeight\n        implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2\n\n        StateLayer {\n            id: upState\n\n            function onClicked(): void {\n                let newValue = Math.min(root.max, root.value + root.step);\n                // Round to avoid floating point precision errors\n                const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;\n                newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);\n                root.value = newValue;\n                root.displayText = newValue.toString();\n                root.valueModified(newValue);\n            }\n\n            color: Colours.palette.m3onPrimary\n\n            onPressAndHold: timer.start()\n            onReleased: timer.stop()\n        }\n\n        MaterialIcon {\n            id: upIcon\n\n            anchors.centerIn: parent\n            text: \"keyboard_arrow_up\"\n            color: Colours.palette.m3onPrimary\n        }\n    }\n\n    StyledRect {\n        radius: Appearance.rounding.small\n        color: Colours.palette.m3primary\n\n        implicitWidth: implicitHeight\n        implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2\n\n        StateLayer {\n            id: downState\n\n            function onClicked(): void {\n                let newValue = Math.max(root.min, root.value - root.step);\n                // Round to avoid floating point precision errors\n                const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;\n                newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);\n                root.value = newValue;\n                root.displayText = newValue.toString();\n                root.valueModified(newValue);\n            }\n\n            color: Colours.palette.m3onPrimary\n\n            onPressAndHold: timer.start()\n            onReleased: timer.stop()\n        }\n\n        MaterialIcon {\n            id: downIcon\n\n            anchors.centerIn: parent\n            text: \"keyboard_arrow_down\"\n            color: Colours.palette.m3onPrimary\n        }\n    }\n\n    Timer {\n        id: timer\n\n        interval: 100\n        repeat: true\n        triggeredOnStart: true\n        onTriggered: {\n            if (upState.pressed)\n                upState.onClicked();\n            else if (downState.pressed)\n                downState.onClicked();\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/FilledSlider.qml",
    "content": "import \"..\"\nimport \"../effects\"\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Templates\n\nSlider {\n    id: root\n\n    required property string icon\n    property real oldValue\n    property bool initialized\n\n    orientation: Qt.Vertical\n\n    background: StyledRect {\n        color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n        radius: Appearance.rounding.full\n\n        StyledRect {\n            anchors.left: parent.left\n            anchors.right: parent.right\n\n            y: root.handle.y\n            implicitHeight: parent.height - y\n\n            color: Colours.palette.m3secondary\n            radius: parent.radius\n        }\n    }\n\n    handle: Item {\n        id: handle\n\n        property alias moving: icon.moving\n\n        y: root.visualPosition * (root.availableHeight - height)\n        implicitWidth: root.width\n        implicitHeight: root.width\n\n        Elevation {\n            anchors.fill: parent\n            radius: rect.radius\n            level: handleInteraction.containsMouse ? 2 : 1\n        }\n\n        StyledRect {\n            id: rect\n\n            anchors.fill: parent\n\n            color: Colours.palette.m3inverseSurface\n            radius: Appearance.rounding.full\n\n            MouseArea {\n                id: handleInteraction\n\n                anchors.fill: parent\n                hoverEnabled: true\n                cursorShape: Qt.PointingHandCursor\n                acceptedButtons: Qt.NoButton\n            }\n\n            MaterialIcon {\n                id: icon\n\n                property bool moving\n\n                function update(): void {\n                    animate = !moving;\n                    binding.when = moving;\n                    font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger;\n                    font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material;\n                }\n\n                text: root.icon\n                color: Colours.palette.m3inverseOnSurface\n                anchors.centerIn: parent\n\n                onMovingChanged: anim.restart()\n\n                Binding {\n                    id: binding\n\n                    target: icon\n                    property: \"text\"\n                    value: Math.round(root.value * 100)\n                    when: false\n                }\n\n                SequentialAnimation {\n                    id: anim\n\n                    Anim {\n                        target: icon\n                        property: \"scale\"\n                        to: 0\n                        duration: Appearance.anim.durations.normal / 2\n                        easing.bezierCurve: Appearance.anim.curves.standardAccel\n                    }\n                    ScriptAction {\n                        script: icon.update()\n                    }\n                    Anim {\n                        target: icon\n                        property: \"scale\"\n                        to: 1\n                        duration: Appearance.anim.durations.normal / 2\n                        easing.bezierCurve: Appearance.anim.curves.standardDecel\n                    }\n                }\n            }\n        }\n    }\n\n    onPressedChanged: handle.moving = pressed\n\n    onValueChanged: {\n        if (!initialized) {\n            initialized = true;\n            return;\n        }\n        if (Math.abs(value - oldValue) < 0.01)\n            return;\n        oldValue = value;\n        handle.moving = true;\n        stateChangeDelay.restart();\n    }\n\n    Timer {\n        id: stateChangeDelay\n\n        interval: 500\n        onTriggered: {\n            if (!root.pressed)\n                handle.moving = false;\n        }\n    }\n\n    Behavior on value {\n        Anim {\n            duration: Appearance.anim.durations.large\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/IconButton.qml",
    "content": "import \"..\"\nimport qs.services\nimport qs.config\nimport QtQuick\n\nStyledRect {\n    id: root\n\n    enum Type {\n        Filled,\n        Tonal,\n        Text\n    }\n\n    property alias icon: label.text\n    property bool checked\n    property bool toggle\n    property real padding: type === IconButton.Text ? Appearance.padding.small / 2 : Appearance.padding.smaller\n    property alias font: label.font\n    property int type: IconButton.Filled\n    property bool disabled\n\n    property alias stateLayer: stateLayer\n    property alias label: label\n    property alias radiusAnim: radiusAnim\n\n    property bool internalChecked\n    property color activeColour: type === IconButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary\n    property color inactiveColour: {\n        if (!toggle && type === IconButton.Filled)\n            return Colours.palette.m3primary;\n        return type === IconButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer;\n    }\n    property color activeOnColour: type === IconButton.Filled ? Colours.palette.m3onPrimary : type === IconButton.Tonal ? Colours.palette.m3onSecondary : Colours.palette.m3primary\n    property color inactiveOnColour: {\n        if (!toggle && type === IconButton.Filled)\n            return Colours.palette.m3onPrimary;\n        return type === IconButton.Tonal ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant;\n    }\n    property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1)\n    property color disabledOnColour: Qt.alpha(Colours.palette.m3onSurface, 0.38)\n\n    signal clicked\n\n    onCheckedChanged: internalChecked = checked\n\n    radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)\n    color: type === IconButton.Text ? \"transparent\" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour\n\n    implicitWidth: implicitHeight\n    implicitHeight: label.implicitHeight + padding * 2\n\n    StateLayer {\n        id: stateLayer\n\n        function onClicked(): void {\n            if (root.toggle)\n                root.internalChecked = !root.internalChecked;\n            root.clicked();\n        }\n\n        color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour\n        disabled: root.disabled\n    }\n\n    MaterialIcon {\n        id: label\n\n        anchors.centerIn: parent\n        color: root.disabled ? root.disabledOnColour : root.internalChecked ? root.activeOnColour : root.inactiveOnColour\n        fill: !root.toggle || root.internalChecked ? 1 : 0\n\n        Behavior on fill {\n            Anim {}\n        }\n    }\n\n    Behavior on radius {\n        Anim {\n            id: radiusAnim\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/IconTextButton.qml",
    "content": "import \"..\"\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    enum Type {\n        Filled,\n        Tonal,\n        Text\n    }\n\n    property alias icon: iconLabel.text\n    property alias text: label.text\n    property bool checked\n    property bool toggle\n    property real horizontalPadding: Appearance.padding.normal\n    property real verticalPadding: Appearance.padding.smaller\n    property alias font: label.font\n    property int type: IconTextButton.Filled\n\n    property alias stateLayer: stateLayer\n    property alias iconLabel: iconLabel\n    property alias label: label\n\n    property bool internalChecked\n    property color activeColour: type === IconTextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary\n    property color inactiveColour: type === IconTextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer\n    property color activeOnColour: type === IconTextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary\n    property color inactiveOnColour: type === IconTextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer\n\n    signal clicked\n\n    onCheckedChanged: internalChecked = checked\n\n    radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)\n    color: type === IconTextButton.Text ? \"transparent\" : internalChecked ? activeColour : inactiveColour\n\n    implicitWidth: row.implicitWidth + horizontalPadding * 2\n    implicitHeight: row.implicitHeight + verticalPadding * 2\n\n    StateLayer {\n        id: stateLayer\n\n        function onClicked(): void {\n            if (root.toggle)\n                root.internalChecked = !root.internalChecked;\n            root.clicked();\n        }\n\n        color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour\n    }\n\n    RowLayout {\n        id: row\n\n        anchors.centerIn: parent\n        spacing: Appearance.spacing.small\n\n        MaterialIcon {\n            id: iconLabel\n\n            Layout.alignment: Qt.AlignVCenter\n            Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575)\n            color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour\n            fill: root.internalChecked ? 1 : 0\n\n            Behavior on fill {\n                Anim {}\n            }\n        }\n\n        StyledText {\n            id: label\n\n            Layout.alignment: Qt.AlignVCenter\n            Layout.topMargin: -Math.round(iconLabel.fontInfo.pointSize * 0.0575)\n            color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour\n        }\n    }\n\n    Behavior on radius {\n        Anim {}\n    }\n}\n"
  },
  {
    "path": "components/controls/Menu.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../effects\"\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nElevation {\n    id: root\n\n    property list<MenuItem> items\n    property MenuItem active: items[0] ?? null\n    property bool expanded\n\n    signal itemSelected(item: MenuItem)\n\n    radius: Appearance.rounding.small / 2\n    level: 2\n\n    implicitWidth: Math.max(200, column.implicitWidth)\n    implicitHeight: root.expanded ? column.implicitHeight : 0\n    opacity: root.expanded ? 1 : 0\n\n    StyledClippingRect {\n        anchors.fill: parent\n        radius: parent.radius\n        color: Colours.palette.m3surfaceContainer\n\n        ColumnLayout {\n            id: column\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            spacing: 0\n\n            Repeater {\n                model: root.items\n\n                StyledRect {\n                    id: item\n\n                    required property int index\n                    required property MenuItem modelData\n                    readonly property bool active: modelData === root.active\n\n                    Layout.fillWidth: true\n                    implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2\n                    implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2\n\n                    color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0)\n\n                    StateLayer {\n                        function onClicked(): void {\n                            root.itemSelected(item.modelData);\n                            root.active = item.modelData;\n                            root.expanded = false;\n                        }\n\n                        color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n                        disabled: !root.expanded\n                    }\n\n                    RowLayout {\n                        id: menuOptionRow\n\n                        anchors.fill: parent\n                        anchors.margins: Appearance.padding.normal\n                        spacing: Appearance.spacing.small\n\n                        MaterialIcon {\n                            Layout.alignment: Qt.AlignVCenter\n                            text: item.modelData.icon\n                            color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant\n                        }\n\n                        StyledText {\n                            Layout.alignment: Qt.AlignVCenter\n                            Layout.fillWidth: true\n                            text: item.modelData.text\n                            color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n                        }\n\n                        Loader {\n                            asynchronous: true\n                            Layout.alignment: Qt.AlignVCenter\n                            active: item.modelData.trailingIcon.length > 0\n                            visible: active\n\n                            sourceComponent: MaterialIcon {\n                                text: item.modelData.trailingIcon\n                                color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    Behavior on opacity {\n        Anim {\n            duration: Appearance.anim.durations.expressiveDefaultSpatial\n        }\n    }\n\n    Behavior on implicitHeight {\n        Anim {\n            duration: Appearance.anim.durations.expressiveDefaultSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/MenuItem.qml",
    "content": "import QtQuick\n\nQtObject {\n    required property string text\n    property string icon\n    property string trailingIcon\n    property string activeIcon: icon\n    property string activeText: text\n\n    signal clicked\n}\n"
  },
  {
    "path": "components/controls/SpinBoxRow.qml",
    "content": "import \"..\"\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property string label\n    required property real value\n    required property real min\n    required property real max\n    property real step: 1\n    property var onValueModified: function (value) {}\n\n    Layout.fillWidth: true\n    implicitHeight: row.implicitHeight + Appearance.padding.large * 2\n    radius: Appearance.rounding.normal\n    color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n\n    Behavior on implicitHeight {\n        Anim {}\n    }\n\n    RowLayout {\n        id: row\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.margins: Appearance.padding.large\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            Layout.fillWidth: true\n            text: root.label\n        }\n\n        CustomSpinBox {\n            min: root.min\n            max: root.max\n            step: root.step\n            value: root.value\n            onValueModified: value => {\n                root.onValueModified(value);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/SplitButton.qml",
    "content": "import \"..\"\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nRow {\n    id: root\n\n    enum Type {\n        Filled,\n        Tonal\n    }\n\n    property real horizontalPadding: Appearance.padding.normal\n    property real verticalPadding: Appearance.padding.smaller\n    property int type: SplitButton.Filled\n    property bool disabled\n    property bool menuOnTop\n    property string fallbackIcon\n    property string fallbackText\n\n    property alias menuItems: menu.items\n    property alias active: menu.active\n    property alias expanded: menu.expanded\n    property alias menu: menu\n    property alias iconLabel: iconLabel\n    property alias label: label\n    property alias stateLayer: stateLayer\n\n    property color colour: type == SplitButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer\n    property color textColour: type == SplitButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer\n    property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1)\n    property color disabledTextColour: Qt.alpha(Colours.palette.m3onSurface, 0.38)\n\n    spacing: Math.floor(Appearance.spacing.small / 2)\n\n    StyledRect {\n        radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)\n        topRightRadius: Appearance.rounding.small / 2\n        bottomRightRadius: Appearance.rounding.small / 2\n        color: root.disabled ? root.disabledColour : root.colour\n\n        implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2\n        implicitHeight: expandBtn.implicitHeight\n\n        StateLayer {\n            id: stateLayer\n\n            function onClicked(): void {\n                root.active?.clicked();\n            }\n\n            rect.topRightRadius: parent.topRightRadius\n            rect.bottomRightRadius: parent.bottomRightRadius\n            color: root.textColour\n            disabled: root.disabled\n        }\n\n        RowLayout {\n            id: textRow\n\n            anchors.centerIn: parent\n            anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4)\n            spacing: Appearance.spacing.small\n\n            MaterialIcon {\n                id: iconLabel\n\n                Layout.alignment: Qt.AlignVCenter\n                animate: true\n                text: root.active?.activeIcon ?? root.fallbackIcon\n                color: root.disabled ? root.disabledTextColour : root.textColour\n                fill: 1\n            }\n\n            StyledText {\n                id: label\n\n                Layout.alignment: Qt.AlignVCenter\n                Layout.preferredWidth: implicitWidth\n                animate: true\n                text: root.active?.activeText ?? root.fallbackText\n                color: root.disabled ? root.disabledTextColour : root.textColour\n                clip: true\n\n                Behavior on Layout.preferredWidth {\n                    Anim {\n                        easing.bezierCurve: Appearance.anim.curves.emphasized\n                    }\n                }\n            }\n        }\n    }\n\n    StyledRect {\n        id: expandBtn\n\n        property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) : Appearance.rounding.small / 2\n\n        radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)\n        topLeftRadius: rad\n        bottomLeftRadius: rad\n        color: root.disabled ? root.disabledColour : root.colour\n\n        implicitWidth: implicitHeight\n        implicitHeight: expandIcon.implicitHeight + root.verticalPadding * 2\n\n        StateLayer {\n            id: expandStateLayer\n\n            function onClicked(): void {\n                root.expanded = !root.expanded;\n            }\n\n            rect.topLeftRadius: parent.topLeftRadius\n            rect.bottomLeftRadius: parent.bottomLeftRadius\n            color: root.textColour\n            disabled: root.disabled\n        }\n\n        MaterialIcon {\n            id: expandIcon\n\n            anchors.centerIn: parent\n            anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4)\n\n            text: \"expand_more\"\n            color: root.disabled ? root.disabledTextColour : root.textColour\n            rotation: root.expanded ? 180 : 0\n\n            Behavior on anchors.horizontalCenterOffset {\n                Anim {}\n            }\n\n            Behavior on rotation {\n                Anim {}\n            }\n        }\n\n        Behavior on rad {\n            Anim {}\n        }\n\n        Menu {\n            id: menu\n\n            states: State {\n                when: root.menuOnTop\n\n                AnchorChanges {\n                    target: menu\n                    anchors.top: undefined\n                    anchors.bottom: expandBtn.top\n                }\n            }\n\n            anchors.top: parent.bottom\n            anchors.right: parent.right\n            anchors.topMargin: Appearance.spacing.small\n            anchors.bottomMargin: Appearance.spacing.small\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/SplitButtonRow.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property string label\n    property int expandedZ: 100\n    property bool enabled: true\n\n    property alias menuItems: splitButton.menuItems\n    property alias active: splitButton.active\n    property alias expanded: splitButton.expanded\n    property alias type: splitButton.type\n\n    signal selected(item: MenuItem)\n\n    Layout.fillWidth: true\n    implicitHeight: row.implicitHeight + Appearance.padding.large * 2\n    radius: Appearance.rounding.normal\n    color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n\n    clip: false\n    z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1\n    opacity: enabled ? 1.0 : 0.5\n\n    RowLayout {\n        id: row\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.large\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            Layout.fillWidth: true\n            text: root.label\n            color: root.enabled ? Colours.palette.m3onSurface : Colours.palette.m3onSurfaceVariant\n        }\n\n        SplitButton {\n            id: splitButton\n\n            enabled: root.enabled\n            type: SplitButton.Filled\n\n            menu.z: 1\n\n            stateLayer.onClicked: {\n                splitButton.expanded = !splitButton.expanded;\n            }\n\n            menu.onItemSelected: item => {\n                root.selected(item);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/StyledInputField.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    property string text: \"\"\n    property var validator: null\n    property bool readOnly: false\n    property int horizontalAlignment: TextInput.AlignHCenter\n    property int implicitWidth: 70\n    property bool enabled: true\n\n    // Expose activeFocus through alias to avoid FINAL property override\n    readonly property alias hasFocus: inputField.activeFocus\n\n    signal textEdited(string text)\n    signal editingFinished\n\n    implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2\n\n    StyledRect {\n        id: container\n\n        anchors.fill: parent\n        color: inputHover.containsMouse || inputField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)\n        radius: Appearance.rounding.small\n        border.width: 1\n        border.color: inputField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)\n        opacity: root.enabled ? 1 : 0.5\n\n        Behavior on color {\n            CAnim {}\n        }\n        Behavior on border.color {\n            CAnim {}\n        }\n\n        MouseArea {\n            id: inputHover\n\n            anchors.fill: parent\n            hoverEnabled: true\n            cursorShape: Qt.IBeamCursor\n            acceptedButtons: Qt.NoButton\n            enabled: root.enabled\n        }\n\n        StyledTextField {\n            id: inputField\n\n            anchors.centerIn: parent\n            width: parent.width - Appearance.padding.normal\n            horizontalAlignment: root.horizontalAlignment\n            validator: root.validator\n            readOnly: root.readOnly\n            enabled: root.enabled\n\n            Binding {\n                target: inputField\n                property: \"text\"\n                value: root.text\n                when: !inputField.activeFocus\n            }\n\n            onTextChanged: {\n                root.text = text;\n                root.textEdited(text);\n            }\n\n            onEditingFinished: {\n                root.editingFinished();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/StyledRadioButton.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Templates\n\nRadioButton {\n    id: root\n\n    font.pointSize: Appearance.font.size.smaller\n\n    implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin\n    implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight)\n\n    indicator: Rectangle {\n        id: outerCircle\n\n        implicitWidth: 20\n        implicitHeight: 20\n        radius: Appearance.rounding.full\n        color: \"transparent\"\n        border.color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant\n        border.width: 2\n        anchors.verticalCenter: parent.verticalCenter\n\n        StateLayer {\n            function onClicked(): void {\n                root.click();\n            }\n\n            anchors.margins: -Appearance.padding.smaller\n            color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary\n            z: -1\n        }\n\n        StyledRect {\n            anchors.centerIn: parent\n            implicitWidth: 8\n            implicitHeight: 8\n\n            radius: Appearance.rounding.full\n            color: Qt.alpha(Colours.palette.m3primary, root.checked ? 1 : 0)\n        }\n\n        Behavior on border.color {\n            CAnim {}\n        }\n    }\n\n    contentItem: StyledText {\n        text: root.text\n        font.pointSize: root.font.pointSize\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.left: outerCircle.right\n        anchors.leftMargin: Appearance.spacing.smaller\n    }\n}\n"
  },
  {
    "path": "components/controls/StyledScrollBar.qml",
    "content": "import \"..\"\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Templates\n\nScrollBar {\n    id: root\n\n    required property Flickable flickable\n    property bool shouldBeActive\n    property real nonAnimPosition\n    property bool animating\n    property bool _updatingFromFlickable: false\n    property bool _updatingFromUser: false\n\n    onHoveredChanged: {\n        if (hovered)\n            shouldBeActive = true;\n        else\n            shouldBeActive = flickable.moving;\n    }\n\n    // Sync nonAnimPosition with Qt's automatic position binding\n    onPositionChanged: {\n        if (_updatingFromUser) {\n            _updatingFromUser = false;\n            return;\n        }\n        if (position === nonAnimPosition) {\n            animating = false;\n            return;\n        }\n        if (!animating && !_updatingFromFlickable && !fullMouse.pressed) {\n            nonAnimPosition = position;\n        }\n    }\n\n    Component.onCompleted: {\n        if (flickable) {\n            const contentHeight = flickable.contentHeight;\n            const height = flickable.height;\n            if (contentHeight > height) {\n                nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height)));\n            }\n        }\n    }\n    implicitWidth: Appearance.padding.small\n\n    contentItem: StyledRect {\n        anchors.left: parent.left\n        anchors.right: parent.right\n        opacity: {\n            if (root.size === 1)\n                return 0;\n            if (fullMouse.pressed)\n                return 1;\n            if (mouse.containsMouse)\n                return 0.8;\n            if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)\n                return 0.6;\n            return 0;\n        }\n        radius: Appearance.rounding.full\n        color: Colours.palette.m3secondary\n\n        MouseArea {\n            id: mouse\n\n            anchors.fill: parent\n            cursorShape: Qt.PointingHandCursor\n            hoverEnabled: true\n            acceptedButtons: Qt.NoButton\n        }\n\n        Behavior on opacity {\n            Anim {}\n        }\n    }\n\n    // Sync nonAnimPosition with flickable when not animating\n    Connections {\n        function onContentYChanged() {\n            if (!root.animating && !fullMouse.pressed) {\n                root._updatingFromFlickable = true;\n                const contentHeight = root.flickable.contentHeight;\n                const height = root.flickable.height;\n                if (contentHeight > height) {\n                    root.nonAnimPosition = Math.max(0, Math.min(1, root.flickable.contentY / (contentHeight - height)));\n                } else {\n                    root.nonAnimPosition = 0;\n                }\n                root._updatingFromFlickable = false;\n            }\n        }\n\n        target: root.flickable\n    }\n\n    Connections {\n        function onMovingChanged(): void {\n            if (root.flickable.moving)\n                root.shouldBeActive = true;\n            else\n                hideDelay.restart();\n        }\n\n        target: root.flickable\n    }\n\n    Timer {\n        id: hideDelay\n\n        interval: 600\n        onTriggered: root.shouldBeActive = root.flickable.moving || root.hovered\n    }\n\n    CustomMouseArea {\n        id: fullMouse\n\n        function onWheel(event: WheelEvent): void {\n            root.animating = true;\n            root._updatingFromUser = true;\n            let newPos = root.nonAnimPosition;\n            if (event.angleDelta.y > 0)\n                newPos = Math.max(0, root.nonAnimPosition - 0.1);\n            else if (event.angleDelta.y < 0)\n                newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1);\n            root.nonAnimPosition = newPos;\n            // Update flickable position\n            // Map scrollbar position [0, 1-size] to contentY [0, maxContentY]\n            if (root.flickable) {\n                const contentHeight = root.flickable.contentHeight;\n                const height = root.flickable.height;\n                if (contentHeight > height) {\n                    const maxContentY = contentHeight - height;\n                    const maxPos = 1 - root.size;\n                    const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;\n                    root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));\n                }\n            }\n        }\n\n        anchors.fill: parent\n        preventStealing: true\n\n        onPressed: event => {\n            root.animating = true;\n            root._updatingFromUser = true;\n            const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2));\n            root.nonAnimPosition = newPos;\n            // Update flickable position\n            // Map scrollbar position [0, 1-size] to contentY [0, maxContentY]\n            if (root.flickable) {\n                const contentHeight = root.flickable.contentHeight;\n                const height = root.flickable.height;\n                if (contentHeight > height) {\n                    const maxContentY = contentHeight - height;\n                    const maxPos = 1 - root.size;\n                    const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;\n                    root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));\n                }\n            }\n        }\n\n        onPositionChanged: event => {\n            root._updatingFromUser = true;\n            const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2));\n            root.nonAnimPosition = newPos;\n            // Update flickable position\n            // Map scrollbar position [0, 1-size] to contentY [0, maxContentY]\n            if (root.flickable) {\n                const contentHeight = root.flickable.contentHeight;\n                const height = root.flickable.height;\n                if (contentHeight > height) {\n                    const maxContentY = contentHeight - height;\n                    const maxPos = 1 - root.size;\n                    const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;\n                    root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));\n                }\n            }\n        }\n    }\n\n    Behavior on position {\n        enabled: !fullMouse.pressed\n\n        Anim {}\n    }\n}\n"
  },
  {
    "path": "components/controls/StyledSlider.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Templates\n\nSlider {\n    id: root\n\n    background: Item {\n        StyledRect {\n            anchors.top: parent.top\n            anchors.bottom: parent.bottom\n            anchors.left: parent.left\n            anchors.topMargin: root.implicitHeight / 3\n            anchors.bottomMargin: root.implicitHeight / 3\n\n            implicitWidth: root.handle.x - root.implicitHeight / 6\n\n            color: Colours.palette.m3primary\n            radius: Appearance.rounding.full\n            topRightRadius: root.implicitHeight / 15\n            bottomRightRadius: root.implicitHeight / 15\n        }\n\n        StyledRect {\n            anchors.top: parent.top\n            anchors.bottom: parent.bottom\n            anchors.right: parent.right\n            anchors.topMargin: root.implicitHeight / 3\n            anchors.bottomMargin: root.implicitHeight / 3\n\n            implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6\n\n            color: Colours.palette.m3surfaceContainerHighest\n            radius: Appearance.rounding.full\n            topLeftRadius: root.implicitHeight / 15\n            bottomLeftRadius: root.implicitHeight / 15\n        }\n    }\n\n    handle: StyledRect {\n        x: root.visualPosition * root.availableWidth - implicitWidth / 2\n\n        implicitWidth: root.implicitHeight / 4.5\n        implicitHeight: root.implicitHeight\n\n        color: Colours.palette.m3primary\n        radius: Appearance.rounding.full\n\n        MouseArea {\n            anchors.fill: parent\n            acceptedButtons: Qt.NoButton\n            cursorShape: Qt.PointingHandCursor\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/StyledSwitch.qml",
    "content": "import \"..\"\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Templates\nimport QtQuick.Shapes\n\nSwitch {\n    id: root\n\n    property int cLayer: 1\n\n    implicitWidth: implicitIndicatorWidth\n    implicitHeight: implicitIndicatorHeight\n\n    indicator: StyledRect {\n        radius: Appearance.rounding.full\n        color: root.checked ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHighest, root.cLayer)\n\n        implicitWidth: implicitHeight * 1.7\n        implicitHeight: Appearance.font.size.normal + Appearance.padding.smaller * 2\n\n        StyledRect {\n            readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight\n\n            radius: Appearance.rounding.full\n            color: root.checked ? Colours.palette.m3onPrimary : Colours.layer(Colours.palette.m3outline, root.cLayer + 1)\n\n            x: root.checked ? parent.implicitWidth - nonAnimWidth - Appearance.padding.small / 2 : Appearance.padding.small / 2\n            implicitWidth: nonAnimWidth\n            implicitHeight: parent.implicitHeight - Appearance.padding.small\n            anchors.verticalCenter: parent.verticalCenter\n\n            StyledRect {\n                anchors.fill: parent\n                radius: parent.radius\n\n                color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurface\n                opacity: root.pressed ? 0.1 : root.hovered ? 0.08 : 0\n\n                Behavior on opacity {\n                    Anim {}\n                }\n            }\n\n            Shape {\n                id: icon\n\n                property point start1: {\n                    if (root.pressed)\n                        return Qt.point(width * 0.2, height / 2);\n                    if (root.checked)\n                        return Qt.point(width * 0.15, height / 2);\n                    return Qt.point(width * 0.15, height * 0.15);\n                }\n                property point end1: {\n                    if (root.pressed) {\n                        if (root.checked)\n                            return Qt.point(width * 0.4, height / 2);\n                        return Qt.point(width * 0.8, height / 2);\n                    }\n                    if (root.checked)\n                        return Qt.point(width * 0.4, height * 0.7);\n                    return Qt.point(width * 0.85, height * 0.85);\n                }\n                property point start2: {\n                    if (root.pressed) {\n                        if (root.checked)\n                            return Qt.point(width * 0.4, height / 2);\n                        return Qt.point(width * 0.2, height / 2);\n                    }\n                    if (root.checked)\n                        return Qt.point(width * 0.4, height * 0.7);\n                    return Qt.point(width * 0.15, height * 0.85);\n                }\n                property point end2: {\n                    if (root.pressed)\n                        return Qt.point(width * 0.8, height / 2);\n                    if (root.checked)\n                        return Qt.point(width * 0.85, height * 0.2);\n                    return Qt.point(width * 0.85, height * 0.15);\n                }\n\n                anchors.centerIn: parent\n                width: height\n                height: parent.implicitHeight - Appearance.padding.small * 2\n                preferredRendererType: Shape.CurveRenderer\n                asynchronous: true\n\n                ShapePath {\n                    strokeWidth: Appearance.font.size.larger * 0.15\n                    strokeColor: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest\n                    fillColor: \"transparent\"\n                    capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap\n\n                    startX: icon.start1.x\n                    startY: icon.start1.y\n\n                    PathLine {\n                        x: icon.end1.x\n                        y: icon.end1.y\n                    }\n                    PathMove {\n                        x: icon.start2.x\n                        y: icon.start2.y\n                    }\n                    PathLine {\n                        x: icon.end2.x\n                        y: icon.end2.y\n                    }\n\n                    Behavior on strokeColor {\n                        CAnim {}\n                    }\n                }\n\n                Behavior on start1 {\n                    PropAnim {}\n                }\n                Behavior on end1 {\n                    PropAnim {}\n                }\n                Behavior on start2 {\n                    PropAnim {}\n                }\n                Behavior on end2 {\n                    PropAnim {}\n                }\n            }\n\n            Behavior on x {\n                Anim {}\n            }\n\n            Behavior on implicitWidth {\n                Anim {}\n            }\n        }\n    }\n\n    MouseArea {\n        anchors.fill: parent\n        cursorShape: Qt.PointingHandCursor\n        enabled: false\n    }\n\n    component PropAnim: PropertyAnimation {\n        duration: Appearance.anim.durations.normal\n        easing.type: Easing.BezierSpline\n        easing.bezierCurve: Appearance.anim.curves.standard\n    }\n}\n"
  },
  {
    "path": "components/controls/StyledTextField.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Controls\n\nTextField {\n    id: root\n\n    color: Colours.palette.m3onSurface\n    placeholderTextColor: Colours.palette.m3outline\n    font.family: Appearance.font.family.sans\n    font.pointSize: Appearance.font.size.smaller\n    renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering\n    cursorVisible: !readOnly\n\n    background: null\n\n    cursorDelegate: StyledRect {\n        id: cursor\n\n        property bool disableBlink\n\n        implicitWidth: 2\n        color: Colours.palette.m3primary\n        radius: Appearance.rounding.normal\n\n        Connections {\n            function onCursorPositionChanged(): void {\n                if (root.activeFocus && root.cursorVisible) {\n                    cursor.opacity = 1;\n                    cursor.disableBlink = true;\n                    enableBlink.restart();\n                }\n            }\n\n            target: root\n        }\n\n        Timer {\n            id: enableBlink\n\n            interval: 100\n            onTriggered: cursor.disableBlink = false\n        }\n\n        Timer {\n            running: root.activeFocus && root.cursorVisible && !cursor.disableBlink\n            repeat: true\n            triggeredOnStart: true\n            interval: 500\n            onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1\n        }\n\n        Binding {\n            when: !root.activeFocus || !root.cursorVisible\n            cursor.opacity: 0\n        }\n\n        Behavior on opacity {\n            Anim {\n                duration: Appearance.anim.durations.small\n            }\n        }\n    }\n\n    Behavior on color {\n        CAnim {}\n    }\n\n    Behavior on placeholderTextColor {\n        CAnim {}\n    }\n}\n"
  },
  {
    "path": "components/controls/SwitchRow.qml",
    "content": "import \"..\"\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property string label\n    required property bool checked\n    property bool enabled: true\n    property var onToggled: function (checked) {}\n\n    Layout.fillWidth: true\n    implicitHeight: row.implicitHeight + Appearance.padding.large * 2\n    radius: Appearance.rounding.normal\n    color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n\n    Behavior on implicitHeight {\n        Anim {}\n    }\n\n    RowLayout {\n        id: row\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.margins: Appearance.padding.large\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            Layout.fillWidth: true\n            text: root.label\n        }\n\n        StyledSwitch {\n            checked: root.checked\n            enabled: root.enabled\n            onToggled: {\n                root.onToggled(checked);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "components/controls/TextButton.qml",
    "content": "import \"..\"\nimport qs.services\nimport qs.config\nimport QtQuick\n\nStyledRect {\n    id: root\n\n    enum Type {\n        Filled,\n        Tonal,\n        Text\n    }\n\n    property alias text: label.text\n    property bool checked\n    property bool toggle\n    property real horizontalPadding: Appearance.padding.normal\n    property real verticalPadding: Appearance.padding.smaller\n    property alias font: label.font\n    property int type: TextButton.Filled\n\n    property alias stateLayer: stateLayer\n    property alias label: label\n\n    property bool internalChecked\n    property color activeColour: type === TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary\n    property color inactiveColour: {\n        if (!toggle && type === TextButton.Filled)\n            return Colours.palette.m3primary;\n        return type === TextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer;\n    }\n    property color activeOnColour: {\n        if (type === TextButton.Text)\n            return Colours.palette.m3primary;\n        return type === TextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary;\n    }\n    property color inactiveOnColour: {\n        if (!toggle && type === TextButton.Filled)\n            return Colours.palette.m3onPrimary;\n        if (type === TextButton.Text)\n            return Colours.palette.m3primary;\n        return type === TextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer;\n    }\n\n    signal clicked\n\n    onCheckedChanged: internalChecked = checked\n\n    radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)\n    color: type === TextButton.Text ? \"transparent\" : internalChecked ? activeColour : inactiveColour\n\n    implicitWidth: label.implicitWidth + horizontalPadding * 2\n    implicitHeight: label.implicitHeight + verticalPadding * 2\n\n    StateLayer {\n        id: stateLayer\n\n        function onClicked(): void {\n            if (root.toggle)\n                root.internalChecked = !root.internalChecked;\n            root.clicked();\n        }\n\n        color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour\n    }\n\n    StyledText {\n        id: label\n\n        anchors.centerIn: parent\n        color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour\n    }\n\n    Behavior on radius {\n        Anim {}\n    }\n}\n"
  },
  {
    "path": "components/controls/ToggleButton.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property bool toggled\n    property string icon\n    property string label\n    property string accent: \"Secondary\"\n    property real iconSize: Appearance.font.size.large\n    property real horizontalPadding: Appearance.padding.large\n    property real verticalPadding: Appearance.padding.normal\n    property string tooltip: \"\"\n    property bool hovered: false\n\n    signal clicked\n\n    Component.onCompleted: {\n        hovered = toggleStateLayer.containsMouse;\n    }\n    Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0)\n    implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2\n    implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2\n    radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale)\n    color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`]\n\n    Connections {\n        function onContainsMouseChanged() {\n            const newHovered = toggleStateLayer.containsMouse;\n            if (root.hovered !== newHovered) {\n                root.hovered = newHovered;\n            }\n        }\n\n        target: toggleStateLayer\n    }\n\n    StateLayer {\n        id: toggleStateLayer\n\n        function onClicked(): void {\n            root.clicked();\n        }\n\n        color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`]\n    }\n\n    RowLayout {\n        id: toggleBtnInner\n\n        anchors.centerIn: parent\n        spacing: Appearance.spacing.normal\n\n        MaterialIcon {\n            id: toggleBtnIcon\n\n            visible: !!text\n            fill: root.toggled ? 1 : 0\n            text: root.icon\n            color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`]\n            font.pointSize: root.iconSize\n\n            Behavior on fill {\n                Anim {}\n            }\n        }\n\n        Loader {\n            asynchronous: true\n            active: !!root.label\n            visible: active\n\n            sourceComponent: StyledText {\n                text: root.label\n                color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`]\n            }\n        }\n    }\n\n    Behavior on radius {\n        Anim {\n            duration: Appearance.anim.durations.expressiveFastSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n        }\n    }\n\n    Behavior on Layout.preferredWidth {\n        Anim {\n            duration: Appearance.anim.durations.expressiveFastSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n        }\n    }\n\n    // Tooltip - positioned absolutely, doesn't affect layout\n    Loader {\n        id: tooltipLoader\n\n        asynchronous: true\n        active: root.tooltip !== \"\"\n        z: 10000\n        width: 0\n        height: 0\n        sourceComponent: Component {\n            Tooltip {\n                target: root\n                text: root.tooltip\n            }\n        }\n        // Completely remove from layout\n        Layout.fillWidth: false\n        Layout.fillHeight: false\n        Layout.preferredWidth: 0\n        Layout.preferredHeight: 0\n        Layout.maximumWidth: 0\n        Layout.maximumHeight: 0\n        Layout.minimumWidth: 0\n        Layout.minimumHeight: 0\n    }\n}\n"
  },
  {
    "path": "components/controls/ToggleRow.qml",
    "content": "import qs.components\nimport qs.components.controls\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nRowLayout {\n    id: root\n\n    required property string label\n    property alias checked: toggle.checked\n    property alias toggle: toggle\n\n    Layout.fillWidth: true\n    spacing: Appearance.spacing.normal\n\n    StyledText {\n        Layout.fillWidth: true\n        text: root.label\n    }\n\n    StyledSwitch {\n        id: toggle\n\n        cLayer: 2\n    }\n}\n"
  },
  {
    "path": "components/controls/Tooltip.qml",
    "content": "import \"..\"\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Controls\n\nPopup {\n    id: root\n\n    required property Item target\n    required property string text\n    property int delay: 500\n    property int timeout: 0\n\n    property bool tooltipVisible: false\n    property Timer showTimer: Timer {\n        interval: root.delay\n        onTriggered: root.tooltipVisible = true\n    }\n    property Timer hideTimer: Timer {\n        interval: root.timeout\n        onTriggered: root.tooltipVisible = false\n    }\n\n    function updatePosition() {\n        if (!target || !parent)\n            return;\n\n        // Wait for tooltipRect to have its size calculated\n        Qt.callLater(() => {\n            if (!target || !parent || !tooltipRect)\n                return;\n\n            // Get target position in parent's coordinate system\n            const targetPos = target.mapToItem(parent, 0, 0);\n            const targetCenterX = targetPos.x + target.width / 2;\n\n            // Get tooltip size (use width/height if available, otherwise implicit)\n            const tooltipWidth = tooltipRect.width > 0 ? tooltipRect.width : tooltipRect.implicitWidth;\n            const tooltipHeight = tooltipRect.height > 0 ? tooltipRect.height : tooltipRect.implicitHeight;\n\n            // Center tooltip horizontally on target\n            let newX = targetCenterX - tooltipWidth / 2;\n\n            // Position tooltip above target\n            let newY = targetPos.y - tooltipHeight - Appearance.spacing.small;\n\n            // Keep within bounds\n            const padding = Appearance.padding.normal;\n            if (newX < padding) {\n                newX = padding;\n            } else if (newX + tooltipWidth > (parent.width - padding)) {\n                newX = parent.width - tooltipWidth - padding;\n            }\n\n            // Update popup position\n            x = newX;\n            y = newY;\n        });\n    }\n\n    // Popup properties - doesn't affect layout\n    parent: {\n        let p = target;\n        // Walk up to find the root Item (usually has anchors.fill: parent)\n        while (p && p.parent) {\n            const parentItem = p.parent;\n            // Check if this looks like a root pane Item\n            if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) {\n                return parentItem;\n            }\n            p = parentItem;\n        }\n        // Fallback\n        return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target;\n    }\n\n    visible: tooltipVisible\n    modal: false\n    closePolicy: Popup.NoAutoClose\n    padding: 0\n    margins: 0\n    background: Item {}\n\n    // Update position when target moves or tooltip becomes visible\n    onTooltipVisibleChanged: {\n        if (tooltipVisible) {\n            Qt.callLater(updatePosition);\n        }\n    }\n    Component.onCompleted: {\n        if (tooltipVisible) {\n            updatePosition();\n        }\n    }\n\n    enter: Transition {\n        Anim {\n            property: \"opacity\"\n            from: 0\n            to: 1\n            duration: Appearance.anim.durations.expressiveFastSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n        }\n    }\n\n    exit: Transition {\n        Anim {\n            property: \"opacity\"\n            from: 1\n            to: 0\n            duration: Appearance.anim.durations.expressiveFastSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n        }\n    }\n\n    contentItem: StyledRect {\n        id: tooltipRect\n\n        implicitWidth: tooltipText.implicitWidth + Appearance.padding.normal * 2\n        implicitHeight: tooltipText.implicitHeight + Appearance.padding.smaller * 2\n\n        color: Colours.palette.m3surfaceContainerHighest\n        radius: Appearance.rounding.small\n        antialiasing: true\n\n        // Add elevation for depth\n        Elevation {\n            anchors.fill: parent\n            radius: parent.radius\n            z: -1\n            level: 3\n        }\n\n        StyledText {\n            id: tooltipText\n\n            anchors.centerIn: parent\n\n            text: root.text\n            color: Colours.palette.m3onSurface\n            font.pointSize: Appearance.font.size.small\n        }\n    }\n\n    Connections {\n        function onXChanged() {\n            if (root.tooltipVisible)\n                root.updatePosition();\n        }\n        function onYChanged() {\n            if (root.tooltipVisible)\n                root.updatePosition();\n        }\n        function onWidthChanged() {\n            if (root.tooltipVisible)\n                root.updatePosition();\n        }\n        function onHeightChanged() {\n            if (root.tooltipVisible)\n                root.updatePosition();\n        }\n\n        target: root.target\n    }\n\n    // Monitor hover state\n    Connections {\n        function onHoveredChanged() {\n            if (target.hovered) {\n                showTimer.start();\n                if (timeout > 0) {\n                    hideTimer.stop();\n                    hideTimer.start();\n                }\n            } else {\n                showTimer.stop();\n                hideTimer.stop();\n                tooltipVisible = false;\n            }\n        }\n\n        target: root.target\n    }\n}\n"
  },
  {
    "path": "components/effects/ColouredIcon.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport Caelestia\nimport Quickshell.Widgets\nimport QtQuick\n\nIconImage {\n    id: root\n\n    required property color colour\n\n    asynchronous: true\n\n    layer.enabled: true\n    layer.effect: Colouriser {\n        sourceColor: analyser.dominantColour\n        colorizationColor: root.colour\n    }\n\n    layer.onEnabledChanged: {\n        if (layer.enabled && status === Image.Ready)\n            analyser.requestUpdate();\n    }\n\n    onStatusChanged: {\n        if (layer.enabled && status === Image.Ready)\n            analyser.requestUpdate();\n    }\n\n    ImageAnalyser {\n        id: analyser\n\n        sourceItem: root\n    }\n}\n"
  },
  {
    "path": "components/effects/Colouriser.qml",
    "content": "import \"..\"\nimport QtQuick\nimport QtQuick.Effects\n\nMultiEffect {\n    property color sourceColor: \"black\"\n\n    colorization: 1\n    brightness: 1 - sourceColor.hslLightness\n\n    Behavior on colorizationColor {\n        CAnim {}\n    }\n}\n"
  },
  {
    "path": "components/effects/Elevation.qml",
    "content": "import \"..\"\nimport qs.services\nimport QtQuick\nimport QtQuick.Effects\n\nRectangularShadow {\n    property int level\n    property real dp: [0, 1, 3, 6, 8, 12][level]\n\n    color: Qt.alpha(Colours.palette.m3shadow, 0.7)\n    blur: (dp * 5) ** 0.7\n    spread: -dp * 0.3 + (dp * 0.1) ** 2\n    offset.y: dp / 2\n\n    Behavior on dp {\n        Anim {}\n    }\n}\n"
  },
  {
    "path": "components/effects/InnerBorder.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Effects\n\nStyledRect {\n    property alias innerRadius: maskInner.radius\n    property alias thickness: maskInner.anchors.margins\n    property alias leftThickness: maskInner.anchors.leftMargin\n    property alias topThickness: maskInner.anchors.topMargin\n    property alias rightThickness: maskInner.anchors.rightMargin\n    property alias bottomThickness: maskInner.anchors.bottomMargin\n\n    anchors.fill: parent\n    color: Colours.tPalette.m3surfaceContainer\n\n    layer.enabled: true\n    layer.effect: MultiEffect {\n        maskSource: mask\n        maskEnabled: true\n        maskInverted: true\n        maskThresholdMin: 0.5\n        maskSpreadAtMin: 1\n    }\n\n    Item {\n        id: mask\n\n        anchors.fill: parent\n        layer.enabled: true\n        visible: false\n\n        Rectangle {\n            id: maskInner\n\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.normal\n            radius: Appearance.rounding.small\n        }\n    }\n}\n"
  },
  {
    "path": "components/effects/OpacityMask.qml",
    "content": "import Quickshell\nimport QtQuick\n\nShaderEffect {\n    required property Item source\n    required property Item maskSource\n\n    fragmentShader: Quickshell.shellPath(\"assets/shaders/opacitymask.frag.qsb\")\n}\n"
  },
  {
    "path": "components/filedialog/CurrentItem.qml",
    "content": "import \"..\"\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Shapes\n\nItem {\n    id: root\n\n    required property var currentItem\n\n    implicitWidth: content.implicitWidth + Appearance.padding.larger + content.anchors.rightMargin\n    implicitHeight: currentItem ? content.implicitHeight + Appearance.padding.normal + content.anchors.bottomMargin : 0\n\n    Shape {\n        preferredRendererType: Shape.CurveRenderer\n\n        ShapePath {\n            id: path\n\n            readonly property real rounding: Appearance.rounding.small\n            readonly property bool flatten: root.implicitHeight < rounding * 2\n            readonly property real roundingY: flatten ? root.implicitHeight / 2 : rounding\n\n            strokeWidth: -1\n            fillColor: Colours.tPalette.m3surfaceContainer\n\n            startX: root.implicitWidth\n            startY: root.implicitHeight\n\n            PathLine {\n                relativeX: -(root.implicitWidth + path.rounding)\n                relativeY: 0\n            }\n            PathArc {\n                relativeX: path.rounding\n                relativeY: -path.roundingY\n                radiusX: path.rounding\n                radiusY: Math.min(path.rounding, root.implicitHeight)\n                direction: PathArc.Counterclockwise\n            }\n            PathLine {\n                relativeX: 0\n                relativeY: -(root.implicitHeight - path.roundingY * 2)\n            }\n            PathArc {\n                relativeX: path.rounding\n                relativeY: -path.roundingY\n                radiusX: path.rounding\n                radiusY: Math.min(path.rounding, root.implicitHeight)\n            }\n            PathLine {\n                relativeX: root.implicitHeight > 0 ? root.implicitWidth - path.rounding * 2 : root.implicitWidth\n                relativeY: 0\n            }\n            PathArc {\n                relativeX: path.rounding\n                relativeY: -path.rounding\n                radiusX: path.rounding\n                radiusY: path.rounding\n                direction: PathArc.Counterclockwise\n            }\n\n            Behavior on fillColor {\n                CAnim {}\n            }\n        }\n    }\n\n    Item {\n        anchors.fill: parent\n        clip: true\n\n        StyledText {\n            id: content\n\n            anchors.right: parent.right\n            anchors.bottom: parent.bottom\n            anchors.rightMargin: Appearance.padding.larger - Appearance.padding.small\n            anchors.bottomMargin: Appearance.padding.normal - Appearance.padding.small\n\n            Connections {\n                function onCurrentItemChanged(): void {\n                    if (root.currentItem)\n                        content.text = qsTr(`\"%1\" selected`).arg(root.currentItem.modelData.name);\n                }\n\n                target: root\n            }\n        }\n    }\n\n    Behavior on implicitWidth {\n        enabled: !!root.currentItem\n\n        Anim {}\n    }\n\n    Behavior on implicitHeight {\n        Anim {}\n    }\n}\n"
  },
  {
    "path": "components/filedialog/DialogButtons.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property var dialog\n    required property FolderContents folder\n\n    implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2\n\n    color: Colours.tPalette.m3surfaceContainer\n\n    RowLayout {\n        id: inner\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.normal\n\n        spacing: Appearance.spacing.small\n\n        StyledText {\n            text: qsTr(\"Filter:\")\n        }\n\n        StyledRect {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n            Layout.rightMargin: Appearance.spacing.normal\n\n            color: Colours.tPalette.m3surfaceContainerHigh\n            radius: Appearance.rounding.small\n\n            StyledText {\n                anchors.fill: parent\n                anchors.margins: Appearance.padding.normal\n\n                text: `${root.dialog.filterLabel} (${root.dialog.filters.map(f => `*.${f}`).join(\", \")})`\n            }\n        }\n\n        StyledRect {\n            color: Colours.tPalette.m3surfaceContainerHigh\n            radius: Appearance.rounding.small\n\n            implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2\n            implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2\n\n            StateLayer {\n                function onClicked(): void {\n                    root.dialog.accepted(root.folder.currentItem.modelData.path);\n                }\n\n                disabled: !root.dialog.selectionValid\n            }\n\n            StyledText {\n                id: selectText\n\n                anchors.centerIn: parent\n                anchors.margins: Appearance.padding.normal\n\n                text: qsTr(\"Select\")\n                color: root.dialog.selectionValid ? Colours.palette.m3onSurface : Colours.palette.m3outline\n            }\n        }\n\n        StyledRect {\n            color: Colours.tPalette.m3surfaceContainerHigh\n            radius: Appearance.rounding.small\n\n            implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2\n            implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2\n\n            StateLayer {\n                function onClicked(): void {\n                    root.dialog.rejected();\n                }\n            }\n\n            StyledText {\n                id: cancelText\n\n                anchors.centerIn: parent\n                anchors.margins: Appearance.padding.normal\n\n                text: qsTr(\"Cancel\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "components/filedialog/FileDialog.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nLazyLoader {\n    id: loader\n\n    property list<string> cwd: [\"Home\"]\n    property string filterLabel: \"All files\"\n    property list<string> filters: [\"*\"]\n    property string title: qsTr(\"Select a file\")\n\n    signal accepted(path: string)\n    signal rejected\n\n    function open(): void {\n        activeAsync = true;\n    }\n\n    function close(): void {\n        rejected();\n    }\n\n    onAccepted: activeAsync = false\n    onRejected: activeAsync = false\n\n    FloatingWindow {\n        id: root\n\n        property list<string> cwd: loader.cwd\n        property string filterLabel: loader.filterLabel\n        property list<string> filters: loader.filters\n\n        readonly property bool selectionValid: {\n            const file = folderContents.currentItem?.modelData;\n            return (file && !file.isDir && (filters.includes(\"*\") || filters.includes(file.suffix))) ?? false;\n        }\n\n        function accepted(path: string): void {\n            loader.accepted(path);\n        }\n\n        function rejected(): void {\n            loader.rejected();\n        }\n\n        implicitWidth: 1000\n        implicitHeight: 600\n        color: Colours.tPalette.m3surface\n        title: loader.title\n\n        onVisibleChanged: {\n            if (!visible)\n                rejected();\n        }\n\n        RowLayout {\n            anchors.fill: parent\n\n            spacing: 0\n\n            Sidebar {\n                Layout.fillHeight: true\n                dialog: root\n            }\n\n            ColumnLayout {\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n\n                spacing: 0\n\n                HeaderBar {\n                    Layout.fillWidth: true\n                    dialog: root\n                }\n\n                FolderContents {\n                    id: folderContents\n\n                    Layout.fillWidth: true\n                    Layout.fillHeight: true\n                    dialog: root\n                }\n\n                DialogButtons {\n                    Layout.fillWidth: true\n                    dialog: root\n                    folder: folderContents\n                }\n            }\n        }\n\n        Behavior on color {\n            CAnim {}\n        }\n    }\n}\n"
  },
  {
    "path": "components/filedialog/FolderContents.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.filedialog\nimport qs.components.controls\nimport qs.components.images\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Caelestia.Models\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Effects\n\nItem {\n    id: root\n\n    required property var dialog\n    readonly property FileEntry currentItem: view.currentItem as FileEntry\n\n    StyledRect {\n        anchors.fill: parent\n        color: Colours.tPalette.m3surfaceContainer\n\n        layer.enabled: true\n        layer.effect: MultiEffect {\n            maskSource: mask\n            maskEnabled: true\n            maskInverted: true\n            maskThresholdMin: 0.5\n            maskSpreadAtMin: 1\n        }\n    }\n\n    Item {\n        id: mask\n\n        anchors.fill: parent\n        layer.enabled: true\n        visible: false\n\n        Rectangle {\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.small\n            radius: Appearance.rounding.small\n        }\n    }\n\n    Loader {\n        asynchronous: true\n        anchors.centerIn: parent\n\n        opacity: view.count === 0 ? 1 : 0\n        active: opacity > 0\n\n        sourceComponent: ColumnLayout {\n            MaterialIcon {\n                Layout.alignment: Qt.AlignHCenter\n                text: \"scan_delete\"\n                color: Colours.palette.m3outline\n                font.pointSize: Appearance.font.size.extraLarge * 2\n                font.weight: 500\n            }\n\n            StyledText {\n                text: qsTr(\"This folder is empty\")\n                color: Colours.palette.m3outline\n                font.pointSize: Appearance.font.size.large\n                font.weight: 500\n            }\n        }\n\n        Behavior on opacity {\n            Anim {}\n        }\n    }\n\n    GridView {\n        id: view\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.small + Appearance.padding.normal\n\n        cellWidth: Sizes.itemWidth + Appearance.spacing.small\n        cellHeight: Sizes.itemWidth + Appearance.spacing.small * 2 + Appearance.padding.normal * 2 + 1\n\n        clip: true\n        focus: true\n        currentIndex: -1\n        Keys.onEscapePressed: currentIndex = -1\n\n        Keys.onReturnPressed: {\n            if (root.dialog.selectionValid)\n                root.dialog.accepted((currentItem as FileEntry).modelData.path);\n        }\n        Keys.onEnterPressed: {\n            if (root.dialog.selectionValid)\n                root.dialog.accepted((currentItem as FileEntry).modelData.path);\n        }\n\n        StyledScrollBar.vertical: StyledScrollBar {\n            flickable: view\n        }\n\n        model: FileSystemModel {\n            path: {\n                if (root.dialog.cwd[0] === \"Home\")\n                    return Paths.home + `/${root.dialog.cwd.slice(1).join(\"/\")}`;\n                else\n                    return root.dialog.cwd.join(\"/\");\n            }\n            onPathChanged: view.currentIndex = -1\n        }\n\n        delegate: FileEntry {}\n\n        add: Transition {\n            Anim {\n                properties: \"opacity,scale\"\n                from: 0\n                to: 1\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        }\n\n        remove: Transition {\n            Anim {\n                property: \"opacity\"\n                to: 0\n            }\n            Anim {\n                property: \"scale\"\n                to: 0.5\n            }\n        }\n\n        displaced: Transition {\n            Anim {\n                properties: \"opacity,scale\"\n                to: 1\n                easing.bezierCurve: Appearance.anim.curves.standardDecel\n            }\n            Anim {\n                properties: \"x,y\"\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        }\n    }\n\n    CurrentItem {\n        anchors.right: parent.right\n        anchors.bottom: parent.bottom\n        anchors.margins: Appearance.padding.small\n\n        currentItem: view.currentItem\n    }\n\n    component FileEntry: StyledRect {\n        id: item\n\n        required property int index\n        required property FileSystemEntry modelData\n\n        readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2\n\n        implicitWidth: Sizes.itemWidth\n        implicitHeight: nonAnimHeight\n\n        radius: Appearance.rounding.normal\n        color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0)\n        z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0\n        clip: true\n\n        StateLayer {\n            function onClicked(): void {\n                view.currentIndex = item.index;\n            }\n\n            onDoubleClicked: {\n                if (item.modelData.isDir)\n                    root.dialog.cwd.push(item.modelData.name);\n                else if (root.dialog.selectionValid)\n                    root.dialog.accepted(item.modelData.path);\n            }\n        }\n\n        CachingIconImage {\n            id: icon\n\n            anchors.horizontalCenter: parent.horizontalCenter\n            anchors.top: parent.top\n            anchors.topMargin: Appearance.padding.normal\n\n            implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2\n\n            Component.onCompleted: {\n                const file = item.modelData;\n                if (file.isImage)\n                    source = Qt.resolvedUrl(file.path);\n                else if (!file.isDir)\n                    source = Quickshell.iconPath(file.mimeType.replace(\"/\", \"-\"), \"application-x-zerosize\");\n                else if (root.dialog.cwd.length === 1 && [\"Desktop\", \"Documents\", \"Downloads\", \"Music\", \"Pictures\", \"Public\", \"Templates\", \"Videos\"].includes(file.name))\n                    source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`);\n                else\n                    source = Quickshell.iconPath(\"inode-directory\");\n            }\n        }\n\n        StyledText {\n            id: name\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            anchors.top: icon.bottom\n            anchors.topMargin: Appearance.spacing.small\n            anchors.margins: Appearance.padding.normal\n\n            horizontalAlignment: Text.AlignHCenter\n            elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight\n            wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap\n\n            Component.onCompleted: text = item.modelData.name\n        }\n\n        Behavior on implicitHeight {\n            Anim {}\n        }\n    }\n}\n"
  },
  {
    "path": "components/filedialog/HeaderBar.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property var dialog\n\n    implicitWidth: inner.implicitWidth + Appearance.padding.normal * 2\n    implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2\n\n    color: Colours.tPalette.m3surfaceContainer\n\n    RowLayout {\n        id: inner\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.normal\n        spacing: Appearance.spacing.small\n\n        Item {\n            implicitWidth: implicitHeight\n            implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2\n\n            StateLayer {\n                function onClicked(): void {\n                    root.dialog.cwd.pop();\n                }\n\n                radius: Appearance.rounding.small\n                disabled: root.dialog.cwd.length === 1\n            }\n\n            MaterialIcon {\n                id: upIcon\n\n                anchors.centerIn: parent\n                text: \"drive_folder_upload\"\n                color: root.dialog.cwd.length === 1 ? Colours.palette.m3outline : Colours.palette.m3onSurface\n                grade: 200\n            }\n        }\n\n        StyledRect {\n            Layout.fillWidth: true\n\n            radius: Appearance.rounding.small\n            color: Colours.tPalette.m3surfaceContainerHigh\n\n            implicitHeight: pathComponents.implicitHeight + pathComponents.anchors.margins * 2\n\n            RowLayout {\n                id: pathComponents\n\n                anchors.fill: parent\n                anchors.margins: Appearance.padding.small / 2\n                anchors.leftMargin: 0\n\n                spacing: Appearance.spacing.small\n\n                Repeater {\n                    model: root.dialog.cwd\n\n                    RowLayout {\n                        id: folder\n\n                        required property string modelData\n                        required property int index\n\n                        spacing: 0\n\n                        Loader {\n                            asynchronous: true\n                            Layout.rightMargin: Appearance.spacing.small\n                            active: folder.index > 0\n                            sourceComponent: StyledText {\n                                text: \"/\"\n                                color: Colours.palette.m3onSurfaceVariant\n                                font.bold: true\n                            }\n                        }\n\n                        Item {\n                            implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Appearance.padding.small : 0) + folderName.implicitWidth + Appearance.padding.normal * 2\n                            implicitHeight: folderName.implicitHeight + Appearance.padding.small * 2\n\n                            Loader {\n                                asynchronous: true\n                                anchors.fill: parent\n                                active: folder.index < root.dialog.cwd.length - 1\n                                sourceComponent: StateLayer {\n                                    function onClicked(): void {\n                                        root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1);\n                                    }\n\n                                    radius: Appearance.rounding.small\n                                }\n                            }\n\n                            Loader {\n                                id: homeIcon\n\n                                asynchronous: true\n\n                                anchors.left: parent.left\n                                anchors.verticalCenter: parent.verticalCenter\n                                anchors.leftMargin: Appearance.padding.normal\n\n                                active: folder.index === 0 && folder.modelData === \"Home\"\n                                sourceComponent: MaterialIcon {\n                                    text: \"home\"\n                                    color: root.dialog.cwd.length === 1 ? Colours.palette.m3onSurface : Colours.palette.m3onSurfaceVariant\n                                    fill: 1\n                                }\n                            }\n\n                            StyledText {\n                                id: folderName\n\n                                anchors.left: homeIcon.right\n                                anchors.verticalCenter: parent.verticalCenter\n                                anchors.leftMargin: homeIcon.active ? Appearance.padding.small : 0\n\n                                text: folder.modelData\n                                color: folder.index < root.dialog.cwd.length - 1 ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSurface\n                                font.bold: true\n                            }\n                        }\n                    }\n                }\n\n                Item {\n                    Layout.fillWidth: true\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "components/filedialog/Sidebar.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.filedialog\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property var dialog\n\n    implicitWidth: Sizes.sidebarWidth\n    implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2\n\n    color: Colours.tPalette.m3surfaceContainer\n\n    ColumnLayout {\n        id: inner\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.top: parent.top\n        anchors.margins: Appearance.padding.normal\n        spacing: Appearance.spacing.small / 2\n\n        StyledText {\n            Layout.alignment: Qt.AlignHCenter\n            Layout.topMargin: Appearance.padding.small / 2\n            Layout.bottomMargin: Appearance.spacing.normal\n            text: qsTr(\"Files\")\n            color: Colours.palette.m3onSurface\n            font.pointSize: Appearance.font.size.larger\n            font.bold: true\n        }\n\n        Repeater {\n            model: [\"Home\", \"Downloads\", \"Desktop\", \"Documents\", \"Music\", \"Pictures\", \"Videos\"]\n\n            StyledRect {\n                id: place\n\n                required property string modelData\n                readonly property bool selected: modelData === root.dialog.cwd[root.dialog.cwd.length - 1]\n\n                Layout.fillWidth: true\n                implicitHeight: placeInner.implicitHeight + Appearance.padding.normal * 2\n\n                radius: Appearance.rounding.full\n                color: Qt.alpha(Colours.palette.m3secondaryContainer, selected ? 1 : 0)\n\n                StateLayer {\n                    function onClicked(): void {\n                        if (place.modelData === \"Home\")\n                            root.dialog.cwd = [\"Home\"];\n                        else\n                            root.dialog.cwd = [\"Home\", place.modelData];\n                    }\n\n                    color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n                }\n\n                RowLayout {\n                    id: placeInner\n\n                    anchors.fill: parent\n                    anchors.margins: Appearance.padding.normal\n                    anchors.leftMargin: Appearance.padding.large\n                    anchors.rightMargin: Appearance.padding.large\n\n                    spacing: Appearance.spacing.normal\n\n                    MaterialIcon {\n                        text: {\n                            const p = place.modelData;\n                            if (p === \"Home\")\n                                return \"home\";\n                            if (p === \"Downloads\")\n                                return \"file_download\";\n                            if (p === \"Desktop\")\n                                return \"desktop_windows\";\n                            if (p === \"Documents\")\n                                return \"description\";\n                            if (p === \"Music\")\n                                return \"music_note\";\n                            if (p === \"Pictures\")\n                                return \"image\";\n                            if (p === \"Videos\")\n                                return \"video_library\";\n                            return \"folder\";\n                        }\n                        color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n                        font.pointSize: Appearance.font.size.large\n                        fill: place.selected ? 1 : 0\n\n                        Behavior on fill {\n                            Anim {}\n                        }\n                    }\n\n                    StyledText {\n                        Layout.fillWidth: true\n                        text: place.modelData\n                        color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n                        font.pointSize: Appearance.font.size.normal\n                        elide: Text.ElideRight\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "components/filedialog/Sizes.qml",
    "content": "pragma Singleton\n\nimport Quickshell\n\nSingleton {\n    property int itemWidth: 103\n    property int sidebarWidth: 200\n}\n"
  },
  {
    "path": "components/images/CachingIconImage.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.utils\nimport Quickshell.Widgets\nimport QtQuick\n\nItem {\n    id: root\n\n    // Easier (and more efficient) to ignore it than to check type and cast\n    readonly property int status: loader.item?.status ?? Image.Null // qmllint disable missing-property\n    readonly property real actualSize: Math.min(width, height)\n    property real implicitSize\n    property url source\n\n    implicitWidth: implicitSize\n    implicitHeight: implicitSize\n\n    Loader {\n        id: loader\n\n        asynchronous: true\n        anchors.fill: parent\n        sourceComponent: root.source ? root.source.toString().startsWith(\"image://icon/\") ? iconImage : cachingImage : null\n    }\n\n    Component {\n        id: cachingImage\n\n        CachingImage {\n            path: Paths.toLocalFile(root.source)\n            fillMode: Image.PreserveAspectFit\n        }\n    }\n\n    Component {\n        id: iconImage\n\n        IconImage {\n            source: root.source\n            asynchronous: true\n        }\n    }\n}\n"
  },
  {
    "path": "components/images/CachingImage.qml",
    "content": "import qs.utils\nimport Caelestia.Internal\nimport Quickshell\nimport QtQuick\n\nImage {\n    id: root\n\n    property alias path: manager.path\n\n    asynchronous: true\n    fillMode: Image.PreserveAspectCrop\n\n    Connections {\n        function onDevicePixelRatioChanged(): void {\n            manager.updateSource();\n        }\n\n        target: QsWindow.window\n    }\n\n    CachingImageManager {\n        id: manager\n\n        item: root\n        cacheDir: Qt.resolvedUrl(Paths.imagecache)\n    }\n}\n"
  },
  {
    "path": "components/misc/CustomShortcut.qml",
    "content": "import Quickshell.Hyprland\n\nGlobalShortcut {\n    appid: \"caelestia\"\n}\n"
  },
  {
    "path": "components/misc/Ref.qml",
    "content": "import QtQuick\n\nQtObject {\n    required property var service\n\n    Component.onCompleted: service.refCount++\n    Component.onDestruction: service.refCount--\n}\n"
  },
  {
    "path": "components/widgets/ExtraIndicator.qml",
    "content": "import \"..\"\nimport \"../effects\"\nimport qs.services\nimport qs.config\nimport QtQuick\n\nStyledRect {\n    required property int extra\n\n    anchors.right: parent.right\n    anchors.margins: Appearance.padding.normal\n\n    color: Colours.palette.m3tertiary\n    radius: Appearance.rounding.small\n\n    implicitWidth: count.implicitWidth + Appearance.padding.normal * 2\n    implicitHeight: count.implicitHeight + Appearance.padding.small * 2\n\n    opacity: extra > 0 ? 1 : 0\n    scale: extra > 0 ? 1 : 0.5\n\n    Elevation {\n        anchors.fill: parent\n        radius: parent.radius\n        opacity: parent.opacity\n        z: -1\n        level: 2\n    }\n\n    StyledText {\n        id: count\n\n        anchors.centerIn: parent\n        animate: parent.opacity > 0\n        text: qsTr(\"+%1\").arg(parent.extra)\n        color: Colours.palette.m3onTertiary\n    }\n\n    Behavior on opacity {\n        Anim {\n            duration: Appearance.anim.durations.expressiveFastSpatial\n        }\n    }\n\n    Behavior on scale {\n        Anim {\n            duration: Appearance.anim.durations.expressiveFastSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n        }\n    }\n}\n"
  },
  {
    "path": "config/Appearance.qml",
    "content": "pragma Singleton\n\nimport Quickshell\n\nSingleton {\n    // Literally just here to shorten accessing stuff :woe:\n    // Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Config.appearance.xxx`\n    readonly property AppearanceConfig.Rounding rounding: Config.appearance.rounding\n    readonly property AppearanceConfig.Spacing spacing: Config.appearance.spacing\n    readonly property AppearanceConfig.Padding padding: Config.appearance.padding\n    readonly property AppearanceConfig.FontStuff font: Config.appearance.font\n    readonly property AppearanceConfig.Anim anim: Config.appearance.anim\n    readonly property AppearanceConfig.Transparency transparency: Config.appearance.transparency\n}\n"
  },
  {
    "path": "config/AppearanceConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property Rounding rounding: Rounding {}\n    property Spacing spacing: Spacing {}\n    property Padding padding: Padding {}\n    property FontStuff font: FontStuff {}\n    property Anim anim: Anim {}\n    property Transparency transparency: Transparency {}\n\n    component Rounding: JsonObject {\n        property real scale: 1\n        property int small: 12 * scale\n        property int normal: 17 * scale\n        property int large: 25 * scale\n        property int full: 1000 * scale\n    }\n\n    component Spacing: JsonObject {\n        property real scale: 1\n        property int small: 7 * scale\n        property int smaller: 10 * scale\n        property int normal: 12 * scale\n        property int larger: 15 * scale\n        property int large: 20 * scale\n    }\n\n    component Padding: JsonObject {\n        property real scale: 1\n        property int small: 5 * scale\n        property int smaller: 7 * scale\n        property int normal: 10 * scale\n        property int larger: 12 * scale\n        property int large: 15 * scale\n    }\n\n    component FontFamily: JsonObject {\n        property string sans: \"Rubik\"\n        property string mono: \"CaskaydiaCove NF\"\n        property string material: \"Material Symbols Rounded\"\n        property string clock: \"Rubik\"\n    }\n\n    component FontSize: JsonObject {\n        property real scale: 1\n        property int small: 11 * scale\n        property int smaller: 12 * scale\n        property int normal: 13 * scale\n        property int larger: 15 * scale\n        property int large: 18 * scale\n        property int extraLarge: 28 * scale\n    }\n\n    component FontStuff: JsonObject {\n        property FontFamily family: FontFamily {}\n        property FontSize size: FontSize {}\n    }\n\n    component AnimCurves: JsonObject {\n        property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]\n        property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]\n        property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]\n        property list<real> standard: [0.2, 0, 0, 1, 1, 1]\n        property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]\n        property list<real> standardDecel: [0, 0, 0, 1, 1, 1]\n        property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]\n        property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]\n        property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]\n    }\n\n    component AnimDurations: JsonObject {\n        property real scale: 1\n        property int small: 200 * scale\n        property int normal: 400 * scale\n        property int large: 600 * scale\n        property int extraLarge: 1000 * scale\n        property int expressiveFastSpatial: 350 * scale\n        property int expressiveDefaultSpatial: 500 * scale\n        property int expressiveEffects: 200 * scale\n    }\n\n    component Anim: JsonObject {\n        property real mediaGifSpeedAdjustment: 300\n        property real sessionGifSpeed: 0.7\n        property AnimCurves curves: AnimCurves {}\n        property AnimDurations durations: AnimDurations {}\n    }\n\n    component Transparency: JsonObject {\n        property bool enabled: false\n        property real base: 0.85\n        property real layers: 0.4\n    }\n}\n"
  },
  {
    "path": "config/BackgroundConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property bool enabled: true\n    property bool wallpaperEnabled: true\n    property DesktopClock desktopClock: DesktopClock {}\n    property Visualiser visualiser: Visualiser {}\n\n    component DesktopClock: JsonObject {\n        property bool enabled: false\n        property real scale: 1.0\n        property string position: \"bottom-right\"\n        property bool invertColors: false\n        property DesktopClockBackground background: DesktopClockBackground {}\n        property DesktopClockShadow shadow: DesktopClockShadow {}\n    }\n\n    component DesktopClockBackground: JsonObject {\n        property bool enabled: false\n        property real opacity: 0.7\n        property bool blur: true\n    }\n\n    component DesktopClockShadow: JsonObject {\n        property bool enabled: true\n        property real opacity: 0.7\n        property real blur: 0.4\n    }\n\n    component Visualiser: JsonObject {\n        property bool enabled: false\n        property bool autoHide: true\n        property bool blur: false\n        property real rounding: 1\n        property real spacing: 1\n    }\n}\n"
  },
  {
    "path": "config/BarConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property bool persistent: true\n    property bool showOnHover: true\n    property int dragThreshold: 20\n    property ScrollActions scrollActions: ScrollActions {}\n    property Popouts popouts: Popouts {}\n    property Workspaces workspaces: Workspaces {}\n    property ActiveWindow activeWindow: ActiveWindow {}\n    property Tray tray: Tray {}\n    property Status status: Status {}\n    property Clock clock: Clock {}\n    property Sizes sizes: Sizes {}\n    property list<string> excludedScreens: []\n\n    property list<var> entries: [\n        {\n            id: \"logo\",\n            enabled: true\n        },\n        {\n            id: \"workspaces\",\n            enabled: true\n        },\n        {\n            id: \"spacer\",\n            enabled: true\n        },\n        {\n            id: \"activeWindow\",\n            enabled: true\n        },\n        {\n            id: \"spacer\",\n            enabled: true\n        },\n        {\n            id: \"tray\",\n            enabled: true\n        },\n        {\n            id: \"clock\",\n            enabled: true\n        },\n        {\n            id: \"statusIcons\",\n            enabled: true\n        },\n        {\n            id: \"power\",\n            enabled: true\n        }\n    ]\n\n    component ScrollActions: JsonObject {\n        property bool workspaces: true\n        property bool volume: true\n        property bool brightness: true\n    }\n\n    component Popouts: JsonObject {\n        property bool activeWindow: true\n        property bool tray: true\n        property bool statusIcons: true\n    }\n\n    component Workspaces: JsonObject {\n        property int shown: 5\n        property bool activeIndicator: true\n        property bool occupiedBg: false\n        property bool showWindows: true\n        property bool showWindowsOnSpecialWorkspaces: showWindows\n        property int maxWindowIcons: 0 // 0 = unlimited\n        property bool activeTrail: false\n        property bool perMonitorWorkspaces: true\n        property string label: \"  \" // if empty, will show workspace name's first letter\n        property string occupiedLabel: \"󰮯\"\n        property string activeLabel: \"󰮯\"\n        property string capitalisation: \"preserve\" // upper, lower, or preserve - relevant only if label is empty\n        property list<var> specialWorkspaceIcons: []\n        property list<var> windowIcons: [\n            {\n                regex: \"steam(_app_(default|[0-9]+))?\",\n                icon: \"sports_esports\"\n            }\n        ]\n    }\n\n    component ActiveWindow: JsonObject {\n        property bool compact: false\n        property bool inverted: false\n        property bool showOnHover: true\n    }\n\n    component Tray: JsonObject {\n        property bool background: false\n        property bool recolour: false\n        property bool compact: false\n        property list<var> iconSubs: []\n        property list<string> hiddenIcons: []\n    }\n\n    component Status: JsonObject {\n        property bool showAudio: false\n        property bool showMicrophone: false\n        property bool showKbLayout: false\n        property bool showNetwork: true\n        property bool showWifi: true\n        property bool showBluetooth: true\n        property bool showBattery: true\n        property bool showLockStatus: true\n    }\n\n    component Clock: JsonObject {\n        property bool background: false\n        property bool showDate: false\n        property bool showIcon: true\n    }\n\n    component Sizes: JsonObject {\n        property int innerWidth: 40\n        property int windowPreviewSize: 400\n        property int trayMenuWidth: 300\n        property int batteryWidth: 250\n        property int networkWidth: 320\n        property int kbLayoutWidth: 320\n    }\n}\n"
  },
  {
    "path": "config/BorderConfig.qml",
    "content": "import Quickshell.Io\nimport qs.config\n\nJsonObject {\n    property int thickness: Config.appearance.padding.normal\n    property int rounding: Config.appearance.rounding.large\n\n    readonly property int minThickness: 2\n    readonly property int clampedThickness: Math.max(minThickness, thickness)\n}\n"
  },
  {
    "path": "config/Config.qml",
    "content": "pragma Singleton\n\nimport qs.utils\nimport Caelestia\nimport Quickshell\nimport Quickshell.Io\nimport QtQuick\n\nSingleton {\n    id: root\n\n    property alias appearance: adapter.appearance\n    property alias general: adapter.general\n    property alias background: adapter.background\n    property alias bar: adapter.bar\n    property alias border: adapter.border\n    property alias dashboard: adapter.dashboard\n    property alias controlCenter: adapter.controlCenter\n    property alias launcher: adapter.launcher\n    property alias notifs: adapter.notifs\n    property alias osd: adapter.osd\n    property alias session: adapter.session\n    property alias winfo: adapter.winfo\n    property alias lock: adapter.lock\n    property alias utilities: adapter.utilities\n    property alias sidebar: adapter.sidebar\n    property alias services: adapter.services\n    property alias paths: adapter.paths\n\n    property bool recentlySaved: false\n\n    // Public save function - call this to persist config changes\n    function save(): void {\n        saveTimer.restart();\n        recentlySaved = true;\n        recentSaveCooldown.restart();\n    }\n\n    // Helper function to serialize the config object\n    function serializeConfig(): var {\n        return {\n            appearance: serializeAppearance(),\n            general: serializeGeneral(),\n            background: serializeBackground(),\n            bar: serializeBar(),\n            border: serializeBorder(),\n            dashboard: serializeDashboard(),\n            controlCenter: serializeControlCenter(),\n            launcher: serializeLauncher(),\n            notifs: serializeNotifs(),\n            osd: serializeOsd(),\n            session: serializeSession(),\n            winfo: serializeWinfo(),\n            lock: serializeLock(),\n            utilities: serializeUtilities(),\n            sidebar: serializeSidebar(),\n            services: serializeServices(),\n            paths: serializePaths()\n        };\n    }\n\n    function serializeAppearance(): var {\n        return {\n            rounding: {\n                scale: appearance.rounding.scale\n            },\n            spacing: {\n                scale: appearance.spacing.scale\n            },\n            padding: {\n                scale: appearance.padding.scale\n            },\n            font: {\n                family: {\n                    sans: appearance.font.family.sans,\n                    mono: appearance.font.family.mono,\n                    material: appearance.font.family.material,\n                    clock: appearance.font.family.clock\n                },\n                size: {\n                    scale: appearance.font.size.scale\n                }\n            },\n            anim: {\n                mediaGifSpeedAdjustment: 300,\n                sessionGifSpeed: 0.7,\n                durations: {\n                    scale: appearance.anim.durations.scale\n                }\n            },\n            transparency: {\n                enabled: appearance.transparency.enabled,\n                base: appearance.transparency.base,\n                layers: appearance.transparency.layers\n            }\n        };\n    }\n\n    function serializeGeneral(): var {\n        return {\n            logo: general.logo,\n            excludedScreens: general.excludedScreens,\n            apps: {\n                terminal: general.apps.terminal,\n                audio: general.apps.audio,\n                playback: general.apps.playback,\n                explorer: general.apps.explorer\n            },\n            idle: {\n                lockBeforeSleep: general.idle.lockBeforeSleep,\n                inhibitWhenAudio: general.idle.inhibitWhenAudio,\n                timeouts: general.idle.timeouts\n            },\n            battery: {\n                warnLevels: general.battery.warnLevels,\n                criticalLevel: general.battery.criticalLevel\n            }\n        };\n    }\n\n    function serializeBackground(): var {\n        return {\n            enabled: background.enabled,\n            wallpaperEnabled: background.wallpaperEnabled,\n            desktopClock: {\n                enabled: background.desktopClock.enabled,\n                scale: background.desktopClock.scale,\n                position: background.desktopClock.position,\n                invertColors: background.desktopClock.invertColors,\n                background: {\n                    enabled: background.desktopClock.background.enabled,\n                    opacity: background.desktopClock.background.opacity,\n                    blur: background.desktopClock.background.blur\n                },\n                shadow: {\n                    enabled: background.desktopClock.shadow.enabled,\n                    opacity: background.desktopClock.shadow.opacity,\n                    blur: background.desktopClock.shadow.blur\n                }\n            },\n            visualiser: {\n                enabled: background.visualiser.enabled,\n                autoHide: background.visualiser.autoHide,\n                blur: background.visualiser.blur,\n                rounding: background.visualiser.rounding,\n                spacing: background.visualiser.spacing\n            }\n        };\n    }\n\n    function serializeBar(): var {\n        return {\n            persistent: bar.persistent,\n            showOnHover: bar.showOnHover,\n            dragThreshold: bar.dragThreshold,\n            scrollActions: {\n                workspaces: bar.scrollActions.workspaces,\n                volume: bar.scrollActions.volume,\n                brightness: bar.scrollActions.brightness\n            },\n            popouts: {\n                activeWindow: bar.popouts.activeWindow,\n                tray: bar.popouts.tray,\n                statusIcons: bar.popouts.statusIcons\n            },\n            workspaces: {\n                shown: bar.workspaces.shown,\n                activeIndicator: bar.workspaces.activeIndicator,\n                occupiedBg: bar.workspaces.occupiedBg,\n                showWindows: bar.workspaces.showWindows,\n                showWindowsOnSpecialWorkspaces: bar.workspaces.showWindowsOnSpecialWorkspaces,\n                maxWindowIcons: bar.workspaces.maxWindowIcons,\n                activeTrail: bar.workspaces.activeTrail,\n                perMonitorWorkspaces: bar.workspaces.perMonitorWorkspaces,\n                label: bar.workspaces.label,\n                occupiedLabel: bar.workspaces.occupiedLabel,\n                activeLabel: bar.workspaces.activeLabel,\n                capitalisation: bar.workspaces.capitalisation,\n                specialWorkspaceIcons: bar.workspaces.specialWorkspaceIcons,\n                windowIcons: bar.workspaces.windowIcons\n            },\n            activeWindow: {\n                compact: bar.activeWindow.compact,\n                inverted: bar.activeWindow.inverted,\n                showOnHover: bar.activeWindow.showOnHover\n            },\n            tray: {\n                background: bar.tray.background,\n                recolour: bar.tray.recolour,\n                compact: bar.tray.compact,\n                iconSubs: bar.tray.iconSubs,\n                hiddenIcons: bar.tray.hiddenIcons\n            },\n            status: {\n                showAudio: bar.status.showAudio,\n                showMicrophone: bar.status.showMicrophone,\n                showKbLayout: bar.status.showKbLayout,\n                showNetwork: bar.status.showNetwork,\n                showWifi: bar.status.showWifi,\n                showBluetooth: bar.status.showBluetooth,\n                showBattery: bar.status.showBattery,\n                showLockStatus: bar.status.showLockStatus\n            },\n            clock: {\n                background: bar.clock.background,\n                showDate: bar.clock.showDate,\n                showIcon: bar.clock.showIcon\n            },\n            entries: bar.entries,\n            excludedScreens: bar.excludedScreens\n        };\n    }\n\n    function serializeBorder(): var {\n        return {\n            thickness: border.thickness,\n            rounding: border.rounding\n        };\n    }\n\n    function serializeDashboard(): var {\n        return {\n            enabled: dashboard.enabled,\n            showOnHover: dashboard.showOnHover,\n            mediaUpdateInterval: dashboard.mediaUpdateInterval,\n            resourceUpdateInterval: dashboard.resourceUpdateInterval,\n            dragThreshold: dashboard.dragThreshold,\n            performance: {\n                showBattery: dashboard.performance.showBattery,\n                showGpu: dashboard.performance.showGpu,\n                showCpu: dashboard.performance.showCpu,\n                showMemory: dashboard.performance.showMemory,\n                showStorage: dashboard.performance.showStorage,\n                showNetwork: dashboard.performance.showNetwork\n            }\n        };\n    }\n\n    function serializeControlCenter(): var {\n        return {};\n    }\n\n    function serializeLauncher(): var {\n        return {\n            enabled: launcher.enabled,\n            showOnHover: launcher.showOnHover,\n            maxShown: launcher.maxShown,\n            maxWallpapers: launcher.maxWallpapers,\n            specialPrefix: launcher.specialPrefix,\n            actionPrefix: launcher.actionPrefix,\n            enableDangerousActions: launcher.enableDangerousActions,\n            dragThreshold: launcher.dragThreshold,\n            vimKeybinds: launcher.vimKeybinds,\n            favouriteApps: launcher.favouriteApps,\n            hiddenApps: launcher.hiddenApps,\n            useFuzzy: {\n                apps: launcher.useFuzzy.apps,\n                actions: launcher.useFuzzy.actions,\n                schemes: launcher.useFuzzy.schemes,\n                variants: launcher.useFuzzy.variants,\n                wallpapers: launcher.useFuzzy.wallpapers\n            },\n            actions: launcher.actions\n        };\n    }\n\n    function serializeNotifs(): var {\n        return {\n            expire: notifs.expire,\n            defaultExpireTimeout: notifs.defaultExpireTimeout,\n            clearThreshold: notifs.clearThreshold,\n            expandThreshold: notifs.expandThreshold,\n            actionOnClick: notifs.actionOnClick,\n            groupPreviewNum: notifs.groupPreviewNum\n        };\n    }\n\n    function serializeOsd(): var {\n        return {\n            enabled: osd.enabled,\n            hideDelay: osd.hideDelay,\n            enableBrightness: osd.enableBrightness,\n            enableMicrophone: osd.enableMicrophone\n        };\n    }\n\n    function serializeSession(): var {\n        return {\n            enabled: session.enabled,\n            dragThreshold: session.dragThreshold,\n            vimKeybinds: session.vimKeybinds,\n            icons: {\n                logout: session.icons.logout,\n                shutdown: session.icons.shutdown,\n                hibernate: session.icons.hibernate,\n                reboot: session.icons.reboot\n            },\n            commands: {\n                logout: session.commands.logout,\n                shutdown: session.commands.shutdown,\n                hibernate: session.commands.hibernate,\n                reboot: session.commands.reboot\n            }\n        };\n    }\n\n    function serializeWinfo(): var {\n        return {};\n    }\n\n    function serializeLock(): var {\n        return {\n            recolourLogo: lock.recolourLogo,\n            enableFprint: lock.enableFprint,\n            maxFprintTries: lock.maxFprintTries,\n            hideNotifs: lock.hideNotifs\n        };\n    }\n\n    function serializeUtilities(): var {\n        return {\n            enabled: utilities.enabled,\n            maxToasts: utilities.maxToasts,\n            toasts: {\n                configLoaded: utilities.toasts.configLoaded,\n                chargingChanged: utilities.toasts.chargingChanged,\n                gameModeChanged: utilities.toasts.gameModeChanged,\n                dndChanged: utilities.toasts.dndChanged,\n                audioOutputChanged: utilities.toasts.audioOutputChanged,\n                audioInputChanged: utilities.toasts.audioInputChanged,\n                capsLockChanged: utilities.toasts.capsLockChanged,\n                numLockChanged: utilities.toasts.numLockChanged,\n                kbLayoutChanged: utilities.toasts.kbLayoutChanged,\n                vpnChanged: utilities.toasts.vpnChanged,\n                nowPlaying: utilities.toasts.nowPlaying\n            },\n            vpn: {\n                enabled: utilities.vpn.enabled,\n                provider: utilities.vpn.provider\n            },\n            quickToggles: utilities.quickToggles\n        };\n    }\n\n    function serializeSidebar(): var {\n        return {\n            enabled: sidebar.enabled,\n            dragThreshold: sidebar.dragThreshold\n        };\n    }\n\n    function serializeServices(): var {\n        return {\n            weatherLocation: services.weatherLocation,\n            useFahrenheit: services.useFahrenheit,\n            useFahrenheitPerformance: services.useFahrenheitPerformance,\n            useTwelveHourClock: services.useTwelveHourClock,\n            gpuType: services.gpuType,\n            visualiserBars: services.visualiserBars,\n            audioIncrement: services.audioIncrement,\n            brightnessIncrement: services.brightnessIncrement,\n            maxVolume: services.maxVolume,\n            smartScheme: services.smartScheme,\n            defaultPlayer: services.defaultPlayer,\n            playerAliases: services.playerAliases,\n            showLyrics: services.showLyrics\n        };\n    }\n\n    function serializePaths(): var {\n        return {\n            wallpaperDir: paths.wallpaperDir,\n            lyricsDir: paths.lyricsDir,\n            sessionGif: paths.sessionGif,\n            mediaGif: paths.mediaGif\n        };\n    }\n\n    ElapsedTimer {\n        id: timer\n    }\n\n    Timer {\n        id: saveTimer\n\n        interval: 500\n        onTriggered: {\n            timer.restart();\n            try {\n                // Parse current config to preserve structure and comments if possible\n                let config = {};\n                try {\n                    config = JSON.parse(fileView.text());\n                } catch (e) {\n                    // If parsing fails, start with empty object\n                    config = {};\n                }\n\n                // Update config with current values\n                config = root.serializeConfig();\n\n                // Save to file with pretty printing\n                fileView.setText(JSON.stringify(config, null, 2));\n            } catch (e) {\n                Toaster.toast(qsTr(\"Failed to serialize config\"), e.message, \"settings_alert\", Toast.Error);\n            }\n        }\n    }\n\n    Timer {\n        id: recentSaveCooldown\n\n        interval: 2000\n        onTriggered: {\n            root.recentlySaved = false;\n        }\n    }\n\n    FileView {\n        id: fileView\n\n        path: `${Paths.config}/shell.json`\n        watchChanges: true\n        onFileChanged: {\n            // Prevent reload loop - don't reload if we just saved\n            if (!root.recentlySaved) {\n                timer.restart();\n                reload();\n            } else {\n                // Self-initiated save - reload without toast\n                reload();\n            }\n        }\n        onLoaded: {\n            try {\n                JSON.parse(text());\n                const elapsed = timer.elapsedMs();\n                // Only show toast for external changes (not our own saves) and when elapsed time is meaningful\n                if (adapter.utilities.toasts.configLoaded && !root.recentlySaved && elapsed > 0) {\n                    Toaster.toast(qsTr(\"Config loaded\"), qsTr(\"Config loaded in %1ms\").arg(elapsed), \"rule_settings\");\n                } else if (adapter.utilities.toasts.configLoaded && root.recentlySaved && elapsed > 0) {\n                    Toaster.toast(qsTr(\"Config saved\"), qsTr(\"Config reloaded in %1ms\").arg(elapsed), \"rule_settings\");\n                }\n            } catch (e) {\n                Toaster.toast(qsTr(\"Failed to load config\"), e.message, \"settings_alert\", Toast.Error);\n            }\n        }\n        onLoadFailed: err => {\n            if (err !== FileViewError.FileNotFound)\n                Toaster.toast(qsTr(\"Failed to read config file\"), FileViewError.toString(err), \"settings_alert\", Toast.Warning);\n        }\n        onSaveFailed: err => Toaster.toast(qsTr(\"Failed to save config\"), FileViewError.toString(err), \"settings_alert\", Toast.Error)\n\n        JsonAdapter {\n            id: adapter\n\n            property AppearanceConfig appearance: AppearanceConfig {}\n            property GeneralConfig general: GeneralConfig {}\n            property BackgroundConfig background: BackgroundConfig {}\n            property BarConfig bar: BarConfig {}\n            property BorderConfig border: BorderConfig {}\n            property DashboardConfig dashboard: DashboardConfig {}\n            property ControlCenterConfig controlCenter: ControlCenterConfig {}\n            property LauncherConfig launcher: LauncherConfig {}\n            property NotifsConfig notifs: NotifsConfig {}\n            property OsdConfig osd: OsdConfig {}\n            property SessionConfig session: SessionConfig {}\n            property WInfoConfig winfo: WInfoConfig {}\n            property LockConfig lock: LockConfig {}\n            property UtilitiesConfig utilities: UtilitiesConfig {}\n            property SidebarConfig sidebar: SidebarConfig {}\n            property ServiceConfig services: ServiceConfig {}\n            property UserPaths paths: UserPaths {}\n        }\n    }\n}\n"
  },
  {
    "path": "config/ControlCenterConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property Sizes sizes: Sizes {}\n\n    component Sizes: JsonObject {\n        property real heightMult: 0.7\n        property real ratio: 16 / 9\n    }\n}\n"
  },
  {
    "path": "config/DashboardConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property bool enabled: true\n    property bool showOnHover: true\n    property int mediaUpdateInterval: 500\n    property int resourceUpdateInterval: 1000\n    property int dragThreshold: 50\n    property bool showDashboard: true\n    property bool showMedia: true\n    property bool showPerformance: true\n    property bool showWeather: true\n    property Sizes sizes: Sizes {}\n    property Performance performance: Performance {}\n\n    component Performance: JsonObject {\n        property bool showBattery: true\n        property bool showGpu: true\n        property bool showCpu: true\n        property bool showMemory: true\n        property bool showStorage: true\n        property bool showNetwork: true\n    }\n\n    component Sizes: JsonObject {\n        readonly property int tabIndicatorHeight: 3\n        readonly property int tabIndicatorSpacing: 5\n        readonly property int infoWidth: 200\n        readonly property int infoIconSize: 25\n        readonly property int dateTimeWidth: 110\n        readonly property int mediaWidth: 200\n        readonly property int mediaProgressSweep: 180\n        readonly property int mediaProgressThickness: 8\n        readonly property int resourceProgessThickness: 10\n        readonly property int weatherWidth: 250\n        readonly property int mediaCoverArtSize: 150\n        readonly property int mediaVisualiserSize: 80\n        readonly property int resourceSize: 200\n    }\n}\n"
  },
  {
    "path": "config/GeneralConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property string logo: \"\"\n    property list<string> excludedScreens: []\n    property Apps apps: Apps {}\n    property Idle idle: Idle {}\n    property Battery battery: Battery {}\n\n    component Apps: JsonObject {\n        property list<string> terminal: [\"foot\"]\n        property list<string> audio: [\"pavucontrol\"]\n        property list<string> playback: [\"mpv\"]\n        property list<string> explorer: [\"thunar\"]\n    }\n\n    component Idle: JsonObject {\n        property bool lockBeforeSleep: true\n        property bool inhibitWhenAudio: true\n        property list<var> timeouts: [\n            {\n                timeout: 180,\n                idleAction: \"lock\"\n            },\n            {\n                timeout: 300,\n                idleAction: \"dpms off\",\n                returnAction: \"dpms on\"\n            },\n            {\n                timeout: 600,\n                idleAction: [\"systemctl\", \"suspend-then-hibernate\"]\n            }\n        ]\n    }\n\n    component Battery: JsonObject {\n        property list<var> warnLevels: [\n            {\n                level: 20,\n                title: qsTr(\"Low battery\"),\n                message: qsTr(\"You might want to plug in a charger\"),\n                icon: \"battery_android_frame_2\"\n            },\n            {\n                level: 10,\n                title: qsTr(\"Did you see the previous message?\"),\n                message: qsTr(\"You should probably plug in a charger <b>now</b>\"),\n                icon: \"battery_android_frame_1\"\n            },\n            {\n                level: 5,\n                title: qsTr(\"Critical battery level\"),\n                message: qsTr(\"PLUG THE CHARGER RIGHT NOW!!\"),\n                icon: \"battery_android_alert\",\n                critical: true\n            },\n        ]\n        property int criticalLevel: 3\n    }\n}\n"
  },
  {
    "path": "config/LauncherConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property bool enabled: true\n    property bool showOnHover: false\n    property int maxShown: 7\n    property int maxWallpapers: 9 // Warning: even numbers look bad\n    property string specialPrefix: \"@\"\n    property string actionPrefix: \">\"\n    property bool enableDangerousActions: false // Allow actions that can cause losing data, like shutdown, reboot and logout\n    property int dragThreshold: 50\n    property bool vimKeybinds: false\n    property list<string> favouriteApps: []\n    property list<string> hiddenApps: []\n    property UseFuzzy useFuzzy: UseFuzzy {}\n    property Sizes sizes: Sizes {}\n\n    component UseFuzzy: JsonObject {\n        property bool apps: false\n        property bool actions: false\n        property bool schemes: false\n        property bool variants: false\n        property bool wallpapers: false\n    }\n\n    component Sizes: JsonObject {\n        property int itemWidth: 600\n        property int itemHeight: 57\n        property int wallpaperWidth: 280\n        property int wallpaperHeight: 200\n    }\n\n    property list<var> actions: [\n        {\n            name: \"Calculator\",\n            icon: \"calculate\",\n            description: \"Do simple math equations (powered by Qalc)\",\n            command: [\"autocomplete\", \"calc\"],\n            enabled: true,\n            dangerous: false\n        },\n        {\n            name: \"Scheme\",\n            icon: \"palette\",\n            description: \"Change the current colour scheme\",\n            command: [\"autocomplete\", \"scheme\"],\n            enabled: true,\n            dangerous: false\n        },\n        {\n            name: \"Wallpaper\",\n            icon: \"image\",\n            description: \"Change the current wallpaper\",\n            command: [\"autocomplete\", \"wallpaper\"],\n            enabled: true,\n            dangerous: false\n        },\n        {\n            name: \"Variant\",\n            icon: \"colors\",\n            description: \"Change the current scheme variant\",\n            command: [\"autocomplete\", \"variant\"],\n            enabled: true,\n            dangerous: false\n        },\n        {\n            name: \"Transparency\",\n            icon: \"opacity\",\n            description: \"Change shell transparency\",\n            command: [\"autocomplete\", \"transparency\"],\n            enabled: false,\n            dangerous: false\n        },\n        {\n            name: \"Random\",\n            icon: \"casino\",\n            description: \"Switch to a random wallpaper\",\n            command: [\"caelestia\", \"wallpaper\", \"-r\"],\n            enabled: true,\n            dangerous: false\n        },\n        {\n            name: \"Light\",\n            icon: \"light_mode\",\n            description: \"Change the scheme to light mode\",\n            command: [\"setMode\", \"light\"],\n            enabled: true,\n            dangerous: false\n        },\n        {\n            name: \"Dark\",\n            icon: \"dark_mode\",\n            description: \"Change the scheme to dark mode\",\n            command: [\"setMode\", \"dark\"],\n            enabled: true,\n            dangerous: false\n        },\n        {\n            name: \"Shutdown\",\n            icon: \"power_settings_new\",\n            description: \"Shutdown the system\",\n            command: [\"systemctl\", \"poweroff\"],\n            enabled: true,\n            dangerous: true\n        },\n        {\n            name: \"Reboot\",\n            icon: \"cached\",\n            description: \"Reboot the system\",\n            command: [\"systemctl\", \"reboot\"],\n            enabled: true,\n            dangerous: true\n        },\n        {\n            name: \"Logout\",\n            icon: \"exit_to_app\",\n            description: \"Log out of the current session\",\n            command: [\"loginctl\", \"terminate-user\", \"\"],\n            enabled: true,\n            dangerous: true\n        },\n        {\n            name: \"Lock\",\n            icon: \"lock\",\n            description: \"Lock the current session\",\n            command: [\"loginctl\", \"lock-session\"],\n            enabled: true,\n            dangerous: false\n        },\n        {\n            name: \"Sleep\",\n            icon: \"bedtime\",\n            description: \"Suspend then hibernate\",\n            command: [\"systemctl\", \"suspend-then-hibernate\"],\n            enabled: true,\n            dangerous: false\n        },\n        {\n            name: \"Settings\",\n            icon: \"settings\",\n            description: \"Configure the shell\",\n            command: [\"caelestia\", \"shell\", \"controlCenter\", \"open\"],\n            enabled: true,\n            dangerous: false\n        }\n    ]\n}\n"
  },
  {
    "path": "config/LockConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property bool recolourLogo: false\n    property bool enableFprint: true\n    property int maxFprintTries: 3\n    property bool hideNotifs: false\n    property Sizes sizes: Sizes {}\n\n    component Sizes: JsonObject {\n        property real heightMult: 0.7\n        property real ratio: 16 / 9\n        property int centerWidth: 600\n    }\n}\n"
  },
  {
    "path": "config/NotifsConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property bool expire: true\n    property int defaultExpireTimeout: 5000\n    property real clearThreshold: 0.3\n    property int expandThreshold: 20\n    property bool actionOnClick: false\n    property int groupPreviewNum: 3\n    property bool openExpanded: false // Show the notifichation in expanded state when opening\n    property Sizes sizes: Sizes {}\n\n    component Sizes: JsonObject {\n        property int width: 400\n        property int image: 41\n        property int badge: 20\n    }\n}\n"
  },
  {
    "path": "config/OsdConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property bool enabled: true\n    property int hideDelay: 2000\n    property bool enableBrightness: true\n    property bool enableMicrophone: false\n    property Sizes sizes: Sizes {}\n\n    component Sizes: JsonObject {\n        property int sliderWidth: 30\n        property int sliderHeight: 150\n    }\n}\n"
  },
  {
    "path": "config/ServiceConfig.qml",
    "content": "import Quickshell.Io\nimport QtQuick\n\nJsonObject {\n    property string weatherLocation: \"\" // A lat,long pair or empty for autodetection, e.g. \"37.8267,-122.4233\"\n    property bool useFahrenheit: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem)\n    property bool useFahrenheitPerformance: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem)\n    property bool useTwelveHourClock: Qt.locale().timeFormat(Locale.ShortFormat).toLowerCase().includes(\"a\")\n    property string gpuType: \"\"\n    property int visualiserBars: 45\n    property real audioIncrement: 0.1\n    property real brightnessIncrement: 0.1\n    property real maxVolume: 1.0\n    property bool smartScheme: true\n    property string defaultPlayer: \"Spotify\"\n    property list<var> playerAliases: [\n        {\n            \"from\": \"com.github.th_ch.youtube_music\",\n            \"to\": \"YT Music\"\n        }\n    ]\n    property bool showLyrics: true\n}\n"
  },
  {
    "path": "config/SessionConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property bool enabled: true\n    property int dragThreshold: 30\n    property bool vimKeybinds: false\n    property Icons icons: Icons {}\n    property Commands commands: Commands {}\n\n    property Sizes sizes: Sizes {}\n\n    component Icons: JsonObject {\n        property string logout: \"logout\"\n        property string shutdown: \"power_settings_new\"\n        property string hibernate: \"downloading\"\n        property string reboot: \"cached\"\n    }\n\n    component Commands: JsonObject {\n        property list<string> logout: [\"loginctl\", \"terminate-user\", \"\"]\n        property list<string> shutdown: [\"systemctl\", \"poweroff\"]\n        property list<string> hibernate: [\"systemctl\", \"hibernate\"]\n        property list<string> reboot: [\"systemctl\", \"reboot\"]\n    }\n\n    component Sizes: JsonObject {\n        property int button: 80\n    }\n}\n"
  },
  {
    "path": "config/SidebarConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property bool enabled: true\n    property int dragThreshold: 80\n    property Sizes sizes: Sizes {}\n\n    component Sizes: JsonObject {\n        property int width: 430\n    }\n}\n"
  },
  {
    "path": "config/UserPaths.qml",
    "content": "import qs.utils\nimport Quickshell.Io\n\nJsonObject {\n    property string wallpaperDir: `${Paths.pictures}/Wallpapers`\n    property string lyricsDir: `${Paths.home}/Music/lyrics/`\n    property string sessionGif: \"root:/assets/kurukuru.gif\"\n    property string mediaGif: \"root:/assets/bongocat.gif\"\n}\n"
  },
  {
    "path": "config/UtilitiesConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property bool enabled: true\n    property int maxToasts: 4\n\n    property Sizes sizes: Sizes {}\n    property Toasts toasts: Toasts {}\n    property Vpn vpn: Vpn {}\n\n    component Sizes: JsonObject {\n        property int width: 430\n        property int toastWidth: 430\n    }\n\n    component Toasts: JsonObject {\n        property bool configLoaded: true\n        property bool chargingChanged: true\n        property bool gameModeChanged: true\n        property bool dndChanged: true\n        property bool audioOutputChanged: true\n        property bool audioInputChanged: true\n        property bool capsLockChanged: true\n        property bool numLockChanged: true\n        property bool kbLayoutChanged: true\n        property bool kbLimit: true\n        property bool vpnChanged: true\n        property bool nowPlaying: false\n    }\n\n    component Vpn: JsonObject {\n        property bool enabled: false\n        property list<var> provider: [\"netbird\"]\n    }\n\n    property list<var> quickToggles: [\n        {\n            id: \"wifi\",\n            enabled: true\n        },\n        {\n            id: \"bluetooth\",\n            enabled: true\n        },\n        {\n            id: \"mic\",\n            enabled: true\n        },\n        {\n            id: \"settings\",\n            enabled: true\n        },\n        {\n            id: \"gameMode\",\n            enabled: true\n        },\n        {\n            id: \"dnd\",\n            enabled: true\n        },\n        {\n            id: \"vpn\",\n            enabled: false\n        }\n    ]\n}\n"
  },
  {
    "path": "config/WInfoConfig.qml",
    "content": "import Quickshell.Io\n\nJsonObject {\n    property Sizes sizes: Sizes {}\n\n    component Sizes: JsonObject {\n        property real heightMult: 0.7\n        property real detailsWidth: 500\n    }\n}\n"
  },
  {
    "path": "extras/CMakeLists.txt",
    "content": "# Version\nadd_executable(version version.cpp)\ntarget_compile_definitions(version PRIVATE\n    PROJECT_NAME=\"${PROJECT_NAME}\"\n    VERSION=\"${VERSION}\"\n    GIT_REVISION=\"${GIT_REVISION}\"\n    DISTRIBUTOR=\"${DISTRIBUTOR}\"\n)\ninstall(TARGETS version DESTINATION ${INSTALL_LIBDIR})\n"
  },
  {
    "path": "extras/version.cpp",
    "content": "#include <iostream>\n\nint main(int argc, char* argv[]) {\n    if (argc > 1) {\n        std::string arg = argv[1];\n\n        if (arg == \"-t\" || arg == \"--terse\") {\n            std::cout << PROJECT_NAME << std::endl;\n            std::cout << VERSION << std::endl;\n            std::cout << GIT_REVISION << std::endl;\n            std::cout << DISTRIBUTOR << std::endl;\n        } else if (arg == \"-s\" || arg == \"--short\") {\n            std::cout << PROJECT_NAME << \" \" << VERSION << \", revision \" << GIT_REVISION\n                      << \", distributed by: \" << DISTRIBUTOR << std::endl;\n        } else {\n            std::cout << \"Usage: \" << argv[0] << \" [-t | --terse] [-s | --short]\" << std::endl;\n            return arg != \"-h\" && arg != \"--help\";\n        }\n    } else {\n        std::cout << \"Project: \" << PROJECT_NAME << std::endl;\n        std::cout << \"Version: \" << VERSION << std::endl;\n        std::cout << \"Git revision: \" << GIT_REVISION << std::endl;\n        std::cout << \"Distributor: \" << DISTRIBUTOR << std::endl;\n    }\n\n    return 0;\n}\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"Desktop shell for Caelestia dots\";\n\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n\n    quickshell = {\n      url = \"git+https://git.outfoxxed.me/outfoxxed/quickshell\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n\n    caelestia-cli = {\n      url = \"github:caelestia-dots/cli\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n      inputs.caelestia-shell.follows = \"\";\n    };\n  };\n\n  outputs = {\n    self,\n    nixpkgs,\n    ...\n  } @ inputs: let\n    forAllSystems = fn:\n      nixpkgs.lib.genAttrs nixpkgs.lib.platforms.linux (\n        system: fn nixpkgs.legacyPackages.${system}\n      );\n  in {\n    formatter = forAllSystems (pkgs: pkgs.alejandra);\n\n    packages = forAllSystems (pkgs: rec {\n      caelestia-shell = pkgs.callPackage ./nix {\n        rev = self.rev or self.dirtyRev;\n        stdenv = pkgs.clangStdenv;\n        quickshell = inputs.quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default.override {\n          withX11 = false;\n          withI3 = false;\n        };\n        caelestia-cli = inputs.caelestia-cli.packages.${pkgs.stdenv.hostPlatform.system}.default;\n      };\n      with-cli = caelestia-shell.override {withCli = true;};\n      debug = caelestia-shell.override {debug = true;};\n      default = caelestia-shell;\n    });\n\n    devShells = forAllSystems (pkgs: {\n      default = let\n        shell = self.packages.${pkgs.stdenv.hostPlatform.system}.caelestia-shell;\n      in\n        pkgs.mkShell.override {stdenv = shell.stdenv;} {\n          inputsFrom = [shell shell.plugin shell.extras];\n          packages = with pkgs; [clazy material-symbols rubik nerd-fonts.caskaydia-cove];\n          CAELESTIA_XKB_RULES_PATH = \"${pkgs.xkeyboard-config}/share/xkeyboard-config-2/rules/base.lst\";\n        };\n    });\n\n    homeManagerModules.default = import ./nix/hm-module.nix self;\n  };\n}\n"
  },
  {
    "path": "modules/BatteryMonitor.qml",
    "content": "import qs.config\nimport Caelestia\nimport Quickshell\nimport Quickshell.Services.UPower\nimport QtQuick\n\nScope {\n    id: root\n\n    readonly property list<var> warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level)\n\n    Connections {\n        function onOnBatteryChanged(): void {\n            if (UPower.onBattery) {\n                if (Config.utilities.toasts.chargingChanged)\n                    Toaster.toast(qsTr(\"Charger unplugged\"), qsTr(\"Battery is discharging\"), \"power_off\");\n            } else {\n                if (Config.utilities.toasts.chargingChanged)\n                    Toaster.toast(qsTr(\"Charger plugged in\"), qsTr(\"Battery is charging\"), \"power\");\n                for (const level of root.warnLevels)\n                    level.warned = false;\n            }\n        }\n\n        target: UPower\n    }\n\n    Connections {\n        function onPercentageChanged(): void {\n            if (!UPower.onBattery)\n                return;\n\n            const p = UPower.displayDevice.percentage * 100;\n            for (const level of root.warnLevels) {\n                if (p <= level.level && !level.warned) {\n                    level.warned = true;\n                    Toaster.toast(level.title ?? qsTr(\"Battery warning\"), level.message ?? qsTr(\"Battery level is low\"), level.icon ?? \"battery_android_alert\", level.critical ? Toast.Error : Toast.Warning);\n                }\n            }\n\n            if (!hibernateTimer.running && p <= Config.general.battery.criticalLevel) {\n                Toaster.toast(qsTr(\"Hibernating in 5 seconds\"), qsTr(\"Hibernating to prevent data loss\"), \"battery_android_alert\", Toast.Error);\n                hibernateTimer.start();\n            }\n        }\n\n        target: UPower.displayDevice\n    }\n\n    Timer {\n        id: hibernateTimer\n\n        interval: 5000\n        onTriggered: Quickshell.execDetached([\"systemctl\", \"hibernate\"])\n    }\n}\n"
  },
  {
    "path": "modules/IdleMonitors.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"lock\"\nimport qs.config\nimport qs.services\nimport Caelestia.Internal\nimport Quickshell\nimport Quickshell.Wayland\n\nScope {\n    id: root\n\n    required property Lock lock\n    readonly property bool enabled: !Config.general.idle.inhibitWhenAudio || !Players.list.some(p => p.isPlaying)\n\n    function handleIdleAction(action: var): void {\n        if (!action)\n            return;\n\n        if (action === \"lock\")\n            lock.lock.locked = true;\n        else if (action === \"unlock\")\n            lock.lock.locked = false;\n        else if (typeof action === \"string\")\n            Hypr.dispatch(action);\n        else\n            Quickshell.execDetached(action);\n    }\n\n    LogindManager {\n        onAboutToSleep: {\n            if (Config.general.idle.lockBeforeSleep)\n                root.lock.lock.locked = true;\n        }\n        onLockRequested: root.lock.lock.locked = true\n        onUnlockRequested: root.lock.lock.unlock()\n    }\n\n    Variants {\n        model: Config.general.idle.timeouts\n\n        IdleMonitor {\n            required property var modelData\n\n            enabled: root.enabled && (modelData.enabled ?? true)\n            timeout: modelData.timeout\n            respectInhibitors: modelData.respectInhibitors ?? true\n            onIsIdleChanged: root.handleIdleAction(isIdle ? modelData.idleAction : modelData.returnAction)\n        }\n    }\n}\n"
  },
  {
    "path": "modules/Shortcuts.qml",
    "content": "import qs.components.misc\nimport qs.modules.controlcenter\nimport qs.services\nimport Caelestia\nimport Quickshell\nimport Quickshell.Io\n\nScope {\n    id: root\n\n    property bool launcherInterrupted\n    readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false\n\n    CustomShortcut {\n        name: \"controlCenter\"\n        description: \"Open control center\"\n        onPressed: WindowFactory.create()\n    }\n\n    CustomShortcut {\n        name: \"showall\"\n        description: \"Toggle launcher, dashboard and osd\"\n        onPressed: {\n            if (root.hasFullscreen)\n                return;\n            const v = Visibilities.getForActive();\n            v.launcher = v.dashboard = v.osd = v.utilities = !(v.launcher || v.dashboard || v.osd || v.utilities);\n        }\n    }\n\n    CustomShortcut {\n        name: \"dashboard\"\n        description: \"Toggle dashboard\"\n        onPressed: {\n            if (root.hasFullscreen)\n                return;\n            const visibilities = Visibilities.getForActive();\n            visibilities.dashboard = !visibilities.dashboard;\n        }\n    }\n\n    CustomShortcut {\n        name: \"session\"\n        description: \"Toggle session menu\"\n        onPressed: {\n            if (root.hasFullscreen)\n                return;\n            const visibilities = Visibilities.getForActive();\n            visibilities.session = !visibilities.session;\n        }\n    }\n\n    CustomShortcut {\n        name: \"launcher\"\n        description: \"Toggle launcher\"\n        onPressed: root.launcherInterrupted = false\n        onReleased: {\n            if (!root.launcherInterrupted && !root.hasFullscreen) {\n                const visibilities = Visibilities.getForActive();\n                visibilities.launcher = !visibilities.launcher;\n            }\n            root.launcherInterrupted = false;\n        }\n    }\n\n    CustomShortcut {\n        name: \"launcherInterrupt\"\n        description: \"Interrupt launcher keybind\"\n        onPressed: root.launcherInterrupted = true\n    }\n\n    CustomShortcut {\n        name: \"sidebar\"\n        description: \"Toggle sidebar\"\n        onPressed: {\n            if (root.hasFullscreen)\n                return;\n            const visibilities = Visibilities.getForActive();\n            visibilities.sidebar = !visibilities.sidebar;\n        }\n    }\n\n    CustomShortcut {\n        name: \"utilities\"\n        description: \"Toggle utilities\"\n        onPressed: {\n            if (root.hasFullscreen)\n                return;\n            const visibilities = Visibilities.getForActive();\n            visibilities.utilities = !visibilities.utilities;\n        }\n    }\n\n    IpcHandler {\n        function toggle(drawer: string): void {\n            if (list().split(\"\\n\").includes(drawer)) {\n                if (root.hasFullscreen && [\"launcher\", \"session\", \"dashboard\"].includes(drawer))\n                    return;\n                const visibilities = Visibilities.getForActive();\n                visibilities[drawer] = !visibilities[drawer];\n            } else {\n                console.warn(`[IPC] Drawer \"${drawer}\" does not exist`);\n            }\n        }\n\n        function list(): string {\n            const visibilities = Visibilities.getForActive();\n            return Object.keys(visibilities).filter(k => typeof visibilities[k] === \"boolean\").join(\"\\n\");\n        }\n\n        target: \"drawers\"\n    }\n\n    IpcHandler {\n        function open(): void {\n            WindowFactory.create();\n        }\n\n        target: \"controlCenter\"\n    }\n\n    IpcHandler {\n        function info(title: string, message: string, icon: string): void {\n            Toaster.toast(title, message, icon, Toast.Info);\n        }\n\n        function success(title: string, message: string, icon: string): void {\n            Toaster.toast(title, message, icon, Toast.Success);\n        }\n\n        function warn(title: string, message: string, icon: string): void {\n            Toaster.toast(title, message, icon, Toast.Warning);\n        }\n\n        function error(title: string, message: string, icon: string): void {\n            Toaster.toast(title, message, icon, Toast.Error);\n        }\n\n        target: \"toaster\"\n    }\n}\n"
  },
  {
    "path": "modules/areapicker/AreaPicker.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components.containers\nimport qs.components.misc\nimport qs.services\nimport Quickshell\nimport Quickshell.Wayland\nimport Quickshell.Io\n\nScope {\n    LazyLoader {\n        id: root\n\n        property bool freeze\n        property bool closing\n        property bool clipboardOnly\n\n        Variants {\n            model: Screens.screens\n\n            StyledWindow {\n                id: win\n\n                required property ShellScreen modelData\n\n                screen: modelData\n                name: \"area-picker\"\n                WlrLayershell.exclusionMode: ExclusionMode.Ignore\n                WlrLayershell.layer: WlrLayer.Overlay\n                WlrLayershell.keyboardFocus: root.closing ? WlrKeyboardFocus.None : WlrKeyboardFocus.Exclusive\n                mask: root.closing ? empty : null\n\n                anchors.top: true\n                anchors.bottom: true\n                anchors.left: true\n                anchors.right: true\n\n                Region {\n                    id: empty\n                }\n\n                Picker {\n                    loader: root\n                    screen: win.modelData\n                }\n            }\n        }\n    }\n\n    IpcHandler {\n        function open(): void {\n            root.freeze = false;\n            root.closing = false;\n            root.clipboardOnly = false;\n            root.activeAsync = true;\n        }\n\n        function openFreeze(): void {\n            root.freeze = true;\n            root.closing = false;\n            root.clipboardOnly = false;\n            root.activeAsync = true;\n        }\n\n        function openClip(): void {\n            root.freeze = false;\n            root.closing = false;\n            root.clipboardOnly = true;\n            root.activeAsync = true;\n        }\n\n        function openFreezeClip(): void {\n            root.freeze = true;\n            root.closing = false;\n            root.clipboardOnly = true;\n            root.activeAsync = true;\n        }\n\n        target: \"picker\"\n    }\n\n    CustomShortcut {\n        name: \"screenshot\"\n        description: \"Open screenshot tool\"\n        onPressed: {\n            root.freeze = false;\n            root.closing = false;\n            root.clipboardOnly = false;\n            root.activeAsync = true;\n        }\n    }\n\n    CustomShortcut {\n        name: \"screenshotFreeze\"\n        description: \"Open screenshot tool (freeze mode)\"\n        onPressed: {\n            root.freeze = true;\n            root.closing = false;\n            root.clipboardOnly = false;\n            root.activeAsync = true;\n        }\n    }\n\n    CustomShortcut {\n        name: \"screenshotClip\"\n        description: \"Open screenshot tool (clipboard)\"\n        onPressed: {\n            root.freeze = false;\n            root.closing = false;\n            root.clipboardOnly = true;\n            root.activeAsync = true;\n        }\n    }\n\n    CustomShortcut {\n        name: \"screenshotFreezeClip\"\n        description: \"Open screenshot tool (freeze mode, clipboard)\"\n        onPressed: {\n            root.freeze = true;\n            root.closing = false;\n            root.clipboardOnly = true;\n            root.activeAsync = true;\n        }\n    }\n}\n"
  },
  {
    "path": "modules/areapicker/Picker.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Caelestia\nimport Quickshell\nimport Quickshell.Io\nimport Quickshell.Wayland\nimport QtQuick\nimport QtQuick.Effects\n\nMouseArea {\n    id: root\n\n    required property LazyLoader loader\n    required property ShellScreen screen\n\n    property bool onClient\n\n    property real realBorderWidth: onClient ? (Hypr.options[\"general:border_size\"] ?? 1) : 2\n    property real realRounding: onClient ? (Hypr.options[\"decoration:rounding\"] ?? 0) : 0\n\n    property real ssx\n    property real ssy\n\n    property real sx: 0\n    property real sy: 0\n    property real ex: screen.width\n    property real ey: screen.height\n\n    property real rsx: Math.min(sx, ex)\n    property real rsy: Math.min(sy, ey)\n    property real sw: Math.abs(sx - ex)\n    property real sh: Math.abs(sy - ey)\n\n    property list<var> clients: {\n        const mon = Hypr.monitorFor(screen);\n        if (!mon)\n            return [];\n\n        const special = mon.lastIpcObject.specialWorkspace;\n        const wsId = special.name ? special.id : mon.activeWorkspace.id;\n\n        return Hypr.toplevels.values.filter(c => c.workspace?.id === wsId).sort((a, b) => {\n            // Pinned first, then fullscreen, then floating, then any other\n            const ac = a.lastIpcObject;\n            const bc = b.lastIpcObject;\n            return (bc.pinned - ac.pinned) || ((bc.fullscreen !== 0) - (ac.fullscreen !== 0)) || (bc.floating - ac.floating);\n        });\n    }\n\n    function checkClientRects(x: real, y: real): void {\n        for (const client of clients) {\n            if (!client)\n                continue;\n\n            let {\n                at: [cx, cy],\n                size: [cw, ch]\n            } = client.lastIpcObject;\n            cx -= screen.x;\n            cy -= screen.y;\n            if (cx <= x && cy <= y && cx + cw >= x && cy + ch >= y) {\n                onClient = true;\n                sx = cx;\n                sy = cy;\n                ex = cx + cw;\n                ey = cy + ch;\n                break;\n            }\n        }\n    }\n\n    function save(): void {\n        const tmpfile = Qt.resolvedUrl(`/tmp/caelestia-picker-${Quickshell.processId}-${Date.now()}.png`);\n        CUtils.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => {\n            if (root.loader.clipboardOnly) {\n                Quickshell.execDetached([\"sh\", \"-c\", \"wl-copy --type image/png < \" + path]);\n                Quickshell.execDetached([\"notify-send\", \"-a\", \"caelestia-cli\", \"-i\", path, \"Screenshot taken\", \"Screenshot copied to clipboard\"]);\n            } else {\n                Quickshell.execDetached([\"swappy\", \"-f\", path]);\n            }\n            closeAnim.start();\n        });\n    }\n\n    onClientsChanged: checkClientRects(mouseX, mouseY)\n\n    anchors.fill: parent\n    opacity: 0\n    hoverEnabled: true\n    cursorShape: Qt.CrossCursor\n\n    Component.onCompleted: {\n        Hypr.extras.refreshOptions();\n\n        // Break binding if frozen\n        if (loader.freeze)\n            clients = clients;\n\n        opacity = 1;\n\n        const c = clients[0];\n        if (c) {\n            const cx = c.lastIpcObject.at[0] - screen.x;\n            const cy = c.lastIpcObject.at[1] - screen.y;\n            onClient = true;\n            sx = cx;\n            sy = cy;\n            ex = cx + c.lastIpcObject.size[0];\n            ey = cy + c.lastIpcObject.size[1];\n        } else {\n            sx = screen.width / 2 - 100;\n            sy = screen.height / 2 - 100;\n            ex = screen.width / 2 + 100;\n            ey = screen.height / 2 + 100;\n        }\n    }\n\n    onPressed: event => {\n        ssx = event.x;\n        ssy = event.y;\n    }\n\n    onReleased: {\n        if (closeAnim.running)\n            return;\n\n        if (root.loader.freeze) {\n            save();\n        } else {\n            overlay.visible = border.visible = false;\n            screencopy.visible = false;\n            screencopy.active = true;\n        }\n    }\n\n    onPositionChanged: event => {\n        const x = event.x;\n        const y = event.y;\n\n        if (pressed) {\n            onClient = false;\n            sx = ssx;\n            sy = ssy;\n            ex = x;\n            ey = y;\n        } else {\n            checkClientRects(x, y);\n        }\n    }\n\n    focus: true\n    Keys.onEscapePressed: closeAnim.start()\n\n    SequentialAnimation {\n        id: closeAnim\n\n        PropertyAction {\n            target: root.loader\n            property: \"closing\"\n            value: true\n        }\n        ParallelAnimation {\n            Anim {\n                target: root\n                property: \"opacity\"\n                to: 0\n                duration: Appearance.anim.durations.large\n            }\n            ExAnim {\n                target: root\n                properties: \"rsx,rsy\"\n                to: 0\n            }\n            ExAnim {\n                target: root\n                property: \"sw\"\n                to: root.screen.width\n            }\n            ExAnim {\n                target: root\n                property: \"sh\"\n                to: root.screen.height\n            }\n        }\n        PropertyAction {\n            target: root.loader\n            property: \"activeAsync\"\n            value: false\n        }\n    }\n\n    Process {\n        running: true\n        command: [\"hyprctl\", \"cursorpos\", \"-j\"]\n        stdout: StdioCollector {\n            onStreamFinished: {\n                const pos = JSON.parse(text);\n                root.checkClientRects(pos.x - root.screen.x, pos.y - root.screen.y);\n            }\n        }\n    }\n\n    Loader {\n        id: screencopy\n\n        asynchronous: true\n        anchors.fill: parent\n\n        active: root.loader.freeze\n\n        sourceComponent: ScreencopyView {\n            captureSource: root.screen\n\n            onHasContentChanged: {\n                if (hasContent && !root.loader.freeze) {\n                    overlay.visible = border.visible = true;\n                    root.save();\n                }\n            }\n        }\n    }\n\n    StyledRect {\n        id: overlay\n\n        anchors.fill: parent\n        color: Colours.palette.m3secondaryContainer\n        opacity: 0.3\n\n        layer.enabled: true\n        layer.effect: MultiEffect {\n            maskSource: selectionWrapper\n            maskEnabled: true\n            maskInverted: true\n            maskSpreadAtMin: 1\n            maskThresholdMin: 0.5\n        }\n    }\n\n    Item {\n        id: selectionWrapper\n\n        anchors.fill: parent\n        layer.enabled: true\n        visible: false\n\n        Rectangle {\n            id: selectionRect\n\n            radius: root.realRounding\n            x: root.rsx\n            y: root.rsy\n            implicitWidth: root.sw\n            implicitHeight: root.sh\n        }\n    }\n\n    Rectangle {\n        id: border\n\n        color: \"transparent\"\n        radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0\n        border.width: root.realBorderWidth\n        border.color: Colours.palette.m3primary\n\n        x: selectionRect.x - root.realBorderWidth\n        y: selectionRect.y - root.realBorderWidth\n        implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2\n        implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2\n\n        Behavior on border.color {\n            CAnim {}\n        }\n    }\n\n    Behavior on opacity {\n        Anim {\n            duration: Appearance.anim.durations.large\n        }\n    }\n\n    Behavior on rsx {\n        enabled: !root.pressed\n\n        ExAnim {}\n    }\n\n    Behavior on rsy {\n        enabled: !root.pressed\n\n        ExAnim {}\n    }\n\n    Behavior on sw {\n        enabled: !root.pressed\n\n        ExAnim {}\n    }\n\n    Behavior on sh {\n        enabled: !root.pressed\n\n        ExAnim {}\n    }\n\n    component ExAnim: Anim {\n        duration: Appearance.anim.durations.expressiveDefaultSpatial\n        easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n    }\n}\n"
  },
  {
    "path": "modules/background/Background.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport Quickshell\nimport Quickshell.Wayland\nimport QtQuick\n\nLoader {\n    asynchronous: true\n    active: Config.background.enabled\n\n    sourceComponent: Variants {\n        model: Screens.screens\n\n        StyledWindow {\n            id: win\n\n            required property ShellScreen modelData\n\n            screen: modelData\n            name: \"background\"\n            WlrLayershell.exclusionMode: ExclusionMode.Ignore\n            WlrLayershell.layer: Config.background.wallpaperEnabled ? WlrLayer.Background : WlrLayer.Bottom\n            color: Config.background.wallpaperEnabled ? \"black\" : \"transparent\"\n            surfaceFormat.opaque: false\n\n            anchors.top: true\n            anchors.bottom: true\n            anchors.left: true\n            anchors.right: true\n\n            Item {\n                id: behindClock\n\n                anchors.fill: parent\n\n                Loader {\n                    id: wallpaper\n\n                    asynchronous: true\n\n                    anchors.fill: parent\n                    active: Config.background.wallpaperEnabled\n\n                    sourceComponent: Wallpaper {}\n                }\n\n                Visualiser {\n                    anchors.fill: parent\n                    screen: win.modelData\n                    wallpaper: wallpaper\n                }\n            }\n\n            Loader {\n                id: clockLoader\n\n                asynchronous: true\n                active: Config.background.desktopClock.enabled\n\n                anchors.margins: Appearance.padding.large * 2\n                anchors.leftMargin: Appearance.padding.large * 2 + Config.bar.sizes.innerWidth + Math.max(Appearance.padding.smaller, Config.border.thickness)\n\n                state: Config.background.desktopClock.position\n                states: [\n                    State {\n                        name: \"top-left\"\n\n                        AnchorChanges {\n                            target: clockLoader\n                            anchors.top: parent.top\n                            anchors.left: parent.left\n                        }\n                    },\n                    State {\n                        name: \"top-center\"\n\n                        AnchorChanges {\n                            target: clockLoader\n                            anchors.top: parent.top\n                            anchors.horizontalCenter: parent.horizontalCenter\n                        }\n                    },\n                    State {\n                        name: \"top-right\"\n\n                        AnchorChanges {\n                            target: clockLoader\n                            anchors.top: parent.top\n                            anchors.right: parent.right\n                        }\n                    },\n                    State {\n                        name: \"middle-left\"\n\n                        AnchorChanges {\n                            target: clockLoader\n                            anchors.verticalCenter: parent.verticalCenter\n                            anchors.left: parent.left\n                        }\n                    },\n                    State {\n                        name: \"middle-center\"\n\n                        AnchorChanges {\n                            target: clockLoader\n                            anchors.verticalCenter: parent.verticalCenter\n                            anchors.horizontalCenter: parent.horizontalCenter\n                        }\n                    },\n                    State {\n                        name: \"middle-right\"\n\n                        AnchorChanges {\n                            target: clockLoader\n                            anchors.verticalCenter: parent.verticalCenter\n                            anchors.right: parent.right\n                        }\n                    },\n                    State {\n                        name: \"bottom-left\"\n\n                        AnchorChanges {\n                            target: clockLoader\n                            anchors.bottom: parent.bottom\n                            anchors.left: parent.left\n                        }\n                    },\n                    State {\n                        name: \"bottom-center\"\n\n                        AnchorChanges {\n                            target: clockLoader\n                            anchors.bottom: parent.bottom\n                            anchors.horizontalCenter: parent.horizontalCenter\n                        }\n                    },\n                    State {\n                        name: \"bottom-right\"\n\n                        AnchorChanges {\n                            target: clockLoader\n                            anchors.bottom: parent.bottom\n                            anchors.right: parent.right\n                        }\n                    }\n                ]\n\n                transitions: Transition {\n                    AnchorAnimation {\n                        duration: Appearance.anim.durations.expressiveDefaultSpatial\n                        easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                    }\n                }\n\n                sourceComponent: DesktopClock {\n                    wallpaper: behindClock\n                    absX: clockLoader.x\n                    absY: clockLoader.y\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/background/DesktopClock.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Effects\n\nItem {\n    id: root\n\n    required property Item wallpaper\n    required property real absX\n    required property real absY\n\n    property real scale: Config.background.desktopClock.scale\n    readonly property bool bgEnabled: Config.background.desktopClock.background.enabled\n    readonly property bool blurEnabled: bgEnabled && Config.background.desktopClock.background.blur && !GameMode.enabled\n    readonly property bool invertColors: Config.background.desktopClock.invertColors\n    readonly property bool useLightSet: Colours.light ? !invertColors : invertColors\n    readonly property color safePrimary: useLightSet ? Colours.palette.m3primaryContainer : Colours.palette.m3primary\n    readonly property color safeSecondary: useLightSet ? Colours.palette.m3secondaryContainer : Colours.palette.m3secondary\n    readonly property color safeTertiary: useLightSet ? Colours.palette.m3tertiaryContainer : Colours.palette.m3tertiary\n\n    implicitWidth: layout.implicitWidth + (Appearance.padding.large * 4 * root.scale)\n    implicitHeight: layout.implicitHeight + (Appearance.padding.large * 2 * root.scale)\n\n    Item {\n        id: clockContainer\n\n        anchors.fill: parent\n\n        layer.enabled: Config.background.desktopClock.shadow.enabled\n        layer.effect: MultiEffect {\n            shadowEnabled: true\n            shadowColor: Colours.palette.m3shadow\n            shadowOpacity: Config.background.desktopClock.shadow.opacity\n            shadowBlur: Config.background.desktopClock.shadow.blur\n        }\n\n        Loader {\n            asynchronous: true\n            anchors.fill: parent\n            active: root.blurEnabled\n\n            sourceComponent: MultiEffect {\n                source: ShaderEffectSource {\n                    sourceItem: root.wallpaper\n                    sourceRect: Qt.rect(root.absX, root.absY, root.width, root.height)\n                }\n                maskSource: backgroundPlate\n                maskEnabled: true\n                blurEnabled: true\n                blur: 1\n                blurMax: 64\n                autoPaddingEnabled: false\n            }\n        }\n\n        StyledRect {\n            id: backgroundPlate\n\n            visible: root.bgEnabled\n            anchors.fill: parent\n            radius: Appearance.rounding.large * root.scale\n            opacity: Config.background.desktopClock.background.opacity\n            color: Colours.palette.m3surface\n\n            layer.enabled: root.blurEnabled\n        }\n\n        RowLayout {\n            id: layout\n\n            anchors.centerIn: parent\n            spacing: Appearance.spacing.larger * root.scale\n\n            RowLayout {\n                spacing: Appearance.spacing.small\n\n                StyledText {\n                    text: Time.hourStr\n                    font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale\n                    font.weight: Font.Bold\n                    color: root.safePrimary\n                }\n\n                StyledText {\n                    text: \":\"\n                    font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale\n                    color: root.safeTertiary\n                    opacity: 0.8\n                    Layout.topMargin: -Appearance.padding.large * 1.5 * root.scale\n                }\n\n                StyledText {\n                    text: Time.minuteStr\n                    font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale\n                    font.weight: Font.Bold\n                    color: root.safeSecondary\n                }\n\n                Loader {\n                    asynchronous: true\n                    Layout.alignment: Qt.AlignTop\n                    Layout.topMargin: Appearance.padding.large * 1.4 * root.scale\n\n                    active: Config.services.useTwelveHourClock\n                    visible: active\n\n                    sourceComponent: StyledText {\n                        text: Time.amPmStr\n                        font.pointSize: Appearance.font.size.large * root.scale\n                        color: root.safeSecondary\n                    }\n                }\n            }\n\n            StyledRect {\n                Layout.fillHeight: true\n                Layout.preferredWidth: 4 * root.scale\n                Layout.topMargin: Appearance.spacing.larger * root.scale\n                Layout.bottomMargin: Appearance.spacing.larger * root.scale\n                radius: Appearance.rounding.full\n                color: root.safePrimary\n                opacity: 0.8\n            }\n\n            ColumnLayout {\n                spacing: 0\n\n                StyledText {\n                    text: Time.format(\"MMMM\").toUpperCase()\n                    font.pointSize: Appearance.font.size.large * root.scale\n                    font.letterSpacing: 4\n                    font.weight: Font.Bold\n                    color: root.safeSecondary\n                }\n\n                StyledText {\n                    text: Time.format(\"dd\")\n                    font.pointSize: Appearance.font.size.extraLarge * root.scale\n                    font.letterSpacing: 2\n                    font.weight: Font.Medium\n                    color: root.safePrimary\n                }\n\n                StyledText {\n                    text: Time.format(\"dddd\")\n                    font.pointSize: Appearance.font.size.larger * root.scale\n                    font.letterSpacing: 2\n                    color: root.safeSecondary\n                }\n            }\n        }\n    }\n\n    Behavior on scale {\n        Anim {\n            duration: Appearance.anim.durations.expressiveDefaultSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n        }\n    }\n\n    Behavior on implicitWidth {\n        Anim {\n            duration: Appearance.anim.durations.small\n        }\n    }\n}\n"
  },
  {
    "path": "modules/background/Visualiser.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Caelestia.Services\nimport Quickshell\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Effects\n\nItem {\n    id: root\n\n    required property ShellScreen screen\n    required property Item wallpaper\n\n    readonly property bool shouldBeActive: Config.background.visualiser.enabled && (!Config.background.visualiser.autoHide || (Hypr.monitorFor(screen)?.activeWorkspace?.toplevels?.values.every(t => t.lastIpcObject?.floating) ?? true))\n    property real offset: shouldBeActive ? 0 : screen.height * 0.2\n\n    opacity: shouldBeActive ? 1 : 0\n\n    Loader {\n        asynchronous: true\n        anchors.fill: parent\n        active: root.opacity > 0 && Config.background.visualiser.blur\n\n        sourceComponent: MultiEffect {\n            source: root.wallpaper\n            maskSource: wrapper\n            maskEnabled: true\n            blurEnabled: true\n            blur: 1\n            blurMax: 32\n            autoPaddingEnabled: false\n        }\n    }\n\n    Item {\n        id: wrapper\n\n        anchors.fill: parent\n        layer.enabled: true\n\n        Loader {\n            asynchronous: true\n            anchors.fill: parent\n            anchors.topMargin: root.offset\n            anchors.bottomMargin: -root.offset\n\n            active: root.opacity > 0\n\n            sourceComponent: Item {\n                ServiceRef {\n                    service: Audio.cava\n                }\n\n                Item {\n                    id: content\n\n                    anchors.fill: parent\n                    anchors.margins: Config.border.thickness\n                    anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Appearance.spacing.small * Config.background.visualiser.spacing\n\n                    Side {\n                        content: content\n                    }\n                    Side {\n                        content: content\n                        isRight: true\n                    }\n\n                    Behavior on anchors.leftMargin {\n                        Anim {}\n                    }\n                }\n            }\n        }\n    }\n\n    Behavior on offset {\n        Anim {}\n    }\n\n    Behavior on opacity {\n        Anim {}\n    }\n\n    component Side: Repeater {\n        id: side\n\n        required property Item content\n        property bool isRight\n\n        model: Config.services.visualiserBars\n\n        ClippingRectangle {\n            id: bar\n\n            required property int modelData\n            property real value: Math.max(0, Math.min(1, Audio.cava.values[side.isRight ? modelData : side.count - modelData - 1]))\n\n            clip: true\n\n            x: modelData * ((side.content.width * 0.4) / Config.services.visualiserBars) + (side.isRight ? side.content.width * 0.6 : 0)\n            implicitWidth: (side.content.width * 0.4) / Config.services.visualiserBars - Appearance.spacing.small * Config.background.visualiser.spacing\n\n            y: side.content.height - height\n            implicitHeight: bar.value * side.content.height * 0.4\n\n            color: \"transparent\"\n            topLeftRadius: Appearance.rounding.small * Config.background.visualiser.rounding\n            topRightRadius: Appearance.rounding.small * Config.background.visualiser.rounding\n\n            Rectangle {\n                topLeftRadius: parent.topLeftRadius\n                topRightRadius: parent.topRightRadius\n\n                gradient: Gradient {\n                    orientation: Gradient.Vertical\n\n                    GradientStop {\n                        position: 0\n                        color: Qt.alpha(Colours.palette.m3primary, 0.7)\n\n                        Behavior on color {\n                            CAnim {}\n                        }\n                    }\n                    GradientStop {\n                        position: 1\n                        color: Qt.alpha(Colours.palette.m3inversePrimary, 0.7)\n\n                        Behavior on color {\n                            CAnim {}\n                        }\n                    }\n                }\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                y: parent.height - height\n                implicitHeight: side.content.height * 0.4\n            }\n\n            Behavior on value {\n                Anim {\n                    duration: Appearance.anim.durations.small\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/background/Wallpaper.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.images\nimport qs.components.filedialog\nimport qs.services\nimport qs.config\nimport qs.utils\nimport QtQuick\n\nItem {\n    id: root\n\n    property string source: Wallpapers.current\n    property Image current: one\n    property bool completed\n\n    onSourceChanged: {\n        if (!source)\n            current = null;\n        else if (current === one)\n            two.update();\n        else\n            one.update();\n    }\n\n    Component.onCompleted: {\n        if (source)\n            Qt.callLater(() => {\n                one.update();\n                completed = true;\n            });\n    }\n\n    Loader {\n        asynchronous: true\n        anchors.fill: parent\n\n        active: root.completed && !root.source\n\n        sourceComponent: StyledRect {\n            color: Colours.palette.m3surfaceContainer\n\n            Row {\n                anchors.centerIn: parent\n                spacing: Appearance.spacing.large\n\n                MaterialIcon {\n                    text: \"sentiment_stressed\"\n                    color: Colours.palette.m3onSurfaceVariant\n                    font.pointSize: Appearance.font.size.extraLarge * 5\n                }\n\n                Column {\n                    anchors.verticalCenter: parent.verticalCenter\n                    spacing: Appearance.spacing.small\n\n                    StyledText {\n                        text: qsTr(\"Wallpaper missing?\")\n                        color: Colours.palette.m3onSurfaceVariant\n                        font.pointSize: Appearance.font.size.extraLarge * 2\n                        font.bold: true\n                    }\n\n                    StyledRect {\n                        implicitWidth: selectWallText.implicitWidth + Appearance.padding.large * 2\n                        implicitHeight: selectWallText.implicitHeight + Appearance.padding.small * 2\n\n                        radius: Appearance.rounding.full\n                        color: Colours.palette.m3primary\n\n                        FileDialog {\n                            id: dialog\n\n                            title: qsTr(\"Select a wallpaper\")\n                            filterLabel: qsTr(\"Image files\")\n                            filters: Images.validImageExtensions\n                            onAccepted: path => Wallpapers.setWallpaper(path)\n                        }\n\n                        StateLayer {\n                            function onClicked(): void {\n                                dialog.open();\n                            }\n\n                            radius: parent.radius\n                            color: Colours.palette.m3onPrimary\n                        }\n\n                        StyledText {\n                            id: selectWallText\n\n                            anchors.centerIn: parent\n\n                            text: qsTr(\"Set it now!\")\n                            color: Colours.palette.m3onPrimary\n                            font.pointSize: Appearance.font.size.large\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    Img {\n        id: one\n    }\n\n    Img {\n        id: two\n    }\n\n    component Img: CachingImage {\n        id: img\n\n        function update(): void {\n            if (path === root.source)\n                root.current = this;\n            else\n                path = root.source;\n        }\n\n        anchors.fill: parent\n\n        opacity: 0\n        scale: Wallpapers.showPreview ? 1 : 0.8\n\n        onStatusChanged: {\n            if (status === Image.Ready)\n                root.current = this;\n        }\n\n        states: State {\n            name: \"visible\"\n            when: root.current === img\n\n            PropertyChanges {\n                img.opacity: 1\n                img.scale: 1\n            }\n        }\n\n        transitions: Transition {\n            Anim {\n                target: img\n                properties: \"opacity,scale\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/Bar.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport \"popouts\" as BarPopouts\nimport \"components\"\nimport \"components/workspaces\"\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property ShellScreen screen\n    required property DrawerVisibilities visibilities\n    required property BarPopouts.Wrapper popouts\n    readonly property int vPadding: Appearance.padding.large\n\n    function closeTray(): void {\n        if (!Config.bar.tray.compact)\n            return;\n\n        for (let i = 0; i < repeater.count; i++) {\n            const loader = repeater.itemAt(i) as WrappedLoader;\n            if (loader?.enabled && loader.id === \"tray\") {\n                (loader.item as Tray).expanded = false;\n            }\n        }\n    }\n\n    function checkPopout(y: real): void {\n        const ch = childAt(width / 2, y) as WrappedLoader;\n\n        if (ch?.id !== \"tray\")\n            closeTray();\n\n        if (!ch) {\n            popouts.hasCurrent = false;\n            return;\n        }\n\n        const id = ch.id;\n        const top = ch.y;\n\n        if (id === \"statusIcons\" && Config.bar.popouts.statusIcons) {\n            const items = (ch.item as StatusIcons).items;\n            const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y);\n            if (icon) {\n                popouts.currentName = icon.name;\n                popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, 0, icon.implicitHeight / 2).y);\n                popouts.hasCurrent = true;\n            }\n        } else if (id === \"tray\" && Config.bar.popouts.tray) {\n            const tray = ch.item as Tray;\n            if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, tray.implicitWidth / 2, y)))) {\n                const index = Math.floor(((y - top - tray.padding * 2 + tray.spacing) / tray.layout.implicitHeight) * tray.items.count);\n                const trayItem = tray.items.itemAt(index);\n                if (trayItem) {\n                    popouts.currentName = `traymenu${index}`;\n                    popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y);\n                    popouts.hasCurrent = true;\n                } else {\n                    popouts.hasCurrent = false;\n                }\n            } else {\n                popouts.hasCurrent = false;\n                tray.expanded = true;\n            }\n        } else if (id === \"activeWindow\" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) {\n            popouts.currentName = id.toLowerCase();\n            popouts.currentCenter = (ch.item as Item).mapToItem(root, 0, (ch.item as Item).implicitHeight / 2).y ?? 0;\n            popouts.hasCurrent = true;\n        }\n    }\n\n    function handleWheel(y: real, angleDelta: point): void {\n        const ch = childAt(width / 2, y) as WrappedLoader;\n        if (ch?.id === \"workspaces\" && Config.bar.scrollActions.workspaces) {\n            // Workspace scroll\n            const mon = (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor);\n            const specialWs = mon?.lastIpcObject.specialWorkspace.name;\n            if (specialWs?.length > 0)\n                Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`);\n            else if (angleDelta.y < 0 || (Config.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1)\n                Hypr.dispatch(`workspace r${angleDelta.y > 0 ? \"-\" : \"+\"}1`);\n        } else if (y < screen.height / 2 && Config.bar.scrollActions.volume) {\n            // Volume scroll on top half\n            if (angleDelta.y > 0)\n                Audio.incrementVolume();\n            else if (angleDelta.y < 0)\n                Audio.decrementVolume();\n        } else if (Config.bar.scrollActions.brightness) {\n            // Brightness scroll on bottom half\n            const monitor = Brightness.getMonitorForScreen(screen);\n            if (angleDelta.y > 0)\n                monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement);\n            else if (angleDelta.y < 0)\n                monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement);\n        }\n    }\n\n    spacing: Appearance.spacing.normal\n\n    Repeater {\n        id: repeater\n\n        model: Config.bar.entries\n\n        DelegateChooser {\n            role: \"id\"\n\n            DelegateChoice {\n                roleValue: \"spacer\"\n                delegate: WrappedLoader {\n                    Layout.fillHeight: enabled\n                }\n            }\n            DelegateChoice {\n                roleValue: \"logo\"\n                delegate: WrappedLoader {\n                    sourceComponent: OsIcon {}\n                }\n            }\n            DelegateChoice {\n                roleValue: \"workspaces\"\n                delegate: WrappedLoader {\n                    sourceComponent: Workspaces {\n                        screen: root.screen\n                    }\n                }\n            }\n            DelegateChoice {\n                roleValue: \"activeWindow\"\n                delegate: WrappedLoader {\n                    Layout.fillWidth: true\n                    sourceComponent: ActiveWindow {\n                        bar: root\n                        monitor: Brightness.getMonitorForScreen(root.screen)\n                    }\n                }\n            }\n            DelegateChoice {\n                roleValue: \"tray\"\n                delegate: WrappedLoader {\n                    sourceComponent: Tray {}\n                }\n            }\n            DelegateChoice {\n                roleValue: \"clock\"\n                delegate: WrappedLoader {\n                    sourceComponent: Clock {}\n                }\n            }\n            DelegateChoice {\n                roleValue: \"statusIcons\"\n                delegate: WrappedLoader {\n                    sourceComponent: StatusIcons {}\n                }\n            }\n            DelegateChoice {\n                roleValue: \"power\"\n                delegate: WrappedLoader {\n                    sourceComponent: Power {\n                        visibilities: root.visibilities\n                    }\n                }\n            }\n        }\n    }\n\n    component WrappedLoader: Loader {\n        required property bool enabled\n        required property string id\n        required property int index\n\n        function findFirstEnabled(): Item {\n            const count = repeater.count;\n            for (let i = 0; i < count; i++) {\n                const item = repeater.itemAt(i);\n                if (item?.enabled)\n                    return item;\n            }\n            return null;\n        }\n\n        function findLastEnabled(): Item {\n            for (let i = repeater.count - 1; i >= 0; i--) {\n                const item = repeater.itemAt(i);\n                if (item?.enabled)\n                    return item;\n            }\n            return null;\n        }\n\n        asynchronous: true\n        Layout.alignment: Qt.AlignHCenter\n\n        // Cursed ahh thing to add padding to first and last enabled components\n        Layout.topMargin: findFirstEnabled() === this ? root.vPadding : 0\n        Layout.bottomMargin: findLastEnabled() === this ? root.vPadding : 0\n\n        visible: enabled\n        active: enabled\n    }\n}\n"
  },
  {
    "path": "modules/bar/BarWrapper.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.config\nimport qs.modules.bar.popouts as BarPopouts\nimport Quickshell\nimport QtQuick\n\nItem {\n    id: root\n\n    required property ShellScreen screen\n    required property DrawerVisibilities visibilities\n    required property BarPopouts.Wrapper popouts\n    required property bool disabled\n\n    readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth)\n    readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness)\n    readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2\n    readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness\n    readonly property bool shouldBeVisible: !disabled && (Config.bar.persistent || visibilities.bar || isHovered)\n    property bool isHovered\n\n    function closeTray(): void {\n        (content.item as Bar)?.closeTray();\n    }\n\n    function checkPopout(y: real): void {\n        (content.item as Bar)?.checkPopout(y);\n    }\n\n    function handleWheel(y: real, angleDelta: point): void {\n        (content.item as Bar)?.handleWheel(y, angleDelta);\n    }\n\n    visible: width > Config.border.thickness\n    implicitWidth: Config.border.thickness\n\n    states: State {\n        name: \"visible\"\n        when: root.shouldBeVisible\n\n        PropertyChanges {\n            root.implicitWidth: root.contentWidth\n        }\n    }\n\n    transitions: [\n        Transition {\n            from: \"\"\n            to: \"visible\"\n\n            Anim {\n                target: root\n                property: \"implicitWidth\"\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        },\n        Transition {\n            from: \"visible\"\n            to: \"\"\n\n            Anim {\n                target: root\n                property: \"implicitWidth\"\n                easing.bezierCurve: Appearance.anim.curves.emphasized\n            }\n        }\n    ]\n\n    Loader {\n        id: content\n\n        anchors.top: parent.top\n        anchors.bottom: parent.bottom\n        anchors.right: parent.right\n\n        active: root.shouldBeVisible || root.visible\n\n        sourceComponent: Bar {\n            width: root.contentWidth\n            screen: root.screen\n            visibilities: root.visibilities\n            popouts: root.popouts\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/ActiveWindow.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.utils\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    required property var bar\n    required property Brightness.Monitor monitor\n    property color colour: Colours.palette.m3primary\n\n    readonly property string windowTitle: {\n        const title = Hypr.activeToplevel?.title;\n        if (!title)\n            return qsTr(\"Desktop\");\n        if (Config.bar.activeWindow.compact) {\n            // \" - \" (standard hyphen), \" — \" (em dash), \" – \" (en dash)\n            const parts = title.split(/\\s+[\\-\\u2013\\u2014]\\s+/);\n            if (parts.length > 1)\n                return parts[parts.length - 1].trim();\n        }\n        return title;\n    }\n\n    readonly property int maxHeight: {\n        const otherModules = bar.children.filter(c => c.id && c.item !== this && c.id !== \"spacer\");\n        const otherHeight = otherModules.reduce((acc, curr) => acc + (curr.item.nonAnimHeight ?? curr.height), 0);\n        // Length - 2 cause repeater counts as a child\n        return bar.height - otherHeight - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2;\n    }\n    property Title current: text1\n\n    clip: true\n    implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight)\n    implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin\n\n    Loader {\n        asynchronous: true\n        anchors.fill: parent\n        active: !Config.bar.activeWindow.showOnHover\n\n        sourceComponent: MouseArea {\n            cursorShape: Qt.PointingHandCursor\n            hoverEnabled: true\n            onPositionChanged: {\n                const popouts = root.bar.popouts;\n                if (popouts.hasCurrent && popouts.currentName !== \"activewindow\")\n                    popouts.hasCurrent = false;\n            }\n            onClicked: {\n                const popouts = root.bar.popouts;\n                if (popouts.hasCurrent) {\n                    popouts.hasCurrent = false;\n                } else {\n                    popouts.currentName = \"activewindow\";\n                    popouts.currentCenter = root.mapToItem(root.bar, 0, root.implicitHeight / 2).y;\n                    popouts.hasCurrent = true;\n                }\n            }\n        }\n    }\n\n    MaterialIcon {\n        id: icon\n\n        anchors.horizontalCenter: parent.horizontalCenter\n\n        animate: true\n        text: Icons.getAppCategoryIcon(Hypr.activeToplevel?.lastIpcObject.class, \"desktop_windows\")\n        color: root.colour\n    }\n\n    Title {\n        id: text1\n    }\n\n    Title {\n        id: text2\n    }\n\n    TextMetrics {\n        id: metrics\n\n        text: root.windowTitle\n        font.pointSize: Appearance.font.size.smaller\n        font.family: Appearance.font.family.mono\n        elide: Qt.ElideRight\n        elideWidth: root.maxHeight - icon.height\n\n        onTextChanged: {\n            const next = root.current === text1 ? text2 : text1;\n            next.text = elidedText;\n            root.current = next;\n        }\n        onElideWidthChanged: root.current.text = elidedText\n    }\n\n    Behavior on implicitHeight {\n        Anim {\n            duration: Appearance.anim.durations.expressiveDefaultSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n        }\n    }\n\n    component Title: StyledText {\n        id: text\n\n        anchors.horizontalCenter: icon.horizontalCenter\n        anchors.top: icon.bottom\n        anchors.topMargin: Appearance.spacing.small\n\n        font.pointSize: metrics.font.pointSize\n        font.family: metrics.font.family\n        color: root.colour\n        opacity: root.current === this ? 1 : 0\n\n        transform: [\n            Translate {\n                x: Config.bar.activeWindow.inverted ? -text.implicitWidth + text.implicitHeight : 0\n            },\n            Rotation {\n                angle: Config.bar.activeWindow.inverted ? 270 : 90\n                origin.x: text.implicitHeight / 2\n                origin.y: text.implicitHeight / 2\n            }\n        ]\n\n        width: implicitHeight\n        height: implicitWidth\n\n        Behavior on opacity {\n            Anim {}\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/Clock.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\n\nStyledRect {\n    id: root\n\n    readonly property color colour: Colours.palette.m3tertiary\n    readonly property int padding: Config.bar.clock.background ? Appearance.padding.normal : Appearance.padding.small\n\n    implicitWidth: Config.bar.sizes.innerWidth\n    implicitHeight: layout.implicitHeight + root.padding * 2\n\n    color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0)\n    radius: Appearance.rounding.full\n\n    Column {\n        id: layout\n\n        anchors.centerIn: parent\n        spacing: Appearance.spacing.small\n\n        Loader {\n            asynchronous: true\n            anchors.horizontalCenter: parent.horizontalCenter\n\n            active: Config.bar.clock.showIcon\n            visible: active\n\n            sourceComponent: MaterialIcon {\n                text: \"calendar_month\"\n                color: root.colour\n            }\n        }\n\n        StyledText {\n            anchors.horizontalCenter: parent.horizontalCenter\n\n            visible: Config.bar.clock.showDate\n\n            horizontalAlignment: StyledText.AlignHCenter\n            text: Time.format(\"ddd\\nd\")\n            font.pointSize: Appearance.font.size.smaller\n            font.family: Appearance.font.family.sans\n            color: root.colour\n        }\n\n        Rectangle {\n            anchors.horizontalCenter: parent.horizontalCenter\n            visible: Config.bar.clock.showDate\n            height: visible ? 1 : 0\n\n            width: parent.width * 0.8\n            color: root.colour\n            opacity: 0.2\n        }\n\n        StyledText {\n            anchors.horizontalCenter: parent.horizontalCenter\n\n            horizontalAlignment: StyledText.AlignHCenter\n            text: Time.format(Config.services.useTwelveHourClock ? \"hh\\nmm\\nA\" : \"hh\\nmm\")\n            font.pointSize: Appearance.font.size.smaller\n            font.family: Appearance.font.family.mono\n            color: root.colour\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/OsIcon.qml",
    "content": "import qs.components\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport qs.utils\nimport QtQuick\n\nItem {\n    id: root\n\n    implicitWidth: Math.round(Appearance.font.size.large * 1.2)\n    implicitHeight: Math.round(Appearance.font.size.large * 1.2)\n\n    MouseArea {\n        anchors.fill: parent\n        cursorShape: Qt.PointingHandCursor\n        onClicked: {\n            const visibilities = Visibilities.getForActive();\n            visibilities.launcher = !visibilities.launcher;\n        }\n    }\n\n    Loader {\n        asynchronous: true\n        anchors.centerIn: parent\n        sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon\n    }\n\n    Component {\n        id: caelestiaLogo\n\n        Logo {\n            implicitWidth: Math.round(Appearance.font.size.large * 1.6)\n            implicitHeight: Math.round(Appearance.font.size.large * 1.6)\n        }\n    }\n\n    Component {\n        id: distroIcon\n\n        ColouredIcon {\n            source: SysInfo.osLogo\n            implicitSize: Math.round(Appearance.font.size.large * 1.2)\n            colour: Colours.palette.m3tertiary\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/Power.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    required property DrawerVisibilities visibilities\n\n    implicitWidth: icon.implicitHeight + Appearance.padding.small * 2\n    implicitHeight: icon.implicitHeight\n\n    StateLayer {\n        // Cursed workaround to make the height larger than the parent\n        function onClicked(): void {\n            root.visibilities.session = !root.visibilities.session;\n        }\n\n        anchors.fill: undefined\n        anchors.centerIn: parent\n        implicitWidth: implicitHeight\n        implicitHeight: icon.implicitHeight + Appearance.padding.small * 2\n        radius: Appearance.rounding.full\n    }\n\n    MaterialIcon {\n        id: icon\n\n        anchors.centerIn: parent\n        anchors.horizontalCenterOffset: -1\n\n        text: \"power_settings_new\"\n        color: Colours.palette.m3error\n        font.bold: true\n        font.pointSize: Appearance.font.size.normal\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/Settings.qml",
    "content": "import qs.components\nimport qs.modules.controlcenter\nimport qs.services\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    implicitWidth: icon.implicitHeight + Appearance.padding.small * 2\n    implicitHeight: icon.implicitHeight\n\n    StateLayer {\n        // Cursed workaround to make the height larger than the parent\n        function onClicked(): void {\n            WindowFactory.create(null, {\n                active: \"network\"\n            });\n        }\n\n        anchors.fill: undefined\n        anchors.centerIn: parent\n        implicitWidth: implicitHeight\n        implicitHeight: icon.implicitHeight + Appearance.padding.small * 2\n        radius: Appearance.rounding.full\n    }\n\n    MaterialIcon {\n        id: icon\n\n        anchors.centerIn: parent\n        anchors.horizontalCenterOffset: -1\n\n        text: \"settings\"\n        color: Colours.palette.m3onSurface\n        font.bold: true\n        font.pointSize: Appearance.font.size.normal\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/SettingsIcon.qml",
    "content": "import qs.components\nimport qs.modules.controlcenter\nimport qs.services\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    implicitWidth: icon.implicitHeight + Appearance.padding.small * 2\n    implicitHeight: icon.implicitHeight\n\n    StateLayer {\n        // Cursed workaround to make the height larger than the parent\n        function onClicked(): void {\n            WindowFactory.create(null, {\n                active: \"network\"\n            });\n        }\n\n        anchors.fill: undefined\n        anchors.centerIn: parent\n        implicitWidth: implicitHeight\n        implicitHeight: icon.implicitHeight + Appearance.padding.small * 2\n        radius: Appearance.rounding.full\n    }\n\n    MaterialIcon {\n        id: icon\n\n        anchors.centerIn: parent\n        anchors.horizontalCenterOffset: -1\n\n        text: \"settings\"\n        color: Colours.palette.m3onSurface\n        font.bold: true\n        font.pointSize: Appearance.font.size.normal\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/StatusIcons.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.utils\nimport qs.config\nimport Quickshell\nimport Quickshell.Bluetooth\nimport Quickshell.Services.UPower\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    property color colour: Colours.palette.m3secondary\n    readonly property alias items: iconColumn\n\n    color: Colours.tPalette.m3surfaceContainer\n    radius: Appearance.rounding.full\n\n    clip: true\n    implicitWidth: Config.bar.sizes.innerWidth\n    implicitHeight: iconColumn.implicitHeight + Appearance.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0)\n\n    ColumnLayout {\n        id: iconColumn\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.bottom: parent.bottom\n        anchors.bottomMargin: Appearance.padding.normal\n\n        spacing: Appearance.spacing.smaller / 2\n\n        // Lock keys status\n        WrappedLoader {\n            name: \"lockstatus\"\n            active: Config.bar.status.showLockStatus\n\n            sourceComponent: ColumnLayout {\n                spacing: 0\n\n                Item {\n                    implicitWidth: capslockIcon.implicitWidth\n                    implicitHeight: Hypr.capsLock ? capslockIcon.implicitHeight : 0\n\n                    MaterialIcon {\n                        id: capslockIcon\n\n                        anchors.centerIn: parent\n\n                        scale: Hypr.capsLock ? 1 : 0.5\n                        opacity: Hypr.capsLock ? 1 : 0\n\n                        text: \"keyboard_capslock_badge\"\n                        color: root.colour\n\n                        Behavior on opacity {\n                            Anim {}\n                        }\n\n                        Behavior on scale {\n                            Anim {}\n                        }\n                    }\n\n                    Behavior on implicitHeight {\n                        Anim {}\n                    }\n                }\n\n                Item {\n                    Layout.topMargin: Hypr.capsLock && Hypr.numLock ? iconColumn.spacing : 0\n\n                    implicitWidth: numlockIcon.implicitWidth\n                    implicitHeight: Hypr.numLock ? numlockIcon.implicitHeight : 0\n\n                    MaterialIcon {\n                        id: numlockIcon\n\n                        anchors.centerIn: parent\n\n                        scale: Hypr.numLock ? 1 : 0.5\n                        opacity: Hypr.numLock ? 1 : 0\n\n                        text: \"looks_one\"\n                        color: root.colour\n\n                        Behavior on opacity {\n                            Anim {}\n                        }\n\n                        Behavior on scale {\n                            Anim {}\n                        }\n                    }\n\n                    Behavior on implicitHeight {\n                        Anim {}\n                    }\n                }\n            }\n        }\n\n        // Audio icon\n        WrappedLoader {\n            name: \"audio\"\n            active: Config.bar.status.showAudio\n\n            sourceComponent: MaterialIcon {\n                animate: true\n                text: Icons.getVolumeIcon(Audio.volume, Audio.muted)\n                color: root.colour\n            }\n        }\n\n        // Microphone icon\n        WrappedLoader {\n            name: \"audio\"\n            active: Config.bar.status.showMicrophone\n\n            sourceComponent: MaterialIcon {\n                animate: true\n                text: Icons.getMicVolumeIcon(Audio.sourceVolume, Audio.sourceMuted)\n                color: root.colour\n            }\n        }\n\n        // Keyboard layout icon\n        WrappedLoader {\n            name: \"kblayout\"\n            active: Config.bar.status.showKbLayout\n\n            sourceComponent: StyledText {\n                animate: true\n                text: Hypr.kbLayout\n                color: root.colour\n                font.family: Appearance.font.family.mono\n            }\n        }\n\n        // Network icon\n        WrappedLoader {\n            name: \"network\"\n            active: Config.bar.status.showNetwork && (!Nmcli.activeEthernet || Config.bar.status.showWifi)\n\n            sourceComponent: MaterialIcon {\n                animate: true\n                text: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : \"wifi_off\"\n                color: root.colour\n            }\n        }\n\n        // Ethernet icon\n        WrappedLoader {\n            name: \"ethernet\"\n            active: Config.bar.status.showNetwork && Nmcli.activeEthernet\n\n            sourceComponent: MaterialIcon {\n                animate: true\n                text: \"cable\"\n                color: root.colour\n            }\n        }\n\n        // Bluetooth section\n        WrappedLoader {\n            Layout.preferredHeight: implicitHeight\n\n            name: \"bluetooth\"\n            active: Config.bar.status.showBluetooth\n\n            sourceComponent: ColumnLayout {\n                spacing: Appearance.spacing.smaller / 2\n\n                // Bluetooth icon\n                MaterialIcon {\n                    animate: true\n                    text: {\n                        if (!Bluetooth.defaultAdapter?.enabled)\n                            return \"bluetooth_disabled\";\n                        if (Bluetooth.devices.values.some(d => d.connected))\n                            return \"bluetooth_connected\";\n                        return \"bluetooth\";\n                    }\n                    color: root.colour\n                }\n\n                // Connected bluetooth devices\n                Repeater {\n                    model: ScriptModel {\n                        values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected)\n                    }\n\n                    MaterialIcon {\n                        id: device\n\n                        required property BluetoothDevice modelData\n\n                        animate: true\n                        text: Icons.getBluetoothIcon(modelData?.icon)\n                        color: root.colour\n                        fill: 1\n\n                        SequentialAnimation on opacity {\n                            running: device.modelData?.state !== BluetoothDeviceState.Connected\n                            alwaysRunToEnd: true\n                            loops: Animation.Infinite\n\n                            Anim {\n                                from: 1\n                                to: 0\n                                duration: Appearance.anim.durations.large\n                                easing.bezierCurve: Appearance.anim.curves.standardAccel\n                            }\n                            Anim {\n                                from: 0\n                                to: 1\n                                duration: Appearance.anim.durations.large\n                                easing.bezierCurve: Appearance.anim.curves.standardDecel\n                            }\n                        }\n                    }\n                }\n            }\n\n            Behavior on Layout.preferredHeight {\n                Anim {}\n            }\n        }\n\n        // Battery icon\n        WrappedLoader {\n            name: \"battery\"\n            active: Config.bar.status.showBattery\n\n            sourceComponent: MaterialIcon {\n                animate: true\n                text: {\n                    if (!UPower.displayDevice.isLaptopBattery) {\n                        if (PowerProfiles.profile === PowerProfile.PowerSaver)\n                            return \"energy_savings_leaf\";\n                        if (PowerProfiles.profile === PowerProfile.Performance)\n                            return \"rocket_launch\";\n                        return \"balance\";\n                    }\n\n                    const perc = UPower.displayDevice.percentage;\n                    const charging = [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state);\n                    if (perc === 1)\n                        return charging ? \"battery_charging_full\" : \"battery_full\";\n                    let level = Math.floor(perc * 7);\n                    if (charging && (level === 4 || level === 1))\n                        level--;\n                    return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`;\n                }\n                color: !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? root.colour : Colours.palette.m3error\n                fill: 1\n            }\n        }\n    }\n\n    component WrappedLoader: Loader {\n        required property string name\n\n        asynchronous: true\n        Layout.alignment: Qt.AlignHCenter\n        visible: active\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/Tray.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Quickshell\nimport Quickshell.Services.SystemTray\nimport QtQuick\n\nStyledRect {\n    id: root\n\n    readonly property alias layout: layout\n    readonly property alias items: items\n    readonly property alias expandIcon: expandIcon\n\n    readonly property int padding: Config.bar.tray.background ? Appearance.padding.normal : Appearance.padding.small\n    readonly property int spacing: Config.bar.tray.background ? Appearance.spacing.small : 0\n\n    property bool expanded\n\n    readonly property real nonAnimHeight: {\n        if (!Config.bar.tray.compact)\n            return layout.implicitHeight + padding * 2;\n        return (expanded ? expandIcon.implicitHeight + layout.implicitHeight + spacing : expandIcon.implicitHeight) + padding * 2;\n    }\n\n    clip: true\n    visible: height > 0\n\n    implicitWidth: Config.bar.sizes.innerWidth\n    implicitHeight: nonAnimHeight\n\n    color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0)\n    radius: Appearance.rounding.full\n\n    Column {\n        id: layout\n\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.top: parent.top\n        anchors.topMargin: root.padding\n        spacing: Appearance.spacing.small\n\n        opacity: root.expanded || !Config.bar.tray.compact ? 1 : 0\n\n        add: Transition {\n            Anim {\n                properties: \"scale\"\n                from: 0\n                to: 1\n                easing.bezierCurve: Appearance.anim.curves.standardDecel\n            }\n        }\n\n        move: Transition {\n            Anim {\n                properties: \"scale\"\n                to: 1\n                easing.bezierCurve: Appearance.anim.curves.standardDecel\n            }\n            Anim {\n                properties: \"x,y\"\n            }\n        }\n\n        Repeater {\n            id: items\n\n            model: ScriptModel {\n                values: SystemTray.items.values.filter(i => !Config.bar.tray.hiddenIcons.includes(i.id))\n            }\n\n            TrayItem {}\n        }\n\n        Behavior on opacity {\n            Anim {}\n        }\n    }\n\n    Loader {\n        id: expandIcon\n\n        asynchronous: true\n\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.bottom: parent.bottom\n\n        active: Config.bar.tray.compact && items.count > 0\n\n        sourceComponent: Item {\n            implicitWidth: expandIconInner.implicitWidth\n            implicitHeight: expandIconInner.implicitHeight - Appearance.padding.small * 2\n\n            MaterialIcon {\n                id: expandIconInner\n\n                anchors.horizontalCenter: parent.horizontalCenter\n                anchors.bottom: parent.bottom\n                anchors.bottomMargin: Config.bar.tray.background ? Appearance.padding.small : -Appearance.padding.small\n                text: \"expand_less\"\n                font.pointSize: Appearance.font.size.large\n                rotation: root.expanded ? 180 : 0\n\n                Behavior on rotation {\n                    Anim {}\n                }\n\n                Behavior on anchors.bottomMargin {\n                    Anim {}\n                }\n            }\n        }\n    }\n\n    Behavior on implicitHeight {\n        Anim {\n            duration: Appearance.anim.durations.expressiveDefaultSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/TrayItem.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell.Services.SystemTray\nimport QtQuick\n\nMouseArea {\n    id: root\n\n    required property SystemTrayItem modelData\n\n    acceptedButtons: Qt.LeftButton | Qt.RightButton\n    implicitWidth: Appearance.font.size.small * 2\n    implicitHeight: Appearance.font.size.small * 2\n\n    onClicked: event => {\n        if (event.button === Qt.LeftButton)\n            modelData.activate();\n        else\n            modelData.secondaryActivate();\n    }\n\n    ColouredIcon {\n        id: icon\n\n        anchors.fill: parent\n        source: Icons.getTrayIcon(root.modelData.id, root.modelData.icon)\n        colour: Colours.palette.m3secondary\n        layer.enabled: Config.bar.tray.recolour\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/workspaces/ActiveIndicator.qml",
    "content": "import qs.components\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport QtQuick\n\nStyledRect {\n    id: root\n\n    required property int activeWsId\n    required property Repeater workspaces\n    required property Item mask\n\n    readonly property int currentWsIdx: {\n        let i = activeWsId - 1;\n        while (i < 0)\n            i += Config.bar.workspaces.shown;\n        return i % Config.bar.workspaces.shown;\n    }\n\n    property real leading: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0\n    property real trailing: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0\n    property real currentSize: workspaces.count > 0 ? (workspaces.itemAt(currentWsIdx) as Workspace)?.size ?? 0 : 0\n    property real offset: Math.min(leading, trailing)\n    property real size: {\n        const s = Math.abs(leading - trailing) + currentSize;\n        if (Config.bar.workspaces.activeTrail && lastWs > currentWsIdx) {\n            const ws = workspaces.itemAt(lastWs) as Workspace;\n            return ws ? Math.min(ws.y + ws.size - offset, s) : 0;\n        }\n        return s;\n    }\n\n    property int cWs\n    property int lastWs\n\n    onCurrentWsIdxChanged: {\n        lastWs = cWs;\n        cWs = currentWsIdx;\n    }\n\n    clip: true\n    y: offset + mask.y\n    implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2\n    implicitHeight: size\n    radius: Appearance.rounding.full\n    color: Colours.palette.m3primary\n\n    Colouriser {\n        source: root.mask\n        sourceColor: Colours.palette.m3onSurface\n        colorizationColor: Colours.palette.m3onPrimary\n\n        x: 0\n        y: -parent.offset\n        implicitWidth: root.mask.implicitWidth\n        implicitHeight: root.mask.implicitHeight\n\n        anchors.horizontalCenter: parent.horizontalCenter\n    }\n\n    Behavior on leading {\n        enabled: Config.bar.workspaces.activeTrail\n\n        EAnim {}\n    }\n\n    Behavior on trailing {\n        enabled: Config.bar.workspaces.activeTrail\n\n        EAnim {\n            duration: Appearance.anim.durations.normal * 2\n        }\n    }\n\n    Behavior on currentSize {\n        enabled: Config.bar.workspaces.activeTrail\n\n        EAnim {}\n    }\n\n    Behavior on offset {\n        enabled: !Config.bar.workspaces.activeTrail\n\n        EAnim {}\n    }\n\n    Behavior on size {\n        enabled: !Config.bar.workspaces.activeTrail\n\n        EAnim {}\n    }\n\n    component EAnim: Anim {\n        easing.bezierCurve: Appearance.anim.curves.emphasized\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/workspaces/OccupiedBg.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\n\nItem {\n    id: root\n\n    required property Repeater workspaces\n    required property var occupied\n    required property int groupOffset\n\n    property list<var> pills: []\n\n    onOccupiedChanged: {\n        if (!occupied)\n            return;\n        let count = 0;\n        const start = groupOffset;\n        const end = start + Config.bar.workspaces.shown;\n        for (const [ws, occ] of Object.entries(occupied)) {\n            if (ws > start && ws <= end && occ) {\n                const isFirstInGroup = Number(ws) === start + 1;\n                const isLastInGroup = Number(ws) === end;\n                if (isFirstInGroup || !occupied[ws - 1]) {\n                    if (pills[count])\n                        pills[count].start = ws;\n                    else\n                        pills.push(pillComp.createObject(root, {\n                            start: ws\n                        }));\n                    count++;\n                }\n                if ((isLastInGroup || !occupied[ws + 1]) && pills[count - 1])\n                    pills[count - 1].end = ws;\n            }\n        }\n        if (pills.length > count)\n            pills.splice(count, pills.length - count).forEach(p => p.destroy());\n    }\n\n    Repeater {\n        model: ScriptModel {\n            values: root.pills.filter(p => p)\n        }\n\n        StyledRect {\n            id: rect\n\n            required property var modelData\n\n            readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null\n            readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null\n\n            function getWsIdx(ws: int): int {\n                let i = ws - 1;\n                while (i < 0)\n                    i += Config.bar.workspaces.shown;\n                return i % Config.bar.workspaces.shown;\n            }\n\n            anchors.horizontalCenter: root.horizontalCenter\n\n            y: (start?.y ?? 0) - 1\n            implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + 2\n            implicitHeight: start && end ? end.y + end.size - start.y + 2 : 0\n\n            color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)\n            radius: Appearance.rounding.full\n\n            scale: 0\n            Component.onCompleted: scale = 1\n\n            Behavior on scale {\n                Anim {\n                    easing.bezierCurve: Appearance.anim.curves.standardDecel\n                }\n            }\n\n            Behavior on y {\n                Anim {}\n            }\n\n            Behavior on implicitHeight {\n                Anim {}\n            }\n        }\n    }\n\n    component Pill: QtObject {\n        property int start\n        property int end\n    }\n\n    Component {\n        id: pillComp\n\n        Pill {}\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/workspaces/SpecialWorkspaces.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.effects\nimport qs.services\nimport qs.utils\nimport qs.config\nimport Quickshell\nimport Quickshell.Hyprland\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property ShellScreen screen\n    readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)\n    readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name ?? \"\"\n\n    layer.enabled: true\n    layer.effect: OpacityMask {\n        maskSource: mask\n    }\n\n    Item {\n        id: mask\n\n        anchors.fill: parent\n        layer.enabled: true\n        visible: false\n\n        Rectangle {\n            anchors.fill: parent\n            radius: Appearance.rounding.full\n\n            gradient: Gradient {\n                orientation: Gradient.Vertical\n\n                GradientStop {\n                    position: 0\n                    color: Qt.rgba(0, 0, 0, 0)\n                }\n                GradientStop {\n                    position: 0.3\n                    color: Qt.rgba(0, 0, 0, 1)\n                }\n                GradientStop {\n                    position: 0.7\n                    color: Qt.rgba(0, 0, 0, 1)\n                }\n                GradientStop {\n                    position: 1\n                    color: Qt.rgba(0, 0, 0, 0)\n                }\n            }\n        }\n\n        Rectangle {\n            anchors.top: parent.top\n            anchors.left: parent.left\n            anchors.right: parent.right\n\n            radius: Appearance.rounding.full\n            implicitHeight: parent.height / 2\n            opacity: view.contentY > 0 ? 0 : 1\n\n            Behavior on opacity {\n                Anim {}\n            }\n        }\n\n        Rectangle {\n            anchors.bottom: parent.bottom\n            anchors.left: parent.left\n            anchors.right: parent.right\n\n            radius: Appearance.rounding.full\n            implicitHeight: parent.height / 2\n            opacity: view.contentY < view.contentHeight - parent.height + Appearance.padding.small ? 0 : 1\n\n            Behavior on opacity {\n                Anim {}\n            }\n        }\n    }\n\n    ListView {\n        id: view\n\n        anchors.fill: parent\n        spacing: Appearance.spacing.normal\n        interactive: false\n\n        currentIndex: model.values.findIndex(w => w.name === root.activeSpecial)\n        onCurrentIndexChanged: currentIndex = Qt.binding(() => model.values.findIndex(w => w.name === root.activeSpecial))\n\n        model: ScriptModel {\n            values: Hypr.workspaces.values.filter(w => w.name.startsWith(\"special:\") && (!Config.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor))\n        }\n\n        preferredHighlightBegin: 0\n        preferredHighlightEnd: height\n        highlightRangeMode: ListView.StrictlyEnforceRange\n\n        highlightFollowsCurrentItem: false\n        highlight: Item {\n            y: view.currentItem?.y ?? 0\n            implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0\n\n            Behavior on y {\n                Anim {}\n            }\n        }\n\n        delegate: SpecialWsDelegate {}\n\n        add: Transition {\n            Anim {\n                properties: \"scale\"\n                from: 0\n                to: 1\n                easing.bezierCurve: Appearance.anim.curves.standardDecel\n            }\n        }\n\n        remove: Transition {\n            Anim {\n                property: \"scale\"\n                to: 0.5\n                duration: Appearance.anim.durations.small\n            }\n            Anim {\n                property: \"opacity\"\n                to: 0\n                duration: Appearance.anim.durations.small\n            }\n        }\n\n        move: Transition {\n            Anim {\n                properties: \"scale\"\n                to: 1\n                easing.bezierCurve: Appearance.anim.curves.standardDecel\n            }\n            Anim {\n                properties: \"x,y\"\n            }\n        }\n\n        displaced: Transition {\n            Anim {\n                properties: \"scale\"\n                to: 1\n                easing.bezierCurve: Appearance.anim.curves.standardDecel\n            }\n            Anim {\n                properties: \"x,y\"\n            }\n        }\n    }\n\n    component SpecialWsDelegate: ColumnLayout {\n        id: ws\n\n        required property HyprlandWorkspace modelData\n        readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0)\n        property int wsId\n        property string icon\n        property bool hasWindows\n\n        anchors.left: view.contentItem.left\n        anchors.right: view.contentItem.right\n\n        spacing: 0\n\n        Component.onCompleted: {\n            wsId = modelData.id;\n            icon = Icons.getSpecialWsIcon(modelData.name);\n            hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0;\n        }\n\n        // Hacky thing cause modelData gets destroyed before the remove anim finishes\n        Connections {\n            function onIdChanged(): void {\n                if (ws.modelData)\n                    ws.wsId = ws.modelData.id;\n            }\n\n            function onNameChanged(): void {\n                if (ws.modelData)\n                    ws.icon = Icons.getSpecialWsIcon(ws.modelData.name);\n            }\n\n            function onLastIpcObjectChanged(): void {\n                if (ws.modelData)\n                    ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0;\n            }\n\n            target: ws.modelData\n        }\n\n        Connections {\n            function onShowWindowsOnSpecialWorkspacesChanged(): void {\n                if (ws.modelData)\n                    ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0;\n            }\n\n            target: Config.bar.workspaces\n        }\n\n        Loader {\n            id: label\n\n            asynchronous: true\n\n            Layout.alignment: Qt.AlignHCenter | Qt.AlignTop\n            Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2\n\n            sourceComponent: ws.icon.length === 1 ? letterComp : iconComp\n\n            Component {\n                id: iconComp\n\n                MaterialIcon {\n                    fill: 1\n                    text: ws.icon\n                    verticalAlignment: Qt.AlignVCenter\n                }\n            }\n\n            Component {\n                id: letterComp\n\n                StyledText {\n                    text: ws.icon\n                    verticalAlignment: Qt.AlignVCenter\n                }\n            }\n        }\n\n        Loader {\n            id: windows\n\n            asynchronous: true\n\n            Layout.alignment: Qt.AlignHCenter\n            Layout.fillHeight: true\n            Layout.preferredHeight: implicitHeight\n\n            visible: active\n            active: ws.hasWindows\n\n            sourceComponent: Column {\n                spacing: 0\n\n                add: Transition {\n                    Anim {\n                        properties: \"scale\"\n                        from: 0\n                        to: 1\n                        easing.bezierCurve: Appearance.anim.curves.standardDecel\n                    }\n                }\n\n                move: Transition {\n                    Anim {\n                        properties: \"scale\"\n                        to: 1\n                        easing.bezierCurve: Appearance.anim.curves.standardDecel\n                    }\n                    Anim {\n                        properties: \"x,y\"\n                    }\n                }\n\n                Repeater {\n                    model: ScriptModel {\n                        values: {\n                            const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId);\n                            const maxIcons = Config.bar.workspaces.maxWindowIcons;\n                            return maxIcons > 0 ? windows.slice(0, maxIcons) : windows;\n                        }\n                    }\n\n                    MaterialIcon {\n                        required property var modelData\n\n                        grade: 0\n                        text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, \"terminal\")\n                        color: Colours.palette.m3onSurfaceVariant\n                    }\n                }\n            }\n\n            Behavior on Layout.preferredHeight {\n                Anim {}\n            }\n        }\n    }\n\n    Loader {\n        asynchronous: true\n        active: Config.bar.workspaces.activeIndicator\n        anchors.fill: parent\n\n        sourceComponent: Item {\n            StyledClippingRect {\n                id: indicator\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n\n                y: (view.currentItem?.y ?? 0) - view.contentY\n                implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0\n\n                color: Colours.palette.m3tertiary\n                radius: Appearance.rounding.full\n\n                Colouriser {\n                    source: view\n                    sourceColor: Colours.palette.m3onSurface\n                    colorizationColor: Colours.palette.m3onTertiary\n\n                    anchors.horizontalCenter: parent.horizontalCenter\n\n                    x: 0\n                    y: -indicator.y\n                    implicitWidth: view.width\n                    implicitHeight: view.height\n                }\n\n                Behavior on y {\n                    Anim {\n                        easing.bezierCurve: Appearance.anim.curves.emphasized\n                    }\n                }\n\n                Behavior on implicitHeight {\n                    Anim {\n                        easing.bezierCurve: Appearance.anim.curves.emphasized\n                    }\n                }\n            }\n        }\n    }\n\n    MouseArea {\n        property real startY\n\n        anchors.fill: view\n\n        drag.target: view.contentItem\n        drag.axis: Drag.YAxis\n        drag.maximumY: 0\n        drag.minimumY: Math.min(0, view.height - view.contentHeight - Appearance.padding.small)\n\n        onPressed: event => startY = event.y\n\n        onClicked: event => {\n            if (Math.abs(event.y - startY) > drag.threshold)\n                return;\n\n            const ws = view.itemAt(event.x, event.y) as SpecialWsDelegate;\n            if (ws?.modelData)\n                Hypr.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`);\n            else\n                Hypr.dispatch(\"togglespecialworkspace special\");\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/workspaces/Workspace.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.utils\nimport qs.config\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property int index\n    required property int activeWsId\n    required property var occupied\n    required property int groupOffset\n\n    readonly property bool isWorkspace: true // Flag for finding workspace children\n    // Unanimated prop for others to use as reference\n    readonly property int size: implicitHeight + (hasWindows ? Appearance.padding.small : 0)\n\n    readonly property int ws: groupOffset + index + 1\n    readonly property bool isOccupied: occupied[ws] ?? false\n    readonly property bool hasWindows: isOccupied && Config.bar.workspaces.showWindows\n\n    Layout.alignment: Qt.AlignHCenter\n    Layout.preferredHeight: size\n\n    spacing: 0\n\n    StyledText {\n        id: indicator\n\n        Layout.alignment: Qt.AlignHCenter | Qt.AlignTop\n        Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2\n\n        animate: true\n        text: {\n            const ws = Hypr.workspaces.values.find(w => w.id === root.ws);\n            const wsName = !ws || ws.name == root.ws ? root.ws : ws.name[0];\n            let displayName = wsName.toString();\n            if (Config.bar.workspaces.capitalisation.toLowerCase() === \"upper\") {\n                displayName = displayName.toUpperCase();\n            } else if (Config.bar.workspaces.capitalisation.toLowerCase() === \"lower\") {\n                displayName = displayName.toLowerCase();\n            }\n            const label = Config.bar.workspaces.label || displayName;\n            const occupiedLabel = Config.bar.workspaces.occupiedLabel || label;\n            const activeLabel = Config.bar.workspaces.activeLabel || (root.isOccupied ? occupiedLabel : label);\n            return root.activeWsId === root.ws ? activeLabel : root.isOccupied ? occupiedLabel : label;\n        }\n        color: Config.bar.workspaces.occupiedBg || root.isOccupied || root.activeWsId === root.ws ? Colours.palette.m3onSurface : Colours.layer(Colours.palette.m3outlineVariant, 2)\n        verticalAlignment: Qt.AlignVCenter\n    }\n\n    Loader {\n        id: windows\n\n        asynchronous: true\n\n        Layout.alignment: Qt.AlignHCenter\n        Layout.fillHeight: true\n        Layout.topMargin: -Config.bar.sizes.innerWidth / 10\n\n        visible: active\n        active: root.hasWindows\n\n        sourceComponent: Column {\n            spacing: 0\n\n            add: Transition {\n                Anim {\n                    properties: \"scale\"\n                    from: 0\n                    to: 1\n                    easing.bezierCurve: Appearance.anim.curves.standardDecel\n                }\n            }\n\n            move: Transition {\n                Anim {\n                    properties: \"scale\"\n                    to: 1\n                    easing.bezierCurve: Appearance.anim.curves.standardDecel\n                }\n                Anim {\n                    properties: \"x,y\"\n                }\n            }\n\n            Repeater {\n                model: ScriptModel {\n                    values: {\n                        const ws = root.ws;\n                        const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws);\n                        const maxIcons = Config.bar.workspaces.maxWindowIcons;\n                        return maxIcons > 0 ? windows.slice(0, maxIcons) : windows;\n                    }\n                }\n\n                MaterialIcon {\n                    required property var modelData\n\n                    grade: 0\n                    text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, \"terminal\")\n                    color: Colours.palette.m3onSurfaceVariant\n                }\n            }\n        }\n    }\n\n    Behavior on Layout.preferredHeight {\n        Anim {}\n    }\n}\n"
  },
  {
    "path": "modules/bar/components/workspaces/Workspaces.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.services\nimport qs.config\nimport qs.components\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Effects\n\nStyledClippingRect {\n    id: root\n\n    required property ShellScreen screen\n\n    readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== \"\"\n    readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId\n\n    readonly property var occupied: {\n        const occ = {};\n        for (const ws of Hypr.workspaces.values)\n            occ[ws.id] = ws.lastIpcObject.windows > 0;\n        return occ;\n    }\n    readonly property int groupOffset: Math.floor((activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown\n\n    property real blur: onSpecial ? 1 : 0\n\n    implicitWidth: Config.bar.sizes.innerWidth\n    implicitHeight: layout.implicitHeight + Appearance.padding.small * 2\n\n    color: Colours.tPalette.m3surfaceContainer\n    radius: Appearance.rounding.full\n\n    Item {\n        anchors.fill: parent\n        scale: root.onSpecial ? 0.8 : 1\n        opacity: root.onSpecial ? 0.5 : 1\n\n        layer.enabled: root.blur > 0\n        layer.effect: MultiEffect {\n            blurEnabled: true\n            blur: root.blur\n            blurMax: 32\n        }\n\n        Loader {\n            asynchronous: true\n            active: Config.bar.workspaces.occupiedBg\n\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.small\n\n            sourceComponent: OccupiedBg {\n                workspaces: workspaces\n                occupied: root.occupied\n                groupOffset: root.groupOffset\n            }\n        }\n\n        ColumnLayout {\n            id: layout\n\n            anchors.centerIn: parent\n            spacing: Math.floor(Appearance.spacing.small / 2)\n\n            Repeater {\n                id: workspaces\n\n                model: Config.bar.workspaces.shown\n\n                Workspace {\n                    activeWsId: root.activeWsId\n                    occupied: root.occupied\n                    groupOffset: root.groupOffset\n                }\n            }\n        }\n\n        Loader {\n            asynchronous: true\n            anchors.horizontalCenter: parent.horizontalCenter\n            active: Config.bar.workspaces.activeIndicator\n\n            sourceComponent: ActiveIndicator {\n                activeWsId: root.activeWsId\n                workspaces: workspaces\n                mask: layout\n            }\n        }\n\n        MouseArea {\n            anchors.fill: layout\n            onClicked: event => {\n                const ws = (layout.childAt(event.x, event.y) as Workspace)?.ws;\n                if (Hypr.activeWsId !== ws)\n                    Hypr.dispatch(`workspace ${ws}`);\n                else\n                    Hypr.dispatch(\"togglespecialworkspace special\");\n            }\n        }\n\n        Behavior on scale {\n            Anim {}\n        }\n\n        Behavior on opacity {\n            Anim {}\n        }\n    }\n\n    Loader {\n        id: specialWs\n\n        asynchronous: true\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.small\n\n        active: opacity > 0\n\n        scale: root.onSpecial ? 1 : 0.5\n        opacity: root.onSpecial ? 1 : 0\n\n        sourceComponent: SpecialWorkspaces {\n            screen: root.screen\n        }\n\n        Behavior on scale {\n            Anim {}\n        }\n\n        Behavior on opacity {\n            Anim {}\n        }\n    }\n\n    Behavior on blur {\n        Anim {\n            duration: Appearance.anim.durations.small\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/ActiveWindow.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.utils\nimport qs.config\nimport Quickshell.Widgets\nimport Quickshell.Wayland\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property PopoutState popouts\n\n    implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Appearance.padding.large * 2\n    implicitHeight: child.implicitHeight\n\n    Column {\n        id: child\n\n        anchors.centerIn: parent\n        spacing: Appearance.spacing.normal\n\n        RowLayout {\n            id: detailsRow\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            spacing: Appearance.spacing.normal\n\n            IconImage {\n                id: icon\n\n                asynchronous: true\n                Layout.alignment: Qt.AlignVCenter\n                implicitSize: details.implicitHeight\n                source: Icons.getAppIcon(Hypr.activeToplevel?.lastIpcObject.class ?? \"\", \"image-missing\")\n            }\n\n            ColumnLayout {\n                id: details\n\n                spacing: 0\n                Layout.fillWidth: true\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: Hypr.activeToplevel?.title ?? \"\"\n                    font.pointSize: Appearance.font.size.normal\n                    elide: Text.ElideRight\n                }\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: Hypr.activeToplevel?.lastIpcObject.class ?? \"\"\n                    color: Colours.palette.m3onSurfaceVariant\n                    elide: Text.ElideRight\n                }\n            }\n\n            Item {\n                implicitWidth: expandIcon.implicitHeight + Appearance.padding.small * 2\n                implicitHeight: expandIcon.implicitHeight + Appearance.padding.small * 2\n\n                Layout.alignment: Qt.AlignVCenter\n\n                StateLayer {\n                    function onClicked(): void {\n                        root.popouts.detachRequested(\"winfo\");\n                    }\n\n                    radius: Appearance.rounding.normal\n                }\n\n                MaterialIcon {\n                    id: expandIcon\n\n                    anchors.centerIn: parent\n                    anchors.horizontalCenterOffset: font.pointSize * 0.05\n\n                    text: \"chevron_right\"\n\n                    font.pointSize: Appearance.font.size.large\n                }\n            }\n        }\n\n        ClippingWrapperRectangle {\n            color: \"transparent\"\n            radius: Appearance.rounding.small\n\n            ScreencopyView {\n                id: preview\n\n                captureSource: Hypr.activeToplevel?.wayland ?? null\n                live: visible\n\n                constraintSize.width: Config.bar.sizes.windowPreviewSize\n                constraintSize.height: Config.bar.sizes.windowPreviewSize\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/Audio.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport Quickshell.Services.Pipewire\nimport QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nItem {\n    id: root\n\n    required property PopoutState popouts\n\n    implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2\n    implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2\n\n    ButtonGroup {\n        id: sinks\n    }\n\n    ButtonGroup {\n        id: sources\n    }\n\n    ColumnLayout {\n        id: layout\n\n        anchors.left: parent.left\n        anchors.verticalCenter: parent.verticalCenter\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            text: qsTr(\"Output device\")\n            font.weight: 500\n        }\n\n        Repeater {\n            model: Audio.sinks\n\n            StyledRadioButton {\n                id: control\n\n                required property PwNode modelData\n\n                ButtonGroup.group: sinks\n                checked: Audio.sink?.id === modelData.id\n                onClicked: Audio.setAudioSink(modelData)\n                text: modelData.description\n            }\n        }\n\n        StyledText {\n            Layout.topMargin: Appearance.spacing.smaller\n            text: qsTr(\"Input device\")\n            font.weight: 500\n        }\n\n        Repeater {\n            model: Audio.sources\n\n            StyledRadioButton {\n                required property PwNode modelData\n\n                ButtonGroup.group: sources\n                checked: Audio.source?.id === modelData.id\n                onClicked: Audio.setAudioSource(modelData)\n                text: modelData.description\n            }\n        }\n\n        StyledText {\n            Layout.topMargin: Appearance.spacing.smaller\n            Layout.bottomMargin: -Appearance.spacing.small / 2\n            text: qsTr(\"Volume (%1)\").arg(Audio.muted ? qsTr(\"Muted\") : `${Math.round(Audio.volume * 100)}%`)\n            font.weight: 500\n        }\n\n        CustomMouseArea {\n            Layout.fillWidth: true\n            implicitHeight: Appearance.padding.normal * 3\n\n            onWheel: event => {\n                if (event.angleDelta.y > 0)\n                    Audio.incrementVolume();\n                else if (event.angleDelta.y < 0)\n                    Audio.decrementVolume();\n            }\n\n            StyledSlider {\n                anchors.left: parent.left\n                anchors.right: parent.right\n                implicitHeight: parent.implicitHeight\n\n                value: Audio.volume\n                onMoved: Audio.setVolume(value)\n\n                Behavior on value {\n                    Anim {}\n                }\n            }\n        }\n\n        IconTextButton {\n            Layout.fillWidth: true\n            Layout.topMargin: Appearance.spacing.normal\n            inactiveColour: Colours.palette.m3primaryContainer\n            inactiveOnColour: Colours.palette.m3onPrimaryContainer\n            verticalPadding: Appearance.padding.small\n            text: qsTr(\"Open settings\")\n            icon: \"settings\"\n\n            onClicked: root.popouts.detachRequested(\"audio\")\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/Background.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Shapes\n\nShapePath {\n    id: root\n\n    required property Wrapper wrapper\n    required property bool invertBottomRounding\n    readonly property real rounding: wrapper.isDetached ? Appearance.rounding.normal : Config.border.rounding\n    readonly property bool flatten: wrapper.width < rounding * 2\n    readonly property real roundingX: flatten ? wrapper.width / 2 : rounding\n    property real ibr: invertBottomRounding ? -1 : 1\n\n    property real sideRounding: startX > 0 ? -1 : 1\n\n    strokeWidth: -1\n    fillColor: Colours.palette.m3surface\n\n    PathArc {\n        relativeX: root.roundingX\n        relativeY: root.rounding * root.sideRounding\n        radiusX: Math.min(root.rounding, root.wrapper.width)\n        radiusY: root.rounding\n        direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise\n    }\n    PathLine {\n        relativeX: root.wrapper.width - root.roundingX * 2\n        relativeY: 0\n    }\n    PathArc {\n        relativeX: root.roundingX\n        relativeY: root.rounding\n        radiusX: Math.min(root.rounding, root.wrapper.width)\n        radiusY: root.rounding\n    }\n    PathLine {\n        relativeX: 0\n        relativeY: root.wrapper.height - root.rounding * 2\n    }\n    PathArc {\n        relativeX: -root.roundingX * root.ibr\n        relativeY: root.rounding\n        radiusX: Math.min(root.rounding, root.wrapper.width)\n        radiusY: root.rounding\n        direction: root.ibr < 0 ? PathArc.Counterclockwise : PathArc.Clockwise\n    }\n    PathLine {\n        relativeX: -(root.wrapper.width - root.roundingX - root.roundingX * root.ibr)\n        relativeY: 0\n    }\n    PathArc {\n        relativeX: -root.roundingX\n        relativeY: root.rounding * root.sideRounding\n        radiusX: Math.min(root.rounding, root.wrapper.width)\n        radiusY: root.rounding\n        direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise\n    }\n\n    Behavior on fillColor {\n        CAnim {}\n    }\n\n    Behavior on ibr {\n        Anim {}\n    }\n\n    Behavior on sideRounding {\n        Anim {}\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/Battery.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Quickshell.Services.UPower\nimport QtQuick\n\nColumn {\n    id: root\n\n    spacing: Appearance.spacing.normal\n    width: Config.bar.sizes.batteryWidth\n\n    StyledText {\n        text: UPower.displayDevice.isLaptopBattery ? qsTr(\"Remaining: %1%\").arg(Math.round(UPower.displayDevice.percentage * 100)) : qsTr(\"No battery detected\")\n    }\n\n    StyledText {\n        function formatSeconds(s: int, fallback: string): string {\n            const day = Math.floor(s / 86400);\n            const hr = Math.floor(s / 3600) % 60;\n            const min = Math.floor(s / 60) % 60;\n\n            let comps = [];\n            if (day > 0)\n                comps.push(`${day} days`);\n            if (hr > 0)\n                comps.push(`${hr} hours`);\n            if (min > 0)\n                comps.push(`${min} mins`);\n\n            return comps.join(\", \") || fallback;\n        }\n\n        text: UPower.displayDevice.isLaptopBattery ? qsTr(\"Time %1: %2\").arg(UPower.onBattery ? \"remaining\" : \"until charged\").arg(UPower.onBattery ? formatSeconds(UPower.displayDevice.timeToEmpty, \"Calculating...\") : formatSeconds(UPower.displayDevice.timeToFull, \"Fully charged!\")) : qsTr(\"Power profile: %1\").arg(PowerProfile.toString(PowerProfiles.profile))\n    }\n\n    Loader {\n        asynchronous: true\n        anchors.horizontalCenter: parent.horizontalCenter\n\n        active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None\n\n        height: active ? ((item as Item)?.implicitHeight ?? 0) : 0\n\n        sourceComponent: StyledRect {\n            implicitWidth: child.implicitWidth + Appearance.padding.normal * 2\n            implicitHeight: child.implicitHeight + Appearance.padding.smaller * 2\n\n            color: Colours.palette.m3error\n            radius: Appearance.rounding.normal\n\n            Column {\n                id: child\n\n                anchors.centerIn: parent\n\n                Row {\n                    anchors.horizontalCenter: parent.horizontalCenter\n                    spacing: Appearance.spacing.small\n\n                    MaterialIcon {\n                        anchors.verticalCenter: parent.verticalCenter\n                        anchors.verticalCenterOffset: -font.pointSize / 10\n\n                        text: \"warning\"\n                        color: Colours.palette.m3onError\n                    }\n\n                    StyledText {\n                        anchors.verticalCenter: parent.verticalCenter\n                        text: qsTr(\"Performance Degraded\")\n                        color: Colours.palette.m3onError\n                        font.family: Appearance.font.family.mono\n                        font.weight: 500\n                    }\n\n                    MaterialIcon {\n                        anchors.verticalCenter: parent.verticalCenter\n                        anchors.verticalCenterOffset: -font.pointSize / 10\n\n                        text: \"warning\"\n                        color: Colours.palette.m3onError\n                    }\n                }\n\n                StyledText {\n                    anchors.horizontalCenter: parent.horizontalCenter\n\n                    text: qsTr(\"Reason: %1\").arg(PerformanceDegradationReason.toString(PowerProfiles.degradationReason))\n                    color: Colours.palette.m3onError\n                }\n            }\n        }\n    }\n\n    StyledRect {\n        id: profiles\n\n        property string current: {\n            const p = PowerProfiles.profile;\n            if (p === PowerProfile.PowerSaver)\n                return saver.icon;\n            if (p === PowerProfile.Performance)\n                return perf.icon;\n            return balance.icon;\n        }\n\n        anchors.horizontalCenter: parent.horizontalCenter\n\n        implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Appearance.padding.normal * 2 + Appearance.spacing.large * 2\n        implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Appearance.padding.small * 2\n\n        color: Colours.tPalette.m3surfaceContainer\n        radius: Appearance.rounding.full\n\n        StyledRect {\n            id: indicator\n\n            color: Colours.palette.m3primary\n            radius: Appearance.rounding.full\n            state: profiles.current\n\n            states: [\n                State {\n                    name: saver.icon\n\n                    Fill {\n                        item: saver\n                    }\n                },\n                State {\n                    name: balance.icon\n\n                    Fill {\n                        item: balance\n                    }\n                },\n                State {\n                    name: perf.icon\n\n                    Fill {\n                        item: perf\n                    }\n                }\n            ]\n\n            transitions: Transition {\n                AnchorAnimation {\n                    duration: Appearance.anim.durations.normal\n                    easing.type: Easing.BezierSpline\n                    easing.bezierCurve: Appearance.anim.curves.emphasized\n                }\n            }\n        }\n\n        Profile {\n            id: saver\n\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.left: parent.left\n            anchors.leftMargin: Appearance.padding.small\n\n            profile: PowerProfile.PowerSaver\n            icon: \"energy_savings_leaf\"\n        }\n\n        Profile {\n            id: balance\n\n            anchors.centerIn: parent\n\n            profile: PowerProfile.Balanced\n            icon: \"balance\"\n        }\n\n        Profile {\n            id: perf\n\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.right: parent.right\n            anchors.rightMargin: Appearance.padding.small\n\n            profile: PowerProfile.Performance\n            icon: \"rocket_launch\"\n        }\n    }\n\n    component Fill: AnchorChanges {\n        required property Item item\n\n        target: indicator\n        anchors.left: item.left\n        anchors.right: item.right\n        anchors.top: item.top\n        anchors.bottom: item.bottom\n    }\n\n    component Profile: Item {\n        required property string icon\n        required property int profile\n\n        implicitWidth: icon.implicitHeight + Appearance.padding.small * 2\n        implicitHeight: icon.implicitHeight + Appearance.padding.small * 2\n\n        StateLayer {\n            function onClicked(): void {\n                PowerProfiles.profile = parent.profile;\n            }\n\n            radius: Appearance.rounding.full\n            color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n        }\n\n        MaterialIcon {\n            id: icon\n\n            anchors.centerIn: parent\n\n            text: parent.icon\n            font.pointSize: Appearance.font.size.large\n            color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n            fill: profiles.current === text ? 1 : 0\n\n            Behavior on fill {\n                Anim {}\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/Bluetooth.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport Quickshell.Bluetooth\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property PopoutState popouts\n\n    spacing: Appearance.spacing.small\n\n    StyledText {\n        Layout.topMargin: Appearance.padding.normal\n        Layout.rightMargin: Appearance.padding.small\n        text: qsTr(\"Bluetooth\")\n        font.weight: 500\n    }\n\n    Toggle {\n        label: qsTr(\"Enabled\")\n        checked: Bluetooth.defaultAdapter?.enabled ?? false\n        toggle.onToggled: {\n            const adapter = Bluetooth.defaultAdapter;\n            if (adapter)\n                adapter.enabled = checked;\n        }\n    }\n\n    Toggle {\n        label: qsTr(\"Discovering\")\n        checked: Bluetooth.defaultAdapter?.discovering ?? false\n        toggle.onToggled: {\n            const adapter = Bluetooth.defaultAdapter;\n            if (adapter)\n                adapter.discovering = checked;\n        }\n    }\n\n    StyledText {\n        Layout.topMargin: Appearance.spacing.small\n        Layout.rightMargin: Appearance.padding.small\n        text: {\n            const devices = Bluetooth.devices.values;\n            let available = qsTr(\"%1 device%2 available\").arg(devices.length).arg(devices.length === 1 ? \"\" : \"s\");\n            const connected = devices.filter(d => d.connected).length;\n            if (connected > 0)\n                available += qsTr(\" (%1 connected)\").arg(connected);\n            return available;\n        }\n        color: Colours.palette.m3onSurfaceVariant\n        font.pointSize: Appearance.font.size.small\n    }\n\n    Repeater {\n        model: ScriptModel {\n            values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5)\n        }\n\n        RowLayout {\n            id: device\n\n            required property BluetoothDevice modelData\n            readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting\n\n            Layout.fillWidth: true\n            Layout.rightMargin: Appearance.padding.small\n            spacing: Appearance.spacing.small\n\n            opacity: 0\n            scale: 0.7\n\n            Component.onCompleted: {\n                opacity = 1;\n                scale = 1;\n            }\n\n            Behavior on opacity {\n                Anim {}\n            }\n\n            Behavior on scale {\n                Anim {}\n            }\n\n            MaterialIcon {\n                text: Icons.getBluetoothIcon(device.modelData.icon)\n            }\n\n            StyledText {\n                Layout.leftMargin: Appearance.spacing.small / 2\n                Layout.rightMargin: Appearance.spacing.small / 2\n                Layout.fillWidth: true\n                text: device.modelData.name\n            }\n\n            StyledRect {\n                id: connectBtn\n\n                implicitWidth: implicitHeight\n                implicitHeight: connectIcon.implicitHeight + Appearance.padding.small\n\n                radius: Appearance.rounding.full\n                color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0)\n\n                CircularIndicator {\n                    anchors.fill: parent\n                    running: device.loading\n                }\n\n                StateLayer {\n                    function onClicked(): void {\n                        device.modelData.connected = !device.modelData.connected;\n                    }\n\n                    color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n                    disabled: device.loading\n                }\n\n                MaterialIcon {\n                    id: connectIcon\n\n                    anchors.centerIn: parent\n                    animate: true\n                    text: device.modelData.connected ? \"link_off\" : \"link\"\n                    color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n\n                    opacity: device.loading ? 0 : 1\n\n                    Behavior on opacity {\n                        Anim {}\n                    }\n                }\n            }\n\n            Loader {\n                asynchronous: true\n                active: device.modelData.bonded\n                sourceComponent: Item {\n                    implicitWidth: connectBtn.implicitWidth\n                    implicitHeight: connectBtn.implicitHeight\n\n                    StateLayer {\n                        function onClicked(): void {\n                            device.modelData.forget();\n                        }\n\n                        radius: Appearance.rounding.full\n                    }\n\n                    MaterialIcon {\n                        anchors.centerIn: parent\n                        text: \"delete\"\n                    }\n                }\n            }\n        }\n    }\n\n    IconTextButton {\n        Layout.fillWidth: true\n        Layout.topMargin: Appearance.spacing.normal\n        inactiveColour: Colours.palette.m3primaryContainer\n        inactiveOnColour: Colours.palette.m3onPrimaryContainer\n        verticalPadding: Appearance.padding.small\n        text: qsTr(\"Open settings\")\n        icon: \"settings\"\n\n        onClicked: root.popouts.detachRequested(\"bluetooth\")\n    }\n\n    component Toggle: RowLayout {\n        required property string label\n        property alias checked: toggle.checked\n        property alias toggle: toggle\n\n        Layout.fillWidth: true\n        Layout.rightMargin: Appearance.padding.small\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            Layout.fillWidth: true\n            text: parent.label\n        }\n\n        StyledSwitch {\n            id: toggle\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/Content.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.config\nimport Quickshell\nimport Quickshell.Services.SystemTray\nimport QtQuick\n\nimport \"./kblayout\"\n\nItem {\n    id: root\n\n    required property PopoutState popouts\n    readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null\n    readonly property Item current: currentPopout?.item ?? null\n\n    anchors.centerIn: parent\n\n    implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2\n    implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2\n\n    Item {\n        id: content\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.large\n\n        Popout {\n            name: \"activewindow\"\n            sourceComponent: ActiveWindow {\n                popouts: root.popouts\n            }\n        }\n\n        Popout {\n            id: networkPopout\n\n            name: \"network\"\n            sourceComponent: Network {\n                popouts: root.popouts\n                view: \"wireless\"\n            }\n        }\n\n        Popout {\n            name: \"ethernet\"\n            sourceComponent: Network {\n                popouts: root.popouts\n                view: \"ethernet\"\n            }\n        }\n\n        Popout {\n            id: passwordPopout\n\n            name: \"wirelesspassword\"\n            sourceComponent: WirelessPassword {\n                id: passwordComponent\n\n                popouts: root.popouts\n                network: (networkPopout.item as Network)?.passwordNetwork ?? null\n            }\n\n            Connections {\n                function onCurrentNameChanged() {\n                    // Update network immediately when password popout becomes active\n                    if (root.popouts.currentName === \"wirelesspassword\") {\n                        // Set network immediately if available\n                        if ((networkPopout.item as Network)?.passwordNetwork) {\n                            if (passwordPopout.item) {\n                                (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork;\n                            }\n                        }\n                        // Also try after a short delay in case networkPopout.item wasn't ready\n                        Qt.callLater(() => {\n                            if (passwordPopout.item && (networkPopout.item as Network)?.passwordNetwork) {\n                                (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork;\n                            }\n                        }, 100);\n                    }\n                }\n\n                target: root.popouts\n            }\n\n            Connections {\n                function onItemChanged() {\n                    // When network popout loads, update password popout if it's active\n                    if (root.popouts.currentName === \"wirelesspassword\" && passwordPopout.item) {\n                        Qt.callLater(() => {\n                            if ((networkPopout.item as Network)?.passwordNetwork) {\n                                (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork;\n                            }\n                        });\n                    }\n                }\n\n                target: networkPopout\n            }\n        }\n\n        Popout {\n            name: \"bluetooth\"\n            sourceComponent: Bluetooth {\n                popouts: root.popouts\n            }\n        }\n\n        Popout {\n            name: \"battery\"\n            sourceComponent: Battery {}\n        }\n\n        Popout {\n            name: \"audio\"\n            sourceComponent: Audio {\n                popouts: root.popouts\n            }\n        }\n\n        Popout {\n            name: \"kblayout\"\n            sourceComponent: KbLayout {}\n        }\n\n        Popout {\n            name: \"lockstatus\"\n            sourceComponent: LockStatus {}\n        }\n\n        Repeater {\n            model: ScriptModel {\n                values: SystemTray.items.values.filter(i => !Config.bar.tray.hiddenIcons.includes(i.id))\n            }\n\n            Popout {\n                id: trayMenu\n\n                required property SystemTrayItem modelData\n                required property int index\n\n                name: `traymenu${index}`\n                sourceComponent: trayMenuComp\n\n                Connections {\n                    function onHasCurrentChanged(): void {\n                        if (root.popouts.hasCurrent && trayMenu.shouldBeActive) {\n                            trayMenu.sourceComponent = null;\n                            trayMenu.sourceComponent = trayMenuComp;\n                        }\n                    }\n\n                    target: root.popouts\n                }\n\n                Component {\n                    id: trayMenuComp\n\n                    TrayMenu {\n                        popouts: root.popouts\n                        trayItem: trayMenu.modelData.menu // qmllint disable unresolved-type\n                    }\n                }\n            }\n        }\n    }\n\n    component Popout: Loader {\n        id: popout\n\n        required property string name\n        readonly property bool shouldBeActive: root.popouts.currentName === name\n\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.right: parent.right\n\n        opacity: 0\n        scale: 0.8\n        active: false\n\n        states: State {\n            name: \"active\"\n            when: popout.shouldBeActive\n\n            PropertyChanges {\n                popout.active: true\n                popout.opacity: 1\n                popout.scale: 1\n            }\n        }\n\n        transitions: [\n            Transition {\n                from: \"active\"\n                to: \"\"\n\n                SequentialAnimation {\n                    Anim {\n                        properties: \"opacity,scale\"\n                        duration: Appearance.anim.durations.small\n                    }\n                    PropertyAction {\n                        target: popout\n                        property: \"active\"\n                    }\n                }\n            },\n            Transition {\n                from: \"\"\n                to: \"active\"\n\n                SequentialAnimation {\n                    PropertyAction {\n                        target: popout\n                        property: \"active\"\n                    }\n                    Anim {\n                        properties: \"opacity,scale\"\n                    }\n                }\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/LockStatus.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick.Layouts\n\nColumnLayout {\n    spacing: Appearance.spacing.small\n\n    StyledText {\n        text: qsTr(\"Capslock: %1\").arg(Hypr.capsLock ? \"Enabled\" : \"Disabled\")\n    }\n\n    StyledText {\n        text: qsTr(\"Numlock: %1\").arg(Hypr.numLock ? \"Enabled\" : \"Disabled\")\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/Network.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property PopoutState popouts\n\n    property string connectingToSsid: \"\"\n    property string view: \"wireless\" // \"wireless\" or \"ethernet\"\n    property var passwordNetwork: null\n    property bool showPasswordDialog: false\n\n    spacing: Appearance.spacing.small\n    width: Config.bar.sizes.networkWidth\n\n    // Wireless section\n    StyledText {\n        visible: root.view === \"wireless\"\n        Layout.preferredHeight: visible ? implicitHeight : 0\n        Layout.topMargin: visible ? Appearance.padding.normal : 0\n        Layout.rightMargin: Appearance.padding.small\n        text: qsTr(\"Wireless\")\n        font.weight: 500\n    }\n\n    Toggle {\n        visible: root.view === \"wireless\"\n        Layout.preferredHeight: visible ? implicitHeight : 0\n        label: qsTr(\"Enabled\")\n        checked: Nmcli.wifiEnabled\n        toggle.onToggled: Nmcli.enableWifi(checked)\n    }\n\n    StyledText {\n        visible: root.view === \"wireless\"\n        Layout.preferredHeight: visible ? implicitHeight : 0\n        Layout.topMargin: visible ? Appearance.spacing.small : 0\n        Layout.rightMargin: Appearance.padding.small\n        text: qsTr(\"%1 networks available\").arg(Nmcli.networks.length)\n        color: Colours.palette.m3onSurfaceVariant\n        font.pointSize: Appearance.font.size.small\n    }\n\n    Repeater {\n        visible: root.view === \"wireless\"\n        model: ScriptModel {\n            values: [...Nmcli.networks].sort((a, b) => {\n                if (a.active !== b.active)\n                    return b.active - a.active;\n                return b.strength - a.strength;\n            }).slice(0, 8)\n        }\n\n        RowLayout {\n            id: networkItem\n\n            required property Nmcli.AccessPoint modelData\n            readonly property bool isConnecting: root.connectingToSsid === modelData.ssid\n            readonly property bool loading: networkItem.isConnecting\n\n            visible: root.view === \"wireless\"\n            Layout.preferredHeight: visible ? implicitHeight : 0\n            Layout.fillWidth: true\n            Layout.rightMargin: Appearance.padding.small\n            spacing: Appearance.spacing.small\n\n            opacity: 0\n            scale: 0.7\n\n            Component.onCompleted: {\n                opacity = 1;\n                scale = 1;\n            }\n\n            Behavior on opacity {\n                Anim {}\n            }\n\n            Behavior on scale {\n                Anim {}\n            }\n\n            MaterialIcon {\n                text: Icons.getNetworkIcon(networkItem.modelData.strength)\n                color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant\n            }\n\n            MaterialIcon {\n                visible: networkItem.modelData.isSecure\n                text: \"lock\"\n                font.pointSize: Appearance.font.size.small\n            }\n\n            StyledText {\n                Layout.leftMargin: Appearance.spacing.small / 2\n                Layout.rightMargin: Appearance.spacing.small / 2\n                Layout.fillWidth: true\n                text: networkItem.modelData.ssid\n                elide: Text.ElideRight\n                font.weight: networkItem.modelData.active ? 500 : 400\n                color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurface\n            }\n\n            StyledRect {\n                implicitWidth: implicitHeight\n                implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.small\n\n                radius: Appearance.rounding.full\n                color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0)\n\n                CircularIndicator {\n                    anchors.fill: parent\n                    running: networkItem.loading\n                }\n\n                StateLayer {\n                    function onClicked(): void {\n                        if (networkItem.modelData.active) {\n                            Nmcli.disconnectFromNetwork();\n                        } else {\n                            root.connectingToSsid = networkItem.modelData.ssid;\n                            NetworkConnection.handleConnect(networkItem.modelData, null, network => {\n                                // Password is required - show password dialog\n                                root.passwordNetwork = network;\n                                root.showPasswordDialog = true;\n                                root.popouts.currentName = \"wirelesspassword\";\n                            });\n\n                            // Clear connecting state if connection succeeds immediately (saved profile)\n                            // This is handled by the onActiveChanged connection below\n                        }\n                    }\n\n                    color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n                    disabled: networkItem.loading || !Nmcli.wifiEnabled\n                }\n\n                MaterialIcon {\n                    id: wirelessConnectIcon\n\n                    anchors.centerIn: parent\n                    animate: true\n                    text: networkItem.modelData.active ? \"link_off\" : \"link\"\n                    color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n\n                    opacity: networkItem.loading ? 0 : 1\n\n                    Behavior on opacity {\n                        Anim {}\n                    }\n                }\n            }\n        }\n    }\n\n    StyledRect {\n        visible: root.view === \"wireless\"\n        Layout.preferredHeight: visible ? implicitHeight : 0\n        Layout.topMargin: visible ? Appearance.spacing.small : 0\n        Layout.fillWidth: true\n        implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small * 2\n\n        radius: Appearance.rounding.full\n        color: Colours.palette.m3primaryContainer\n\n        StateLayer {\n            function onClicked(): void {\n                Nmcli.rescanWifi();\n            }\n\n            color: Colours.palette.m3onPrimaryContainer\n            disabled: Nmcli.scanning || !Nmcli.wifiEnabled\n        }\n\n        RowLayout {\n            id: rescanBtn\n\n            anchors.centerIn: parent\n            spacing: Appearance.spacing.small\n            opacity: Nmcli.scanning ? 0 : 1\n\n            MaterialIcon {\n                id: scanIcon\n\n                Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575)\n                animate: true\n                text: \"wifi_find\"\n                color: Colours.palette.m3onPrimaryContainer\n            }\n\n            StyledText {\n                Layout.topMargin: -Math.round(scanIcon.fontInfo.pointSize * 0.0575)\n                text: qsTr(\"Rescan networks\")\n                color: Colours.palette.m3onPrimaryContainer\n            }\n\n            Behavior on opacity {\n                Anim {}\n            }\n        }\n\n        CircularIndicator {\n            anchors.centerIn: parent\n            strokeWidth: Appearance.padding.small / 2\n            bgColour: \"transparent\"\n            implicitSize: parent.implicitHeight - Appearance.padding.smaller * 2\n            running: Nmcli.scanning\n        }\n    }\n\n    // Ethernet section\n    StyledText {\n        visible: root.view === \"ethernet\"\n        Layout.preferredHeight: visible ? implicitHeight : 0\n        Layout.topMargin: visible ? Appearance.padding.normal : 0\n        Layout.rightMargin: Appearance.padding.small\n        text: qsTr(\"Ethernet\")\n        font.weight: 500\n    }\n\n    StyledText {\n        visible: root.view === \"ethernet\"\n        Layout.preferredHeight: visible ? implicitHeight : 0\n        Layout.topMargin: visible ? Appearance.spacing.small : 0\n        Layout.rightMargin: Appearance.padding.small\n        text: qsTr(\"%1 devices available\").arg(Nmcli.ethernetDevices.length)\n        color: Colours.palette.m3onSurfaceVariant\n        font.pointSize: Appearance.font.size.small\n    }\n\n    Repeater {\n        visible: root.view === \"ethernet\"\n        model: ScriptModel {\n            values: [...Nmcli.ethernetDevices].sort((a, b) => {\n                if (a.connected !== b.connected)\n                    return b.connected - a.connected;\n                return (a.interface || \"\").localeCompare(b.interface || \"\");\n            }).slice(0, 8)\n        }\n\n        RowLayout {\n            id: ethernetItem\n\n            required property var modelData\n            readonly property bool loading: false\n\n            visible: root.view === \"ethernet\"\n            Layout.preferredHeight: visible ? implicitHeight : 0\n            Layout.fillWidth: true\n            Layout.rightMargin: Appearance.padding.small\n            spacing: Appearance.spacing.small\n\n            opacity: 0\n            scale: 0.7\n\n            Component.onCompleted: {\n                opacity = 1;\n                scale = 1;\n            }\n\n            Behavior on opacity {\n                Anim {}\n            }\n\n            Behavior on scale {\n                Anim {}\n            }\n\n            MaterialIcon {\n                text: \"cable\"\n                color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant\n            }\n\n            StyledText {\n                Layout.leftMargin: Appearance.spacing.small / 2\n                Layout.rightMargin: Appearance.spacing.small / 2\n                Layout.fillWidth: true\n                text: ethernetItem.modelData.interface || qsTr(\"Unknown\")\n                elide: Text.ElideRight\n                font.weight: ethernetItem.modelData.connected ? 500 : 400\n                color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface\n            }\n\n            StyledRect {\n                implicitWidth: implicitHeight\n                implicitHeight: connectIcon.implicitHeight + Appearance.padding.small\n\n                radius: Appearance.rounding.full\n                color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0)\n\n                CircularIndicator {\n                    anchors.fill: parent\n                    running: ethernetItem.loading\n                }\n\n                StateLayer {\n                    function onClicked(): void {\n                        if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) {\n                            Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {});\n                        } else {\n                            Nmcli.connectEthernet(ethernetItem.modelData.connection || \"\", ethernetItem.modelData.interface || \"\", () => {});\n                        }\n                    }\n\n                    color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n                    disabled: ethernetItem.loading\n                }\n\n                MaterialIcon {\n                    id: connectIcon\n\n                    anchors.centerIn: parent\n                    animate: true\n                    text: ethernetItem.modelData.connected ? \"link_off\" : \"link\"\n                    color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n\n                    opacity: ethernetItem.loading ? 0 : 1\n\n                    Behavior on opacity {\n                        Anim {}\n                    }\n                }\n            }\n        }\n    }\n\n    Connections {\n        function onActiveChanged(): void {\n            if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) {\n                root.connectingToSsid = \"\";\n                // Close password dialog if we successfully connected\n                if (root.showPasswordDialog && root.passwordNetwork && Nmcli.active.ssid === root.passwordNetwork.ssid) {\n                    root.showPasswordDialog = false;\n                    root.passwordNetwork = null;\n                    if (root.popouts.currentName === \"wirelesspassword\") {\n                        root.popouts.currentName = \"network\";\n                    }\n                }\n            }\n        }\n\n        function onScanningChanged(): void {\n            if (!Nmcli.scanning)\n                scanIcon.rotation = 0;\n        }\n\n        target: Nmcli\n    }\n\n    Connections {\n        function onCurrentNameChanged(): void {\n            // Clear password network when leaving password dialog\n            if (root.popouts.currentName !== \"wirelesspassword\" && root.showPasswordDialog) {\n                root.showPasswordDialog = false;\n                root.passwordNetwork = null;\n            }\n        }\n\n        target: root.popouts\n    }\n\n    component Toggle: RowLayout {\n        required property string label\n        property alias checked: toggle.checked\n        property alias toggle: toggle\n\n        Layout.fillWidth: true\n        Layout.rightMargin: Appearance.padding.small\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            Layout.fillWidth: true\n            text: parent.label\n        }\n\n        StyledSwitch {\n            id: toggle\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/PopoutState.qml",
    "content": "import QtQuick\n\nQtObject {\n    property string currentName\n    property bool hasCurrent\n\n    signal detachRequested(mode: string)\n}\n"
  },
  {
    "path": "modules/bar/popouts/TrayMenu.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Quickshell\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Controls\n\nStackView {\n    id: root\n\n    required property PopoutState popouts\n    required property QsMenuHandle trayItem\n\n    implicitWidth: currentItem?.implicitWidth ?? 0\n    implicitHeight: currentItem?.implicitHeight ?? 0\n\n    initialItem: SubMenu {\n        handle: root.trayItem\n    }\n\n    pushEnter: NoAnim {}\n    pushExit: NoAnim {}\n    popEnter: NoAnim {}\n    popExit: NoAnim {}\n\n    component NoAnim: Transition {\n        NumberAnimation {\n            duration: 0\n        }\n    }\n\n    component SubMenu: Column {\n        id: menu\n\n        required property QsMenuHandle handle\n        property bool isSubMenu\n        property bool shown\n\n        padding: Appearance.padding.smaller\n        spacing: Appearance.spacing.small\n\n        opacity: shown ? 1 : 0\n        scale: shown ? 1 : 0.8\n\n        Component.onCompleted: shown = true\n        StackView.onActivating: shown = true\n        StackView.onDeactivating: shown = false\n        StackView.onRemoved: destroy()\n\n        Behavior on opacity {\n            Anim {}\n        }\n\n        Behavior on scale {\n            Anim {}\n        }\n\n        QsMenuOpener {\n            id: menuOpener\n\n            menu: menu.handle\n        }\n\n        Repeater {\n            model: menuOpener.children\n\n            StyledRect {\n                id: item\n\n                required property QsMenuEntry modelData\n\n                implicitWidth: Config.bar.sizes.trayMenuWidth\n                implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight\n\n                radius: Appearance.rounding.full\n                color: modelData.isSeparator ? Colours.palette.m3outlineVariant : \"transparent\"\n\n                Loader {\n                    id: children\n\n                    asynchronous: true\n                    anchors.left: parent.left\n                    anchors.right: parent.right\n\n                    active: !item.modelData.isSeparator\n\n                    sourceComponent: Item {\n                        implicitHeight: label.implicitHeight\n\n                        StateLayer {\n                            function onClicked(): void {\n                                const entry = item.modelData;\n                                if (entry.hasChildren)\n                                    root.push(subMenuComp.createObject(null, {\n                                        handle: entry,\n                                        isSubMenu: true\n                                    }));\n                                else {\n                                    item.modelData.triggered();\n                                    root.popouts.hasCurrent = false;\n                                }\n                            }\n\n                            anchors.margins: -Appearance.padding.small / 2\n                            anchors.leftMargin: -Appearance.padding.smaller\n                            anchors.rightMargin: -Appearance.padding.smaller\n\n                            radius: item.radius\n                            disabled: !item.modelData.enabled\n                        }\n\n                        Loader {\n                            id: icon\n\n                            asynchronous: true\n                            anchors.left: parent.left\n\n                            active: item.modelData.icon !== \"\"\n\n                            sourceComponent: IconImage {\n                                asynchronous: true\n                                implicitSize: label.implicitHeight\n\n                                source: item.modelData.icon\n                            }\n                        }\n\n                        StyledText {\n                            id: label\n\n                            anchors.left: icon.right\n                            anchors.leftMargin: icon.active ? Appearance.spacing.smaller : 0\n\n                            text: labelMetrics.elidedText\n                            color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline\n                        }\n\n                        TextMetrics {\n                            id: labelMetrics\n\n                            text: item.modelData.text\n                            font.pointSize: label.font.pointSize\n                            font.family: label.font.family\n\n                            elide: Text.ElideRight\n                            elideWidth: Config.bar.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0)\n                        }\n\n                        Loader {\n                            id: expand\n\n                            asynchronous: true\n                            anchors.verticalCenter: parent.verticalCenter\n                            anchors.right: parent.right\n\n                            active: item.modelData.hasChildren\n\n                            sourceComponent: MaterialIcon {\n                                text: \"chevron_right\"\n                                color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        Loader {\n            asynchronous: true\n            active: menu.isSubMenu\n\n            sourceComponent: Item {\n                implicitWidth: back.implicitWidth\n                implicitHeight: back.implicitHeight + Appearance.spacing.small / 2\n\n                Item {\n                    anchors.bottom: parent.bottom\n                    implicitWidth: back.implicitWidth\n                    implicitHeight: back.implicitHeight\n\n                    StyledRect {\n                        anchors.fill: parent\n                        anchors.margins: -Appearance.padding.small / 2\n                        anchors.leftMargin: -Appearance.padding.smaller\n                        anchors.rightMargin: -Appearance.padding.smaller * 2\n\n                        radius: Appearance.rounding.full\n                        color: Colours.palette.m3secondaryContainer\n\n                        StateLayer {\n                            function onClicked(): void {\n                                root.pop();\n                            }\n\n                            radius: parent.radius\n                            color: Colours.palette.m3onSecondaryContainer\n                        }\n                    }\n\n                    Row {\n                        id: back\n\n                        anchors.verticalCenter: parent.verticalCenter\n\n                        MaterialIcon {\n                            anchors.verticalCenter: parent.verticalCenter\n                            text: \"chevron_left\"\n                            color: Colours.palette.m3onSecondaryContainer\n                        }\n\n                        StyledText {\n                            anchors.verticalCenter: parent.verticalCenter\n                            text: qsTr(\"Back\")\n                            color: Colours.palette.m3onSecondaryContainer\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    Component {\n        id: subMenuComp\n\n        SubMenu {}\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/WirelessPassword.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property PopoutState popouts\n    property var network: null\n    property bool isClosing: false\n\n    readonly property bool shouldBeVisible: root.popouts.currentName === \"wirelesspassword\"\n\n    function checkConnectionStatus(): void {\n        if (!root.shouldBeVisible || !connectButton.connecting) {\n            return;\n        }\n\n        // Check if we're connected to the target network (case-insensitive SSID comparison)\n        const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();\n\n        if (isConnected) {\n            // Successfully connected - give it a moment for network list to update\n            // Use Timer for actual delay\n            connectionSuccessTimer.start();\n            return;\n        }\n\n        // Check for connection failures - if pending connection was cleared but we're not connected\n        if (Nmcli.pendingConnection === null && connectButton.connecting) {\n            // Wait a bit more before giving up (allow time for connection to establish)\n            if (connectionMonitor.repeatCount > 10) {\n                connectionMonitor.stop();\n                connectButton.connecting = false;\n                connectButton.hasError = true;\n                connectButton.enabled = true;\n                connectButton.text = qsTr(\"Connect\");\n                passwordContainer.passwordBuffer = \"\";\n                // Delete the failed connection\n                if (root.network && root.network.ssid) {\n                    Nmcli.forgetNetwork(root.network.ssid);\n                }\n            }\n        }\n    }\n\n    function closeDialog(): void {\n        if (isClosing) {\n            return;\n        }\n\n        isClosing = true;\n        passwordContainer.passwordBuffer = \"\";\n        connectButton.connecting = false;\n        connectButton.hasError = false;\n        connectButton.text = qsTr(\"Connect\");\n        connectionMonitor.stop();\n\n        // Return to network popout\n        if (root.popouts.currentName === \"wirelesspassword\") {\n            root.popouts.currentName = \"network\";\n        }\n    }\n\n    spacing: Appearance.spacing.normal\n    implicitWidth: 400\n    implicitHeight: content.implicitHeight + Appearance.padding.large * 2\n    visible: shouldBeVisible || isClosing\n    enabled: shouldBeVisible && !isClosing\n    focus: enabled\n\n    Component.onCompleted: {\n        if (shouldBeVisible) {\n            // Use Timer for actual delay to ensure dialog is fully rendered\n            focusTimer.start();\n        }\n    }\n\n    onShouldBeVisibleChanged: {\n        if (shouldBeVisible) {\n            // Use Timer for actual delay to ensure dialog is fully rendered\n            focusTimer.start();\n        }\n    }\n\n    Keys.onEscapePressed: closeDialog()\n\n    Connections {\n        function onCurrentNameChanged() {\n            if (root.popouts.currentName === \"wirelesspassword\") {\n                // Update network when popout becomes active\n                Qt.callLater(() => {\n                    // Try to get network from parent Content's networkPopout\n                    const content = root.parent?.parent?.parent;\n                    if (content) {\n                        const networkPopout = content.children.find(c => c.name === \"network\");\n                        if (networkPopout && networkPopout.item) {\n                            root.network = networkPopout.item.passwordNetwork;\n                        }\n                    }\n                    // Force focus to password container when popout becomes active\n                    // Use Timer for actual delay to ensure dialog is fully rendered\n                    focusTimer.start();\n                });\n            }\n        }\n\n        target: root.popouts\n    }\n\n    Timer {\n        id: focusTimer\n\n        interval: 150\n        onTriggered: {\n            root.forceActiveFocus();\n            passwordContainer.forceActiveFocus();\n        }\n    }\n\n    StyledRect {\n        Layout.fillWidth: true\n        Layout.preferredWidth: 400\n        implicitHeight: content.implicitHeight + Appearance.padding.large * 2\n        radius: Appearance.rounding.normal\n        color: Colours.tPalette.m3surfaceContainer\n        visible: root.shouldBeVisible || root.isClosing\n        opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0\n        scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7\n        Keys.onEscapePressed: root.closeDialog()\n\n        Behavior on opacity {\n            Anim {}\n        }\n\n        Behavior on scale {\n            Anim {}\n        }\n\n        ParallelAnimation {\n            running: root.isClosing\n            onFinished: {\n                if (root.isClosing) {\n                    root.isClosing = false;\n                }\n            }\n\n            Anim {\n                target: parent\n                property: \"opacity\"\n                to: 0\n            }\n            Anim {\n                target: parent\n                property: \"scale\"\n                to: 0.7\n            }\n        }\n\n        ColumnLayout {\n            id: content\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.margins: Appearance.padding.large\n\n            spacing: Appearance.spacing.normal\n\n            MaterialIcon {\n                Layout.alignment: Qt.AlignHCenter\n                text: \"lock\"\n                font.pointSize: Appearance.font.size.extraLarge * 2\n            }\n\n            StyledText {\n                Layout.alignment: Qt.AlignHCenter\n                text: qsTr(\"Enter password\")\n                font.pointSize: Appearance.font.size.large\n                font.weight: 500\n            }\n\n            StyledText {\n                id: networkNameText\n\n                Layout.alignment: Qt.AlignHCenter\n                text: {\n                    if (root.network) {\n                        const ssid = root.network.ssid;\n                        if (ssid && ssid.length > 0) {\n                            return qsTr(\"Network: %1\").arg(ssid);\n                        }\n                    }\n                    return qsTr(\"Network: Unknown\");\n                }\n                color: Colours.palette.m3outline\n                font.pointSize: Appearance.font.size.small\n            }\n\n            Timer {\n                property int attempts: 0\n\n                interval: 50\n                running: root.shouldBeVisible && (!root.network || !root.network.ssid)\n                repeat: true\n                onTriggered: {\n                    attempts++;\n                    // Keep trying to get network from Network component\n                    const content = root.parent?.parent?.parent;\n                    if (content) {\n                        const networkPopout = content.children.find(c => c.name === \"network\");\n                        if (networkPopout && networkPopout.item && networkPopout.item.passwordNetwork) {\n                            root.network = networkPopout.item.passwordNetwork;\n                        }\n                    }\n                    // Stop if we got it or after 20 attempts (1 second)\n                    if ((root.network && root.network.ssid) || attempts >= 20) {\n                        stop();\n                        attempts = 0;\n                    }\n                }\n                onRunningChanged: {\n                    if (!running) {\n                        attempts = 0;\n                    }\n                }\n            }\n\n            StyledText {\n                id: statusText\n\n                Layout.alignment: Qt.AlignHCenter\n                Layout.topMargin: Appearance.spacing.small\n                visible: connectButton.connecting || connectButton.hasError\n                text: {\n                    if (connectButton.hasError) {\n                        return qsTr(\"Connection failed. Please check your password and try again.\");\n                    }\n                    if (connectButton.connecting) {\n                        return qsTr(\"Connecting...\");\n                    }\n                    return \"\";\n                }\n                color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.small\n                font.weight: 400\n                wrapMode: Text.WordWrap\n                Layout.maximumWidth: parent.width - Appearance.padding.large * 2\n            }\n\n            FocusScope {\n                id: passwordContainer\n\n                property string passwordBuffer: \"\"\n\n                objectName: \"passwordContainer\"\n                Layout.topMargin: Appearance.spacing.large\n                Layout.fillWidth: true\n                implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2)\n                focus: true\n                activeFocusOnTab: true\n\n                Keys.onPressed: event => {\n                    // Ensure we have focus when receiving keyboard input\n                    if (!activeFocus) {\n                        forceActiveFocus();\n                    }\n\n                    // Clear error when user starts typing\n                    if (connectButton.hasError && event.text && event.text.length > 0) {\n                        connectButton.hasError = false;\n                    }\n\n                    if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {\n                        if (connectButton.enabled) {\n                            connectButton.clicked();\n                        }\n                        event.accepted = true;\n                    } else if (event.key === Qt.Key_Backspace) {\n                        if (event.modifiers & Qt.ControlModifier) {\n                            passwordBuffer = \"\";\n                        } else {\n                            passwordBuffer = passwordBuffer.slice(0, -1);\n                        }\n                        event.accepted = true;\n                    } else if (event.text && event.text.length > 0) {\n                        passwordBuffer += event.text;\n                        event.accepted = true;\n                    }\n                }\n\n                Connections {\n                    function onShouldBeVisibleChanged(): void {\n                        if (root.shouldBeVisible) {\n                            // Use Timer for actual delay to ensure focus works correctly\n                            passwordFocusTimer.start();\n                            passwordContainer.passwordBuffer = \"\";\n                            connectButton.hasError = false;\n                        }\n                    }\n\n                    target: root\n                }\n\n                Timer {\n                    id: passwordFocusTimer\n\n                    interval: 50\n                    onTriggered: {\n                        passwordContainer.forceActiveFocus();\n                    }\n                }\n\n                Component.onCompleted: {\n                    if (root.shouldBeVisible) {\n                        // Use Timer for actual delay to ensure focus works correctly\n                        passwordFocusTimer.start();\n                    }\n                }\n\n                StyledRect {\n                    anchors.fill: parent\n                    radius: Appearance.rounding.normal\n                    color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer\n                    border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0)\n                    border.color: {\n                        if (connectButton.hasError) {\n                            return Colours.palette.m3error;\n                        }\n                        if (passwordContainer.activeFocus) {\n                            return Colours.palette.m3primary;\n                        }\n                        return root.shouldBeVisible ? Colours.palette.m3outline : \"transparent\";\n                    }\n\n                    Behavior on border.color {\n                        CAnim {}\n                    }\n\n                    Behavior on border.width {\n                        CAnim {}\n                    }\n\n                    Behavior on color {\n                        CAnim {}\n                    }\n                }\n\n                StateLayer {\n                    function onClicked(): void {\n                        passwordContainer.forceActiveFocus();\n                    }\n\n                    hoverEnabled: false\n                    cursorShape: Qt.IBeamCursor\n                    radius: Appearance.rounding.normal\n                }\n\n                StyledText {\n                    id: placeholder\n\n                    anchors.centerIn: parent\n                    text: qsTr(\"Password\")\n                    color: Colours.palette.m3outline\n                    font.pointSize: Appearance.font.size.normal\n                    font.family: Appearance.font.family.mono\n                    opacity: passwordContainer.passwordBuffer ? 0 : 1\n\n                    Behavior on opacity {\n                        Anim {}\n                    }\n                }\n\n                ListView {\n                    id: charList\n\n                    readonly property int fullWidth: count * (implicitHeight + spacing) - spacing\n\n                    anchors.centerIn: parent\n                    implicitWidth: fullWidth\n                    implicitHeight: Appearance.font.size.normal\n\n                    orientation: Qt.Horizontal\n                    spacing: Appearance.spacing.small / 2\n                    interactive: false\n\n                    model: ScriptModel {\n                        values: passwordContainer.passwordBuffer.split(\"\")\n                    }\n\n                    delegate: StyledRect {\n                        id: ch\n\n                        implicitWidth: implicitHeight\n                        implicitHeight: charList.implicitHeight\n\n                        color: Colours.palette.m3onSurface\n                        radius: Appearance.rounding.small / 2\n\n                        opacity: 0\n                        scale: 0\n                        Component.onCompleted: {\n                            opacity = 1;\n                            scale = 1;\n                        }\n                        ListView.onRemove: removeAnim.start()\n\n                        SequentialAnimation {\n                            id: removeAnim\n\n                            PropertyAction {\n                                target: ch\n                                property: \"ListView.delayRemove\"\n                                value: true\n                            }\n                            ParallelAnimation {\n                                Anim {\n                                    target: ch\n                                    property: \"opacity\"\n                                    to: 0\n                                }\n                                Anim {\n                                    target: ch\n                                    property: \"scale\"\n                                    to: 0.5\n                                }\n                            }\n                            PropertyAction {\n                                target: ch\n                                property: \"ListView.delayRemove\"\n                                value: false\n                            }\n                        }\n\n                        Behavior on opacity {\n                            Anim {}\n                        }\n\n                        Behavior on scale {\n                            Anim {\n                                duration: Appearance.anim.durations.expressiveFastSpatial\n                                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                            }\n                        }\n                    }\n\n                    Behavior on implicitWidth {\n                        Anim {}\n                    }\n                }\n            }\n\n            RowLayout {\n                Layout.topMargin: Appearance.spacing.normal\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.normal\n\n                TextButton {\n                    id: cancelButton\n\n                    Layout.fillWidth: true\n                    Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2\n                    inactiveColour: Colours.palette.m3secondaryContainer\n                    inactiveOnColour: Colours.palette.m3onSecondaryContainer\n                    text: qsTr(\"Cancel\")\n\n                    onClicked: root.closeDialog()\n                }\n\n                TextButton {\n                    id: connectButton\n\n                    property bool connecting: false\n                    property bool hasError: false\n\n                    Layout.fillWidth: true\n                    Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2\n                    inactiveColour: Colours.palette.m3primary\n                    inactiveOnColour: Colours.palette.m3onPrimary\n                    text: qsTr(\"Connect\")\n                    enabled: passwordContainer.passwordBuffer.length > 0 && !connecting\n\n                    onClicked: {\n                        if (!root.network || connecting) {\n                            return;\n                        }\n\n                        const password = passwordContainer.passwordBuffer;\n                        if (!password || password.length === 0) {\n                            return;\n                        }\n\n                        // Clear any previous error\n                        hasError = false;\n\n                        // Set connecting state\n                        connecting = true;\n                        enabled = false;\n                        text = qsTr(\"Connecting...\");\n\n                        // Connect to network\n                        NetworkConnection.connectWithPassword(root.network, password, result => {\n                            if (result && result.success)\n                            // Connection successful, monitor will handle the rest\n                            {} else if (result && result.needsPassword) {\n                                // Shouldn't happen since we provided password\n                                connectionMonitor.stop();\n                                connecting = false;\n                                hasError = true;\n                                enabled = true;\n                                text = qsTr(\"Connect\");\n                                passwordContainer.passwordBuffer = \"\";\n                                // Delete the failed connection\n                                if (root.network && root.network.ssid) {\n                                    Nmcli.forgetNetwork(root.network.ssid);\n                                }\n                            } else {\n                                // Connection failed immediately - show error\n                                connectionMonitor.stop();\n                                connecting = false;\n                                hasError = true;\n                                enabled = true;\n                                text = qsTr(\"Connect\");\n                                passwordContainer.passwordBuffer = \"\";\n                                // Delete the failed connection\n                                if (root.network && root.network.ssid) {\n                                    Nmcli.forgetNetwork(root.network.ssid);\n                                }\n                            }\n                        });\n\n                        // Start monitoring connection\n                        connectionMonitor.start();\n                    }\n                }\n            }\n        }\n    }\n\n    Timer {\n        id: connectionMonitor\n\n        property int repeatCount: 0\n\n        interval: 1000\n        repeat: true\n        triggeredOnStart: false\n\n        onTriggered: {\n            repeatCount++;\n            root.checkConnectionStatus();\n        }\n\n        onRunningChanged: {\n            if (!running) {\n                repeatCount = 0;\n            }\n        }\n    }\n\n    Timer {\n        id: connectionSuccessTimer\n\n        interval: 500\n        onTriggered: {\n            // Double-check connection is still active\n            if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) {\n                const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();\n                if (stillConnected) {\n                    connectionMonitor.stop();\n                    connectButton.connecting = false;\n                    connectButton.text = qsTr(\"Connect\");\n                    // Return to network popout on successful connection\n                    if (root.popouts.currentName === \"wirelesspassword\") {\n                        root.popouts.currentName = \"network\";\n                    }\n                    closeDialog();\n                }\n            }\n        }\n    }\n\n    Connections {\n        function onActiveChanged() {\n            if (root.shouldBeVisible) {\n                root.checkConnectionStatus();\n            }\n        }\n\n        function onConnectionFailed(ssid: string) {\n            if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) {\n                connectionMonitor.stop();\n                connectButton.connecting = false;\n                connectButton.hasError = true;\n                connectButton.enabled = true;\n                connectButton.text = qsTr(\"Connect\");\n                passwordContainer.passwordBuffer = \"\";\n                // Delete the failed connection\n                Nmcli.forgetNetwork(ssid);\n            }\n        }\n\n        target: Nmcli\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/Wrapper.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport qs.modules.windowinfo\nimport qs.modules.controlcenter\nimport Quickshell\nimport Quickshell.Wayland\nimport Quickshell.Hyprland\nimport QtQuick\n\nItem {\n    id: root\n\n    required property ShellScreen screen\n\n    readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0\n    readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight\n    readonly property Item current: (content.item as Content)?.current ?? null\n\n    property alias currentName: popoutState.currentName\n    property real currentCenter\n    property alias hasCurrent: popoutState.hasCurrent\n    readonly property PopoutState state: popoutState\n\n    property string detachedMode\n    property string queuedMode\n    readonly property bool isDetached: detachedMode.length > 0\n\n    property int animLength: Appearance.anim.durations.normal\n    property list<real> animCurve: Appearance.anim.curves.emphasized\n\n    function detach(mode: string): void {\n        animLength = Appearance.anim.durations.large;\n        if (mode === \"winfo\") {\n            detachedMode = mode;\n        } else {\n            queuedMode = mode;\n            detachedMode = \"any\";\n        }\n        focus = true;\n    }\n\n    function close(): void {\n        hasCurrent = false;\n        animCurve = Appearance.anim.curves.emphasizedAccel;\n        animLength = Appearance.anim.durations.normal;\n        detachedMode = \"\";\n        animCurve = Appearance.anim.curves.emphasized;\n    }\n\n    visible: width > 0 && height > 0\n    clip: true\n\n    implicitWidth: nonAnimWidth\n    implicitHeight: nonAnimHeight\n\n    focus: hasCurrent\n    Keys.onEscapePressed: {\n        // Forward escape to password popout if active, otherwise close\n        if (currentName === \"wirelesspassword\" && content.item) {\n            const passwordPopout = (content.item as Content)?.children.find(c => c.name === \"wirelesspassword\");\n            if (passwordPopout && passwordPopout.item) {\n                passwordPopout.item.closeDialog();\n                return;\n            }\n        }\n        close();\n    }\n\n    Keys.onPressed: event => {\n        // Don't intercept keys when password popout is active - let it handle them\n        if (currentName === \"wirelesspassword\") {\n            event.accepted = false;\n        }\n    }\n\n    PopoutState {\n        id: popoutState\n\n        onDetachRequested: mode => root.detach(mode)\n    }\n\n    HyprlandFocusGrab {\n        active: root.isDetached\n        windows: [QsWindow.window]\n        onCleared: root.close()\n    }\n\n    Binding {\n        when: root.isDetached\n\n        target: QsWindow.window\n        property: \"WlrLayershell.keyboardFocus\"\n        value: WlrKeyboardFocus.OnDemand\n    }\n\n    Binding {\n        when: root.hasCurrent && root.currentName === \"wirelesspassword\"\n\n        target: QsWindow.window\n        property: \"WlrLayershell.keyboardFocus\"\n        value: WlrKeyboardFocus.OnDemand\n    }\n\n    Comp {\n        id: content\n\n        shouldBeActive: root.hasCurrent && !root.detachedMode\n        anchors.right: parent.right\n        anchors.verticalCenter: parent.verticalCenter\n\n        sourceComponent: Content {\n            popouts: popoutState\n        }\n    }\n\n    Comp {\n        shouldBeActive: root.detachedMode === \"winfo\"\n        anchors.centerIn: parent\n\n        sourceComponent: WindowInfo {\n            screen: root.screen\n            client: Hypr.activeToplevel\n        }\n    }\n\n    Comp {\n        shouldBeActive: root.detachedMode === \"any\"\n        anchors.centerIn: parent\n\n        sourceComponent: ControlCenter {\n            function close(): void {\n                root.close();\n            }\n\n            screen: root.screen\n            active: root.queuedMode\n        }\n    }\n\n    Behavior on x {\n        Anim {\n            duration: root.animLength\n            easing.bezierCurve: root.animCurve\n        }\n    }\n\n    Behavior on y {\n        enabled: root.implicitWidth > 0\n\n        Anim {\n            duration: root.animLength\n            easing.bezierCurve: root.animCurve\n        }\n    }\n\n    Behavior on implicitWidth {\n        Anim {\n            duration: root.animLength\n            easing.bezierCurve: root.animCurve\n        }\n    }\n\n    Behavior on implicitHeight {\n        enabled: root.implicitWidth > 0\n\n        Anim {\n            duration: root.animLength\n            easing.bezierCurve: root.animCurve\n        }\n    }\n\n    component Comp: Loader {\n        id: comp\n\n        property bool shouldBeActive\n\n        active: false\n        opacity: 0\n\n        states: State {\n            name: \"active\"\n            when: comp.shouldBeActive\n\n            PropertyChanges {\n                comp.opacity: 1\n                comp.active: true\n            }\n        }\n\n        transitions: [\n            Transition {\n                from: \"\"\n                to: \"active\"\n\n                SequentialAnimation {\n                    PropertyAction {\n                        property: \"active\"\n                    }\n                    Anim {\n                        property: \"opacity\"\n                    }\n                }\n            },\n            Transition {\n                from: \"active\"\n                to: \"\"\n\n                SequentialAnimation {\n                    Anim {\n                        property: \"opacity\"\n                    }\n                    PropertyAction {\n                        property: \"active\"\n                    }\n                }\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/kblayout/KbLayout.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\nimport qs.components\nimport qs.services\nimport qs.config\n\nColumnLayout {\n    id: root\n\n    function refresh() {\n        kb.refresh();\n    }\n\n    spacing: Appearance.spacing.small\n    width: Config.bar.sizes.kbLayoutWidth\n\n    Component.onCompleted: kb.start()\n\n    KbLayoutModel {\n        id: kb\n    }\n\n    StyledText {\n        Layout.topMargin: Appearance.padding.normal\n        Layout.rightMargin: Appearance.padding.small\n        text: qsTr(\"Keyboard Layouts\")\n        font.weight: 500\n    }\n\n    ListView {\n        id: list\n\n        model: kb.visibleModel\n\n        Layout.fillWidth: true\n        Layout.rightMargin: Appearance.padding.small\n        Layout.topMargin: Appearance.spacing.small\n\n        clip: true\n        interactive: true\n        implicitHeight: Math.min(contentHeight, 320)\n        visible: kb.visibleModel.count > 0\n        spacing: Appearance.spacing.small\n\n        add: Transition {\n            NumberAnimation {\n                properties: \"opacity\"\n                from: 0\n                to: 1\n                duration: 140\n            }\n            NumberAnimation {\n                properties: \"y\"\n                duration: 180\n                easing.type: Easing.OutCubic\n            }\n        }\n        remove: Transition {\n            NumberAnimation {\n                properties: \"opacity\"\n                to: 0\n                duration: 100\n            }\n        }\n        move: Transition {\n            NumberAnimation {\n                properties: \"y\"\n                duration: 180\n                easing.type: Easing.OutCubic\n            }\n        }\n        displaced: Transition {\n            NumberAnimation {\n                properties: \"y\"\n                duration: 180\n                easing.type: Easing.OutCubic\n            }\n        }\n\n        delegate: Item {\n            id: kbDelegate\n\n            required property int layoutIndex\n            required property string label\n            readonly property bool isDisabled: layoutIndex > 3\n\n            width: list.width\n            height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2)\n            ToolTip.visible: isDisabled && layer.containsMouse\n            ToolTip.text: \"XKB limitation: maximum 4 layouts allowed\"\n\n            StateLayer {\n                id: layer\n\n                function onClicked(): void {\n                    if (!kbDelegate.isDisabled)\n                        kb.switchTo(kbDelegate.layoutIndex);\n                }\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.verticalCenter: parent.verticalCenter\n                implicitHeight: parent.height - 4\n                radius: Appearance.rounding.full\n                enabled: !kbDelegate.isDisabled\n            }\n\n            StyledText {\n                id: rowText\n\n                anchors.verticalCenter: layer.verticalCenter\n                anchors.left: layer.left\n                anchors.right: layer.right\n                anchors.leftMargin: Appearance.padding.small\n                anchors.rightMargin: Appearance.padding.small\n                text: kbDelegate.label\n                elide: Text.ElideRight\n                opacity: kbDelegate.isDisabled ? 0.4 : 1.0\n            }\n        }\n    }\n\n    Rectangle {\n        visible: kb.activeLabel.length > 0\n        Layout.fillWidth: true\n        Layout.rightMargin: Appearance.padding.small\n        Layout.topMargin: Appearance.spacing.small\n\n        height: 1\n        color: Colours.palette.m3onSurfaceVariant\n        opacity: 0.35\n    }\n\n    RowLayout {\n        id: activeRow\n\n        visible: kb.activeLabel.length > 0\n        Layout.fillWidth: true\n        Layout.rightMargin: Appearance.padding.small\n        Layout.topMargin: Appearance.spacing.small\n        spacing: Appearance.spacing.small\n\n        opacity: 1\n        scale: 1\n\n        MaterialIcon {\n            text: \"keyboard\"\n            color: Colours.palette.m3primary\n        }\n\n        StyledText {\n            Layout.fillWidth: true\n            text: kb.activeLabel\n            elide: Text.ElideRight\n            font.weight: 500\n            color: Colours.palette.m3primary\n        }\n\n        Connections {\n            function onActiveLabelChanged() {\n                if (!activeRow.visible)\n                    return;\n                popIn.restart();\n            }\n\n            target: kb\n        }\n\n        SequentialAnimation {\n            id: popIn\n\n            running: false\n\n            ParallelAnimation {\n                NumberAnimation {\n                    target: activeRow\n                    property: \"opacity\"\n                    to: 0.0\n                    duration: 70\n                }\n                NumberAnimation {\n                    target: activeRow\n                    property: \"scale\"\n                    to: 0.92\n                    duration: 70\n                }\n            }\n\n            ParallelAnimation {\n                NumberAnimation {\n                    target: activeRow\n                    property: \"opacity\"\n                    to: 1.0\n                    duration: 160\n                    easing.type: Easing.OutCubic\n                }\n                NumberAnimation {\n                    target: activeRow\n                    property: \"scale\"\n                    to: 1.0\n                    duration: 220\n                    easing.type: Easing.OutBack\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/bar/popouts/kblayout/KbLayoutModel.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport QtQuick\n\nimport Quickshell.Io\n\nimport qs.config\nimport Caelestia\n\nItem {\n    id: model\n\n    visible: false\n\n    ListModel {\n        id: _visibleModel\n    }\n    property alias visibleModel: _visibleModel\n\n    property string activeLabel: \"\"\n    property int activeIndex: -1\n\n    function start() {\n        _xkbXmlBase.running = true;\n        _getKbLayoutOpt.running = true;\n    }\n\n    function refresh() {\n        _notifiedLimit = false;\n        _getKbLayoutOpt.running = true;\n    }\n\n    function switchTo(idx) {\n        _switchProc.command = [\"hyprctl\", \"switchxkblayout\", \"all\", String(idx)];\n        _switchProc.running = true;\n    }\n\n    function _buildXmlMap(xml) {\n        const map = {};\n\n        const re = /<name>\\s*([^<]+?)\\s*<\\/name>[\\s\\S]*?<description>\\s*([^<]+?)\\s*<\\/description>/g;\n\n        let m;\n        while ((m = re.exec(xml)) !== null) {\n            const code = (m[1] || \"\").trim();\n            const desc = (m[2] || \"\").trim();\n            if (!code || !desc)\n                continue;\n            map[code] = _short(desc);\n        }\n\n        if (Object.keys(map).length === 0)\n            return;\n\n        _xkbMap = map;\n\n        if (_layoutsModel.count > 0) {\n            const tmp = [];\n            for (let i = 0; i < _layoutsModel.count; i++) {\n                const it = _layoutsModel.get(i);\n                tmp.push({\n                    layoutIndex: it.layoutIndex,\n                    token: it.token,\n                    label: _pretty(it.token)\n                });\n            }\n            _layoutsModel.clear();\n            tmp.forEach(t => _layoutsModel.append(t));\n            _fetchActiveLayouts.running = true;\n        }\n    }\n\n    function _short(desc) {\n        const m = desc.match(/^(.*)\\((.*)\\)$/);\n        if (!m)\n            return desc;\n        const lang = m[1].trim();\n        const region = m[2].trim();\n        const code = (region.split(/[,\\s-]/)[0] || region).slice(0, 2).toUpperCase();\n        return `${lang} (${code})`;\n    }\n\n    function _setLayouts(raw) {\n        const parts = raw.split(\",\").map(s => s.trim()).filter(Boolean);\n        _layoutsModel.clear();\n\n        const seen = new Set();\n        let idx = 0;\n\n        for (const p of parts) {\n            if (seen.has(p))\n                continue;\n            seen.add(p);\n            _layoutsModel.append({\n                layoutIndex: idx,\n                token: p,\n                label: _pretty(p)\n            });\n            idx++;\n        }\n    }\n\n    function _rebuildVisible() {\n        _visibleModel.clear();\n\n        let arr = [];\n        for (let i = 0; i < _layoutsModel.count; i++)\n            arr.push(_layoutsModel.get(i));\n\n        arr = arr.filter(i => i.layoutIndex !== activeIndex);\n        arr.forEach(i => _visibleModel.append(i));\n\n        if (!Config.utilities.toasts.kbLimit)\n            return;\n\n        if (_layoutsModel.count > 4) {\n            Toaster.toast(qsTr(\"Keyboard layout limit\"), qsTr(\"XKB supports only 4 layouts at a time\"), \"warning\");\n        }\n    }\n\n    function _pretty(token) {\n        const code = token.replace(/\\(.*\\)$/, \"\").trim();\n        if (_xkbMap[code])\n            return code.toUpperCase() + \" - \" + _xkbMap[code];\n        return code.toUpperCase() + \" - \" + code;\n    }\n\n    ListModel {\n        id: _layoutsModel\n    }\n\n    property var _xkbMap: ({})\n    property bool _notifiedLimit: false\n\n    Process {\n        id: _xkbXmlBase\n\n        command: [\"xmllint\", \"--xpath\", \"//layout/configItem[name and description]\", \"/usr/share/X11/xkb/rules/base.xml\"]\n        stdout: StdioCollector {\n            onStreamFinished: model._buildXmlMap(text)\n        }\n        onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== \"undefined\") && _xkbXmlBase.exitCode !== 0)\n            _xkbXmlEvdev.running = true\n    }\n\n    Process {\n        id: _xkbXmlEvdev\n\n        command: [\"xmllint\", \"--xpath\", \"//layout/configItem[name and description]\", \"/usr/share/X11/xkb/rules/evdev.xml\"]\n        stdout: StdioCollector {\n            onStreamFinished: model._buildXmlMap(text)\n        }\n    }\n\n    Process {\n        id: _getKbLayoutOpt\n\n        command: [\"hyprctl\", \"-j\", \"getoption\", \"input:kb_layout\"]\n        stdout: StdioCollector {\n            onStreamFinished: {\n                try {\n                    const j = JSON.parse(text);\n                    const raw = (j?.str || j?.value || \"\").toString().trim();\n                    if (raw.length) {\n                        model._setLayouts(raw);\n                        _fetchActiveLayouts.running = true;\n                        return;\n                    }\n                } catch (e) {}\n                _fetchLayoutsFromDevices.running = true;\n            }\n        }\n    }\n\n    Process {\n        id: _fetchLayoutsFromDevices\n\n        command: [\"hyprctl\", \"-j\", \"devices\"]\n        stdout: StdioCollector {\n            onStreamFinished: {\n                try {\n                    const dev = JSON.parse(text);\n                    const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0];\n                    const raw = (kb?.layout || \"\").trim();\n                    if (raw.length)\n                        model._setLayouts(raw);\n                } catch (e) {}\n                _fetchActiveLayouts.running = true;\n            }\n        }\n    }\n\n    Process {\n        id: _fetchActiveLayouts\n\n        command: [\"hyprctl\", \"-j\", \"devices\"]\n        stdout: StdioCollector {\n            onStreamFinished: {\n                try {\n                    const dev = JSON.parse(text);\n                    const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0];\n                    const idx = kb?.active_layout_index ?? -1;\n\n                    model.activeIndex = idx >= 0 ? idx : -1;\n                    model.activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : \"\";\n                } catch (e) {\n                    model.activeIndex = -1;\n                    model.activeLabel = \"\";\n                }\n\n                model._rebuildVisible();\n            }\n        }\n    }\n\n    Process {\n        id: _switchProc\n\n        onRunningChanged: if (!running)\n            _fetchActiveLayouts.running = true\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/ControlCenter.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property ShellScreen screen\n    readonly property int rounding: floating ? 0 : Appearance.rounding.normal\n\n    property alias floating: session.floating\n    property alias active: session.active\n    property alias navExpanded: session.navExpanded\n\n    readonly property bool initialOpeningComplete: panes.initialOpeningComplete\n    readonly property Session session: Session {\n        id: session\n\n        root: root\n    }\n\n    function close(): void {\n    }\n\n    implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio\n    implicitHeight: screen.height * Config.controlCenter.sizes.heightMult\n\n    GridLayout {\n        anchors.fill: parent\n\n        rowSpacing: 0\n        columnSpacing: 0\n        rows: root.floating ? 2 : 1\n        columns: 2\n\n        Loader {\n            Layout.fillWidth: true\n            Layout.columnSpan: 2\n\n            asynchronous: true\n            active: root.floating\n            visible: active\n\n            sourceComponent: WindowTitle {\n                screen: root.screen\n                session: root.session\n            }\n        }\n\n        StyledRect {\n            Layout.fillHeight: true\n\n            topLeftRadius: root.rounding\n            bottomLeftRadius: root.rounding\n            implicitWidth: navRail.implicitWidth\n            color: Colours.tPalette.m3surfaceContainer\n\n            CustomMouseArea {\n                function onWheel(event: WheelEvent): void {\n                    // Prevent tab switching during initial opening animation to avoid blank pages\n                    if (!panes.initialOpeningComplete) {\n                        return;\n                    }\n\n                    if (event.angleDelta.y < 0)\n                        root.session.activeIndex = Math.min(root.session.activeIndex + 1, root.session.panes.length - 1);\n                    else if (event.angleDelta.y > 0)\n                        root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0);\n                }\n\n                anchors.fill: parent\n            }\n\n            NavRail {\n                id: navRail\n\n                screen: root.screen\n                session: root.session\n                initialOpeningComplete: root.initialOpeningComplete\n            }\n        }\n\n        Panes {\n            id: panes\n\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            topRightRadius: root.rounding\n            bottomRightRadius: root.rounding\n            session: root.session\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/NavRail.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport qs.modules.controlcenter\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property ShellScreen screen\n    required property Session session\n    required property bool initialOpeningComplete\n\n    implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4\n    implicitHeight: layout.implicitHeight + Appearance.padding.large * 2\n\n    ColumnLayout {\n        id: layout\n\n        anchors.left: parent.left\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.leftMargin: Appearance.padding.larger * 2\n        spacing: Appearance.spacing.normal\n\n        states: State {\n            name: \"expanded\"\n            when: root.session.navExpanded\n\n            PropertyChanges {\n                layout.spacing: Appearance.spacing.small\n            }\n        }\n\n        transitions: Transition {\n            Anim {\n                properties: \"spacing\"\n            }\n        }\n\n        Loader {\n            Layout.topMargin: Appearance.spacing.large\n            asynchronous: true\n            active: !root.session.floating\n            visible: active\n\n            sourceComponent: StyledRect {\n                readonly property int nonAnimWidth: normalWinIcon.implicitWidth + (root.session.navExpanded ? normalWinLabel.anchors.leftMargin + normalWinLabel.implicitWidth : 0) + normalWinIcon.anchors.leftMargin * 2\n\n                implicitWidth: nonAnimWidth\n                implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Appearance.padding.normal * 2 : nonAnimWidth\n\n                color: Colours.palette.m3primaryContainer\n                radius: Appearance.rounding.small\n\n                StateLayer {\n                    id: normalWinState\n\n                    function onClicked(): void {\n                        root.session.root.close();\n                        WindowFactory.create(null, {\n                            active: root.session.active,\n                            navExpanded: root.session.navExpanded\n                        });\n                    }\n\n                    color: Colours.palette.m3onPrimaryContainer\n                }\n\n                MaterialIcon {\n                    id: normalWinIcon\n\n                    anchors.left: parent.left\n                    anchors.verticalCenter: parent.verticalCenter\n                    anchors.leftMargin: Appearance.padding.large\n\n                    text: \"select_window\"\n                    color: Colours.palette.m3onPrimaryContainer\n                    font.pointSize: Appearance.font.size.large\n                    fill: 1\n                }\n\n                StyledText {\n                    id: normalWinLabel\n\n                    anchors.left: normalWinIcon.right\n                    anchors.verticalCenter: parent.verticalCenter\n                    anchors.leftMargin: Appearance.spacing.normal\n\n                    text: qsTr(\"Float window\")\n                    color: Colours.palette.m3onPrimaryContainer\n                    opacity: root.session.navExpanded ? 1 : 0\n\n                    Behavior on opacity {\n                        Anim {\n                            duration: Appearance.anim.durations.small\n                        }\n                    }\n                }\n\n                Behavior on implicitWidth {\n                    Anim {\n                        duration: Appearance.anim.durations.expressiveDefaultSpatial\n                        easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                    }\n                }\n\n                Behavior on implicitHeight {\n                    Anim {\n                        duration: Appearance.anim.durations.expressiveDefaultSpatial\n                        easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                    }\n                }\n            }\n        }\n\n        Repeater {\n            model: PaneRegistry.count\n\n            NavItem {\n                required property int index\n\n                Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0\n                icon: PaneRegistry.getByIndex(index).icon\n                label: PaneRegistry.getByIndex(index).label\n            }\n        }\n    }\n\n    component NavItem: Item {\n        id: item\n\n        required property string icon\n        required property string label\n        readonly property bool active: root.session.active === label\n\n        implicitWidth: background.implicitWidth\n        implicitHeight: background.implicitHeight + smallLabel.implicitHeight + smallLabel.anchors.topMargin\n\n        states: State {\n            name: \"expanded\"\n            when: root.session.navExpanded\n\n            PropertyChanges {\n                expandedLabel.opacity: 1\n                smallLabel.opacity: 0\n                background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth\n                background.implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2\n                item.implicitHeight: background.implicitHeight\n            }\n        }\n\n        transitions: Transition {\n            Anim {\n                property: \"opacity\"\n                duration: Appearance.anim.durations.small\n            }\n\n            Anim {\n                properties: \"implicitWidth,implicitHeight\"\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        }\n\n        StyledRect {\n            id: background\n\n            radius: Appearance.rounding.full\n            color: Qt.alpha(Colours.palette.m3secondaryContainer, item.active ? 1 : 0)\n\n            implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2\n            implicitHeight: icon.implicitHeight + Appearance.padding.small\n\n            StateLayer {\n                function onClicked(): void {\n                    // Prevent tab switching during initial opening animation to avoid blank pages\n                    if (!root.initialOpeningComplete) {\n                        return;\n                    }\n                    root.session.active = item.label;\n                }\n\n                color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n            }\n\n            MaterialIcon {\n                id: icon\n\n                anchors.left: parent.left\n                anchors.verticalCenter: parent.verticalCenter\n                anchors.leftMargin: Appearance.padding.large\n\n                text: item.icon\n                color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n                font.pointSize: Appearance.font.size.large\n                fill: item.active ? 1 : 0\n\n                Behavior on fill {\n                    Anim {}\n                }\n            }\n\n            StyledText {\n                id: expandedLabel\n\n                anchors.left: icon.right\n                anchors.verticalCenter: parent.verticalCenter\n                anchors.leftMargin: Appearance.spacing.normal\n\n                opacity: 0\n                text: item.label\n                color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n                font.capitalization: Font.Capitalize\n            }\n\n            StyledText {\n                id: smallLabel\n\n                anchors.horizontalCenter: icon.horizontalCenter\n                anchors.top: icon.bottom\n                anchors.topMargin: Appearance.spacing.small / 2\n\n                text: item.label\n                font.pointSize: Appearance.font.size.small\n                font.capitalization: Font.Capitalize\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/PaneRegistry.qml",
    "content": "pragma Singleton\n\nimport QtQuick\n\nQtObject {\n    id: root\n\n    readonly property list<QtObject> panes: [\n        QtObject {\n            readonly property string id: \"network\"\n            readonly property string label: \"network\"\n            readonly property string icon: \"router\"\n            readonly property string component: \"network/NetworkingPane.qml\"\n        },\n        QtObject {\n            readonly property string id: \"bluetooth\"\n            readonly property string label: \"bluetooth\"\n            readonly property string icon: \"settings_bluetooth\"\n            readonly property string component: \"bluetooth/BtPane.qml\"\n        },\n        QtObject {\n            readonly property string id: \"audio\"\n            readonly property string label: \"audio\"\n            readonly property string icon: \"volume_up\"\n            readonly property string component: \"audio/AudioPane.qml\"\n        },\n        QtObject {\n            readonly property string id: \"appearance\"\n            readonly property string label: \"appearance\"\n            readonly property string icon: \"palette\"\n            readonly property string component: \"appearance/AppearancePane.qml\"\n        },\n        QtObject {\n            readonly property string id: \"taskbar\"\n            readonly property string label: \"taskbar\"\n            readonly property string icon: \"task_alt\"\n            readonly property string component: \"taskbar/TaskbarPane.qml\"\n        },\n        QtObject {\n            readonly property string id: \"launcher\"\n            readonly property string label: \"launcher\"\n            readonly property string icon: \"apps\"\n            readonly property string component: \"launcher/LauncherPane.qml\"\n        },\n        QtObject {\n            readonly property string id: \"dashboard\"\n            readonly property string label: \"dashboard\"\n            readonly property string icon: \"dashboard\"\n            readonly property string component: \"dashboard/DashboardPane.qml\"\n        }\n    ]\n\n    readonly property int count: panes.length\n\n    readonly property var labels: {\n        const result = [];\n        for (let i = 0; i < panes.length; i++) {\n            result.push(panes[i].label);\n        }\n        return result;\n    }\n\n    function getByIndex(index: int): QtObject {\n        if (index >= 0 && index < panes.length) {\n            return panes[index];\n        }\n        return null;\n    }\n\n    function getIndexByLabel(label: string): int {\n        for (let i = 0; i < panes.length; i++) {\n            if (panes[i].label === label) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    function getByLabel(label: string): QtObject {\n        const index = getIndexByLabel(label);\n        return getByIndex(index);\n    }\n\n    function getById(id: string): QtObject {\n        for (let i = 0; i < panes.length; i++) {\n            if (panes[i].id === id) {\n                return panes[i];\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/Panes.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.config\nimport qs.modules.controlcenter\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Layouts\n\nClippingRectangle {\n    id: root\n\n    required property Session session\n\n    readonly property bool initialOpeningComplete: layout.initialOpeningComplete\n\n    color: \"transparent\"\n    clip: true\n    focus: false\n    activeFocusOnTab: false\n\n    MouseArea {\n        anchors.fill: parent\n        z: -1\n        onPressed: function (mouse) {\n            root.focus = true;\n            mouse.accepted = false;\n        }\n    }\n\n    Connections {\n        function onActiveIndexChanged(): void {\n            root.focus = true;\n        }\n\n        target: root.session\n    }\n\n    ColumnLayout {\n        id: layout\n\n        property bool animationComplete: true\n        property bool initialOpeningComplete: false\n\n        spacing: 0\n        y: -root.session.activeIndex * root.height\n        clip: true\n\n        Timer {\n            id: animationDelayTimer\n\n            interval: Appearance.anim.durations.normal\n            onTriggered: {\n                layout.animationComplete = true;\n            }\n        }\n\n        Timer {\n            id: initialOpeningTimer\n\n            interval: Appearance.anim.durations.large\n            running: true\n            onTriggered: {\n                layout.initialOpeningComplete = true;\n            }\n        }\n\n        Repeater {\n            model: PaneRegistry.count\n\n            Pane {\n                required property int index\n\n                paneIndex: index\n                componentPath: PaneRegistry.getByIndex(index).component\n            }\n        }\n\n        Behavior on y {\n            Anim {}\n        }\n\n        Connections {\n            function onActiveIndexChanged(): void {\n                layout.animationComplete = false;\n                animationDelayTimer.restart();\n            }\n\n            target: root.session\n        }\n    }\n\n    component Pane: Item {\n        id: pane\n\n        required property int paneIndex\n        required property string componentPath\n        property bool hasBeenLoaded: false\n\n        function updateActive(): void {\n            const diff = Math.abs(root.session.activeIndex - pane.paneIndex);\n            const isActivePane = diff === 0;\n            let shouldBeActive = false;\n\n            if (!layout.initialOpeningComplete) {\n                shouldBeActive = isActivePane;\n            } else {\n                if (diff <= 1) {\n                    shouldBeActive = true;\n                } else if (pane.hasBeenLoaded) {\n                    shouldBeActive = true;\n                } else {\n                    shouldBeActive = layout.animationComplete;\n                }\n            }\n\n            loader.active = shouldBeActive;\n        }\n\n        implicitWidth: root.width\n        implicitHeight: root.height\n\n        Loader {\n            id: loader\n\n            anchors.fill: parent\n            asynchronous: true\n            clip: false\n            active: false\n\n            Component.onCompleted: {\n                Qt.callLater(pane.updateActive);\n            }\n\n            onActiveChanged: {\n                if (active && !pane.hasBeenLoaded) {\n                    pane.hasBeenLoaded = true;\n                }\n\n                if (active && !item) {\n                    loader.setSource(pane.componentPath, {\n                        \"session\": root.session\n                    });\n                }\n            }\n\n            onItemChanged: {\n                if (item) {\n                    pane.hasBeenLoaded = true;\n                }\n            }\n        }\n\n        Connections {\n            function onActiveIndexChanged(): void {\n                pane.updateActive();\n            }\n\n            target: root.session\n        }\n\n        Connections {\n            function onInitialOpeningCompleteChanged(): void {\n                pane.updateActive();\n            }\n            function onAnimationCompleteChanged(): void {\n                pane.updateActive();\n            }\n\n            target: layout\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/Session.qml",
    "content": "import QtQuick\nimport \"./state\"\nimport qs.modules.controlcenter\n\nQtObject {\n    readonly property list<string> panes: PaneRegistry.labels\n\n    required property var root\n    property bool floating: false\n    property string active: \"network\"\n    property int activeIndex: 0\n    property bool navExpanded: false\n\n    readonly property BluetoothState bt: BluetoothState {}\n    readonly property NetworkState network: NetworkState {}\n    readonly property EthernetState ethernet: EthernetState {}\n    readonly property LauncherState launcher: LauncherState {}\n    readonly property VpnState vpn: VpnState {}\n\n    onActiveChanged: activeIndex = Math.max(0, panes.indexOf(active))\n    onActiveIndexChanged: if (panes[activeIndex])\n        active = panes[activeIndex]\n}\n"
  },
  {
    "path": "modules/controlcenter/WindowFactory.qml",
    "content": "pragma Singleton\n\nimport qs.components\nimport qs.services\nimport Quickshell\nimport QtQuick\n\nSingleton {\n    id: root\n\n    function create(parent: Item, props: var): void {\n        controlCenter.createObject(parent ?? dummy, props);\n    }\n\n    QtObject {\n        id: dummy\n    }\n\n    Component {\n        id: controlCenter\n\n        FloatingWindow {\n            id: win\n\n            property alias active: cc.active\n            property alias navExpanded: cc.navExpanded\n\n            color: Colours.tPalette.m3surface\n\n            onVisibleChanged: {\n                if (!visible)\n                    destroy();\n            }\n\n            implicitWidth: cc.implicitWidth\n            implicitHeight: cc.implicitHeight\n\n            minimumSize.width: implicitWidth\n            minimumSize.height: implicitHeight\n            maximumSize.width: implicitWidth\n            maximumSize.height: implicitHeight\n\n            title: qsTr(\"Caelestia Settings - %1\").arg(cc.active.slice(0, 1).toUpperCase() + cc.active.slice(1))\n\n            ControlCenter {\n                id: cc\n\n                function close(): void {\n                    win.destroy();\n                }\n\n                anchors.fill: parent\n                screen: win.screen\n                floating: true\n            }\n\n            Behavior on color {\n                CAnim {}\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/WindowTitle.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\n\nStyledRect {\n    id: root\n\n    required property ShellScreen screen\n    required property Session session\n\n    implicitHeight: text.implicitHeight + Appearance.padding.normal\n    color: Colours.tPalette.m3surfaceContainer\n\n    StyledText {\n        id: text\n\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.bottom: parent.bottom\n\n        text: qsTr(\"Caelestia Settings - %1\").arg(root.session.active)\n        font.capitalization: Font.Capitalize\n        font.pointSize: Appearance.font.size.larger\n        font.weight: 500\n    }\n\n    Item {\n        anchors.right: parent.right\n        anchors.top: parent.top\n        anchors.margins: Appearance.padding.normal\n\n        implicitWidth: implicitHeight\n        implicitHeight: closeIcon.implicitHeight + Appearance.padding.small\n\n        StateLayer {\n            function onClicked(): void {\n                QsWindow.window.destroy();\n            }\n\n            radius: Appearance.rounding.full\n        }\n\n        MaterialIcon {\n            id: closeIcon\n\n            anchors.centerIn: parent\n            text: \"close\"\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/appearance/AppearancePane.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport \"./sections\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.containers\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property Session session\n\n    property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1\n    property string fontFamilyMaterial: Config.appearance.font.family.material ?? \"Material Symbols Rounded\"\n    property string fontFamilyMono: Config.appearance.font.family.mono ?? \"CaskaydiaCove NF\"\n    property string fontFamilySans: Config.appearance.font.family.sans ?? \"Rubik\"\n    property real fontSizeScale: Config.appearance.font.size.scale ?? 1\n    property real paddingScale: Config.appearance.padding.scale ?? 1\n    property real roundingScale: Config.appearance.rounding.scale ?? 1\n    property real spacingScale: Config.appearance.spacing.scale ?? 1\n    property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false\n    property real transparencyBase: Config.appearance.transparency.base ?? 0.85\n    property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4\n    property real borderRounding: Config.border.rounding ?? 1\n    property real borderThickness: Config.border.thickness ?? 1\n\n    property bool desktopClockEnabled: Config.background.desktopClock.enabled ?? false\n    property real desktopClockScale: Config.background.desktopClock.scale ?? 1\n    property string desktopClockPosition: Config.background.desktopClock.position ?? \"bottom-right\"\n    property bool desktopClockShadowEnabled: Config.background.desktopClock.shadow.enabled ?? true\n    property real desktopClockShadowOpacity: Config.background.desktopClock.shadow.opacity ?? 0.7\n    property real desktopClockShadowBlur: Config.background.desktopClock.shadow.blur ?? 0.4\n    property bool desktopClockBackgroundEnabled: Config.background.desktopClock.background.enabled ?? false\n    property real desktopClockBackgroundOpacity: Config.background.desktopClock.background.opacity ?? 0.7\n    property bool desktopClockBackgroundBlur: Config.background.desktopClock.background.blur ?? false\n    property bool desktopClockInvertColors: Config.background.desktopClock.invertColors ?? false\n    property bool backgroundEnabled: Config.background.enabled ?? true\n    property bool wallpaperEnabled: Config.background.wallpaperEnabled ?? true\n    property bool visualiserEnabled: Config.background.visualiser.enabled ?? false\n    property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true\n    property real visualiserRounding: Config.background.visualiser.rounding ?? 1\n    property real visualiserSpacing: Config.background.visualiser.spacing ?? 1\n\n    function saveConfig() {\n        Config.appearance.anim.durations.scale = root.animDurationsScale;\n\n        Config.appearance.font.family.material = root.fontFamilyMaterial;\n        Config.appearance.font.family.mono = root.fontFamilyMono;\n        Config.appearance.font.family.sans = root.fontFamilySans;\n        Config.appearance.font.size.scale = root.fontSizeScale;\n\n        Config.appearance.padding.scale = root.paddingScale;\n        Config.appearance.rounding.scale = root.roundingScale;\n        Config.appearance.spacing.scale = root.spacingScale;\n\n        Config.appearance.transparency.enabled = root.transparencyEnabled;\n        Config.appearance.transparency.base = root.transparencyBase;\n        Config.appearance.transparency.layers = root.transparencyLayers;\n\n        Config.background.desktopClock.enabled = root.desktopClockEnabled;\n        Config.background.enabled = root.backgroundEnabled;\n        Config.background.desktopClock.scale = root.desktopClockScale;\n        Config.background.desktopClock.position = root.desktopClockPosition;\n        Config.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled;\n        Config.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity;\n        Config.background.desktopClock.shadow.blur = root.desktopClockShadowBlur;\n        Config.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled;\n        Config.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity;\n        Config.background.desktopClock.background.blur = root.desktopClockBackgroundBlur;\n        Config.background.desktopClock.invertColors = root.desktopClockInvertColors;\n\n        Config.background.wallpaperEnabled = root.wallpaperEnabled;\n\n        Config.background.visualiser.enabled = root.visualiserEnabled;\n        Config.background.visualiser.autoHide = root.visualiserAutoHide;\n        Config.background.visualiser.rounding = root.visualiserRounding;\n        Config.background.visualiser.spacing = root.visualiserSpacing;\n\n        Config.border.rounding = root.borderRounding;\n        Config.border.thickness = root.borderThickness;\n\n        Config.save();\n    }\n\n    anchors.fill: parent\n\n    Component {\n        id: appearanceRightContentComponent\n\n        Item {\n            id: rightAppearanceFlickable\n\n            ColumnLayout {\n                id: contentLayout\n\n                anchors.fill: parent\n                spacing: 0\n\n                StyledText {\n                    Layout.alignment: Qt.AlignHCenter\n                    Layout.bottomMargin: Appearance.spacing.normal\n                    text: qsTr(\"Wallpaper\")\n                    font.pointSize: Appearance.font.size.extraLarge\n                    font.weight: 600\n                }\n\n                Loader {\n                    id: wallpaperLoader\n\n                    Layout.fillWidth: true\n                    Layout.fillHeight: true\n                    Layout.bottomMargin: -Appearance.padding.large * 2\n\n                    asynchronous: true\n                    active: {\n                        const isActive = root.session.activeIndex === 3;\n                        const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1;\n                        const splitLayout = root.children[0];\n                        const loader = splitLayout && splitLayout.rightLoader ? splitLayout.rightLoader : null;\n                        const shouldActivate = loader && loader.item !== null && (isActive || isAdjacent);\n                        return shouldActivate;\n                    }\n\n                    onStatusChanged: {\n                        if (status === Loader.Error) {\n                            console.error(\"[AppearancePane] Wallpaper loader error!\");\n                        }\n                    }\n\n                    sourceComponent: WallpaperGrid {\n                        session: root.session\n                    }\n                }\n            }\n        }\n    }\n\n    SplitPaneLayout {\n        anchors.fill: parent\n\n        leftContent: Component {\n            StyledFlickable {\n                id: sidebarFlickable\n\n                readonly property var rootPane: root\n\n                flickableDirection: Flickable.VerticalFlick\n                contentHeight: sidebarLayout.height\n\n                StyledScrollBar.vertical: StyledScrollBar {\n                    flickable: sidebarFlickable\n                }\n\n                ColumnLayout {\n                    id: sidebarLayout\n\n                    readonly property var rootPane: sidebarFlickable.rootPane\n                    readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded\n\n                    anchors.left: parent.left\n                    anchors.right: parent.right\n                    spacing: Appearance.spacing.small\n\n                    RowLayout {\n                        spacing: Appearance.spacing.smaller\n\n                        StyledText {\n                            text: qsTr(\"Appearance\")\n                            font.pointSize: Appearance.font.size.large\n                            font.weight: 500\n                        }\n\n                        Item {\n                            Layout.fillWidth: true\n                        }\n\n                        IconButton {\n                            icon: sidebarLayout.allSectionsExpanded ? \"unfold_less\" : \"unfold_more\"\n                            type: IconButton.Text\n                            label.animate: true\n                            onClicked: {\n                                const shouldExpand = !sidebarLayout.allSectionsExpanded;\n                                themeModeSection.expanded = shouldExpand;\n                                colorVariantSection.expanded = shouldExpand;\n                                colorSchemeSection.expanded = shouldExpand;\n                                animationsSection.expanded = shouldExpand;\n                                fontsSection.expanded = shouldExpand;\n                                scalesSection.expanded = shouldExpand;\n                                transparencySection.expanded = shouldExpand;\n                                borderSection.expanded = shouldExpand;\n                                backgroundSection.expanded = shouldExpand;\n                            }\n                        }\n                    }\n\n                    ThemeModeSection {\n                        id: themeModeSection\n                    }\n\n                    ColorVariantSection {\n                        id: colorVariantSection\n                    }\n\n                    ColorSchemeSection {\n                        id: colorSchemeSection\n                    }\n\n                    AnimationsSection {\n                        id: animationsSection\n\n                        rootPane: sidebarFlickable.rootPane\n                    }\n\n                    FontsSection {\n                        id: fontsSection\n\n                        rootPane: sidebarFlickable.rootPane\n                    }\n\n                    ScalesSection {\n                        id: scalesSection\n\n                        rootPane: sidebarFlickable.rootPane\n                    }\n\n                    TransparencySection {\n                        id: transparencySection\n\n                        rootPane: sidebarFlickable.rootPane\n                    }\n\n                    BorderSection {\n                        id: borderSection\n\n                        rootPane: sidebarFlickable.rootPane\n                    }\n\n                    BackgroundSection {\n                        id: backgroundSection\n\n                        rootPane: sidebarFlickable.rootPane\n                    }\n                }\n            }\n        }\n\n        rightContent: appearanceRightContentComponent\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/appearance/sections/AnimationsSection.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"../../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nCollapsibleSection {\n    id: root\n\n    required property var rootPane\n\n    title: qsTr(\"Animations\")\n    showBackground: true\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.normal\n\n        SliderInput {\n            Layout.fillWidth: true\n\n            label: qsTr(\"Animation duration scale\")\n            value: root.rootPane.animDurationsScale\n            from: 0.1\n            to: 5.0\n            decimals: 1\n            suffix: \"×\"\n            validator: DoubleValidator {\n                bottom: 0.1\n                top: 5.0\n            }\n\n            onValueModified: newValue => {\n                root.rootPane.animDurationsScale = newValue;\n                root.rootPane.saveConfig();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/appearance/sections/BackgroundSection.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"../../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nCollapsibleSection {\n    id: root\n\n    required property var rootPane\n\n    title: qsTr(\"Background\")\n    showBackground: true\n\n    SwitchRow {\n        label: qsTr(\"Background enabled\")\n        checked: root.rootPane.backgroundEnabled\n        onToggled: checked => {\n            root.rootPane.backgroundEnabled = checked;\n            root.rootPane.saveConfig();\n        }\n    }\n\n    SwitchRow {\n        label: qsTr(\"Wallpaper enabled\")\n        checked: root.rootPane.wallpaperEnabled\n        onToggled: checked => {\n            root.rootPane.wallpaperEnabled = checked;\n            root.rootPane.saveConfig();\n        }\n    }\n\n    StyledText {\n        Layout.topMargin: Appearance.spacing.normal\n        text: qsTr(\"Desktop Clock\")\n        font.pointSize: Appearance.font.size.larger\n        font.weight: 500\n    }\n\n    SwitchRow {\n        label: qsTr(\"Desktop Clock enabled\")\n        checked: root.rootPane.desktopClockEnabled\n        onToggled: checked => {\n            root.rootPane.desktopClockEnabled = checked;\n            root.rootPane.saveConfig();\n        }\n    }\n\n    SectionContainer {\n        id: posContainer\n\n        readonly property var pos: (root.rootPane.desktopClockPosition || \"top-left\").split('-')\n        readonly property string currentV: pos[0]\n        readonly property string currentH: pos[1]\n\n        function updateClockPos(v, h) {\n            root.rootPane.desktopClockPosition = v + \"-\" + h;\n            root.rootPane.saveConfig();\n        }\n\n        contentSpacing: Appearance.spacing.small\n        z: 1\n\n        StyledText {\n            text: qsTr(\"Positioning\")\n            font.pointSize: Appearance.font.size.larger\n            font.weight: 500\n        }\n\n        SplitButtonRow {\n            label: qsTr(\"Vertical Position\")\n            enabled: root.rootPane.desktopClockEnabled\n\n            menuItems: [\n                MenuItem {\n                    property string val: \"top\"\n\n                    text: qsTr(\"Top\")\n                    icon: \"vertical_align_top\"\n                },\n                MenuItem {\n                    property string val: \"middle\"\n\n                    text: qsTr(\"Middle\")\n                    icon: \"vertical_align_center\"\n                },\n                MenuItem {\n                    property string val: \"bottom\"\n\n                    text: qsTr(\"Bottom\")\n                    icon: \"vertical_align_bottom\"\n                }\n            ]\n\n            Component.onCompleted: {\n                for (let i = 0; i < menuItems.length; i++) {\n                    if (menuItems[i].val === posContainer.currentV)\n                        active = menuItems[i];\n                }\n            }\n\n            // The signal from SplitButtonRow\n            onSelected: item => posContainer.updateClockPos(item.val, posContainer.currentH)\n        }\n\n        SplitButtonRow {\n            label: qsTr(\"Horizontal Position\")\n            enabled: root.rootPane.desktopClockEnabled\n            expandedZ: 99\n\n            menuItems: [\n                MenuItem {\n                    property string val: \"left\"\n\n                    text: qsTr(\"Left\")\n                    icon: \"align_horizontal_left\"\n                },\n                MenuItem {\n                    property string val: \"center\"\n\n                    text: qsTr(\"Center\")\n                    icon: \"align_horizontal_center\"\n                },\n                MenuItem {\n                    property string val: \"right\"\n\n                    text: qsTr(\"Right\")\n                    icon: \"align_horizontal_right\"\n                }\n            ]\n\n            Component.onCompleted: {\n                for (let i = 0; i < menuItems.length; i++) {\n                    if (menuItems[i].val === posContainer.currentH)\n                        active = menuItems[i];\n                }\n            }\n\n            onSelected: item => posContainer.updateClockPos(posContainer.currentV, item.val)\n        }\n    }\n\n    SwitchRow {\n        label: qsTr(\"Invert colors\")\n        checked: root.rootPane.desktopClockInvertColors\n        onToggled: checked => {\n            root.rootPane.desktopClockInvertColors = checked;\n            root.rootPane.saveConfig();\n        }\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.small\n\n        StyledText {\n            text: qsTr(\"Shadow\")\n            font.pointSize: Appearance.font.size.larger\n            font.weight: 500\n        }\n\n        SwitchRow {\n            label: qsTr(\"Enabled\")\n            checked: root.rootPane.desktopClockShadowEnabled\n            onToggled: checked => {\n                root.rootPane.desktopClockShadowEnabled = checked;\n                root.rootPane.saveConfig();\n            }\n        }\n\n        SectionContainer {\n            contentSpacing: Appearance.spacing.normal\n\n            SliderInput {\n                Layout.fillWidth: true\n\n                label: qsTr(\"Opacity\")\n                value: root.rootPane.desktopClockShadowOpacity * 100\n                from: 0\n                to: 100\n                suffix: \"%\"\n                validator: IntValidator {\n                    bottom: 0\n                    top: 100\n                }\n                formatValueFunction: val => Math.round(val).toString()\n                parseValueFunction: text => parseInt(text)\n\n                onValueModified: newValue => {\n                    root.rootPane.desktopClockShadowOpacity = newValue / 100;\n                    root.rootPane.saveConfig();\n                }\n            }\n        }\n\n        SectionContainer {\n            contentSpacing: Appearance.spacing.normal\n\n            SliderInput {\n                Layout.fillWidth: true\n\n                label: qsTr(\"Blur\")\n                value: root.rootPane.desktopClockShadowBlur * 100\n                from: 0\n                to: 100\n                suffix: \"%\"\n                validator: IntValidator {\n                    bottom: 0\n                    top: 100\n                }\n                formatValueFunction: val => Math.round(val).toString()\n                parseValueFunction: text => parseInt(text)\n\n                onValueModified: newValue => {\n                    root.rootPane.desktopClockShadowBlur = newValue / 100;\n                    root.rootPane.saveConfig();\n                }\n            }\n        }\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.small\n\n        StyledText {\n            text: qsTr(\"Background\")\n            font.pointSize: Appearance.font.size.larger\n            font.weight: 500\n        }\n\n        SwitchRow {\n            label: qsTr(\"Enabled\")\n            checked: root.rootPane.desktopClockBackgroundEnabled\n            onToggled: checked => {\n                root.rootPane.desktopClockBackgroundEnabled = checked;\n                root.rootPane.saveConfig();\n            }\n        }\n\n        SwitchRow {\n            label: qsTr(\"Blur enabled\")\n            checked: root.rootPane.desktopClockBackgroundBlur\n            onToggled: checked => {\n                root.rootPane.desktopClockBackgroundBlur = checked;\n                root.rootPane.saveConfig();\n            }\n        }\n\n        SectionContainer {\n            contentSpacing: Appearance.spacing.normal\n\n            SliderInput {\n                Layout.fillWidth: true\n\n                label: qsTr(\"Opacity\")\n                value: root.rootPane.desktopClockBackgroundOpacity * 100\n                from: 0\n                to: 100\n                suffix: \"%\"\n                validator: IntValidator {\n                    bottom: 0\n                    top: 100\n                }\n                formatValueFunction: val => Math.round(val).toString()\n                parseValueFunction: text => parseInt(text)\n\n                onValueModified: newValue => {\n                    root.rootPane.desktopClockBackgroundOpacity = newValue / 100;\n                    root.rootPane.saveConfig();\n                }\n            }\n        }\n    }\n\n    StyledText {\n        Layout.topMargin: Appearance.spacing.normal\n        text: qsTr(\"Visualiser\")\n        font.pointSize: Appearance.font.size.larger\n        font.weight: 500\n    }\n\n    SwitchRow {\n        label: qsTr(\"Visualiser enabled\")\n        checked: root.rootPane.visualiserEnabled\n        onToggled: checked => {\n            root.rootPane.visualiserEnabled = checked;\n            root.rootPane.saveConfig();\n        }\n    }\n\n    SwitchRow {\n        label: qsTr(\"Visualiser auto hide\")\n        checked: root.rootPane.visualiserAutoHide\n        onToggled: checked => {\n            root.rootPane.visualiserAutoHide = checked;\n            root.rootPane.saveConfig();\n        }\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.normal\n\n        SliderInput {\n            Layout.fillWidth: true\n\n            label: qsTr(\"Visualiser rounding\")\n            value: root.rootPane.visualiserRounding\n            from: 0\n            to: 10\n            stepSize: 1\n            validator: IntValidator {\n                bottom: 0\n                top: 10\n            }\n            formatValueFunction: val => Math.round(val).toString()\n            parseValueFunction: text => parseInt(text)\n\n            onValueModified: newValue => {\n                root.rootPane.visualiserRounding = Math.round(newValue);\n                root.rootPane.saveConfig();\n            }\n        }\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.normal\n\n        SliderInput {\n            Layout.fillWidth: true\n\n            label: qsTr(\"Visualiser spacing\")\n            value: root.rootPane.visualiserSpacing\n            from: 0\n            to: 2\n            validator: DoubleValidator {\n                bottom: 0\n                top: 2\n            }\n\n            onValueModified: newValue => {\n                root.rootPane.visualiserSpacing = newValue;\n                root.rootPane.saveConfig();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/appearance/sections/BorderSection.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"../../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nCollapsibleSection {\n    id: root\n\n    required property var rootPane\n\n    title: qsTr(\"Border\")\n    showBackground: true\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.normal\n\n        SliderInput {\n            Layout.fillWidth: true\n\n            label: qsTr(\"Border rounding\")\n            value: root.rootPane.borderRounding\n            from: 0.1\n            to: 100\n            decimals: 1\n            suffix: \"px\"\n            validator: DoubleValidator {\n                bottom: 0.1\n                top: 100\n            }\n\n            onValueModified: newValue => {\n                root.rootPane.borderRounding = newValue;\n                root.rootPane.saveConfig();\n            }\n        }\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.normal\n\n        SliderInput {\n            Layout.fillWidth: true\n\n            label: qsTr(\"Border thickness\")\n            value: root.rootPane.borderThickness\n            from: 0\n            to: 100\n            decimals: 1\n            suffix: \"px\"\n            validator: DoubleValidator {\n                bottom: 0.1\n                top: 100\n            }\n\n            onValueModified: newValue => {\n                root.rootPane.borderThickness = newValue;\n                root.rootPane.saveConfig();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/appearance/sections/ColorSchemeSection.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"../../../launcher/services\"\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nCollapsibleSection {\n    title: qsTr(\"Color scheme\")\n    description: qsTr(\"Available color schemes\")\n    showBackground: true\n\n    ColumnLayout {\n        Layout.fillWidth: true\n        spacing: Appearance.spacing.small / 2\n\n        Repeater {\n            model: Schemes.list\n\n            delegate: StyledRect {\n                id: schemeDelegate\n\n                required property var modelData\n\n                Layout.fillWidth: true\n\n                readonly property string schemeKey: `${modelData.name} ${modelData.flavour}`\n                readonly property bool isCurrent: schemeKey === Schemes.currentScheme\n\n                color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)\n                radius: Appearance.rounding.normal\n                border.width: isCurrent ? 1 : 0\n                border.color: Colours.palette.m3primary\n\n                StateLayer {\n                    function onClicked(): void {\n                        const name = schemeDelegate.modelData.name;\n                        const flavour = schemeDelegate.modelData.flavour;\n                        const schemeKey = `${name} ${flavour}`;\n\n                        Schemes.currentScheme = schemeKey;\n                        Quickshell.execDetached([\"caelestia\", \"scheme\", \"set\", \"-n\", name, \"-f\", flavour]);\n\n                        Qt.callLater(() => {\n                            reloadTimer.restart();\n                        });\n                    }\n                }\n\n                Timer {\n                    id: reloadTimer\n\n                    interval: 300\n                    onTriggered: {\n                        Schemes.reload();\n                    }\n                }\n\n                RowLayout {\n                    id: schemeRow\n\n                    anchors.fill: parent\n                    anchors.margins: Appearance.padding.normal\n\n                    spacing: Appearance.spacing.normal\n\n                    StyledRect {\n                        id: preview\n\n                        Layout.alignment: Qt.AlignVCenter\n\n                        border.width: 1\n                        border.color: Qt.alpha(`#${schemeDelegate.modelData.colours?.outline}`, 0.5)\n\n                        color: `#${schemeDelegate.modelData.colours?.surface}`\n                        radius: Appearance.rounding.full\n                        implicitWidth: iconPlaceholder.implicitWidth\n                        implicitHeight: iconPlaceholder.implicitWidth\n\n                        MaterialIcon {\n                            id: iconPlaceholder\n\n                            visible: false\n                            text: \"circle\"\n                            font.pointSize: Appearance.font.size.large\n                        }\n\n                        Item {\n                            anchors.top: parent.top\n                            anchors.bottom: parent.bottom\n                            anchors.right: parent.right\n\n                            implicitWidth: parent.implicitWidth / 2\n                            clip: true\n\n                            StyledRect {\n                                anchors.top: parent.top\n                                anchors.bottom: parent.bottom\n                                anchors.right: parent.right\n\n                                implicitWidth: preview.implicitWidth\n                                color: `#${schemeDelegate.modelData.colours?.primary}`\n                                radius: Appearance.rounding.full\n                            }\n                        }\n                    }\n\n                    Column {\n                        Layout.fillWidth: true\n                        spacing: 0\n\n                        StyledText {\n                            text: schemeDelegate.modelData.flavour ?? \"\"\n                            font.pointSize: Appearance.font.size.normal\n                        }\n\n                        StyledText {\n                            text: schemeDelegate.modelData.name ?? \"\"\n                            font.pointSize: Appearance.font.size.small\n                            color: Colours.palette.m3outline\n\n                            elide: Text.ElideRight\n                            anchors.left: parent.left\n                            anchors.right: parent.right\n                        }\n                    }\n\n                    Loader {\n                        asynchronous: true\n                        active: schemeDelegate.isCurrent\n\n                        sourceComponent: MaterialIcon {\n                            text: \"check\"\n                            color: Colours.palette.m3onSurfaceVariant\n                            font.pointSize: Appearance.font.size.large\n                        }\n                    }\n                }\n\n                implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/appearance/sections/ColorVariantSection.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"../../../launcher/services\"\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nCollapsibleSection {\n    title: qsTr(\"Color variant\")\n    description: qsTr(\"Material theme variant\")\n    showBackground: true\n\n    ColumnLayout {\n        Layout.fillWidth: true\n        spacing: Appearance.spacing.small / 2\n\n        Repeater {\n            model: M3Variants.list\n\n            delegate: StyledRect {\n                id: variantDelegate\n\n                required property var modelData\n\n                Layout.fillWidth: true\n\n                color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0)\n                radius: Appearance.rounding.normal\n                border.width: modelData.variant === Schemes.currentVariant ? 1 : 0\n                border.color: Colours.palette.m3primary\n\n                StateLayer {\n                    function onClicked(): void {\n                        const variant = variantDelegate.modelData.variant;\n\n                        Schemes.currentVariant = variant;\n                        Quickshell.execDetached([\"caelestia\", \"scheme\", \"set\", \"-v\", variant]);\n\n                        Qt.callLater(() => {\n                            reloadTimer.restart();\n                        });\n                    }\n                }\n\n                Timer {\n                    id: reloadTimer\n\n                    interval: 300\n                    onTriggered: {\n                        Schemes.reload();\n                    }\n                }\n\n                RowLayout {\n                    id: variantRow\n\n                    anchors.left: parent.left\n                    anchors.right: parent.right\n                    anchors.verticalCenter: parent.verticalCenter\n                    anchors.margins: Appearance.padding.normal\n\n                    spacing: Appearance.spacing.normal\n\n                    MaterialIcon {\n                        text: variantDelegate.modelData.icon\n                        font.pointSize: Appearance.font.size.large\n                        fill: variantDelegate.modelData.variant === Schemes.currentVariant ? 1 : 0\n                    }\n\n                    StyledText {\n                        Layout.fillWidth: true\n                        text: variantDelegate.modelData.name\n                        font.weight: variantDelegate.modelData.variant === Schemes.currentVariant ? 500 : 400\n                    }\n\n                    MaterialIcon {\n                        visible: variantDelegate.modelData.variant === Schemes.currentVariant\n                        text: \"check\"\n                        color: Colours.palette.m3primary\n                        font.pointSize: Appearance.font.size.large\n                    }\n                }\n\n                implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/appearance/sections/FontsSection.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"../../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nCollapsibleSection {\n    id: root\n\n    required property var rootPane\n\n    title: qsTr(\"Fonts\")\n    showBackground: true\n\n    CollapsibleSection {\n        id: sansFontSection\n\n        title: qsTr(\"Sans-serif font family\")\n        expanded: true\n        showBackground: true\n        nested: true\n\n        Loader {\n            Layout.fillWidth: true\n            Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0\n            asynchronous: true\n            active: sansFontSection.expanded\n\n            sourceComponent: StyledListView {\n                id: sansFontList\n\n                property alias contentHeight: sansFontList.contentHeight\n\n                clip: true\n                spacing: Appearance.spacing.small / 2\n                model: Qt.fontFamilies()\n\n                StyledScrollBar.vertical: StyledScrollBar {\n                    flickable: sansFontList\n                }\n\n                delegate: StyledRect {\n                    id: sansDelegate\n\n                    required property string modelData\n                    required property int index\n                    readonly property bool isCurrent: modelData === root.rootPane.fontFamilySans\n\n                    width: ListView.view.width\n                    color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)\n                    radius: Appearance.rounding.normal\n                    border.width: isCurrent ? 1 : 0\n                    border.color: Colours.palette.m3primary\n\n                    StateLayer {\n                        function onClicked(): void {\n                            root.rootPane.fontFamilySans = sansDelegate.modelData;\n                            root.rootPane.saveConfig();\n                        }\n                    }\n\n                    RowLayout {\n                        id: fontFamilySansRow\n\n                        anchors.left: parent.left\n                        anchors.right: parent.right\n                        anchors.verticalCenter: parent.verticalCenter\n                        anchors.margins: Appearance.padding.normal\n\n                        spacing: Appearance.spacing.normal\n\n                        StyledText {\n                            text: sansDelegate.modelData\n                            font.pointSize: Appearance.font.size.normal\n                        }\n\n                        Item {\n                            Layout.fillWidth: true\n                        }\n\n                        Loader {\n                            asynchronous: true\n                            active: sansDelegate.isCurrent\n\n                            sourceComponent: MaterialIcon {\n                                text: \"check\"\n                                color: Colours.palette.m3onSurfaceVariant\n                                font.pointSize: Appearance.font.size.large\n                            }\n                        }\n                    }\n\n                    implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2\n                }\n            }\n        }\n    }\n\n    CollapsibleSection {\n        id: monoFontSection\n\n        title: qsTr(\"Monospace font family\")\n        expanded: false\n        showBackground: true\n        nested: true\n\n        Loader {\n            Layout.fillWidth: true\n            Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0\n            asynchronous: true\n            active: monoFontSection.expanded\n\n            sourceComponent: StyledListView {\n                id: monoFontList\n\n                property alias contentHeight: monoFontList.contentHeight\n\n                clip: true\n                spacing: Appearance.spacing.small / 2\n                model: Qt.fontFamilies()\n\n                StyledScrollBar.vertical: StyledScrollBar {\n                    flickable: monoFontList\n                }\n\n                delegate: StyledRect {\n                    id: monoDelegate\n\n                    required property string modelData\n                    required property int index\n                    readonly property bool isCurrent: modelData === root.rootPane.fontFamilyMono\n\n                    width: ListView.view.width\n                    color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)\n                    radius: Appearance.rounding.normal\n                    border.width: isCurrent ? 1 : 0\n                    border.color: Colours.palette.m3primary\n\n                    StateLayer {\n                        function onClicked(): void {\n                            root.rootPane.fontFamilyMono = monoDelegate.modelData;\n                            root.rootPane.saveConfig();\n                        }\n                    }\n\n                    RowLayout {\n                        id: fontFamilyMonoRow\n\n                        anchors.left: parent.left\n                        anchors.right: parent.right\n                        anchors.verticalCenter: parent.verticalCenter\n                        anchors.margins: Appearance.padding.normal\n\n                        spacing: Appearance.spacing.normal\n\n                        StyledText {\n                            text: monoDelegate.modelData\n                            font.pointSize: Appearance.font.size.normal\n                        }\n\n                        Item {\n                            Layout.fillWidth: true\n                        }\n\n                        Loader {\n                            asynchronous: true\n                            active: monoDelegate.isCurrent\n\n                            sourceComponent: MaterialIcon {\n                                text: \"check\"\n                                color: Colours.palette.m3onSurfaceVariant\n                                font.pointSize: Appearance.font.size.large\n                            }\n                        }\n                    }\n\n                    implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2\n                }\n            }\n        }\n    }\n\n    CollapsibleSection {\n        id: materialFontSection\n\n        title: qsTr(\"Material font family\")\n        expanded: false\n        showBackground: true\n        nested: true\n\n        Loader {\n            id: materialFontLoader\n\n            Layout.fillWidth: true\n            Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0\n            asynchronous: true\n            active: materialFontSection.expanded\n\n            sourceComponent: StyledListView {\n                id: materialFontList\n\n                property alias contentHeight: materialFontList.contentHeight\n\n                clip: true\n                spacing: Appearance.spacing.small / 2\n                model: Qt.fontFamilies().filter(f => f.startsWith(\"Material Symbols\"))\n\n                StyledScrollBar.vertical: StyledScrollBar {\n                    flickable: materialFontList\n                }\n\n                delegate: StyledRect {\n                    id: materialDelegate\n\n                    required property string modelData\n                    required property int index\n                    readonly property bool isCurrent: modelData === root.rootPane.fontFamilyMaterial\n\n                    width: ListView.view.width\n                    color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)\n                    radius: Appearance.rounding.normal\n                    border.width: isCurrent ? 1 : 0\n                    border.color: Colours.palette.m3primary\n\n                    StateLayer {\n                        function onClicked(): void {\n                            root.rootPane.fontFamilyMaterial = materialDelegate.modelData;\n                            root.rootPane.saveConfig();\n                        }\n                    }\n\n                    RowLayout {\n                        id: fontFamilyMaterialRow\n\n                        anchors.left: parent.left\n                        anchors.right: parent.right\n                        anchors.verticalCenter: parent.verticalCenter\n                        anchors.margins: Appearance.padding.normal\n\n                        spacing: Appearance.spacing.normal\n\n                        StyledText {\n                            text: materialDelegate.modelData\n                            font.pointSize: Appearance.font.size.normal\n                        }\n\n                        Item {\n                            Layout.fillWidth: true\n                        }\n\n                        Loader {\n                            asynchronous: true\n                            active: materialDelegate.isCurrent\n\n                            sourceComponent: MaterialIcon {\n                                text: \"check\"\n                                color: Colours.palette.m3onSurfaceVariant\n                                font.pointSize: Appearance.font.size.large\n                            }\n                        }\n                    }\n\n                    implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2\n                }\n            }\n        }\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.normal\n\n        SliderInput {\n            Layout.fillWidth: true\n\n            label: qsTr(\"Font size scale\")\n            value: root.rootPane.fontSizeScale\n            from: 0.7\n            to: 1.5\n            decimals: 2\n            suffix: \"×\"\n            validator: DoubleValidator {\n                bottom: 0.7\n                top: 1.5\n            }\n\n            onValueModified: newValue => {\n                root.rootPane.fontSizeScale = newValue;\n                root.rootPane.saveConfig();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/appearance/sections/ScalesSection.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"../../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nCollapsibleSection {\n    id: root\n\n    required property var rootPane\n\n    title: qsTr(\"Scales\")\n    showBackground: true\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.normal\n\n        SliderInput {\n            Layout.fillWidth: true\n\n            label: qsTr(\"Padding scale\")\n            value: root.rootPane.paddingScale\n            from: 0.5\n            to: 2.0\n            decimals: 1\n            suffix: \"×\"\n            validator: DoubleValidator {\n                bottom: 0.5\n                top: 2.0\n            }\n\n            onValueModified: newValue => {\n                root.rootPane.paddingScale = newValue;\n                root.rootPane.saveConfig();\n            }\n        }\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.normal\n\n        SliderInput {\n            Layout.fillWidth: true\n\n            label: qsTr(\"Rounding scale\")\n            value: root.rootPane.roundingScale\n            from: 0.1\n            to: 5.0\n            decimals: 1\n            suffix: \"×\"\n            validator: DoubleValidator {\n                bottom: 0.1\n                top: 5.0\n            }\n\n            onValueModified: newValue => {\n                root.rootPane.roundingScale = newValue;\n                root.rootPane.saveConfig();\n            }\n        }\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.normal\n\n        SliderInput {\n            Layout.fillWidth: true\n\n            label: qsTr(\"Spacing scale\")\n            value: root.rootPane.spacingScale\n            from: 0.1\n            to: 2.0\n            decimals: 1\n            suffix: \"×\"\n            validator: DoubleValidator {\n                bottom: 0.1\n                top: 2.0\n            }\n\n            onValueModified: newValue => {\n                root.rootPane.spacingScale = newValue;\n                root.rootPane.saveConfig();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/appearance/sections/ThemeModeSection.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components.controls\nimport qs.services\nimport QtQuick\n\nCollapsibleSection {\n    title: qsTr(\"Theme mode\")\n    description: qsTr(\"Light or dark theme\")\n    showBackground: true\n\n    SwitchRow {\n        label: qsTr(\"Dark mode\")\n        checked: !Colours.currentLight\n        onToggled: checked => {\n            Colours.setMode(checked ? \"dark\" : \"light\");\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/appearance/sections/TransparencySection.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"../../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nCollapsibleSection {\n    id: root\n\n    required property var rootPane\n\n    title: qsTr(\"Transparency\")\n    showBackground: true\n\n    SwitchRow {\n        label: qsTr(\"Transparency enabled\")\n        checked: root.rootPane.transparencyEnabled\n        onToggled: checked => {\n            root.rootPane.transparencyEnabled = checked;\n            root.rootPane.saveConfig();\n        }\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.normal\n\n        SliderInput {\n            Layout.fillWidth: true\n\n            label: qsTr(\"Transparency base\")\n            value: root.rootPane.transparencyBase * 100\n            from: 0\n            to: 100\n            suffix: \"%\"\n            validator: IntValidator {\n                bottom: 0\n                top: 100\n            }\n            formatValueFunction: val => Math.round(val).toString()\n            parseValueFunction: text => parseInt(text)\n\n            onValueModified: newValue => {\n                root.rootPane.transparencyBase = newValue / 100;\n                root.rootPane.saveConfig();\n            }\n        }\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.normal\n\n        SliderInput {\n            Layout.fillWidth: true\n\n            label: qsTr(\"Transparency layers\")\n            value: root.rootPane.transparencyLayers * 100\n            from: 0\n            to: 100\n            suffix: \"%\"\n            validator: IntValidator {\n                bottom: 0\n                top: 100\n            }\n            formatValueFunction: val => Math.round(val).toString()\n            parseValueFunction: text => parseInt(text)\n\n            onValueModified: newValue => {\n                root.rootPane.transparencyLayers = newValue / 100;\n                root.rootPane.saveConfig();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/audio/AudioPane.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property Session session\n\n    anchors.fill: parent\n\n    SplitPaneLayout {\n        anchors.fill: parent\n\n        leftContent: Component {\n            StyledFlickable {\n                id: leftAudioFlickable\n\n                flickableDirection: Flickable.VerticalFlick\n                contentHeight: leftContent.height\n\n                StyledScrollBar.vertical: StyledScrollBar {\n                    flickable: leftAudioFlickable\n                }\n\n                ColumnLayout {\n                    id: leftContent\n\n                    anchors.left: parent.left\n                    anchors.right: parent.right\n                    spacing: Appearance.spacing.normal\n\n                    RowLayout {\n                        Layout.fillWidth: true\n                        spacing: Appearance.spacing.smaller\n\n                        StyledText {\n                            text: qsTr(\"Audio\")\n                            font.pointSize: Appearance.font.size.large\n                            font.weight: 500\n                        }\n\n                        Item {\n                            Layout.fillWidth: true\n                        }\n                    }\n\n                    CollapsibleSection {\n                        id: outputDevicesSection\n\n                        Layout.fillWidth: true\n                        title: qsTr(\"Output devices\")\n                        expanded: true\n\n                        ColumnLayout {\n                            Layout.fillWidth: true\n                            spacing: Appearance.spacing.small\n\n                            RowLayout {\n                                Layout.fillWidth: true\n                                spacing: Appearance.spacing.small\n\n                                StyledText {\n                                    text: qsTr(\"Devices (%1)\").arg(Audio.sinks.length)\n                                    font.pointSize: Appearance.font.size.normal\n                                    font.weight: 500\n                                }\n                            }\n\n                            StyledText {\n                                Layout.fillWidth: true\n                                text: qsTr(\"All available output devices\")\n                                color: Colours.palette.m3outline\n                            }\n\n                            Repeater {\n                                Layout.fillWidth: true\n                                model: Audio.sinks\n\n                                delegate: StyledRect {\n                                    id: outputDeviceDelegate\n\n                                    required property var modelData\n\n                                    Layout.fillWidth: true\n\n                                    color: Audio.sink?.id === outputDeviceDelegate.modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : \"transparent\"\n                                    radius: Appearance.rounding.normal\n\n                                    StateLayer {\n                                        function onClicked(): void {\n                                            Audio.setAudioSink(outputDeviceDelegate.modelData);\n                                        }\n                                    }\n\n                                    RowLayout {\n                                        id: outputRowLayout\n\n                                        anchors.left: parent.left\n                                        anchors.right: parent.right\n                                        anchors.verticalCenter: parent.verticalCenter\n                                        anchors.margins: Appearance.padding.normal\n\n                                        spacing: Appearance.spacing.normal\n\n                                        MaterialIcon {\n                                            text: Audio.sink?.id === outputDeviceDelegate.modelData.id ? \"speaker\" : \"speaker_group\"\n                                            font.pointSize: Appearance.font.size.large\n                                            fill: Audio.sink?.id === outputDeviceDelegate.modelData.id ? 1 : 0\n                                        }\n\n                                        StyledText {\n                                            Layout.fillWidth: true\n                                            elide: Text.ElideRight\n                                            maximumLineCount: 1\n\n                                            text: outputDeviceDelegate.modelData.description || qsTr(\"Unknown\")\n                                            font.weight: Audio.sink?.id === outputDeviceDelegate.modelData.id ? 500 : 400\n                                        }\n                                    }\n\n                                    implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2\n                                }\n                            }\n                        }\n                    }\n\n                    CollapsibleSection {\n                        id: inputDevicesSection\n\n                        Layout.fillWidth: true\n                        title: qsTr(\"Input devices\")\n                        expanded: true\n\n                        ColumnLayout {\n                            Layout.fillWidth: true\n                            spacing: Appearance.spacing.small\n\n                            RowLayout {\n                                Layout.fillWidth: true\n                                spacing: Appearance.spacing.small\n\n                                StyledText {\n                                    text: qsTr(\"Devices (%1)\").arg(Audio.sources.length)\n                                    font.pointSize: Appearance.font.size.normal\n                                    font.weight: 500\n                                }\n                            }\n\n                            StyledText {\n                                Layout.fillWidth: true\n                                text: qsTr(\"All available input devices\")\n                                color: Colours.palette.m3outline\n                            }\n\n                            Repeater {\n                                Layout.fillWidth: true\n                                model: Audio.sources\n\n                                delegate: StyledRect {\n                                    id: inputDeviceDelegate\n\n                                    required property var modelData\n\n                                    Layout.fillWidth: true\n\n                                    color: Audio.source?.id === inputDeviceDelegate.modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : \"transparent\"\n                                    radius: Appearance.rounding.normal\n\n                                    StateLayer {\n                                        function onClicked(): void {\n                                            Audio.setAudioSource(inputDeviceDelegate.modelData);\n                                        }\n                                    }\n\n                                    RowLayout {\n                                        id: inputRowLayout\n\n                                        anchors.left: parent.left\n                                        anchors.right: parent.right\n                                        anchors.verticalCenter: parent.verticalCenter\n                                        anchors.margins: Appearance.padding.normal\n\n                                        spacing: Appearance.spacing.normal\n\n                                        MaterialIcon {\n                                            text: \"mic\"\n                                            font.pointSize: Appearance.font.size.large\n                                            fill: Audio.source?.id === inputDeviceDelegate.modelData.id ? 1 : 0\n                                        }\n\n                                        StyledText {\n                                            Layout.fillWidth: true\n                                            elide: Text.ElideRight\n                                            maximumLineCount: 1\n\n                                            text: inputDeviceDelegate.modelData.description || qsTr(\"Unknown\")\n                                            font.weight: Audio.source?.id === inputDeviceDelegate.modelData.id ? 500 : 400\n                                        }\n                                    }\n\n                                    implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        rightContent: Component {\n            StyledFlickable {\n                id: rightAudioFlickable\n\n                flickableDirection: Flickable.VerticalFlick\n                contentHeight: contentLayout.height\n\n                StyledScrollBar.vertical: StyledScrollBar {\n                    flickable: rightAudioFlickable\n                }\n\n                ColumnLayout {\n                    id: contentLayout\n\n                    anchors.left: parent.left\n                    anchors.right: parent.right\n                    anchors.top: parent.top\n                    spacing: Appearance.spacing.normal\n\n                    SettingsHeader {\n                        icon: \"volume_up\"\n                        title: qsTr(\"Audio Settings\")\n                    }\n\n                    SectionHeader {\n                        title: qsTr(\"Output volume\")\n                        description: qsTr(\"Control the volume of your output device\")\n                    }\n\n                    SectionContainer {\n                        contentSpacing: Appearance.spacing.normal\n\n                        ColumnLayout {\n                            Layout.fillWidth: true\n                            spacing: Appearance.spacing.small\n\n                            RowLayout {\n                                Layout.fillWidth: true\n                                spacing: Appearance.spacing.normal\n\n                                StyledText {\n                                    text: qsTr(\"Volume\")\n                                    font.pointSize: Appearance.font.size.normal\n                                    font.weight: 500\n                                }\n\n                                Item {\n                                    Layout.fillWidth: true\n                                }\n\n                                StyledInputField {\n                                    id: outputVolumeInput\n\n                                    Layout.preferredWidth: 70\n                                    validator: IntValidator {\n                                        bottom: 0\n                                        top: 100\n                                    }\n                                    enabled: !Audio.muted\n\n                                    Component.onCompleted: {\n                                        text = Math.round(Audio.volume * 100).toString();\n                                    }\n\n                                    Connections {\n                                        function onVolumeChanged() {\n                                            if (!outputVolumeInput.hasFocus) {\n                                                outputVolumeInput.text = Math.round(Audio.volume * 100).toString();\n                                            }\n                                        }\n\n                                        target: Audio\n                                    }\n\n                                    onTextEdited: text => {\n                                        if (hasFocus) {\n                                            const val = parseInt(text);\n                                            if (!isNaN(val) && val >= 0 && val <= 100) {\n                                                Audio.setVolume(val / 100);\n                                            }\n                                        }\n                                    }\n\n                                    onEditingFinished: {\n                                        const val = parseInt(text);\n                                        if (isNaN(val) || val < 0 || val > 100) {\n                                            text = Math.round(Audio.volume * 100).toString();\n                                        }\n                                    }\n                                }\n\n                                StyledText {\n                                    text: \"%\"\n                                    color: Colours.palette.m3outline\n                                    font.pointSize: Appearance.font.size.normal\n                                    opacity: Audio.muted ? 0.5 : 1\n                                }\n\n                                StyledRect {\n                                    implicitWidth: implicitHeight\n                                    implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2\n\n                                    radius: Appearance.rounding.normal\n                                    color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer\n\n                                    StateLayer {\n                                        function onClicked(): void {\n                                            if (Audio.sink?.audio) {\n                                                Audio.sink.audio.muted = !Audio.sink.audio.muted;\n                                            }\n                                        }\n                                    }\n\n                                    MaterialIcon {\n                                        id: muteIcon\n\n                                        anchors.centerIn: parent\n                                        text: Audio.muted ? \"volume_off\" : \"volume_up\"\n                                        color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer\n                                    }\n                                }\n                            }\n\n                            StyledSlider {\n                                id: outputVolumeSlider\n\n                                Layout.fillWidth: true\n                                implicitHeight: Appearance.padding.normal * 3\n\n                                value: Audio.volume\n                                enabled: !Audio.muted\n                                opacity: enabled ? 1 : 0.5\n                                onMoved: {\n                                    Audio.setVolume(value);\n                                    if (!outputVolumeInput.hasFocus) {\n                                        outputVolumeInput.text = Math.round(value * 100).toString();\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    SectionHeader {\n                        title: qsTr(\"Input volume\")\n                        description: qsTr(\"Control the volume of your input device\")\n                    }\n\n                    SectionContainer {\n                        contentSpacing: Appearance.spacing.normal\n\n                        ColumnLayout {\n                            Layout.fillWidth: true\n                            spacing: Appearance.spacing.small\n\n                            RowLayout {\n                                Layout.fillWidth: true\n                                spacing: Appearance.spacing.normal\n\n                                StyledText {\n                                    text: qsTr(\"Volume\")\n                                    font.pointSize: Appearance.font.size.normal\n                                    font.weight: 500\n                                }\n\n                                Item {\n                                    Layout.fillWidth: true\n                                }\n\n                                StyledInputField {\n                                    id: inputVolumeInput\n\n                                    Layout.preferredWidth: 70\n                                    validator: IntValidator {\n                                        bottom: 0\n                                        top: 100\n                                    }\n                                    enabled: !Audio.sourceMuted\n\n                                    Component.onCompleted: {\n                                        text = Math.round(Audio.sourceVolume * 100).toString();\n                                    }\n\n                                    Connections {\n                                        function onSourceVolumeChanged() {\n                                            if (!inputVolumeInput.hasFocus) {\n                                                inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString();\n                                            }\n                                        }\n\n                                        target: Audio\n                                    }\n\n                                    onTextEdited: text => {\n                                        if (hasFocus) {\n                                            const val = parseInt(text);\n                                            if (!isNaN(val) && val >= 0 && val <= 100) {\n                                                Audio.setSourceVolume(val / 100);\n                                            }\n                                        }\n                                    }\n\n                                    onEditingFinished: {\n                                        const val = parseInt(text);\n                                        if (isNaN(val) || val < 0 || val > 100) {\n                                            text = Math.round(Audio.sourceVolume * 100).toString();\n                                        }\n                                    }\n                                }\n\n                                StyledText {\n                                    text: \"%\"\n                                    color: Colours.palette.m3outline\n                                    font.pointSize: Appearance.font.size.normal\n                                    opacity: Audio.sourceMuted ? 0.5 : 1\n                                }\n\n                                StyledRect {\n                                    implicitWidth: implicitHeight\n                                    implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2\n\n                                    radius: Appearance.rounding.normal\n                                    color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer\n\n                                    StateLayer {\n                                        function onClicked(): void {\n                                            if (Audio.source?.audio) {\n                                                Audio.source.audio.muted = !Audio.source.audio.muted;\n                                            }\n                                        }\n                                    }\n\n                                    MaterialIcon {\n                                        id: muteInputIcon\n\n                                        anchors.centerIn: parent\n                                        text: \"mic_off\"\n                                        color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer\n                                    }\n                                }\n                            }\n\n                            StyledSlider {\n                                id: inputVolumeSlider\n\n                                Layout.fillWidth: true\n                                implicitHeight: Appearance.padding.normal * 3\n\n                                value: Audio.sourceVolume\n                                enabled: !Audio.sourceMuted\n                                opacity: enabled ? 1 : 0.5\n                                onMoved: {\n                                    Audio.setSourceVolume(value);\n                                    if (!inputVolumeInput.hasFocus) {\n                                        inputVolumeInput.text = Math.round(value * 100).toString();\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    SectionHeader {\n                        title: qsTr(\"Applications\")\n                        description: qsTr(\"Control volume for individual applications\")\n                    }\n\n                    SectionContainer {\n                        contentSpacing: Appearance.spacing.normal\n\n                        ColumnLayout {\n                            Layout.fillWidth: true\n                            spacing: Appearance.spacing.small\n\n                            Repeater {\n                                model: Audio.streams\n                                Layout.fillWidth: true\n\n                                delegate: ColumnLayout {\n                                    id: streamDelegate\n\n                                    required property var modelData\n                                    required property int index\n\n                                    Layout.fillWidth: true\n                                    spacing: Appearance.spacing.smaller\n\n                                    RowLayout {\n                                        Layout.fillWidth: true\n                                        spacing: Appearance.spacing.normal\n\n                                        MaterialIcon {\n                                            text: \"apps\"\n                                            font.pointSize: Appearance.font.size.normal\n                                            fill: 0\n                                        }\n\n                                        StyledText {\n                                            Layout.fillWidth: true\n                                            elide: Text.ElideRight\n                                            maximumLineCount: 1\n                                            text: Audio.getStreamName(streamDelegate.modelData)\n                                            font.pointSize: Appearance.font.size.normal\n                                            font.weight: 500\n                                        }\n\n                                        StyledInputField {\n                                            id: streamVolumeInput\n\n                                            Layout.preferredWidth: 70\n                                            validator: IntValidator {\n                                                bottom: 0\n                                                top: 100\n                                            }\n                                            enabled: !Audio.getStreamMuted(streamDelegate.modelData)\n\n                                            Component.onCompleted: {\n                                                text = Math.round(Audio.getStreamVolume(streamDelegate.modelData) * 100).toString();\n                                            }\n\n                                            Connections {\n                                                function onAudioChanged() {\n                                                    if (!streamVolumeInput.hasFocus && streamDelegate.modelData?.audio) {\n                                                        streamVolumeInput.text = Math.round(streamDelegate.modelData.audio.volume * 100).toString();\n                                                    }\n                                                }\n\n                                                target: streamDelegate.modelData\n                                            }\n\n                                            onTextEdited: text => {\n                                                if (hasFocus) {\n                                                    const val = parseInt(text);\n                                                    if (!isNaN(val) && val >= 0 && val <= 100) {\n                                                        Audio.setStreamVolume(streamDelegate.modelData, val / 100);\n                                                    }\n                                                }\n                                            }\n\n                                            onEditingFinished: {\n                                                const val = parseInt(text);\n                                                if (isNaN(val) || val < 0 || val > 100) {\n                                                    text = Math.round(Audio.getStreamVolume(streamDelegate.modelData) * 100).toString();\n                                                }\n                                            }\n                                        }\n\n                                        StyledText {\n                                            text: \"%\"\n                                            color: Colours.palette.m3outline\n                                            font.pointSize: Appearance.font.size.normal\n                                            opacity: Audio.getStreamMuted(streamDelegate.modelData) ? 0.5 : 1\n                                        }\n\n                                        StyledRect {\n                                            implicitWidth: implicitHeight\n                                            implicitHeight: streamMuteIcon.implicitHeight + Appearance.padding.normal * 2\n\n                                            radius: Appearance.rounding.normal\n                                            color: Audio.getStreamMuted(streamDelegate.modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer\n\n                                            StateLayer {\n                                                function onClicked(): void {\n                                                    Audio.setStreamMuted(streamDelegate.modelData, !Audio.getStreamMuted(streamDelegate.modelData));\n                                                }\n                                            }\n\n                                            MaterialIcon {\n                                                id: streamMuteIcon\n\n                                                anchors.centerIn: parent\n                                                text: Audio.getStreamMuted(streamDelegate.modelData) ? \"volume_off\" : \"volume_up\"\n                                                color: Audio.getStreamMuted(streamDelegate.modelData) ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer\n                                            }\n                                        }\n                                    }\n\n                                    StyledSlider {\n                                        id: streamSlider\n\n                                        Layout.fillWidth: true\n                                        implicitHeight: Appearance.padding.normal * 3\n\n                                        value: Audio.getStreamVolume(streamDelegate.modelData)\n                                        enabled: !Audio.getStreamMuted(streamDelegate.modelData)\n                                        opacity: enabled ? 1 : 0.5\n                                        onMoved: {\n                                            Audio.setStreamVolume(streamDelegate.modelData, value);\n                                            if (!streamVolumeInput.hasFocus) {\n                                                streamVolumeInput.text = Math.round(value * 100).toString();\n                                            }\n                                        }\n\n                                        Connections {\n                                            function onAudioChanged() {\n                                                if (streamDelegate.modelData?.audio) {\n                                                    streamSlider.value = streamDelegate.modelData.audio.volume;\n                                                }\n                                            }\n\n                                            target: streamDelegate.modelData\n                                        }\n                                    }\n                                }\n                            }\n\n                            StyledText {\n                                Layout.fillWidth: true\n                                visible: Audio.streams.length === 0\n                                text: qsTr(\"No applications currently playing audio\")\n                                color: Colours.palette.m3outline\n                                font.pointSize: Appearance.font.size.small\n                                horizontalAlignment: Text.AlignHCenter\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/bluetooth/BtPane.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport \".\"\nimport qs.components.controls\nimport qs.components.containers\nimport QtQuick\n\nSplitPaneWithDetails {\n    id: root\n\n    required property Session session\n\n    anchors.fill: parent\n\n    activeItem: session.bt.active\n    paneIdGenerator: function (item) {\n        return item ? (item.address || \"\") : \"\";\n    }\n\n    leftContent: Component {\n        StyledFlickable {\n            id: leftFlickable\n\n            flickableDirection: Flickable.VerticalFlick\n            contentHeight: deviceList.height\n\n            StyledScrollBar.vertical: StyledScrollBar {\n                flickable: leftFlickable\n            }\n\n            DeviceList {\n                id: deviceList\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                session: root.session\n            }\n        }\n    }\n\n    rightDetailsComponent: Component {\n        Details {\n            session: root.session\n        }\n    }\n\n    rightSettingsComponent: Component {\n        StyledFlickable {\n            id: settingsFlickable\n\n            flickableDirection: Flickable.VerticalFlick\n            contentHeight: settingsInner.height\n\n            StyledScrollBar.vertical: StyledScrollBar {\n                flickable: settingsFlickable\n            }\n\n            Settings {\n                id: settingsInner\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.top: parent.top\n                session: root.session\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/bluetooth/Details.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.effects\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell.Bluetooth\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledFlickable {\n    id: root\n\n    required property Session session\n    readonly property BluetoothDevice device: session.bt.active\n\n    flickableDirection: Flickable.VerticalFlick\n    contentHeight: detailsWrapper.height\n\n    StyledScrollBar.vertical: StyledScrollBar {\n        flickable: root\n    }\n\n    Item {\n        id: detailsWrapper\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.top: parent.top\n        implicitHeight: details.implicitHeight\n\n        DeviceDetails {\n            id: details\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            anchors.top: parent.top\n\n            session: root.session\n            device: root.device\n\n            headerComponent: Component {\n                SettingsHeader {\n                    icon: Icons.getBluetoothIcon(root.device?.icon ?? \"\")\n                    title: root.device?.name ?? \"\"\n                }\n            }\n\n            sections: [\n                Component {\n                    ColumnLayout {\n                        spacing: Appearance.spacing.normal\n\n                        StyledText {\n                            Layout.topMargin: Appearance.spacing.large\n                            text: qsTr(\"Connection status\")\n                            font.pointSize: Appearance.font.size.larger\n                            font.weight: 500\n                        }\n\n                        StyledText {\n                            text: qsTr(\"Connection settings for this device\")\n                            color: Colours.palette.m3outline\n                        }\n\n                        StyledRect {\n                            Layout.fillWidth: true\n                            implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2\n\n                            radius: Appearance.rounding.normal\n                            color: Colours.tPalette.m3surfaceContainer\n\n                            ColumnLayout {\n                                id: deviceStatus\n\n                                anchors.left: parent.left\n                                anchors.right: parent.right\n                                anchors.verticalCenter: parent.verticalCenter\n                                anchors.margins: Appearance.padding.large\n\n                                spacing: Appearance.spacing.larger\n\n                                Toggle {\n                                    label: qsTr(\"Connected\")\n                                    checked: root.device?.connected ?? false\n                                    toggle.onToggled: root.device.connected = checked\n                                }\n\n                                Toggle {\n                                    label: qsTr(\"Paired\")\n                                    checked: root.device?.paired ?? false\n                                    toggle.onToggled: {\n                                        if (root.device.paired)\n                                            root.device.forget();\n                                        else\n                                            root.device.pair();\n                                    }\n                                }\n\n                                Toggle {\n                                    label: qsTr(\"Blocked\")\n                                    checked: root.device?.blocked ?? false\n                                    toggle.onToggled: root.device.blocked = checked\n                                }\n                            }\n                        }\n                    }\n                },\n                Component {\n                    ColumnLayout {\n                        spacing: Appearance.spacing.normal\n\n                        StyledText {\n                            Layout.topMargin: Appearance.spacing.large\n                            text: qsTr(\"Device properties\")\n                            font.pointSize: Appearance.font.size.larger\n                            font.weight: 500\n                        }\n\n                        StyledText {\n                            text: qsTr(\"Additional settings\")\n                            color: Colours.palette.m3outline\n                        }\n\n                        StyledRect {\n                            Layout.fillWidth: true\n                            implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2\n\n                            radius: Appearance.rounding.normal\n                            color: Colours.tPalette.m3surfaceContainer\n\n                            ColumnLayout {\n                                id: deviceProps\n\n                                anchors.left: parent.left\n                                anchors.right: parent.right\n                                anchors.verticalCenter: parent.verticalCenter\n                                anchors.margins: Appearance.padding.large\n\n                                spacing: Appearance.spacing.larger\n\n                                RowLayout {\n                                    Layout.fillWidth: true\n                                    spacing: Appearance.spacing.small\n\n                                    Item {\n                                        id: renameDevice\n\n                                        Layout.fillWidth: true\n                                        Layout.rightMargin: Appearance.spacing.small\n\n                                        implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight\n\n                                        states: State {\n                                            name: \"editingDeviceName\"\n                                            when: root.session.bt.editingDeviceName\n\n                                            AnchorChanges {\n                                                target: deviceNameEdit\n                                                anchors.top: renameDevice.top\n                                            }\n                                            PropertyChanges {\n                                                renameDevice.implicitHeight: deviceNameEdit.implicitHeight\n                                                renameLabel.opacity: 0\n                                                deviceNameEdit.padding: Appearance.padding.normal\n                                            }\n                                        }\n\n                                        transitions: Transition {\n                                            AnchorAnimation {\n                                                duration: Appearance.anim.durations.normal\n                                                easing.type: Easing.BezierSpline\n                                                easing.bezierCurve: Appearance.anim.curves.standard\n                                            }\n                                            Anim {\n                                                properties: \"implicitHeight,opacity,padding\"\n                                            }\n                                        }\n\n                                        StyledText {\n                                            id: renameLabel\n\n                                            anchors.left: parent.left\n\n                                            text: qsTr(\"Device name\")\n                                            color: Colours.palette.m3outline\n                                            font.pointSize: Appearance.font.size.small\n                                        }\n\n                                        StyledTextField {\n                                            id: deviceNameEdit\n\n                                            anchors.left: parent.left\n                                            anchors.right: parent.right\n                                            anchors.top: renameLabel.bottom\n                                            anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal\n\n                                            text: root.device?.name ?? \"\"\n                                            readOnly: !root.session.bt.editingDeviceName\n                                            onAccepted: {\n                                                root.session.bt.editingDeviceName = false;\n                                                root.device.name = text;\n                                            }\n\n                                            leftPadding: Appearance.padding.normal\n                                            rightPadding: Appearance.padding.normal\n\n                                            background: StyledRect {\n                                                radius: Appearance.rounding.small\n                                                border.width: 2\n                                                border.color: Colours.palette.m3primary\n                                                opacity: root.session.bt.editingDeviceName ? 1 : 0\n\n                                                Behavior on border.color {\n                                                    CAnim {}\n                                                }\n\n                                                Behavior on opacity {\n                                                    Anim {}\n                                                }\n                                            }\n\n                                            Behavior on anchors.leftMargin {\n                                                Anim {}\n                                            }\n                                        }\n                                    }\n\n                                    StyledRect {\n                                        implicitWidth: implicitHeight\n                                        implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2\n\n                                        radius: Appearance.rounding.small\n                                        color: Colours.palette.m3secondaryContainer\n                                        opacity: root.session.bt.editingDeviceName ? 1 : 0\n                                        scale: root.session.bt.editingDeviceName ? 1 : 0.5\n\n                                        StateLayer {\n                                            function onClicked(): void {\n                                                root.session.bt.editingDeviceName = false;\n                                                deviceNameEdit.text = Qt.binding(() => root.device?.name ?? \"\");\n                                            }\n\n                                            color: Colours.palette.m3onSecondaryContainer\n                                            disabled: !root.session.bt.editingDeviceName\n                                        }\n\n                                        MaterialIcon {\n                                            id: cancelEditIcon\n\n                                            anchors.centerIn: parent\n                                            animate: true\n                                            text: \"cancel\"\n                                            color: Colours.palette.m3onSecondaryContainer\n                                        }\n\n                                        Behavior on opacity {\n                                            Anim {}\n                                        }\n\n                                        Behavior on scale {\n                                            Anim {\n                                                duration: Appearance.anim.durations.expressiveFastSpatial\n                                                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                                            }\n                                        }\n                                    }\n\n                                    StyledRect {\n                                        implicitWidth: implicitHeight\n                                        implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2\n\n                                        radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)\n                                        color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0)\n\n                                        StateLayer {\n                                            function onClicked(): void {\n                                                root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName;\n                                                if (root.session.bt.editingDeviceName)\n                                                    deviceNameEdit.forceActiveFocus();\n                                                else\n                                                    deviceNameEdit.accepted();\n                                            }\n\n                                            color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n                                        }\n\n                                        MaterialIcon {\n                                            id: editIcon\n\n                                            anchors.centerIn: parent\n                                            animate: true\n                                            text: root.session.bt.editingDeviceName ? \"check_circle\" : \"edit\"\n                                            color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n                                        }\n\n                                        Behavior on radius {\n                                            Anim {}\n                                        }\n                                    }\n                                }\n\n                                Toggle {\n                                    label: qsTr(\"Trusted\")\n                                    checked: root.device?.trusted ?? false\n                                    toggle.onToggled: root.device.trusted = checked\n                                }\n\n                                Toggle {\n                                    label: qsTr(\"Wake allowed\")\n                                    checked: root.device?.wakeAllowed ?? false\n                                    toggle.onToggled: root.device.wakeAllowed = checked\n                                }\n                            }\n                        }\n                    }\n                },\n                Component {\n                    ColumnLayout {\n                        spacing: Appearance.spacing.normal\n\n                        StyledText {\n                            Layout.topMargin: Appearance.spacing.large\n                            text: qsTr(\"Device information\")\n                            font.pointSize: Appearance.font.size.larger\n                            font.weight: 500\n                        }\n\n                        StyledText {\n                            text: qsTr(\"Information about this device\")\n                            color: Colours.palette.m3outline\n                        }\n\n                        StyledRect {\n                            Layout.fillWidth: true\n                            implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2\n\n                            radius: Appearance.rounding.normal\n                            color: Colours.tPalette.m3surfaceContainer\n\n                            ColumnLayout {\n                                id: deviceInfo\n\n                                anchors.left: parent.left\n                                anchors.right: parent.right\n                                anchors.verticalCenter: parent.verticalCenter\n                                anchors.margins: Appearance.padding.large\n\n                                spacing: Appearance.spacing.small / 2\n\n                                StyledText {\n                                    text: root.device?.batteryAvailable ? qsTr(\"Device battery (%1%)\").arg(root.device.battery * 100) : qsTr(\"Battery unavailable\")\n                                }\n\n                                RowLayout {\n                                    id: batteryPercent\n\n                                    Layout.topMargin: Appearance.spacing.small / 2\n                                    Layout.fillWidth: true\n                                    Layout.preferredHeight: Appearance.padding.smaller\n                                    spacing: Appearance.spacing.small / 2\n\n                                    StyledRect {\n                                        Layout.fillWidth: true\n                                        Layout.fillHeight: true\n                                        radius: Appearance.rounding.full\n                                        color: Colours.palette.m3secondaryContainer\n\n                                        StyledRect {\n                                            anchors.left: parent.left\n                                            anchors.top: parent.top\n                                            anchors.bottom: parent.bottom\n                                            anchors.margins: parent.height * 0.25\n\n                                            implicitWidth: root.device?.batteryAvailable ? batteryPercent.width * root.device.battery : 0\n                                            radius: Appearance.rounding.full\n                                            color: Colours.palette.m3primary\n                                        }\n                                    }\n                                }\n\n                                StyledText {\n                                    Layout.topMargin: Appearance.spacing.normal\n                                    text: qsTr(\"Dbus path\")\n                                }\n\n                                StyledText {\n                                    text: root.device?.dbusPath ?? \"\"\n                                    color: Colours.palette.m3outline\n                                    font.pointSize: Appearance.font.size.small\n                                }\n\n                                StyledText {\n                                    Layout.topMargin: Appearance.spacing.normal\n                                    text: qsTr(\"MAC address\")\n                                }\n\n                                StyledText {\n                                    text: root.device?.address ?? \"\"\n                                    color: Colours.palette.m3outline\n                                    font.pointSize: Appearance.font.size.small\n                                }\n\n                                StyledText {\n                                    Layout.topMargin: Appearance.spacing.normal\n                                    text: qsTr(\"Bonded\")\n                                }\n\n                                StyledText {\n                                    text: root.device?.bonded ? qsTr(\"Yes\") : qsTr(\"No\")\n                                    color: Colours.palette.m3outline\n                                    font.pointSize: Appearance.font.size.small\n                                }\n\n                                StyledText {\n                                    Layout.topMargin: Appearance.spacing.normal\n                                    text: qsTr(\"System name\")\n                                }\n\n                                StyledText {\n                                    text: root.device?.deviceName ?? \"\"\n                                    color: Colours.palette.m3outline\n                                    font.pointSize: Appearance.font.size.small\n                                }\n                            }\n                        }\n                    }\n                }\n            ]\n        }\n    }\n\n    ColumnLayout {\n        anchors.right: fabRoot.right\n        anchors.bottom: fabRoot.top\n        anchors.bottomMargin: Appearance.padding.normal\n\n        Repeater {\n            id: fabMenu\n\n            model: ListModel {\n                ListElement {\n                    name: \"trust\"\n                    icon: \"handshake\"\n                }\n                ListElement {\n                    name: \"block\"\n                    icon: \"block\"\n                }\n                ListElement {\n                    name: \"pair\"\n                    icon: \"missing_controller\"\n                }\n                ListElement {\n                    name: \"connect\"\n                    icon: \"bluetooth_connected\"\n                }\n            }\n\n            StyledClippingRect {\n                id: fabMenuItem\n\n                required property var modelData\n                required property int index\n\n                Layout.alignment: Qt.AlignRight\n\n                implicitHeight: fabMenuItemInner.implicitHeight + Appearance.padding.larger * 2\n\n                radius: Appearance.rounding.full\n                color: Colours.palette.m3primaryContainer\n\n                opacity: 0\n\n                states: State {\n                    name: \"visible\"\n                    when: root.session.bt.fabMenuOpen\n\n                    PropertyChanges {\n                        fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Appearance.padding.large * 2\n                        fabMenuItem.opacity: 1\n                        fabMenuItemInner.opacity: 1\n                    }\n                }\n\n                transitions: [\n                    Transition {\n                        to: \"visible\"\n\n                        SequentialAnimation {\n                            PauseAnimation {\n                                duration: (fabMenu.count - 1 - fabMenuItem.index) * Appearance.anim.durations.small / 8\n                            }\n                            ParallelAnimation {\n                                Anim {\n                                    property: \"implicitWidth\"\n                                    duration: Appearance.anim.durations.expressiveFastSpatial\n                                    easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                                }\n                                Anim {\n                                    property: \"opacity\"\n                                    duration: Appearance.anim.durations.small\n                                }\n                            }\n                        }\n                    },\n                    Transition {\n                        from: \"visible\"\n\n                        SequentialAnimation {\n                            PauseAnimation {\n                                duration: fabMenuItem.index * Appearance.anim.durations.small / 8\n                            }\n                            ParallelAnimation {\n                                Anim {\n                                    property: \"implicitWidth\"\n                                    duration: Appearance.anim.durations.expressiveFastSpatial\n                                    easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                                }\n                                Anim {\n                                    property: \"opacity\"\n                                    duration: Appearance.anim.durations.small\n                                }\n                            }\n                        }\n                    }\n                ]\n\n                StateLayer {\n                    function onClicked(): void {\n                        root.session.bt.fabMenuOpen = false;\n\n                        const name = fabMenuItem.modelData.name;\n                        if (fabMenuItem.modelData.name !== \"pair\")\n                            root.device[`${name}ed`] = !root.device[`${name}ed`];\n                        else if (root.device.paired)\n                            root.device.forget();\n                        else\n                            root.device.pair();\n                    }\n                }\n\n                RowLayout {\n                    id: fabMenuItemInner\n\n                    anchors.centerIn: parent\n                    spacing: Appearance.spacing.normal\n                    opacity: 0\n\n                    MaterialIcon {\n                        text: fabMenuItem.modelData.icon\n                        color: Colours.palette.m3onPrimaryContainer\n                        fill: 1\n                    }\n\n                    StyledText {\n                        animate: true\n                        text: (root.device && root.device[`${fabMenuItem.modelData.name}ed`] ? fabMenuItem.modelData.name === \"connect\" ? \"dis\" : \"un\" : \"\") + fabMenuItem.modelData.name\n                        color: Colours.palette.m3onPrimaryContainer\n                        font.capitalization: Font.Capitalize\n                        Layout.preferredWidth: implicitWidth\n\n                        Behavior on Layout.preferredWidth {\n                            Anim {\n                                duration: Appearance.anim.durations.small\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    Item {\n        id: fabRoot\n\n        x: root.contentX + root.width - width\n        y: root.contentY + root.height - height\n        width: 64\n        height: 64\n        z: 10000\n\n        StyledRect {\n            id: fabBg\n\n            anchors.right: parent.right\n            anchors.top: parent.top\n\n            implicitWidth: 64\n            implicitHeight: 64\n\n            radius: Appearance.rounding.normal\n            color: root.session.bt.fabMenuOpen ? Colours.palette.m3primary : Colours.palette.m3primaryContainer\n\n            states: State {\n                name: \"expanded\"\n                when: root.session.bt.fabMenuOpen\n\n                PropertyChanges {\n                    fabBg.implicitWidth: 48\n                    fabBg.implicitHeight: 48\n                    fabBg.radius: 48 / 2\n                    fab.font.pointSize: Appearance.font.size.larger\n                }\n            }\n\n            transitions: Transition {\n                Anim {\n                    properties: \"implicitWidth,implicitHeight\"\n                    duration: Appearance.anim.durations.expressiveFastSpatial\n                    easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                }\n                Anim {\n                    properties: \"radius,font.pointSize\"\n                }\n            }\n\n            Elevation {\n                anchors.fill: parent\n                radius: parent.radius\n                z: -1\n                level: fabState.containsMouse && !fabState.pressed ? 4 : 3\n            }\n\n            StateLayer {\n                id: fabState\n\n                function onClicked(): void {\n                    root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen;\n                }\n\n                color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer\n            }\n\n            MaterialIcon {\n                id: fab\n\n                anchors.centerIn: parent\n                animate: true\n                text: root.session.bt.fabMenuOpen ? \"close\" : \"settings\"\n                color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer\n                font.pointSize: Appearance.font.size.large\n                fill: 1\n            }\n        }\n    }\n\n    component Toggle: RowLayout {\n        required property string label\n        property alias checked: toggle.checked\n        property alias toggle: toggle\n\n        Layout.fillWidth: true\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            Layout.fillWidth: true\n            text: parent.label\n        }\n\n        StyledSwitch {\n            id: toggle\n\n            cLayer: 2\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/bluetooth/DeviceList.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport Quickshell.Bluetooth\nimport QtQuick\nimport QtQuick.Layouts\n\nDeviceList {\n    id: root\n\n    required property Session session\n    readonly property bool smallDiscoverable: width <= 540\n    readonly property bool smallPairable: width <= 480\n\n    title: qsTr(\"Devices (%1)\").arg(Bluetooth.devices.values.length)\n    description: qsTr(\"All available bluetooth devices\")\n    activeItem: session.bt.active\n\n    model: ScriptModel {\n        id: deviceModel\n\n        values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name))\n    }\n\n    headerComponent: Component {\n        RowLayout {\n            spacing: Appearance.spacing.smaller\n\n            StyledText {\n                text: qsTr(\"Bluetooth\")\n                font.pointSize: Appearance.font.size.large\n                font.weight: 500\n            }\n\n            Item {\n                Layout.fillWidth: true\n            }\n\n            ToggleButton {\n                toggled: Bluetooth.defaultAdapter?.enabled ?? false\n                icon: \"power\"\n                accent: \"Tertiary\"\n                iconSize: Appearance.font.size.normal\n                horizontalPadding: Appearance.padding.normal\n                verticalPadding: Appearance.padding.smaller\n                tooltip: qsTr(\"Toggle Bluetooth\")\n\n                onClicked: {\n                    const adapter = Bluetooth.defaultAdapter;\n                    if (adapter)\n                        adapter.enabled = !adapter.enabled;\n                }\n            }\n\n            ToggleButton {\n                toggled: Bluetooth.defaultAdapter?.discoverable ?? false\n                icon: root.smallDiscoverable ? \"group_search\" : \"\"\n                label: root.smallDiscoverable ? \"\" : qsTr(\"Discoverable\")\n                iconSize: Appearance.font.size.normal\n                horizontalPadding: Appearance.padding.normal\n                verticalPadding: Appearance.padding.smaller\n                tooltip: qsTr(\"Make discoverable\")\n\n                onClicked: {\n                    const adapter = Bluetooth.defaultAdapter;\n                    if (adapter)\n                        adapter.discoverable = !adapter.discoverable;\n                }\n            }\n\n            ToggleButton {\n                toggled: Bluetooth.defaultAdapter?.pairable ?? false\n                icon: \"missing_controller\"\n                label: root.smallPairable ? \"\" : qsTr(\"Pairable\")\n                iconSize: Appearance.font.size.normal\n                horizontalPadding: Appearance.padding.normal\n                verticalPadding: Appearance.padding.smaller\n                tooltip: qsTr(\"Make pairable\")\n\n                onClicked: {\n                    const adapter = Bluetooth.defaultAdapter;\n                    if (adapter)\n                        adapter.pairable = !adapter.pairable;\n                }\n            }\n\n            ToggleButton {\n                toggled: Bluetooth.defaultAdapter?.discovering ?? false\n                icon: \"bluetooth_searching\"\n                accent: \"Secondary\"\n                iconSize: Appearance.font.size.normal\n                horizontalPadding: Appearance.padding.normal\n                verticalPadding: Appearance.padding.smaller\n                tooltip: qsTr(\"Scan for devices\")\n\n                onClicked: {\n                    const adapter = Bluetooth.defaultAdapter;\n                    if (adapter)\n                        adapter.discovering = !adapter.discovering;\n                }\n            }\n\n            ToggleButton {\n                toggled: !root.session.bt.active\n                icon: \"settings\"\n                accent: \"Primary\"\n                iconSize: Appearance.font.size.normal\n                horizontalPadding: Appearance.padding.normal\n                verticalPadding: Appearance.padding.smaller\n                tooltip: qsTr(\"Bluetooth settings\")\n\n                onClicked: {\n                    if (root.session.bt.active)\n                        root.session.bt.active = null;\n                    else {\n                        root.session.bt.active = root.model.values[0] ?? null;\n                    }\n                }\n            }\n        }\n    }\n\n    delegate: Component {\n        StyledRect {\n            id: device\n\n            required property BluetoothDevice modelData\n            readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting)\n            readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected\n\n            width: ListView.view ? ListView.view.width : undefined\n            implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2\n\n            color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)\n            radius: Appearance.rounding.normal\n\n            StateLayer {\n                id: stateLayer\n\n                function onClicked(): void {\n                    if (device.modelData)\n                        root.session.bt.active = device.modelData;\n                }\n            }\n\n            RowLayout {\n                id: deviceInner\n\n                anchors.fill: parent\n                anchors.margins: Appearance.padding.normal\n\n                spacing: Appearance.spacing.normal\n\n                StyledRect {\n                    implicitWidth: implicitHeight\n                    implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2\n\n                    radius: Appearance.rounding.normal\n                    color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh\n\n                    StyledRect {\n                        anchors.fill: parent\n                        radius: parent.radius\n                        color: Qt.alpha(device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0)\n                    }\n\n                    MaterialIcon {\n                        id: icon\n\n                        anchors.centerIn: parent\n                        text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : \"\")\n                        color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n                        font.pointSize: Appearance.font.size.large\n                        fill: device.connected ? 1 : 0\n\n                        Behavior on fill {\n                            Anim {}\n                        }\n                    }\n                }\n\n                ColumnLayout {\n                    Layout.fillWidth: true\n\n                    spacing: 0\n\n                    StyledText {\n                        Layout.fillWidth: true\n                        text: device.modelData ? device.modelData.name : qsTr(\"Unknown\")\n                        elide: Text.ElideRight\n                    }\n\n                    StyledText {\n                        Layout.fillWidth: true\n                        text: (device.modelData ? device.modelData.address : \"\") + (device.connected ? qsTr(\" (Connected)\") : (device.modelData && device.modelData.bonded) ? qsTr(\" (Paired)\") : \"\")\n                        color: Colours.palette.m3outline\n                        font.pointSize: Appearance.font.size.small\n                        elide: Text.ElideRight\n                    }\n                }\n\n                StyledRect {\n                    id: connectBtn\n\n                    implicitWidth: implicitHeight\n                    implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2\n\n                    radius: Appearance.rounding.full\n                    color: Qt.alpha(Colours.palette.m3primaryContainer, device.connected ? 1 : 0)\n\n                    CircularIndicator {\n                        anchors.fill: parent\n                        running: device.loading\n                    }\n\n                    StateLayer {\n                        function onClicked(): void {\n                            if (device.loading)\n                                return;\n\n                            if (device.connected) {\n                                device.modelData.connected = false;\n                            } else {\n                                if (device.modelData.bonded) {\n                                    device.modelData.connected = true;\n                                } else {\n                                    device.modelData.pair();\n                                }\n                            }\n                        }\n\n                        color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface\n                        disabled: device.loading\n                    }\n\n                    MaterialIcon {\n                        id: connectIcon\n\n                        anchors.centerIn: parent\n                        animate: true\n                        text: device.connected ? \"link_off\" : \"link\"\n                        color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface\n\n                        opacity: device.loading ? 0 : 1\n\n                        Behavior on opacity {\n                            Anim {}\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    onItemSelected: item => session.bt.active = item\n}\n"
  },
  {
    "path": "modules/controlcenter/bluetooth/Settings.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport Quickshell.Bluetooth\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property Session session\n\n    spacing: Appearance.spacing.normal\n\n    SettingsHeader {\n        icon: \"bluetooth\"\n        title: qsTr(\"Bluetooth Settings\")\n    }\n\n    StyledText {\n        Layout.topMargin: Appearance.spacing.large\n        text: qsTr(\"Adapter status\")\n        font.pointSize: Appearance.font.size.larger\n        font.weight: 500\n    }\n\n    StyledText {\n        text: qsTr(\"General adapter settings\")\n        color: Colours.palette.m3outline\n    }\n\n    StyledRect {\n        Layout.fillWidth: true\n        implicitHeight: adapterStatus.implicitHeight + Appearance.padding.large * 2\n\n        radius: Appearance.rounding.normal\n        color: Colours.tPalette.m3surfaceContainer\n\n        ColumnLayout {\n            id: adapterStatus\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.margins: Appearance.padding.large\n\n            spacing: Appearance.spacing.larger\n\n            Toggle {\n                label: qsTr(\"Powered\")\n                checked: Bluetooth.defaultAdapter?.enabled ?? false\n                toggle.onToggled: {\n                    const adapter = Bluetooth.defaultAdapter;\n                    if (adapter)\n                        adapter.enabled = checked;\n                }\n            }\n\n            Toggle {\n                label: qsTr(\"Discoverable\")\n                checked: Bluetooth.defaultAdapter?.discoverable ?? false\n                toggle.onToggled: {\n                    const adapter = Bluetooth.defaultAdapter;\n                    if (adapter)\n                        adapter.discoverable = checked;\n                }\n            }\n\n            Toggle {\n                label: qsTr(\"Pairable\")\n                checked: Bluetooth.defaultAdapter?.pairable ?? false\n                toggle.onToggled: {\n                    const adapter = Bluetooth.defaultAdapter;\n                    if (adapter)\n                        adapter.pairable = checked;\n                }\n            }\n        }\n    }\n\n    StyledText {\n        Layout.topMargin: Appearance.spacing.large\n        text: qsTr(\"Adapter properties\")\n        font.pointSize: Appearance.font.size.larger\n        font.weight: 500\n    }\n\n    StyledText {\n        text: qsTr(\"Per-adapter settings\")\n        color: Colours.palette.m3outline\n    }\n\n    StyledRect {\n        Layout.fillWidth: true\n        implicitHeight: adapterSettings.implicitHeight + Appearance.padding.large * 2\n\n        radius: Appearance.rounding.normal\n        color: Colours.tPalette.m3surfaceContainer\n\n        ColumnLayout {\n            id: adapterSettings\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.margins: Appearance.padding.large\n\n            spacing: Appearance.spacing.larger\n\n            RowLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.normal\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: qsTr(\"Current adapter\")\n                }\n\n                Item {\n                    id: adapterPickerButton\n\n                    property bool expanded\n\n                    implicitWidth: adapterPicker.implicitWidth + Appearance.padding.normal * 2\n                    implicitHeight: adapterPicker.implicitHeight + Appearance.padding.smaller * 2\n\n                    StateLayer {\n                        function onClicked(): void {\n                            adapterPickerButton.expanded = !adapterPickerButton.expanded;\n                        }\n\n                        radius: Appearance.rounding.small\n                    }\n\n                    RowLayout {\n                        id: adapterPicker\n\n                        anchors.fill: parent\n                        anchors.margins: Appearance.padding.normal\n                        anchors.topMargin: Appearance.padding.smaller\n                        anchors.bottomMargin: Appearance.padding.smaller\n                        spacing: Appearance.spacing.normal\n\n                        StyledText {\n                            Layout.leftMargin: Appearance.padding.small\n                            text: Bluetooth.defaultAdapter?.name ?? qsTr(\"None\")\n                        }\n\n                        MaterialIcon {\n                            text: \"expand_more\"\n                        }\n                    }\n\n                    Elevation {\n                        anchors.fill: adapterListBg\n                        radius: adapterListBg.radius\n                        opacity: adapterPickerButton.expanded ? 1 : 0\n                        scale: adapterPickerButton.expanded ? 1 : 0.7\n                        level: 2\n\n                        Behavior on opacity {\n                            Anim {}\n                        }\n\n                        Behavior on scale {\n                            Anim {\n                                duration: Appearance.anim.durations.expressiveFastSpatial\n                                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                            }\n                        }\n                    }\n\n                    StyledClippingRect {\n                        id: adapterListBg\n\n                        anchors.left: parent.left\n                        anchors.right: parent.right\n                        anchors.bottom: parent.bottom\n                        implicitHeight: adapterPickerButton.expanded ? adapterList.implicitHeight : adapterPickerButton.implicitHeight\n\n                        color: Colours.palette.m3secondaryContainer\n                        radius: Appearance.rounding.small\n                        opacity: adapterPickerButton.expanded ? 1 : 0\n                        scale: adapterPickerButton.expanded ? 1 : 0.7\n\n                        ColumnLayout {\n                            id: adapterList\n\n                            anchors.left: parent.left\n                            anchors.right: parent.right\n                            anchors.verticalCenter: parent.verticalCenter\n\n                            spacing: 0\n\n                            Repeater {\n                                model: Bluetooth.adapters\n\n                                Item {\n                                    id: adapter\n\n                                    required property BluetoothAdapter modelData\n\n                                    Layout.fillWidth: true\n                                    implicitHeight: adapterInner.implicitHeight + Appearance.padding.normal * 2\n\n                                    StateLayer {\n                                        function onClicked(): void {\n                                            adapterPickerButton.expanded = false;\n                                            root.session.bt.currentAdapter = adapter.modelData;\n                                        }\n\n                                        disabled: !adapterPickerButton.expanded\n                                    }\n\n                                    RowLayout {\n                                        id: adapterInner\n\n                                        anchors.left: parent.left\n                                        anchors.right: parent.right\n                                        anchors.verticalCenter: parent.verticalCenter\n                                        anchors.margins: Appearance.padding.normal\n                                        spacing: Appearance.spacing.normal\n\n                                        StyledText {\n                                            Layout.fillWidth: true\n                                            Layout.leftMargin: Appearance.padding.small\n                                            text: adapter.modelData.name\n                                            color: Colours.palette.m3onSecondaryContainer\n                                        }\n\n                                        MaterialIcon {\n                                            text: \"check\"\n                                            color: Colours.palette.m3onSecondaryContainer\n                                            visible: adapter.modelData === root.session.bt.currentAdapter\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        Behavior on opacity {\n                            Anim {}\n                        }\n\n                        Behavior on scale {\n                            Anim {\n                                duration: Appearance.anim.durations.expressiveFastSpatial\n                                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                            }\n                        }\n\n                        Behavior on implicitHeight {\n                            Anim {\n                                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                            }\n                        }\n                    }\n                }\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.normal\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: qsTr(\"Discoverable timeout\")\n                }\n\n                CustomSpinBox {\n                    min: 0\n                    value: root.session.bt.currentAdapter?.discoverableTimeout ?? 0\n                    onValueModified: value => {\n                        if (root.session.bt.currentAdapter) {\n                            root.session.bt.currentAdapter.discoverableTimeout = value;\n                        }\n                    }\n                }\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.small\n\n                Item {\n                    id: renameAdapter\n\n                    Layout.fillWidth: true\n                    Layout.rightMargin: Appearance.spacing.small\n\n                    implicitHeight: renameLabel.implicitHeight + adapterNameEdit.implicitHeight\n\n                    states: State {\n                        name: \"editingAdapterName\"\n                        when: root.session.bt.editingAdapterName\n\n                        AnchorChanges {\n                            target: adapterNameEdit\n                            anchors.top: renameAdapter.top\n                        }\n                        PropertyChanges {\n                            renameAdapter.implicitHeight: adapterNameEdit.implicitHeight\n                            renameLabel.opacity: 0\n                            adapterNameEdit.padding: Appearance.padding.normal\n                        }\n                    }\n\n                    transitions: Transition {\n                        AnchorAnimation {\n                            duration: Appearance.anim.durations.normal\n                            easing.type: Easing.BezierSpline\n                            easing.bezierCurve: Appearance.anim.curves.standard\n                        }\n                        Anim {\n                            properties: \"implicitHeight,opacity,padding\"\n                        }\n                    }\n\n                    StyledText {\n                        id: renameLabel\n\n                        anchors.left: parent.left\n\n                        text: qsTr(\"Rename adapter (currently does not work)\")\n                        color: Colours.palette.m3outline\n                        font.pointSize: Appearance.font.size.small\n                    }\n\n                    StyledTextField {\n                        id: adapterNameEdit\n\n                        anchors.left: parent.left\n                        anchors.right: parent.right\n                        anchors.top: renameLabel.bottom\n                        anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Appearance.padding.normal\n\n                        text: root.session.bt.currentAdapter?.name ?? \"\"\n                        readOnly: !root.session.bt.editingAdapterName\n                        onAccepted: {\n                            root.session.bt.editingAdapterName = false;\n                        }\n\n                        leftPadding: Appearance.padding.normal\n                        rightPadding: Appearance.padding.normal\n\n                        background: StyledRect {\n                            radius: Appearance.rounding.small\n                            border.width: 2\n                            border.color: Colours.palette.m3primary\n                            opacity: root.session.bt.editingAdapterName ? 1 : 0\n\n                            Behavior on border.color {\n                                CAnim {}\n                            }\n\n                            Behavior on opacity {\n                                Anim {}\n                            }\n                        }\n\n                        Behavior on anchors.leftMargin {\n                            Anim {}\n                        }\n                    }\n                }\n\n                StyledRect {\n                    implicitWidth: implicitHeight\n                    implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2\n\n                    radius: Appearance.rounding.small\n                    color: Colours.palette.m3secondaryContainer\n                    opacity: root.session.bt.editingAdapterName ? 1 : 0\n                    scale: root.session.bt.editingAdapterName ? 1 : 0.5\n\n                    StateLayer {\n                        function onClicked(): void {\n                            root.session.bt.editingAdapterName = false;\n                            adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? \"\");\n                        }\n\n                        color: Colours.palette.m3onSecondaryContainer\n                        disabled: !root.session.bt.editingAdapterName\n                    }\n\n                    MaterialIcon {\n                        id: cancelEditIcon\n\n                        anchors.centerIn: parent\n                        animate: true\n                        text: \"cancel\"\n                        color: Colours.palette.m3onSecondaryContainer\n                    }\n\n                    Behavior on opacity {\n                        Anim {}\n                    }\n\n                    Behavior on scale {\n                        Anim {\n                            duration: Appearance.anim.durations.expressiveFastSpatial\n                            easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                        }\n                    }\n                }\n\n                StyledRect {\n                    implicitWidth: implicitHeight\n                    implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2\n\n                    radius: root.session.bt.editingAdapterName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)\n                    color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0)\n\n                    StateLayer {\n                        function onClicked(): void {\n                            root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName;\n                            if (root.session.bt.editingAdapterName)\n                                adapterNameEdit.forceActiveFocus();\n                            else\n                                adapterNameEdit.accepted();\n                        }\n\n                        color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n                    }\n\n                    MaterialIcon {\n                        id: editIcon\n\n                        anchors.centerIn: parent\n                        animate: true\n                        text: root.session.bt.editingAdapterName ? \"check_circle\" : \"edit\"\n                        color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n                    }\n\n                    Behavior on radius {\n                        Anim {}\n                    }\n                }\n            }\n        }\n    }\n\n    StyledText {\n        Layout.topMargin: Appearance.spacing.large\n        text: qsTr(\"Adapter information\")\n        font.pointSize: Appearance.font.size.larger\n        font.weight: 500\n    }\n\n    StyledText {\n        text: qsTr(\"Information about the default adapter\")\n        color: Colours.palette.m3outline\n    }\n\n    StyledRect {\n        Layout.fillWidth: true\n        implicitHeight: adapterInfo.implicitHeight + Appearance.padding.large * 2\n\n        radius: Appearance.rounding.normal\n        color: Colours.tPalette.m3surfaceContainer\n\n        ColumnLayout {\n            id: adapterInfo\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.margins: Appearance.padding.large\n\n            spacing: Appearance.spacing.small / 2\n\n            StyledText {\n                text: qsTr(\"Adapter state\")\n            }\n\n            StyledText {\n                text: Bluetooth.defaultAdapter ? BluetoothAdapterState.toString(Bluetooth.defaultAdapter.state) : qsTr(\"Unknown\")\n                color: Colours.palette.m3outline\n                font.pointSize: Appearance.font.size.small\n            }\n\n            StyledText {\n                Layout.topMargin: Appearance.spacing.normal\n                text: qsTr(\"Dbus path\")\n            }\n\n            StyledText {\n                text: Bluetooth.defaultAdapter?.dbusPath ?? \"\"\n                color: Colours.palette.m3outline\n                font.pointSize: Appearance.font.size.small\n            }\n\n            StyledText {\n                Layout.topMargin: Appearance.spacing.normal\n                text: qsTr(\"Adapter id\")\n            }\n\n            StyledText {\n                text: Bluetooth.defaultAdapter?.adapterId ?? \"\"\n                color: Colours.palette.m3outline\n                font.pointSize: Appearance.font.size.small\n            }\n        }\n    }\n\n    component Toggle: RowLayout {\n        required property string label\n        property alias checked: toggle.checked\n        property alias toggle: toggle\n\n        Layout.fillWidth: true\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            Layout.fillWidth: true\n            text: parent.label\n        }\n\n        StyledSwitch {\n            id: toggle\n\n            cLayer: 2\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/components/ConnectedButtonGroup.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    property var options: [] // Array of {label: string, propertyName: string, onToggled: function, state: bool?}\n    property var rootItem: null // The root item that contains the properties we want to bind to\n    property string title: \"\" // Optional title text\n    property int rows: 1 // Number of rows\n\n    Layout.fillWidth: true\n    implicitHeight: layout.implicitHeight + Appearance.padding.large * 2\n    radius: Appearance.rounding.normal\n    color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n    clip: true\n\n    Behavior on implicitHeight {\n        Anim {}\n    }\n\n    ColumnLayout {\n        id: layout\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.large\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            visible: root.title !== \"\"\n            text: root.title\n            font.pointSize: Appearance.font.size.normal\n        }\n\n        GridLayout {\n            id: buttonGrid\n\n            Layout.alignment: Qt.AlignHCenter\n            rowSpacing: Appearance.spacing.small\n            columnSpacing: Appearance.spacing.small\n            rows: root.rows\n            columns: Math.ceil(root.options.length / root.rows)\n\n            Repeater {\n                id: repeater\n\n                model: root.options\n\n                delegate: TextButton {\n                    id: button\n\n                    required property int index\n                    required property var modelData\n\n                    property bool _checked: false\n\n                    Layout.fillWidth: true\n                    text: modelData.label\n                    checked: _checked\n                    toggle: false\n                    type: TextButton.Tonal\n\n                    // Create binding in Component.onCompleted\n                    Component.onCompleted: {\n                        if (button.modelData.state !== undefined && button.modelData.state) {\n                            _checked = button.modelData.state;\n                        } else if (root.rootItem && button.modelData.propertyName) {\n                            const propName = button.modelData.propertyName;\n                            const rootItem = root.rootItem;\n                            _checked = Qt.binding(function () {\n                                return rootItem[propName] ?? false;\n                            });\n                        }\n                    }\n\n                    // Match utilities Toggles radius styling\n                    // Each button has full rounding (not connected) since they have spacing\n                    radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal\n\n                    // Match utilities Toggles inactive color\n                    inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2)\n\n                    // Adjust width similar to utilities toggles\n                    Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0)\n\n                    onClicked: {\n                        if (button.modelData.onToggled && root.rootItem && button.modelData.propertyName) {\n                            const currentValue = root.rootItem[button.modelData.propertyName] ?? false;\n                            button.modelData.onToggled(!currentValue);\n                        }\n                    }\n\n                    Behavior on Layout.preferredWidth {\n                        Anim {\n                            duration: Appearance.anim.durations.expressiveFastSpatial\n                            easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                        }\n                    }\n\n                    Behavior on radius {\n                        Anim {\n                            duration: Appearance.anim.durations.expressiveFastSpatial\n                            easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/components/DeviceDetails.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    property Session session\n    property var device: null\n\n    property Component headerComponent: null\n    property list<Component> sections: []\n\n    property Component topContent: null\n    property Component bottomContent: null\n\n    implicitWidth: layout.implicitWidth\n    implicitHeight: layout.implicitHeight\n\n    ColumnLayout {\n        id: layout\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.top: parent.top\n        spacing: Appearance.spacing.normal\n\n        Loader {\n            id: headerLoader\n\n            Layout.fillWidth: true\n            asynchronous: true\n            sourceComponent: root.headerComponent\n            visible: root.headerComponent !== null\n        }\n\n        Loader {\n            id: topContentLoader\n\n            Layout.fillWidth: true\n            asynchronous: true\n            sourceComponent: root.topContent\n            visible: root.topContent !== null\n        }\n\n        Repeater {\n            model: root.sections\n\n            Loader {\n                required property Component modelData\n\n                Layout.fillWidth: true\n                asynchronous: true\n                sourceComponent: modelData\n            }\n        }\n\n        Loader {\n            id: bottomContentLoader\n\n            Layout.fillWidth: true\n            asynchronous: true\n            sourceComponent: root.bottomContent\n            visible: root.bottomContent !== null\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/components/DeviceList.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.components\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    property Session session: null\n    property var model: null\n    property Component delegate: null\n\n    property string title: \"\"\n    property string description: \"\"\n    property var activeItem: null\n    property Component headerComponent: null\n    property Component titleSuffix: null\n    property bool showHeader: true\n\n    signal itemSelected(var item)\n\n    spacing: Appearance.spacing.small\n\n    Loader {\n        id: headerLoader\n\n        Layout.fillWidth: true\n        asynchronous: true\n        sourceComponent: root.headerComponent\n        visible: root.headerComponent !== null && root.showHeader\n    }\n\n    RowLayout {\n        Layout.fillWidth: true\n        Layout.topMargin: root.headerComponent ? 0 : 0\n        spacing: Appearance.spacing.small\n        visible: root.title !== \"\" || root.description !== \"\"\n\n        StyledText {\n            visible: root.title !== \"\"\n            text: root.title\n            font.pointSize: Appearance.font.size.large\n            font.weight: 500\n        }\n\n        Loader {\n            asynchronous: true\n            sourceComponent: root.titleSuffix\n            visible: root.titleSuffix !== null\n        }\n\n        Item {\n            Layout.fillWidth: true\n        }\n    }\n\n    property alias view: view\n\n    StyledText {\n        visible: root.description !== \"\"\n        Layout.fillWidth: true\n        text: root.description\n        color: Colours.palette.m3outline\n    }\n\n    StyledListView {\n        id: view\n\n        Layout.fillWidth: true\n        implicitHeight: contentHeight\n\n        model: root.model\n        delegate: root.delegate\n\n        spacing: Appearance.spacing.small / 2\n        interactive: false\n        clip: false\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/components/PaneTransition.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.config\nimport QtQuick\n\nSequentialAnimation {\n    id: root\n\n    required property Item target\n    property list<PropertyAction> propertyActions\n\n    property real scaleFrom: 1.0\n    property real scaleTo: 0.8\n    property real opacityFrom: 1.0\n    property real opacityTo: 0.0\n\n    ParallelAnimation {\n        NumberAnimation {\n            target: root.target\n            property: \"opacity\"\n            from: root.opacityFrom\n            to: root.opacityTo\n            duration: Appearance.anim.durations.normal / 2\n            easing.type: Easing.BezierSpline\n            easing.bezierCurve: Appearance.anim.curves.standardAccel\n        }\n\n        NumberAnimation {\n            target: root.target\n            property: \"scale\"\n            from: root.scaleFrom\n            to: root.scaleTo\n            duration: Appearance.anim.durations.normal / 2\n            easing.type: Easing.BezierSpline\n            easing.bezierCurve: Appearance.anim.curves.standardAccel\n        }\n    }\n\n    ScriptAction {\n        script: {\n            for (let i = 0; i < root.propertyActions.length; i++) {\n                const action = root.propertyActions[i];\n                if (action.target && action.property !== undefined) {\n                    action.target[action.property] = action.value;\n                }\n            }\n        }\n    }\n\n    ParallelAnimation {\n        NumberAnimation {\n            target: root.target\n            property: \"opacity\"\n            from: root.opacityTo\n            to: root.opacityFrom\n            duration: Appearance.anim.durations.normal / 2\n            easing.type: Easing.BezierSpline\n            easing.bezierCurve: Appearance.anim.curves.standardDecel\n        }\n\n        NumberAnimation {\n            target: root.target\n            property: \"scale\"\n            from: root.scaleTo\n            to: root.scaleFrom\n            duration: Appearance.anim.durations.normal / 2\n            easing.type: Easing.BezierSpline\n            easing.bezierCurve: Appearance.anim.curves.standardDecel\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/components/ReadonlySlider.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    property string label: \"\"\n    property real value: 0\n    property real from: 0\n    property real to: 100\n    property string suffix: \"\"\n    property bool readonly: false\n\n    spacing: Appearance.spacing.small\n\n    RowLayout {\n        Layout.fillWidth: true\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            visible: root.label !== \"\"\n            text: root.label\n            font.pointSize: Appearance.font.size.normal\n            color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface\n        }\n\n        Item {\n            Layout.fillWidth: true\n        }\n\n        MaterialIcon {\n            visible: root.readonly\n            text: \"lock\"\n            color: Colours.palette.m3outline\n            font.pointSize: Appearance.font.size.small\n        }\n\n        StyledText {\n            text: Math.round(root.value) + (root.suffix !== \"\" ? \" \" + root.suffix : \"\")\n            font.pointSize: Appearance.font.size.normal\n            color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface\n        }\n    }\n\n    StyledRect {\n        Layout.fillWidth: true\n        implicitHeight: Appearance.padding.normal\n        radius: Appearance.rounding.full\n        color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 1)\n        opacity: root.readonly ? 0.5 : 1.0\n\n        StyledRect {\n            anchors.left: parent.left\n            anchors.top: parent.top\n            anchors.bottom: parent.bottom\n            width: parent.width * ((root.value - root.from) / (root.to - root.from))\n            radius: parent.radius\n            color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3primary\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/components/SettingsHeader.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property string icon\n    required property string title\n\n    Layout.fillWidth: true\n    implicitHeight: column.implicitHeight\n\n    ColumnLayout {\n        id: column\n\n        anchors.centerIn: parent\n        spacing: Appearance.spacing.normal\n\n        MaterialIcon {\n            Layout.alignment: Qt.AlignHCenter\n            text: root.icon\n            font.pointSize: Appearance.font.size.extraLarge * 3\n            font.bold: true\n        }\n\n        StyledText {\n            Layout.alignment: Qt.AlignHCenter\n            text: root.title\n            font.pointSize: Appearance.font.size.large\n            font.bold: true\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/components/SliderInput.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    property string label: \"\"\n    property real value: 0\n    property real from: 0\n    property real to: 100\n    property real stepSize: 0\n    property var validator: null\n    property string suffix: \"\" // Optional suffix text (e.g., \"×\", \"px\")\n    property int decimals: 1 // Number of decimal places to show (default: 1)\n    property var formatValueFunction: null // Optional custom format function\n    property var parseValueFunction: null // Optional custom parse function\n    property bool _initialized: false\n\n    signal valueModified(real newValue)\n\n    function formatValue(val: real): string {\n        if (formatValueFunction) {\n            return formatValueFunction(val);\n        }\n        // Default format function\n        // Check if it's an IntValidator (IntValidator doesn't have a 'decimals' property)\n        if (validator && validator.bottom !== undefined && validator.decimals === undefined) {\n            return Math.round(val).toString();\n        }\n        // For DoubleValidator or no validator, use the decimals property\n        return val.toFixed(root.decimals);\n    }\n\n    function parseValue(text: string): real {\n        if (parseValueFunction) {\n            return parseValueFunction(text);\n        }\n        // Default parse function\n        if (validator && validator.bottom !== undefined) {\n            // Check if it's an integer validator\n            if (validator.top !== undefined && validator.top === Math.floor(validator.top)) {\n                return parseInt(text);\n            }\n        }\n        return parseFloat(text);\n    }\n\n    spacing: Appearance.spacing.small\n\n    Component.onCompleted: {\n        // Set initialized flag after a brief delay to allow component to fully load\n        Qt.callLater(() => {\n            _initialized = true;\n        });\n    }\n\n    // Update input field when value changes externally (slider is already bound)\n    onValueChanged: {\n        // Only update if component is initialized to avoid issues during creation\n        if (root._initialized && !inputField.hasFocus) {\n            inputField.text = root.formatValue(root.value);\n        }\n    }\n\n    RowLayout {\n        Layout.fillWidth: true\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            visible: root.label !== \"\"\n            text: root.label\n            font.pointSize: Appearance.font.size.normal\n        }\n\n        Item {\n            Layout.fillWidth: true\n        }\n\n        StyledInputField {\n            id: inputField\n\n            Layout.preferredWidth: 70\n            validator: root.validator\n\n            Component.onCompleted: {\n                // Initialize text without triggering valueModified signal\n                text = root.formatValue(root.value);\n            }\n\n            onTextEdited: text => {\n                if (hasFocus) {\n                    const val = root.parseValue(text);\n                    if (!isNaN(val)) {\n                        // Validate against validator bounds if available\n                        let isValid = true;\n                        if (root.validator) {\n                            if (root.validator.bottom !== undefined && val < root.validator.bottom) {\n                                isValid = false;\n                            }\n                            if (root.validator.top !== undefined && val > root.validator.top) {\n                                isValid = false;\n                            }\n                        }\n\n                        if (isValid) {\n                            root.valueModified(val);\n                        }\n                    }\n                }\n            }\n\n            onEditingFinished: {\n                const val = root.parseValue(text);\n                let isValid = true;\n                if (root.validator) {\n                    if (root.validator.bottom !== undefined && val < root.validator.bottom) {\n                        isValid = false;\n                    }\n                    if (root.validator.top !== undefined && val > root.validator.top) {\n                        isValid = false;\n                    }\n                }\n\n                if (isNaN(val) || !isValid) {\n                    text = root.formatValue(root.value);\n                }\n            }\n        }\n\n        StyledText {\n            visible: root.suffix !== \"\"\n            text: root.suffix\n            color: Colours.palette.m3outline\n            font.pointSize: Appearance.font.size.normal\n        }\n    }\n\n    StyledSlider {\n        id: slider\n\n        Layout.fillWidth: true\n        implicitHeight: Appearance.padding.normal * 3\n\n        from: root.from\n        to: root.to\n        stepSize: root.stepSize\n\n        // Use Binding to allow slider to move freely during dragging\n        Binding {\n            target: slider\n            property: \"value\"\n            value: root.value\n            when: !slider.pressed\n        }\n\n        onValueChanged: {\n            // Update input field text in real-time as slider moves during dragging\n            // Always update when slider value changes (during dragging or external updates)\n            if (!inputField.hasFocus) {\n                const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value;\n                inputField.text = root.formatValue(newValue);\n            }\n        }\n\n        onMoved: {\n            const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value;\n            root.valueModified(newValue);\n            if (!inputField.hasFocus) {\n                inputField.text = root.formatValue(newValue);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/components/SplitPaneLayout.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components.effects\nimport qs.config\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Layouts\n\nRowLayout {\n    id: root\n\n    property Component leftContent: null\n    property Component rightContent: null\n    property real leftWidthRatio: 0.4\n    property int leftMinimumWidth: 420\n    property var leftLoaderProperties: ({})\n    property var rightLoaderProperties: ({})\n    property alias leftLoader: leftLoader\n    property alias rightLoader: rightLoader\n\n    spacing: 0\n\n    Item {\n        id: leftPane\n\n        Layout.preferredWidth: Math.floor(parent.width * root.leftWidthRatio)\n        Layout.minimumWidth: root.leftMinimumWidth\n        Layout.fillHeight: true\n\n        ClippingRectangle {\n            id: leftClippingRect\n\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.normal\n            anchors.leftMargin: 0\n            anchors.rightMargin: Appearance.padding.normal / 2\n\n            radius: leftBorder.innerRadius\n            color: \"transparent\"\n\n            Loader {\n                id: leftLoader\n\n                anchors.fill: parent\n                anchors.margins: Appearance.padding.large + Appearance.padding.normal\n                anchors.leftMargin: Appearance.padding.large\n                anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2\n\n                asynchronous: true\n                sourceComponent: root.leftContent\n\n                Component.onCompleted: {\n                    for (const key in root.leftLoaderProperties) {\n                        leftLoader[key] = root.leftLoaderProperties[key];\n                    }\n                }\n            }\n        }\n\n        InnerBorder {\n            id: leftBorder\n\n            leftThickness: 0\n            rightThickness: Appearance.padding.normal / 2\n        }\n    }\n\n    Item {\n        id: rightPane\n\n        Layout.fillWidth: true\n        Layout.fillHeight: true\n\n        ClippingRectangle {\n            id: rightClippingRect\n\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.normal\n            anchors.leftMargin: 0\n            anchors.rightMargin: Appearance.padding.normal / 2\n\n            radius: rightBorder.innerRadius\n            color: \"transparent\"\n\n            Loader {\n                id: rightLoader\n\n                anchors.fill: parent\n                anchors.margins: Appearance.padding.large * 2\n\n                asynchronous: true\n                sourceComponent: root.rightContent\n\n                Component.onCompleted: {\n                    for (const key in root.rightLoaderProperties) {\n                        rightLoader[key] = root.rightLoaderProperties[key];\n                    }\n                }\n            }\n        }\n\n        InnerBorder {\n            id: rightBorder\n\n            leftThickness: Appearance.padding.normal / 2\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/components/SplitPaneWithDetails.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport QtQuick\n\nItem {\n    id: root\n\n    required property Component leftContent\n    required property Component rightDetailsComponent\n    required property Component rightSettingsComponent\n\n    property var activeItem: null\n    property var paneIdGenerator: function (item) {\n        return item ? String(item) : \"\";\n    }\n\n    property Component overlayComponent: null\n\n    SplitPaneLayout {\n        id: splitLayout\n\n        anchors.fill: parent\n\n        leftContent: root.leftContent\n\n        rightContent: Component {\n            Item {\n                id: rightPaneItem\n\n                property var pane: root.activeItem\n                property string paneId: root.paneIdGenerator(pane)\n                property Component targetComponent: root.rightSettingsComponent\n                property Component nextComponent: root.rightSettingsComponent\n\n                function getComponentForPane() {\n                    return pane ? root.rightDetailsComponent : root.rightSettingsComponent;\n                }\n\n                Component.onCompleted: {\n                    targetComponent = getComponentForPane();\n                    nextComponent = targetComponent;\n                }\n\n                Loader {\n                    id: rightLoader\n\n                    anchors.fill: parent\n\n                    asynchronous: true\n                    opacity: 1\n                    scale: 1\n                    transformOrigin: Item.Center\n\n                    clip: false\n                    sourceComponent: rightPaneItem.targetComponent\n                }\n\n                Behavior on paneId {\n                    PaneTransition {\n                        target: rightLoader\n                        propertyActions: [\n                            PropertyAction {\n                                target: rightPaneItem\n                                property: \"targetComponent\"\n                                value: rightPaneItem.nextComponent\n                            }\n                        ]\n                    }\n                }\n\n                onPaneChanged: {\n                    nextComponent = getComponentForPane();\n                    paneId = root.paneIdGenerator(pane);\n                }\n            }\n        }\n    }\n\n    Loader {\n        id: overlayLoader\n\n        anchors.fill: parent\n        asynchronous: true\n        z: 1000\n        sourceComponent: root.overlayComponent\n        active: root.overlayComponent !== null\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/components/WallpaperGrid.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.images\nimport qs.services\nimport qs.config\nimport QtQuick\n\nGridView {\n    id: root\n\n    required property Session session\n\n    readonly property int minCellWidth: 200 + Appearance.spacing.normal\n    readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth))\n\n    cellWidth: width / columnsCount\n    cellHeight: 140 + Appearance.spacing.normal\n\n    model: Wallpapers.list\n\n    clip: true\n\n    StyledScrollBar.vertical: StyledScrollBar {\n        flickable: root\n    }\n\n    delegate: Item {\n        id: wpDelegate\n\n        required property var modelData\n        required property int index\n        readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent\n        readonly property real itemMargin: Appearance.spacing.normal / 2\n        readonly property real itemRadius: Appearance.rounding.normal\n\n        width: root.cellWidth\n        height: root.cellHeight\n\n        StateLayer {\n            function onClicked(): void {\n                Wallpapers.setWallpaper(wpDelegate.modelData.path);\n            }\n\n            anchors.fill: parent\n            anchors.leftMargin: wpDelegate.itemMargin\n            anchors.rightMargin: wpDelegate.itemMargin\n            anchors.topMargin: wpDelegate.itemMargin\n            anchors.bottomMargin: wpDelegate.itemMargin\n            radius: wpDelegate.itemRadius\n        }\n\n        StyledClippingRect {\n            id: image\n\n            anchors.fill: parent\n            anchors.leftMargin: wpDelegate.itemMargin\n            anchors.rightMargin: wpDelegate.itemMargin\n            anchors.topMargin: wpDelegate.itemMargin\n            anchors.bottomMargin: wpDelegate.itemMargin\n            color: Colours.tPalette.m3surfaceContainer\n            radius: wpDelegate.itemRadius\n            antialiasing: true\n            layer.enabled: true\n            layer.smooth: true\n\n            CachingImage {\n                id: cachingImage\n\n                path: wpDelegate.modelData.path\n                anchors.fill: parent\n                fillMode: Image.PreserveAspectCrop\n                cache: true\n                visible: opacity > 0\n                antialiasing: true\n                smooth: true\n                sourceSize: Qt.size(width, height)\n\n                opacity: status === Image.Ready ? 1 : 0\n\n                Behavior on opacity {\n                    NumberAnimation {\n                        duration: 1000\n                        easing.type: Easing.OutQuad\n                    }\n                }\n            }\n\n            // Fallback if CachingImage fails to load\n            Image {\n                id: fallbackImage\n\n                anchors.fill: parent\n                source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? wpDelegate.modelData.path : \"\"\n                asynchronous: true\n                fillMode: Image.PreserveAspectCrop\n                cache: true\n                visible: opacity > 0\n                antialiasing: true\n                smooth: true\n                sourceSize: Qt.size(width, height)\n\n                opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0\n\n                Behavior on opacity {\n                    NumberAnimation {\n                        duration: 1000\n                        easing.type: Easing.OutQuad\n                    }\n                }\n            }\n\n            Timer {\n                id: fallbackTimer\n\n                property bool triggered: false\n\n                interval: 800\n                running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null\n                onTriggered: triggered = true\n            }\n\n            // Gradient overlay for filename\n            Rectangle {\n                id: filenameOverlay\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.bottom: parent.bottom\n\n                implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5\n                radius: 0\n\n                gradient: Gradient {\n                    GradientStop {\n                        position: 0.0\n                        color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0)\n                    }\n                    GradientStop {\n                        position: 0.3\n                        color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.7)\n                    }\n                    GradientStop {\n                        position: 0.6\n                        color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.9)\n                    }\n                    GradientStop {\n                        position: 1.0\n                        color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.95)\n                    }\n                }\n\n                opacity: 0\n\n                Behavior on opacity {\n                    NumberAnimation {\n                        duration: 1000\n                        easing.type: Easing.OutCubic\n                    }\n                }\n\n                Component.onCompleted: {\n                    opacity = 1;\n                }\n            }\n        }\n\n        Rectangle {\n            anchors.fill: parent\n            anchors.leftMargin: wpDelegate.itemMargin\n            anchors.rightMargin: wpDelegate.itemMargin\n            anchors.topMargin: wpDelegate.itemMargin\n            anchors.bottomMargin: wpDelegate.itemMargin\n            color: \"transparent\"\n            radius: wpDelegate.itemRadius + border.width\n            border.width: wpDelegate.isCurrent ? 2 : 0\n            border.color: Colours.palette.m3primary\n            antialiasing: true\n            smooth: true\n\n            Behavior on border.width {\n                NumberAnimation {\n                    duration: 150\n                    easing.type: Easing.OutQuad\n                }\n            }\n\n            MaterialIcon {\n                anchors.right: parent.right\n                anchors.top: parent.top\n                anchors.margins: Appearance.padding.small\n\n                visible: wpDelegate.isCurrent\n                text: \"check_circle\"\n                color: Colours.palette.m3primary\n                font.pointSize: Appearance.font.size.large\n            }\n        }\n\n        StyledText {\n            id: filenameText\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            anchors.bottom: parent.bottom\n            anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2\n            anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2\n            anchors.bottomMargin: Appearance.padding.normal\n\n            text: wpDelegate.modelData.name\n            font.pointSize: Appearance.font.size.smaller\n            font.weight: 500\n            color: wpDelegate.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface\n            elide: Text.ElideMiddle\n            maximumLineCount: 1\n            horizontalAlignment: Text.AlignHCenter\n\n            opacity: 0\n\n            Behavior on opacity {\n                NumberAnimation {\n                    duration: 1000\n                    easing.type: Easing.OutCubic\n                }\n            }\n\n            Component.onCompleted: {\n                opacity = 1;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/dashboard/DashboardPane.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.effects\nimport qs.components.containers\nimport qs.config\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property Session session\n\n    // General Settings\n    property bool enabled: Config.dashboard.enabled ?? true\n    property bool showOnHover: Config.dashboard.showOnHover ?? true\n    property int mediaUpdateInterval: Config.dashboard.mediaUpdateInterval ?? 1000\n    property int resourceUpdateInterval: Config.dashboard.resourceUpdateInterval ?? 1000\n    property int dragThreshold: Config.dashboard.dragThreshold ?? 50\n\n    // Dashboard Tabs\n    property bool showDashboard: Config.dashboard.showDashboard ?? true\n    property bool showMedia: Config.dashboard.showMedia ?? true\n    property bool showPerformance: Config.dashboard.showPerformance ?? true\n    property bool showWeather: Config.dashboard.showWeather ?? true\n\n    // Performance Resources\n    property bool showBattery: Config.dashboard.performance.showBattery ?? false\n    property bool showGpu: Config.dashboard.performance.showGpu ?? true\n    property bool showCpu: Config.dashboard.performance.showCpu ?? true\n    property bool showMemory: Config.dashboard.performance.showMemory ?? true\n    property bool showStorage: Config.dashboard.performance.showStorage ?? true\n    property bool showNetwork: Config.dashboard.performance.showNetwork ?? true\n\n    function saveConfig() {\n        Config.dashboard.enabled = root.enabled;\n        Config.dashboard.showOnHover = root.showOnHover;\n        Config.dashboard.mediaUpdateInterval = root.mediaUpdateInterval;\n        Config.dashboard.resourceUpdateInterval = root.resourceUpdateInterval;\n        Config.dashboard.dragThreshold = root.dragThreshold;\n        Config.dashboard.showDashboard = root.showDashboard;\n        Config.dashboard.showMedia = root.showMedia;\n        Config.dashboard.showPerformance = root.showPerformance;\n        Config.dashboard.showWeather = root.showWeather;\n        Config.dashboard.performance.showBattery = root.showBattery;\n        Config.dashboard.performance.showGpu = root.showGpu;\n        Config.dashboard.performance.showCpu = root.showCpu;\n        Config.dashboard.performance.showMemory = root.showMemory;\n        Config.dashboard.performance.showStorage = root.showStorage;\n        Config.dashboard.performance.showNetwork = root.showNetwork;\n        // Note: sizes properties are readonly and cannot be modified\n        Config.save();\n    }\n\n    anchors.fill: parent\n\n    ClippingRectangle {\n        id: dashboardClippingRect\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.normal\n        anchors.leftMargin: 0\n        anchors.rightMargin: Appearance.padding.normal\n\n        radius: dashboardBorder.innerRadius\n        color: \"transparent\"\n\n        Loader {\n            id: dashboardLoader\n\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.large + Appearance.padding.normal\n            anchors.leftMargin: Appearance.padding.large\n            anchors.rightMargin: Appearance.padding.large\n\n            asynchronous: true\n            sourceComponent: dashboardContentComponent\n        }\n    }\n\n    InnerBorder {\n        id: dashboardBorder\n\n        leftThickness: 0\n        rightThickness: Appearance.padding.normal\n    }\n\n    Component {\n        id: dashboardContentComponent\n\n        StyledFlickable {\n            id: dashboardFlickable\n\n            flickableDirection: Flickable.VerticalFlick\n            contentHeight: dashboardLayout.height\n\n            StyledScrollBar.vertical: StyledScrollBar {\n                flickable: dashboardFlickable\n            }\n\n            ColumnLayout {\n                id: dashboardLayout\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.top: parent.top\n\n                spacing: Appearance.spacing.normal\n\n                RowLayout {\n                    spacing: Appearance.spacing.smaller\n\n                    StyledText {\n                        text: qsTr(\"Dashboard\")\n                        font.pointSize: Appearance.font.size.large\n                        font.weight: 500\n                    }\n                }\n\n                // General Settings Section\n                GeneralSection {\n                    rootItem: root\n                }\n\n                // Performance Resources Section\n                PerformanceSection {\n                    rootItem: root\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/dashboard/GeneralSection.qml",
    "content": "import \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nSectionContainer {\n    id: root\n\n    required property var rootItem\n\n    Layout.fillWidth: true\n    alignTop: true\n\n    StyledText {\n        text: qsTr(\"General Settings\")\n        font.pointSize: Appearance.font.size.normal\n    }\n\n    SwitchRow {\n        label: qsTr(\"Enabled\")\n        checked: root.rootItem.enabled\n        onToggled: checked => {\n            root.rootItem.enabled = checked;\n            root.rootItem.saveConfig();\n        }\n    }\n\n    SwitchRow {\n        label: qsTr(\"Show on hover\")\n        checked: root.rootItem.showOnHover\n        onToggled: checked => {\n            root.rootItem.showOnHover = checked;\n            root.rootItem.saveConfig();\n        }\n    }\n\n    RowLayout {\n        Layout.fillWidth: true\n        spacing: Appearance.spacing.normal\n\n        SwitchRow {\n            Layout.fillWidth: true\n            label: qsTr(\"Show Dashboard tab\")\n            checked: root.rootItem.showDashboard\n            onToggled: checked => {\n                root.rootItem.showDashboard = checked;\n                root.rootItem.saveConfig();\n            }\n        }\n\n        SwitchRow {\n            Layout.fillWidth: true\n            label: qsTr(\"Show Media tab\")\n            checked: root.rootItem.showMedia\n            onToggled: checked => {\n                root.rootItem.showMedia = checked;\n                root.rootItem.saveConfig();\n            }\n        }\n\n        SwitchRow {\n            Layout.fillWidth: true\n            label: qsTr(\"Show Performance tab\")\n            checked: root.rootItem.showPerformance\n            onToggled: checked => {\n                root.rootItem.showPerformance = checked;\n                root.rootItem.saveConfig();\n            }\n        }\n\n        SwitchRow {\n            Layout.fillWidth: true\n            label: qsTr(\"Show Weather tab\")\n            checked: root.rootItem.showWeather\n            onToggled: checked => {\n                root.rootItem.showWeather = checked;\n                root.rootItem.saveConfig();\n            }\n        }\n    }\n\n    SliderInput {\n        Layout.fillWidth: true\n\n        label: qsTr(\"Media update interval\")\n        value: root.rootItem.mediaUpdateInterval\n        from: 100\n        to: 10000\n        stepSize: 100\n        suffix: \"ms\"\n        validator: IntValidator {\n            bottom: 100\n            top: 10000\n        }\n        formatValueFunction: val => Math.round(val).toString()\n        parseValueFunction: text => parseInt(text)\n\n        onValueModified: newValue => {\n            root.rootItem.mediaUpdateInterval = Math.round(newValue);\n            root.rootItem.saveConfig();\n        }\n    }\n\n    SliderInput {\n        Layout.fillWidth: true\n\n        label: qsTr(\"Drag threshold\")\n        value: root.rootItem.dragThreshold\n        from: 0\n        to: 100\n        suffix: \"px\"\n        validator: IntValidator {\n            bottom: 0\n            top: 100\n        }\n        formatValueFunction: val => Math.round(val).toString()\n        parseValueFunction: text => parseInt(text)\n\n        onValueModified: newValue => {\n            root.rootItem.dragThreshold = Math.round(newValue);\n            root.rootItem.saveConfig();\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/dashboard/PerformanceSection.qml",
    "content": "import \"../components\"\nimport QtQuick\nimport QtQuick.Layouts\nimport Quickshell.Services.UPower\nimport qs.components\nimport qs.config\nimport qs.services\n\nSectionContainer {\n    id: root\n\n    required property var rootItem\n    // GPU toggle is hidden when gpuType is \"NONE\" (no GPU data available)\n    readonly property bool gpuAvailable: SystemUsage.gpuType !== \"NONE\"\n    // Battery toggle is hidden when no laptop battery is present\n    readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery\n\n    Layout.fillWidth: true\n    alignTop: true\n\n    StyledText {\n        text: qsTr(\"Performance Resources\")\n        font.pointSize: Appearance.font.size.normal\n    }\n\n    ConnectedButtonGroup {\n        rootItem: root.rootItem\n        options: {\n            let opts = [];\n            if (root.batteryAvailable)\n                opts.push({\n                    \"label\": qsTr(\"Battery\"),\n                    \"propertyName\": \"showBattery\",\n                    \"onToggled\": function (checked) {\n                        root.rootItem.showBattery = checked;\n                        root.rootItem.saveConfig();\n                    }\n                });\n\n            if (root.gpuAvailable)\n                opts.push({\n                    \"label\": qsTr(\"GPU\"),\n                    \"propertyName\": \"showGpu\",\n                    \"onToggled\": function (checked) {\n                        root.rootItem.showGpu = checked;\n                        root.rootItem.saveConfig();\n                    }\n                });\n\n            opts.push({\n                \"label\": qsTr(\"CPU\"),\n                \"propertyName\": \"showCpu\",\n                \"onToggled\": function (checked) {\n                    root.rootItem.showCpu = checked;\n                    root.rootItem.saveConfig();\n                }\n            }, {\n                \"label\": qsTr(\"Memory\"),\n                \"propertyName\": \"showMemory\",\n                \"onToggled\": function (checked) {\n                    root.rootItem.showMemory = checked;\n                    root.rootItem.saveConfig();\n                }\n            }, {\n                \"label\": qsTr(\"Storage\"),\n                \"propertyName\": \"showStorage\",\n                \"onToggled\": function (checked) {\n                    root.rootItem.showStorage = checked;\n                    root.rootItem.saveConfig();\n                }\n            }, {\n                \"label\": qsTr(\"Network\"),\n                \"propertyName\": \"showNetwork\",\n                \"onToggled\": function (checked) {\n                    root.rootItem.showNetwork = checked;\n                    root.rootItem.saveConfig();\n                }\n            });\n            return opts;\n        }\n    }\n\n    SliderInput {\n        Layout.fillWidth: true\n\n        label: qsTr(\"Resource update interval\")\n        value: root.rootItem.resourceUpdateInterval\n        from: 100\n        to: 10000\n        stepSize: 100\n        suffix: \"ms\"\n        validator: IntValidator {\n            bottom: 100\n            top: 10000\n        }\n        formatValueFunction: val => Math.round(val).toString()\n        parseValueFunction: text => parseInt(text)\n\n        onValueModified: newValue => {\n            root.rootItem.resourceUpdateInterval = Math.round(newValue);\n            root.rootItem.saveConfig();\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/launcher/LauncherPane.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Caelestia\nimport Quickshell\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Layouts\nimport \"../../../utils/scripts/fuzzysort.js\" as Fuzzy\n\nItem {\n    id: root\n\n    required property Session session\n\n    property var selectedApp: root.session.launcher.active\n    property bool hideFromLauncherChecked: false\n    property bool favouriteChecked: false\n    property string searchText: \"\"\n    property list<var> filteredApps: []\n\n    function updateToggleState() {\n        if (!root.selectedApp) {\n            root.hideFromLauncherChecked = false;\n            root.favouriteChecked = false;\n            return;\n        }\n\n        const appId = root.selectedApp.id || root.selectedApp.entry?.id;\n\n        root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId);\n        root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId);\n    }\n\n    function saveHiddenApps(isHidden) {\n        if (!root.selectedApp) {\n            return;\n        }\n\n        const appId = root.selectedApp.id || root.selectedApp.entry?.id;\n\n        const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];\n\n        if (isHidden) {\n            if (!hiddenApps.includes(appId)) {\n                hiddenApps.push(appId);\n            }\n        } else {\n            const index = hiddenApps.indexOf(appId);\n            if (index !== -1) {\n                hiddenApps.splice(index, 1);\n            }\n        }\n\n        Config.launcher.hiddenApps = hiddenApps;\n        Config.save();\n    }\n\n    function filterApps(search: string): list<var> {\n        if (!search || search.trim() === \"\") {\n            const apps = [];\n            for (let i = 0; i < allAppsDb.apps.length; i++) {\n                apps.push(allAppsDb.apps[i]);\n            }\n            return apps;\n        }\n\n        if (!allAppsDb.apps || allAppsDb.apps.length === 0) {\n            return [];\n        }\n\n        const preparedApps = [];\n        for (let i = 0; i < allAppsDb.apps.length; i++) {\n            const app = allAppsDb.apps[i];\n            const name = app.name || app.entry?.name || \"\";\n            preparedApps.push({\n                _item: app,\n                name: Fuzzy.prepare(name)\n            });\n        }\n\n        const results = Fuzzy.go(search, preparedApps, {\n            all: true,\n            keys: [\"name\"],\n            scoreFn: r => r[0].score\n        });\n\n        return results.sort((a, b) => b._score - a._score).map(r => r.obj._item);\n    }\n\n    function updateFilteredApps() {\n        filteredApps = filterApps(searchText);\n    }\n\n    anchors.fill: parent\n\n    onSelectedAppChanged: {\n        session.launcher.active = selectedApp;\n        updateToggleState();\n    }\n\n    onSearchTextChanged: {\n        updateFilteredApps();\n    }\n\n    Component.onCompleted: {\n        updateFilteredApps();\n    }\n\n    Connections {\n        function onActiveChanged() {\n            root.selectedApp = root.session.launcher.active;\n            root.updateToggleState();\n        }\n\n        target: root.session.launcher\n    }\n\n    AppDb {\n        id: allAppsDb\n\n        path: `${Paths.state}/apps.sqlite`\n        favouriteApps: Config.launcher.favouriteApps\n        entries: DesktopEntries.applications.values\n    }\n\n    Connections {\n        function onAppsChanged() {\n            root.updateFilteredApps();\n        }\n\n        target: allAppsDb\n    }\n\n    SplitPaneLayout {\n        anchors.fill: parent\n\n        leftContent: Component {\n            ColumnLayout {\n                id: leftLauncherLayout\n\n                anchors.fill: parent\n\n                spacing: Appearance.spacing.small\n\n                RowLayout {\n                    spacing: Appearance.spacing.smaller\n\n                    StyledText {\n                        text: qsTr(\"Launcher\")\n                        font.pointSize: Appearance.font.size.large\n                        font.weight: 500\n                    }\n\n                    Item {\n                        Layout.fillWidth: true\n                    }\n\n                    ToggleButton {\n                        toggled: !root.session.launcher.active\n                        icon: \"settings\"\n                        accent: \"Primary\"\n                        iconSize: Appearance.font.size.normal\n                        horizontalPadding: Appearance.padding.normal\n                        verticalPadding: Appearance.padding.smaller\n                        tooltip: qsTr(\"Launcher settings\")\n\n                        onClicked: {\n                            if (root.session.launcher.active) {\n                                root.session.launcher.active = null;\n                            } else {\n                                if (root.filteredApps.length > 0) {\n                                    root.session.launcher.active = root.filteredApps[0];\n                                }\n                            }\n                        }\n                    }\n                }\n\n                StyledText {\n                    Layout.topMargin: Appearance.spacing.large\n                    text: qsTr(\"Applications (%1)\").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length)\n                    font.pointSize: Appearance.font.size.normal\n                    font.weight: 500\n                }\n\n                StyledText {\n                    text: qsTr(\"All applications available in the launcher\")\n                    color: Colours.palette.m3outline\n                }\n\n                StyledRect {\n                    Layout.fillWidth: true\n                    Layout.topMargin: Appearance.spacing.normal\n                    Layout.bottomMargin: Appearance.spacing.small\n\n                    color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n                    radius: Appearance.rounding.full\n\n                    implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight)\n\n                    MaterialIcon {\n                        id: searchIcon\n\n                        anchors.verticalCenter: parent.verticalCenter\n                        anchors.left: parent.left\n                        anchors.leftMargin: Appearance.padding.normal\n\n                        text: \"search\"\n                        color: Colours.palette.m3onSurfaceVariant\n                    }\n\n                    StyledTextField {\n                        id: searchField\n\n                        anchors.left: searchIcon.right\n                        anchors.right: clearIcon.left\n                        anchors.leftMargin: Appearance.spacing.small\n                        anchors.rightMargin: Appearance.spacing.small\n\n                        topPadding: Appearance.padding.normal\n                        bottomPadding: Appearance.padding.normal\n\n                        placeholderText: qsTr(\"Search applications...\")\n\n                        onTextChanged: {\n                            root.searchText = text;\n                        }\n                    }\n\n                    MaterialIcon {\n                        id: clearIcon\n\n                        anchors.verticalCenter: parent.verticalCenter\n                        anchors.right: parent.right\n                        anchors.rightMargin: Appearance.padding.normal\n\n                        width: searchField.text ? implicitWidth : implicitWidth / 2\n                        opacity: {\n                            if (!searchField.text)\n                                return 0;\n                            if (clearMouse.pressed)\n                                return 0.7;\n                            if (clearMouse.containsMouse)\n                                return 0.8;\n                            return 1;\n                        }\n\n                        text: \"close\"\n                        color: Colours.palette.m3onSurfaceVariant\n\n                        MouseArea {\n                            id: clearMouse\n\n                            anchors.fill: parent\n                            hoverEnabled: true\n                            cursorShape: searchField.text ? Qt.PointingHandCursor : undefined\n\n                            onClicked: searchField.text = \"\"\n                        }\n\n                        Behavior on width {\n                            Anim {\n                                duration: Appearance.anim.durations.small\n                            }\n                        }\n\n                        Behavior on opacity {\n                            Anim {\n                                duration: Appearance.anim.durations.small\n                            }\n                        }\n                    }\n                }\n\n                Loader {\n                    id: appsListLoader\n\n                    Layout.fillWidth: true\n                    Layout.fillHeight: true\n                    asynchronous: true\n                    active: true\n\n                    sourceComponent: StyledListView {\n                        id: appsListView\n\n                        Layout.fillWidth: true\n                        Layout.fillHeight: true\n\n                        model: root.filteredApps\n                        spacing: Appearance.spacing.small / 2\n                        clip: true\n\n                        StyledScrollBar.vertical: StyledScrollBar {\n                            flickable: appsListView\n                        }\n\n                        delegate: StyledRect {\n                            id: appDelegate\n\n                            required property var modelData\n\n                            readonly property bool isSelected: root.selectedApp === modelData\n\n                            width: parent ? parent.width : 0\n                            implicitHeight: 40\n\n                            color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : \"transparent\"\n                            radius: Appearance.rounding.normal\n\n                            opacity: 0\n\n                            Behavior on opacity {\n                                NumberAnimation {\n                                    duration: 1000\n                                    easing.type: Easing.OutCubic\n                                }\n                            }\n\n                            Component.onCompleted: {\n                                opacity = 1;\n                            }\n\n                            StateLayer {\n                                function onClicked(): void {\n                                    root.session.launcher.active = appDelegate.modelData;\n                                }\n                            }\n\n                            RowLayout {\n                                anchors.left: parent.left\n                                anchors.right: parent.right\n                                anchors.verticalCenter: parent.verticalCenter\n                                anchors.margins: Appearance.padding.normal\n\n                                spacing: Appearance.spacing.normal\n\n                                IconImage {\n                                    asynchronous: true\n                                    Layout.alignment: Qt.AlignVCenter\n                                    implicitSize: 32\n                                    source: {\n                                        const entry = appDelegate.modelData.entry;\n                                        return entry ? Quickshell.iconPath(entry.icon, \"image-missing\") : \"image-missing\";\n                                    }\n                                }\n\n                                StyledText {\n                                    Layout.fillWidth: true\n                                    text: appDelegate.modelData.name || appDelegate.modelData.entry?.name || qsTr(\"Unknown\")\n                                    font.pointSize: Appearance.font.size.normal\n                                }\n\n                                Loader {\n                                    readonly property bool isHidden: appDelegate.modelData ? Strings.testRegexList(Config.launcher.hiddenApps, appDelegate.modelData.id) : false\n                                    readonly property bool isFav: appDelegate.modelData ? Strings.testRegexList(Config.launcher.favouriteApps, appDelegate.modelData.id) : false\n\n                                    Layout.alignment: Qt.AlignVCenter\n                                    asynchronous: true\n                                    active: isHidden || isFav\n\n                                    sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null)\n                                }\n\n                                Component {\n                                    id: hiddenIcon\n\n                                    MaterialIcon {\n                                        text: \"visibility_off\"\n                                        fill: 1\n                                        color: Colours.palette.m3primary\n                                    }\n                                }\n\n                                Component {\n                                    id: favouriteIcon\n\n                                    MaterialIcon {\n                                        text: \"favorite\"\n                                        fill: 1\n                                        color: Colours.palette.m3primary\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        rightContent: Component {\n            Item {\n                id: rightLauncherPane\n\n                property var pane: root.session.launcher.active\n                property string paneId: pane ? (pane.id || pane.entry?.id || \"\") : \"\"\n                property Component targetComponent: settings\n                property Component nextComponent: settings\n                property var displayedApp: null\n\n                function getComponentForPane() {\n                    return pane ? appDetails : settings;\n                }\n\n                Component.onCompleted: {\n                    displayedApp = pane;\n                    targetComponent = getComponentForPane();\n                    nextComponent = targetComponent;\n                }\n\n                onPaneChanged: {\n                    nextComponent = getComponentForPane();\n                    paneId = pane ? (pane.id || pane.entry?.id || \"\") : \"\";\n                }\n\n                onDisplayedAppChanged: {\n                    if (displayedApp) {\n                        const appId = displayedApp.id || displayedApp.entry?.id;\n                        root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId);\n                        root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId);\n                    } else {\n                        root.hideFromLauncherChecked = false;\n                        root.favouriteChecked = false;\n                    }\n                }\n\n                Loader {\n                    id: rightLauncherLoader\n\n                    property var displayedApp: rightLauncherPane.displayedApp\n\n                    anchors.fill: parent\n\n                    asynchronous: true\n                    opacity: 1\n                    scale: 1\n                    transformOrigin: Item.Center\n                    clip: false\n\n                    sourceComponent: rightLauncherPane.targetComponent\n                    active: true\n\n                    onItemChanged: {\n                        if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) {\n                            rightLauncherPane.displayedApp = rightLauncherPane.pane;\n                        }\n                    }\n                }\n\n                Behavior on paneId {\n                    PaneTransition {\n                        target: rightLauncherLoader\n                        propertyActions: [\n                            PropertyAction {\n                                target: rightLauncherPane\n                                property: \"displayedApp\"\n                                value: rightLauncherPane.pane\n                            },\n                            PropertyAction {\n                                target: rightLauncherLoader\n                                property: \"active\"\n                                value: false\n                            },\n                            PropertyAction {\n                                target: rightLauncherPane\n                                property: \"targetComponent\"\n                                value: rightLauncherPane.nextComponent\n                            },\n                            PropertyAction {\n                                target: rightLauncherLoader\n                                property: \"active\"\n                                value: true\n                            }\n                        ]\n                    }\n                }\n            }\n        }\n    }\n\n    Component {\n        id: settings\n\n        StyledFlickable {\n            id: settingsFlickable\n\n            flickableDirection: Flickable.VerticalFlick\n            contentHeight: settingsInner.height\n\n            StyledScrollBar.vertical: StyledScrollBar {\n                flickable: settingsFlickable\n            }\n\n            Settings {\n                id: settingsInner\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.top: parent.top\n                session: root.session\n            }\n        }\n    }\n\n    Component {\n        id: appDetails\n\n        ColumnLayout {\n            id: appDetailsLayout\n\n            readonly property var displayedApp: parent?.displayedApp ?? null // qmllint disable missing-property\n\n            anchors.fill: parent\n            spacing: Appearance.spacing.normal\n\n            SettingsHeader {\n                Layout.leftMargin: Appearance.padding.large * 2\n                Layout.rightMargin: Appearance.padding.large * 2\n                Layout.topMargin: Appearance.padding.large * 2\n                visible: appDetailsLayout.displayedApp === null\n                icon: \"apps\"\n                title: qsTr(\"Launcher Applications\")\n            }\n\n            Item {\n                Layout.alignment: Qt.AlignHCenter\n                Layout.leftMargin: Appearance.padding.large * 2\n                Layout.rightMargin: Appearance.padding.large * 2\n                Layout.topMargin: Appearance.padding.large * 2\n                visible: appDetailsLayout.displayedApp !== null\n                implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth)\n                implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight\n\n                ColumnLayout {\n                    anchors.centerIn: parent\n                    spacing: Appearance.spacing.normal\n\n                    IconImage {\n                        id: appIconImage\n\n                        asynchronous: true\n                        Layout.alignment: Qt.AlignHCenter\n                        implicitSize: Appearance.font.size.extraLarge * 3 * 2\n                        source: {\n                            const app = appDetailsLayout.displayedApp;\n                            if (!app)\n                                return \"image-missing\";\n                            const entry = app.entry;\n                            if (entry && entry.icon) {\n                                return Quickshell.iconPath(entry.icon, \"image-missing\");\n                            }\n                            return \"image-missing\";\n                        }\n                    }\n\n                    StyledText {\n                        id: appTitleText\n\n                        Layout.alignment: Qt.AlignHCenter\n                        text: appDetailsLayout.displayedApp.displayedApp ? (appDetailsLayout.displayedApp.displayedApp.displayedApp.name || appDetailsLayout.displayedApp.displayedApp.displayedApp.entry?.name || qsTr(\"Application Details\")) : \"\"\n                        font.pointSize: Appearance.font.size.large\n                        font.bold: true\n                    }\n                }\n            }\n\n            Item {\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n                Layout.topMargin: Appearance.spacing.large\n                Layout.leftMargin: Appearance.padding.large * 2\n                Layout.rightMargin: Appearance.padding.large * 2\n\n                StyledFlickable {\n                    id: detailsFlickable\n\n                    anchors.fill: parent\n                    flickableDirection: Flickable.VerticalFlick\n                    contentHeight: debugLayout.height\n\n                    StyledScrollBar.vertical: StyledScrollBar {\n                        flickable: parent\n                    }\n\n                    ColumnLayout {\n                        id: debugLayout\n\n                        anchors.left: parent.left\n                        anchors.right: parent.right\n                        anchors.top: parent.top\n                        spacing: Appearance.spacing.normal\n\n                        SwitchRow {\n                            Layout.topMargin: Appearance.spacing.normal\n                            visible: appDetailsLayout.displayedApp !== null\n                            label: qsTr(\"Mark as favourite\")\n                            checked: root.favouriteChecked\n                            // disabled if:\n                            // * app is hidden\n                            // * app isn't in favouriteApps array but marked as favourite anyway\n                            // ^^^ This means that this app is favourited because of a regex check\n                            //     this button can not toggle regexed apps\n                            enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (Config.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked)\n                            opacity: enabled ? 1 : 0.6\n                            onToggled: checked => {\n                                root.favouriteChecked = checked;\n                                const app = appDetailsLayout.displayedApp;\n                                if (app) {\n                                    const appId = app.id || app.entry?.id;\n                                    const favouriteApps = Config.launcher.favouriteApps ? [...Config.launcher.favouriteApps] : [];\n                                    if (checked) {\n                                        if (!favouriteApps.includes(appId)) {\n                                            favouriteApps.push(appId);\n                                        }\n                                    } else {\n                                        const index = favouriteApps.indexOf(appId);\n                                        if (index !== -1) {\n                                            favouriteApps.splice(index, 1);\n                                        }\n                                    }\n                                    Config.launcher.favouriteApps = favouriteApps;\n                                    Config.save();\n                                }\n                            }\n                        }\n                        SwitchRow {\n                            Layout.topMargin: Appearance.spacing.normal\n                            visible: appDetailsLayout.displayedApp !== null\n                            label: qsTr(\"Hide from launcher\")\n                            checked: root.hideFromLauncherChecked\n                            // disabled if:\n                            // * app is favourited\n                            // * app isn't in hiddenApps array but marked as hidden anyway\n                            // ^^^ This means that this app is hidden because of a regex check\n                            //     this button can not toggle regexed apps\n                            enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (Config.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked)\n                            opacity: enabled ? 1 : 0.6\n                            onToggled: checked => {\n                                root.hideFromLauncherChecked = checked;\n                                const app = appDetailsLayout.displayedApp;\n                                if (app) {\n                                    const appId = app.id || app.entry?.id;\n                                    const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];\n                                    if (checked) {\n                                        if (!hiddenApps.includes(appId)) {\n                                            hiddenApps.push(appId);\n                                        }\n                                    } else {\n                                        const index = hiddenApps.indexOf(appId);\n                                        if (index !== -1) {\n                                            hiddenApps.splice(index, 1);\n                                        }\n                                    }\n                                    Config.launcher.hiddenApps = hiddenApps;\n                                    Config.save();\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/launcher/Settings.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property Session session\n\n    spacing: Appearance.spacing.normal\n\n    SettingsHeader {\n        icon: \"apps\"\n        title: qsTr(\"Launcher Settings\")\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"General\")\n        description: qsTr(\"General launcher settings\")\n    }\n\n    SectionContainer {\n        ToggleRow {\n            label: qsTr(\"Enabled\")\n            checked: Config.launcher.enabled\n            toggle.onToggled: {\n                Config.launcher.enabled = checked;\n                Config.save();\n            }\n        }\n\n        ToggleRow {\n            label: qsTr(\"Show on hover\")\n            checked: Config.launcher.showOnHover\n            toggle.onToggled: {\n                Config.launcher.showOnHover = checked;\n                Config.save();\n            }\n        }\n\n        ToggleRow {\n            label: qsTr(\"Vim keybinds\")\n            checked: Config.launcher.vimKeybinds\n            toggle.onToggled: {\n                Config.launcher.vimKeybinds = checked;\n                Config.save();\n            }\n        }\n\n        ToggleRow {\n            label: qsTr(\"Enable dangerous actions\")\n            checked: Config.launcher.enableDangerousActions\n            toggle.onToggled: {\n                Config.launcher.enableDangerousActions = checked;\n                Config.save();\n            }\n        }\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"Display\")\n        description: qsTr(\"Display and appearance settings\")\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.small / 2\n\n        PropertyRow {\n            label: qsTr(\"Max shown items\")\n            value: qsTr(\"%1\").arg(Config.launcher.maxShown)\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            label: qsTr(\"Max wallpapers\")\n            value: qsTr(\"%1\").arg(Config.launcher.maxWallpapers)\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            label: qsTr(\"Drag threshold\")\n            value: qsTr(\"%1 px\").arg(Config.launcher.dragThreshold)\n        }\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"Prefixes\")\n        description: qsTr(\"Command prefix settings\")\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.small / 2\n\n        PropertyRow {\n            label: qsTr(\"Special prefix\")\n            value: Config.launcher.specialPrefix || qsTr(\"None\")\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            label: qsTr(\"Action prefix\")\n            value: Config.launcher.actionPrefix || qsTr(\"None\")\n        }\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"Fuzzy search\")\n        description: qsTr(\"Fuzzy search settings\")\n    }\n\n    SectionContainer {\n        ToggleRow {\n            label: qsTr(\"Apps\")\n            checked: Config.launcher.useFuzzy.apps\n            toggle.onToggled: {\n                Config.launcher.useFuzzy.apps = checked;\n                Config.save();\n            }\n        }\n\n        ToggleRow {\n            label: qsTr(\"Actions\")\n            checked: Config.launcher.useFuzzy.actions\n            toggle.onToggled: {\n                Config.launcher.useFuzzy.actions = checked;\n                Config.save();\n            }\n        }\n\n        ToggleRow {\n            label: qsTr(\"Schemes\")\n            checked: Config.launcher.useFuzzy.schemes\n            toggle.onToggled: {\n                Config.launcher.useFuzzy.schemes = checked;\n                Config.save();\n            }\n        }\n\n        ToggleRow {\n            label: qsTr(\"Variants\")\n            checked: Config.launcher.useFuzzy.variants\n            toggle.onToggled: {\n                Config.launcher.useFuzzy.variants = checked;\n                Config.save();\n            }\n        }\n\n        ToggleRow {\n            label: qsTr(\"Wallpapers\")\n            checked: Config.launcher.useFuzzy.wallpapers\n            toggle.onToggled: {\n                Config.launcher.useFuzzy.wallpapers = checked;\n                Config.save();\n            }\n        }\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"Sizes\")\n        description: qsTr(\"Size settings for launcher items\")\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.small / 2\n\n        PropertyRow {\n            label: qsTr(\"Item width\")\n            value: qsTr(\"%1 px\").arg(Config.launcher.sizes.itemWidth)\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            label: qsTr(\"Item height\")\n            value: qsTr(\"%1 px\").arg(Config.launcher.sizes.itemHeight)\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            label: qsTr(\"Wallpaper width\")\n            value: qsTr(\"%1 px\").arg(Config.launcher.sizes.wallpaperWidth)\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            label: qsTr(\"Wallpaper height\")\n            value: qsTr(\"%1 px\").arg(Config.launcher.sizes.wallpaperHeight)\n        }\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"Hidden apps\")\n        description: qsTr(\"Applications hidden from launcher\")\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.small / 2\n\n        PropertyRow {\n            label: qsTr(\"Total hidden\")\n            value: qsTr(\"%1\").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0)\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/EthernetDetails.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nDeviceDetails {\n    id: root\n\n    required property Session session\n    readonly property var ethernetDevice: root.session.ethernet.active\n\n    device: ethernetDevice\n\n    Component.onCompleted: {\n        if (ethernetDevice && ethernetDevice.interface) {\n            Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {});\n        }\n    }\n\n    onEthernetDeviceChanged: {\n        if (ethernetDevice && ethernetDevice.interface) {\n            Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {});\n        } else {\n            Nmcli.ethernetDeviceDetails = null;\n        }\n    }\n\n    headerComponent: Component {\n        ConnectionHeader {\n            icon: \"cable\"\n            title: root.ethernetDevice?.interface ?? qsTr(\"Unknown\")\n        }\n    }\n\n    sections: [\n        Component {\n            ColumnLayout {\n                spacing: Appearance.spacing.normal\n\n                SectionHeader {\n                    title: qsTr(\"Connection status\")\n                    description: qsTr(\"Connection settings for this device\")\n                }\n\n                SectionContainer {\n                    ToggleRow {\n                        label: qsTr(\"Connected\")\n                        checked: root.ethernetDevice?.connected ?? false\n                        toggle.onToggled: {\n                            if (checked) {\n                                Nmcli.connectEthernet(root.ethernetDevice?.connection || \"\", root.ethernetDevice?.interface || \"\", () => {});\n                            } else {\n                                if (root.ethernetDevice?.connection) {\n                                    Nmcli.disconnectEthernet(root.ethernetDevice.connection, () => {});\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        },\n        Component {\n            ColumnLayout {\n                spacing: Appearance.spacing.normal\n\n                SectionHeader {\n                    title: qsTr(\"Device properties\")\n                    description: qsTr(\"Additional information\")\n                }\n\n                SectionContainer {\n                    contentSpacing: Appearance.spacing.small / 2\n\n                    PropertyRow {\n                        label: qsTr(\"Interface\")\n                        value: root.ethernetDevice?.interface ?? qsTr(\"Unknown\")\n                    }\n\n                    PropertyRow {\n                        showTopMargin: true\n                        label: qsTr(\"Connection\")\n                        value: root.ethernetDevice?.connection || qsTr(\"Not connected\")\n                    }\n\n                    PropertyRow {\n                        showTopMargin: true\n                        label: qsTr(\"State\")\n                        value: root.ethernetDevice?.state ?? qsTr(\"Unknown\")\n                    }\n                }\n            }\n        },\n        Component {\n            ColumnLayout {\n                spacing: Appearance.spacing.normal\n\n                SectionHeader {\n                    title: qsTr(\"Connection information\")\n                    description: qsTr(\"Network connection details\")\n                }\n\n                SectionContainer {\n                    ConnectionInfoSection {\n                        deviceDetails: Nmcli.ethernetDeviceDetails\n                    }\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "modules/controlcenter/network/EthernetList.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nDeviceList {\n    id: root\n\n    required property Session session\n\n    title: qsTr(\"Devices (%1)\").arg(Nmcli.ethernetDevices.length)\n    description: qsTr(\"All available ethernet devices\")\n    activeItem: session.ethernet.active\n\n    model: Nmcli.ethernetDevices\n\n    headerComponent: Component {\n        RowLayout {\n            spacing: Appearance.spacing.smaller\n\n            StyledText {\n                text: qsTr(\"Settings\")\n                font.pointSize: Appearance.font.size.large\n                font.weight: 500\n            }\n\n            Item {\n                Layout.fillWidth: true\n            }\n\n            ToggleButton {\n                toggled: !root.session.ethernet.active\n                icon: \"settings\"\n                accent: \"Primary\"\n                iconSize: Appearance.font.size.normal\n                horizontalPadding: Appearance.padding.normal\n                verticalPadding: Appearance.padding.smaller\n\n                onClicked: {\n                    if (root.session.ethernet.active)\n                        root.session.ethernet.active = null;\n                    else {\n                        root.session.ethernet.active = root.view.model.get(0)?.modelData ?? null;\n                    }\n                }\n            }\n        }\n    }\n\n    delegate: Component {\n        StyledRect {\n            id: ethernetItem\n\n            required property var modelData\n            readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface\n\n            width: ListView.view ? ListView.view.width : undefined\n            implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2\n\n            color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0)\n            radius: Appearance.rounding.normal\n\n            StateLayer {\n                id: stateLayer\n\n                function onClicked(): void {\n                    root.session.ethernet.active = ethernetItem.modelData;\n                }\n            }\n\n            RowLayout {\n                id: rowLayout\n\n                anchors.fill: parent\n                anchors.margins: Appearance.padding.normal\n\n                spacing: Appearance.spacing.normal\n\n                StyledRect {\n                    implicitWidth: implicitHeight\n                    implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2\n\n                    radius: Appearance.rounding.normal\n                    color: ethernetItem.modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh\n\n                    StyledRect {\n                        anchors.fill: parent\n                        radius: parent.radius\n                        color: Qt.alpha(ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0)\n                    }\n\n                    MaterialIcon {\n                        id: icon\n\n                        anchors.centerIn: parent\n                        text: \"cable\"\n                        font.pointSize: Appearance.font.size.large\n                        fill: ethernetItem.modelData.connected ? 1 : 0\n                        color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface\n\n                        Behavior on fill {\n                            Anim {}\n                        }\n                    }\n                }\n\n                ColumnLayout {\n                    Layout.fillWidth: true\n\n                    spacing: 0\n\n                    StyledText {\n                        Layout.fillWidth: true\n                        text: ethernetItem.modelData.interface || qsTr(\"Unknown\")\n                        elide: Text.ElideRight\n                    }\n\n                    RowLayout {\n                        Layout.fillWidth: true\n                        spacing: Appearance.spacing.smaller\n\n                        StyledText {\n                            Layout.fillWidth: true\n                            text: ethernetItem.modelData.connected ? qsTr(\"Connected\") : qsTr(\"Disconnected\")\n                            color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline\n                            font.pointSize: Appearance.font.size.small\n                            font.weight: ethernetItem.modelData.connected ? 500 : 400\n                            elide: Text.ElideRight\n                        }\n                    }\n                }\n\n                StyledRect {\n                    id: connectBtn\n\n                    implicitWidth: implicitHeight\n                    implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2\n\n                    radius: Appearance.rounding.full\n                    color: Qt.alpha(Colours.palette.m3primaryContainer, ethernetItem.modelData.connected ? 1 : 0)\n\n                    StateLayer {\n                        function onClicked(): void {\n                            if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) {\n                                Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {});\n                            } else {\n                                Nmcli.connectEthernet(ethernetItem.modelData.connection || \"\", ethernetItem.modelData.interface || \"\", () => {});\n                            }\n                        }\n\n                        color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface\n                    }\n\n                    MaterialIcon {\n                        id: connectIcon\n\n                        anchors.centerIn: parent\n                        animate: true\n                        text: ethernetItem.modelData.connected ? \"link_off\" : \"link\"\n                        color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface\n                    }\n                }\n            }\n        }\n    }\n\n    onItemSelected: function (item) {\n        session.ethernet.active = item;\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/EthernetPane.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components.containers\nimport QtQuick\n\nSplitPaneWithDetails {\n    id: root\n\n    required property Session session\n\n    anchors.fill: parent\n\n    activeItem: session.ethernet.active\n    paneIdGenerator: function (item) {\n        return item ? (item.interface || \"\") : \"\";\n    }\n\n    leftContent: Component {\n        EthernetList {\n            session: root.session\n        }\n    }\n\n    rightDetailsComponent: Component {\n        EthernetDetails {\n            session: root.session\n        }\n    }\n\n    rightSettingsComponent: Component {\n        StyledFlickable {\n            flickableDirection: Flickable.VerticalFlick\n            contentHeight: settingsInner.height\n            clip: true\n\n            EthernetSettings {\n                id: settingsInner\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                session: root.session\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/EthernetSettings.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property Session session\n\n    spacing: Appearance.spacing.normal\n\n    SettingsHeader {\n        icon: \"cable\"\n        title: qsTr(\"Ethernet settings\")\n    }\n\n    StyledText {\n        Layout.topMargin: Appearance.spacing.large\n        text: qsTr(\"Ethernet devices\")\n        font.pointSize: Appearance.font.size.larger\n        font.weight: 500\n    }\n\n    StyledText {\n        text: qsTr(\"Available ethernet devices\")\n        color: Colours.palette.m3outline\n    }\n\n    StyledRect {\n        Layout.fillWidth: true\n        implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2\n\n        radius: Appearance.rounding.normal\n        color: Colours.tPalette.m3surfaceContainer\n\n        ColumnLayout {\n            id: ethernetInfo\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.margins: Appearance.padding.large\n\n            spacing: Appearance.spacing.small / 2\n\n            StyledText {\n                text: qsTr(\"Total devices\")\n            }\n\n            StyledText {\n                text: qsTr(\"%1\").arg(Nmcli.ethernetDevices.length)\n                color: Colours.palette.m3outline\n                font.pointSize: Appearance.font.size.small\n            }\n\n            StyledText {\n                Layout.topMargin: Appearance.spacing.normal\n                text: qsTr(\"Connected devices\")\n            }\n\n            StyledText {\n                text: qsTr(\"%1\").arg(Nmcli.ethernetDevices.filter(d => d.connected).length)\n                color: Colours.palette.m3outline\n                font.pointSize: Appearance.font.size.small\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/NetworkSettings.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property Session session\n\n    spacing: Appearance.spacing.normal\n\n    SettingsHeader {\n        icon: \"router\"\n        title: qsTr(\"Network Settings\")\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"Ethernet\")\n        description: qsTr(\"Ethernet device information\")\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.small / 2\n\n        PropertyRow {\n            label: qsTr(\"Total devices\")\n            value: qsTr(\"%1\").arg(Nmcli.ethernetDevices.length)\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            label: qsTr(\"Connected devices\")\n            value: qsTr(\"%1\").arg(Nmcli.ethernetDevices.filter(d => d.connected).length)\n        }\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"Wireless\")\n        description: qsTr(\"WiFi network settings\")\n    }\n\n    SectionContainer {\n        ToggleRow {\n            label: qsTr(\"WiFi enabled\")\n            checked: Nmcli.wifiEnabled\n            toggle.onToggled: {\n                Nmcli.enableWifi(checked);\n            }\n        }\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"VPN\")\n        description: qsTr(\"VPN provider settings\")\n        visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0\n    }\n\n    SectionContainer {\n        visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0\n\n        ToggleRow {\n            label: qsTr(\"VPN enabled\")\n            checked: Config.utilities.vpn.enabled\n            toggle.onToggled: {\n                Config.utilities.vpn.enabled = checked;\n                Config.save();\n            }\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            label: qsTr(\"Providers\")\n            value: qsTr(\"%1\").arg(Config.utilities.vpn.provider.length)\n        }\n\n        TextButton {\n            Layout.fillWidth: true\n            Layout.topMargin: Appearance.spacing.normal\n            Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2\n            text: qsTr(\"⚙ Manage VPN Providers\")\n            inactiveColour: Colours.palette.m3secondaryContainer\n            inactiveOnColour: Colours.palette.m3onSecondaryContainer\n\n            onClicked: {\n                vpnSettingsDialog.open();\n            }\n        }\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"Current connection\")\n        description: qsTr(\"Active network connection information\")\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.small / 2\n\n        PropertyRow {\n            label: qsTr(\"Network\")\n            value: Nmcli.active ? Nmcli.active.ssid : (Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : qsTr(\"Not connected\"))\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            visible: Nmcli.active !== null\n            label: qsTr(\"Signal strength\")\n            value: Nmcli.active ? qsTr(\"%1%\").arg(Nmcli.active.strength) : qsTr(\"N/A\")\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            visible: Nmcli.active !== null\n            label: qsTr(\"Security\")\n            value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr(\"Secured\") : qsTr(\"Open\")) : qsTr(\"N/A\")\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            visible: Nmcli.active !== null\n            label: qsTr(\"Frequency\")\n            value: Nmcli.active ? qsTr(\"%1 MHz\").arg(Nmcli.active.frequency) : qsTr(\"N/A\")\n        }\n    }\n\n    Popup {\n        id: vpnSettingsDialog\n\n        parent: Overlay.overlay\n        anchors.centerIn: parent\n        width: Math.min(600, parent.width - Appearance.padding.large * 2)\n        height: Math.min(700, parent.height - Appearance.padding.large * 2)\n\n        modal: true\n        closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside\n\n        background: StyledRect {\n            color: Colours.palette.m3surface\n            radius: Appearance.rounding.large\n        }\n\n        StyledFlickable {\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.large * 1.5\n            flickableDirection: Flickable.VerticalFlick\n            contentHeight: vpnSettingsContent.height\n            clip: true\n\n            VpnSettings {\n                id: vpnSettingsContent\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                session: root.session\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/NetworkingPane.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport \".\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property Session session\n\n    anchors.fill: parent\n\n    SplitPaneLayout {\n        id: splitLayout\n\n        anchors.fill: parent\n\n        leftContent: Component {\n            StyledFlickable {\n                id: leftFlickable\n\n                flickableDirection: Flickable.VerticalFlick\n                contentHeight: leftContent.height\n\n                StyledScrollBar.vertical: StyledScrollBar {\n                    flickable: leftFlickable\n                }\n\n                ColumnLayout {\n                    id: leftContent\n\n                    anchors.left: parent.left\n                    anchors.right: parent.right\n                    spacing: Appearance.spacing.normal\n\n                    RowLayout {\n                        Layout.fillWidth: true\n                        spacing: Appearance.spacing.smaller\n\n                        StyledText {\n                            text: qsTr(\"Network\")\n                            font.pointSize: Appearance.font.size.large\n                            font.weight: 500\n                        }\n\n                        Item {\n                            Layout.fillWidth: true\n                        }\n\n                        ToggleButton {\n                            toggled: Nmcli.wifiEnabled\n                            icon: \"wifi\"\n                            accent: \"Tertiary\"\n                            iconSize: Appearance.font.size.normal\n                            horizontalPadding: Appearance.padding.normal\n                            verticalPadding: Appearance.padding.smaller\n                            tooltip: qsTr(\"Toggle WiFi\")\n\n                            onClicked: {\n                                Nmcli.toggleWifi(null);\n                            }\n                        }\n\n                        ToggleButton {\n                            toggled: Nmcli.scanning\n                            icon: \"wifi_find\"\n                            accent: \"Secondary\"\n                            iconSize: Appearance.font.size.normal\n                            horizontalPadding: Appearance.padding.normal\n                            verticalPadding: Appearance.padding.smaller\n                            tooltip: qsTr(\"Scan for networks\")\n\n                            onClicked: {\n                                Nmcli.rescanWifi();\n                            }\n                        }\n\n                        ToggleButton {\n                            toggled: !root.session.ethernet.active && !root.session.network.active\n                            icon: \"settings\"\n                            accent: \"Primary\"\n                            iconSize: Appearance.font.size.normal\n                            horizontalPadding: Appearance.padding.normal\n                            verticalPadding: Appearance.padding.smaller\n                            tooltip: qsTr(\"Network settings\")\n\n                            onClicked: {\n                                if (root.session.ethernet.active || root.session.network.active) {\n                                    root.session.ethernet.active = null;\n                                    root.session.network.active = null;\n                                } else {\n                                    if (Nmcli.ethernetDevices.length > 0) {\n                                        root.session.ethernet.active = Nmcli.ethernetDevices[0];\n                                    } else if (Nmcli.networks.length > 0) {\n                                        root.session.network.active = Nmcli.networks[0];\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    CollapsibleSection {\n                        id: vpnListSection\n\n                        Layout.fillWidth: true\n                        title: qsTr(\"VPN\")\n                        expanded: true\n\n                        Loader {\n                            Layout.fillWidth: true\n                            asynchronous: true\n                            sourceComponent: Component {\n                                VpnList {\n                                    session: root.session\n                                    showHeader: false\n                                }\n                            }\n                        }\n                    }\n\n                    CollapsibleSection {\n                        id: ethernetListSection\n\n                        Layout.fillWidth: true\n                        title: qsTr(\"Ethernet\")\n                        expanded: true\n\n                        Loader {\n                            Layout.fillWidth: true\n                            asynchronous: true\n                            sourceComponent: Component {\n                                EthernetList {\n                                    session: root.session\n                                    showHeader: false\n                                }\n                            }\n                        }\n                    }\n\n                    CollapsibleSection {\n                        id: wirelessListSection\n\n                        Layout.fillWidth: true\n                        title: qsTr(\"Wireless\")\n                        expanded: true\n\n                        Loader {\n                            Layout.fillWidth: true\n                            asynchronous: true\n                            sourceComponent: Component {\n                                WirelessList {\n                                    session: root.session\n                                    showHeader: false\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        rightContent: Component {\n            Item {\n                id: rightPaneItem\n\n                property var vpnPane: root.session && root.session.vpn ? root.session.vpn.active : null\n                property var ethernetPane: root.session && root.session.ethernet ? root.session.ethernet.active : null\n                property var wirelessPane: root.session && root.session.network ? root.session.network.active : null\n                property var pane: vpnPane || ethernetPane || wirelessPane\n                property string paneId: vpnPane ? (\"vpn:\" + (vpnPane.name || \"\")) : (ethernetPane ? (\"eth:\" + (ethernetPane.interface || \"\")) : (wirelessPane ? (\"wifi:\" + (wirelessPane.ssid || wirelessPane.bssid || \"\")) : \"settings\"))\n                property Component targetComponent: settingsComponent\n                property Component nextComponent: settingsComponent\n\n                function getComponentForPane() {\n                    if (vpnPane)\n                        return vpnDetailsComponent;\n                    if (ethernetPane)\n                        return ethernetDetailsComponent;\n                    if (wirelessPane)\n                        return wirelessDetailsComponent;\n                    return settingsComponent;\n                }\n\n                Component.onCompleted: {\n                    targetComponent = getComponentForPane();\n                    nextComponent = targetComponent;\n                }\n\n                Connections {\n                    function onActiveChanged() {\n                        // Clear others when VPN is selected\n                        if (root.session && root.session.vpn && root.session.vpn.active) {\n                            if (root.session.ethernet && root.session.ethernet.active)\n                                root.session.ethernet.active = null;\n                            if (root.session.network && root.session.network.active)\n                                root.session.network.active = null;\n                        }\n                        rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();\n                    }\n\n                    target: root.session && root.session.vpn ? root.session.vpn : null\n                    enabled: target !== null\n                }\n\n                Connections {\n                    function onActiveChanged() {\n                        // Clear others when ethernet is selected\n                        if (root.session && root.session.ethernet && root.session.ethernet.active) {\n                            if (root.session.vpn && root.session.vpn.active)\n                                root.session.vpn.active = null;\n                            if (root.session.network && root.session.network.active)\n                                root.session.network.active = null;\n                        }\n                        rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();\n                    }\n\n                    target: root.session && root.session.ethernet ? root.session.ethernet : null\n                    enabled: target !== null\n                }\n\n                Connections {\n                    function onActiveChanged() {\n                        // Clear others when wireless is selected\n                        if (root.session && root.session.network && root.session.network.active) {\n                            if (root.session.vpn && root.session.vpn.active)\n                                root.session.vpn.active = null;\n                            if (root.session.ethernet && root.session.ethernet.active)\n                                root.session.ethernet.active = null;\n                        }\n                        rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();\n                    }\n\n                    target: root.session && root.session.network ? root.session.network : null\n                    enabled: target !== null\n                }\n\n                Loader {\n                    id: rightLoader\n\n                    anchors.fill: parent\n\n                    opacity: 1\n                    scale: 1\n                    transformOrigin: Item.Center\n                    clip: false\n\n                    asynchronous: true\n                    sourceComponent: rightPaneItem.targetComponent\n                }\n\n                Behavior on paneId {\n                    PaneTransition {\n                        target: rightLoader\n                        propertyActions: [\n                            PropertyAction {\n                                target: rightPaneItem\n                                property: \"targetComponent\"\n                                value: rightPaneItem.nextComponent\n                            }\n                        ]\n                    }\n                }\n            }\n        }\n    }\n\n    Component {\n        id: settingsComponent\n\n        StyledFlickable {\n            id: settingsFlickable\n\n            flickableDirection: Flickable.VerticalFlick\n            contentHeight: settingsInner.height\n\n            StyledScrollBar.vertical: StyledScrollBar {\n                flickable: settingsFlickable\n            }\n\n            NetworkSettings {\n                id: settingsInner\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.top: parent.top\n                session: root.session\n            }\n        }\n    }\n\n    Component {\n        id: ethernetDetailsComponent\n\n        StyledFlickable {\n            id: ethernetFlickable\n\n            flickableDirection: Flickable.VerticalFlick\n            contentHeight: ethernetDetailsInner.height\n\n            StyledScrollBar.vertical: StyledScrollBar {\n                flickable: ethernetFlickable\n            }\n\n            EthernetDetails {\n                id: ethernetDetailsInner\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.top: parent.top\n                session: root.session\n            }\n        }\n    }\n\n    Component {\n        id: wirelessDetailsComponent\n\n        StyledFlickable {\n            id: wirelessFlickable\n\n            flickableDirection: Flickable.VerticalFlick\n            contentHeight: wirelessDetailsInner.height\n\n            StyledScrollBar.vertical: StyledScrollBar {\n                flickable: wirelessFlickable\n            }\n\n            WirelessDetails {\n                id: wirelessDetailsInner\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.top: parent.top\n                session: root.session\n            }\n        }\n    }\n\n    Component {\n        id: vpnDetailsComponent\n\n        StyledFlickable {\n            id: vpnFlickable\n\n            flickableDirection: Flickable.VerticalFlick\n            contentHeight: vpnDetailsInner.height\n\n            StyledScrollBar.vertical: StyledScrollBar {\n                flickable: vpnFlickable\n            }\n\n            VpnDetails {\n                id: vpnDetailsInner\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.top: parent.top\n                session: root.session\n            }\n        }\n    }\n\n    WirelessPasswordDialog {\n        anchors.fill: parent\n        session: root.session\n        z: 1000\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/VpnDetails.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\n\nDeviceDetails {\n    id: root\n\n    required property Session session\n    readonly property var vpnProvider: root.session.vpn.active\n    readonly property bool providerEnabled: {\n        if (!vpnProvider || vpnProvider.index === undefined)\n            return false;\n        const provider = Config.utilities.vpn.provider[vpnProvider.index];\n        return provider && typeof provider === \"object\" && provider.enabled === true;\n    }\n\n    device: vpnProvider\n\n    headerComponent: Component {\n        ConnectionHeader {\n            icon: \"vpn_key\"\n            title: root.vpnProvider?.displayName ?? qsTr(\"Unknown\")\n        }\n    }\n\n    sections: [\n        Component {\n            ColumnLayout {\n                spacing: Appearance.spacing.normal\n\n                SectionHeader {\n                    title: qsTr(\"Connection status\")\n                    description: qsTr(\"VPN connection settings\")\n                }\n\n                SectionContainer {\n                    ToggleRow {\n                        label: qsTr(\"Enable this provider\")\n                        checked: root.providerEnabled\n                        toggle.onToggled: {\n                            if (!root.vpnProvider)\n                                return;\n                            const providers = [];\n                            const index = root.vpnProvider.index;\n\n                            // Copy providers and update enabled state\n                            for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {\n                                const p = Config.utilities.vpn.provider[i];\n                                if (typeof p === \"object\") {\n                                    const newProvider = {\n                                        name: p.name,\n                                        displayName: p.displayName,\n                                        interface: p.interface\n                                    };\n\n                                    if (checked) {\n                                        // Enable this one, disable others\n                                        newProvider.enabled = (i === index);\n                                    } else {\n                                        // Just disable this one\n                                        newProvider.enabled = (i === index) ? false : (p.enabled !== false);\n                                    }\n\n                                    providers.push(newProvider);\n                                } else {\n                                    providers.push(p);\n                                }\n                            }\n\n                            Config.utilities.vpn.provider = providers;\n                            Config.save();\n                        }\n                    }\n\n                    RowLayout {\n                        Layout.fillWidth: true\n                        Layout.topMargin: Appearance.spacing.normal\n                        spacing: Appearance.spacing.normal\n\n                        TextButton {\n                            Layout.fillWidth: true\n                            Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2\n                            visible: root.providerEnabled\n                            enabled: !VPN.connecting\n                            inactiveColour: Colours.palette.m3primaryContainer\n                            inactiveOnColour: Colours.palette.m3onPrimaryContainer\n                            text: VPN.connected ? qsTr(\"Disconnect\") : qsTr(\"Connect\")\n\n                            onClicked: {\n                                VPN.toggle();\n                            }\n                        }\n\n                        TextButton {\n                            Layout.fillWidth: true\n                            text: qsTr(\"Edit Provider\")\n                            inactiveColour: Colours.palette.m3secondaryContainer\n                            inactiveOnColour: Colours.palette.m3onSecondaryContainer\n\n                            onClicked: {\n                                editVpnDialog.editIndex = root.vpnProvider.index;\n                                editVpnDialog.providerName = root.vpnProvider.name;\n                                editVpnDialog.displayName = root.vpnProvider.displayName;\n                                editVpnDialog.interfaceName = root.vpnProvider.interface;\n                                editVpnDialog.open();\n                            }\n                        }\n\n                        TextButton {\n                            Layout.fillWidth: true\n                            text: qsTr(\"Delete Provider\")\n                            inactiveColour: Colours.palette.m3errorContainer\n                            inactiveOnColour: Colours.palette.m3onErrorContainer\n\n                            onClicked: {\n                                const providers = [];\n                                for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {\n                                    if (i !== root.vpnProvider.index) {\n                                        providers.push(Config.utilities.vpn.provider[i]);\n                                    }\n                                }\n                                Config.utilities.vpn.provider = providers;\n                                Config.save();\n                                root.session.vpn.active = null;\n                            }\n                        }\n                    }\n                }\n            }\n        },\n        Component {\n            ColumnLayout {\n                spacing: Appearance.spacing.normal\n\n                SectionHeader {\n                    title: qsTr(\"Provider details\")\n                    description: qsTr(\"VPN provider information\")\n                }\n\n                SectionContainer {\n                    contentSpacing: Appearance.spacing.small / 2\n\n                    PropertyRow {\n                        label: qsTr(\"Provider\")\n                        value: root.vpnProvider?.name ?? qsTr(\"Unknown\")\n                    }\n\n                    PropertyRow {\n                        showTopMargin: true\n                        label: qsTr(\"Display name\")\n                        value: root.vpnProvider?.displayName ?? qsTr(\"Unknown\")\n                    }\n\n                    PropertyRow {\n                        showTopMargin: true\n                        label: qsTr(\"Interface\")\n                        value: root.vpnProvider?.interface || qsTr(\"N/A\")\n                    }\n\n                    PropertyRow {\n                        showTopMargin: true\n                        label: qsTr(\"Status\")\n                        value: {\n                            if (!root.providerEnabled)\n                                return qsTr(\"Disabled\");\n                            if (VPN.connecting)\n                                return qsTr(\"Connecting...\");\n                            if (VPN.connected)\n                                return qsTr(\"Connected\");\n                            return qsTr(\"Enabled (Not connected)\");\n                        }\n                    }\n\n                    PropertyRow {\n                        showTopMargin: true\n                        label: qsTr(\"Enabled\")\n                        value: root.providerEnabled ? qsTr(\"Yes\") : qsTr(\"No\")\n                    }\n                }\n            }\n        }\n    ]\n\n    // Edit VPN Dialog\n    Popup {\n        id: editVpnDialog\n\n        property int editIndex: -1\n        property string providerName: \"\"\n        property string displayName: \"\"\n        property string interfaceName: \"\"\n\n        function closeWithAnimation(): void {\n            close();\n        }\n\n        parent: Overlay.overlay\n        anchors.centerIn: parent\n        width: Math.min(400, parent.width - Appearance.padding.large * 2)\n        padding: Appearance.padding.large * 1.5\n\n        modal: true\n        closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside\n\n        opacity: 0\n        scale: 0.7\n\n        enter: Transition {\n            Anim {\n                property: \"opacity\"\n                from: 0\n                to: 1\n                duration: Appearance.anim.durations.expressiveFastSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n            }\n            Anim {\n                property: \"scale\"\n                from: 0.7\n                to: 1\n                duration: Appearance.anim.durations.expressiveFastSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n            }\n        }\n\n        exit: Transition {\n            Anim {\n                property: \"opacity\"\n                from: 1\n                to: 0\n                duration: Appearance.anim.durations.expressiveFastSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n            }\n            Anim {\n                property: \"scale\"\n                from: 1\n                to: 0.7\n                duration: Appearance.anim.durations.expressiveFastSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n            }\n        }\n\n        Overlay.modal: Rectangle {\n            color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity)\n        }\n\n        background: StyledRect {\n            color: Colours.palette.m3surfaceContainerHigh\n            radius: Appearance.rounding.large\n\n            Elevation {\n                anchors.fill: parent\n                radius: parent.radius\n                level: 3\n                z: -1\n            }\n        }\n\n        contentItem: ColumnLayout {\n            spacing: Appearance.spacing.normal\n\n            StyledText {\n                text: qsTr(\"Edit VPN Provider\")\n                font.pointSize: Appearance.font.size.large\n                font.weight: 500\n            }\n\n            ColumnLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.smaller / 2\n\n                StyledText {\n                    text: qsTr(\"Display Name\")\n                    font.pointSize: Appearance.font.size.small\n                    color: Colours.palette.m3onSurfaceVariant\n                }\n\n                StyledRect {\n                    Layout.fillWidth: true\n                    implicitHeight: 40\n                    color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)\n                    radius: Appearance.rounding.small\n                    border.width: 1\n                    border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)\n\n                    Behavior on color {\n                        CAnim {}\n                    }\n                    Behavior on border.color {\n                        CAnim {}\n                    }\n\n                    StyledTextField {\n                        id: displayNameField\n\n                        anchors.centerIn: parent\n                        width: parent.width - Appearance.padding.normal\n                        horizontalAlignment: TextInput.AlignLeft\n                        text: editVpnDialog.displayName\n                        onTextChanged: editVpnDialog.displayName = text\n                    }\n                }\n            }\n\n            ColumnLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.smaller / 2\n\n                StyledText {\n                    text: qsTr(\"Interface (e.g., wg0, torguard)\")\n                    font.pointSize: Appearance.font.size.small\n                    color: Colours.palette.m3onSurfaceVariant\n                }\n\n                StyledRect {\n                    Layout.fillWidth: true\n                    implicitHeight: 40\n                    color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)\n                    radius: Appearance.rounding.small\n                    border.width: 1\n                    border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)\n\n                    Behavior on color {\n                        CAnim {}\n                    }\n                    Behavior on border.color {\n                        CAnim {}\n                    }\n\n                    StyledTextField {\n                        id: interfaceNameField\n\n                        anchors.centerIn: parent\n                        width: parent.width - Appearance.padding.normal\n                        horizontalAlignment: TextInput.AlignLeft\n                        text: editVpnDialog.interfaceName\n                        onTextChanged: editVpnDialog.interfaceName = text\n                    }\n                }\n            }\n\n            RowLayout {\n                Layout.topMargin: Appearance.spacing.normal\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.normal\n\n                TextButton {\n                    Layout.fillWidth: true\n                    text: qsTr(\"Cancel\")\n                    inactiveColour: Colours.tPalette.m3surfaceContainerHigh\n                    inactiveOnColour: Colours.palette.m3onSurface\n                    onClicked: editVpnDialog.closeWithAnimation()\n                }\n\n                TextButton {\n                    Layout.fillWidth: true\n                    text: qsTr(\"Save\")\n                    enabled: editVpnDialog.interfaceName.length > 0\n                    inactiveColour: Colours.palette.m3primaryContainer\n                    inactiveOnColour: Colours.palette.m3onPrimaryContainer\n\n                    onClicked: {\n                        const providers = [];\n                        const oldProvider = Config.utilities.vpn.provider[editVpnDialog.editIndex];\n                        const wasEnabled = typeof oldProvider === \"object\" ? (oldProvider.enabled !== false) : true;\n\n                        for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {\n                            if (i === editVpnDialog.editIndex) {\n                                providers.push({\n                                    name: editVpnDialog.providerName,\n                                    displayName: editVpnDialog.displayName || editVpnDialog.interfaceName,\n                                    interface: editVpnDialog.interfaceName,\n                                    enabled: wasEnabled\n                                });\n                            } else {\n                                providers.push(Config.utilities.vpn.provider[i]);\n                            }\n                        }\n\n                        Config.utilities.vpn.provider = providers;\n                        Config.save();\n                        editVpnDialog.closeWithAnimation();\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/VpnList.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property Session session\n    property bool showHeader: true\n    property int pendingSwitchIndex: -1\n\n    spacing: Appearance.spacing.normal\n\n    Connections {\n        function onConnectedChanged() {\n            if (!VPN.connected && root.pendingSwitchIndex >= 0) {\n                const targetIndex = root.pendingSwitchIndex;\n                root.pendingSwitchIndex = -1;\n\n                const providers = [];\n                for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {\n                    const p = Config.utilities.vpn.provider[i];\n                    if (typeof p === \"object\") {\n                        const newProvider = {\n                            name: p.name,\n                            displayName: p.displayName,\n                            interface: p.interface,\n                            enabled: (i === targetIndex)\n                        };\n                        providers.push(newProvider);\n                    } else {\n                        providers.push(p);\n                    }\n                }\n                Config.utilities.vpn.provider = providers;\n                Config.save();\n\n                Qt.callLater(function () {\n                    VPN.toggle();\n                });\n            }\n        }\n\n        target: VPN\n    }\n\n    TextButton {\n        Layout.fillWidth: true\n        text: qsTr(\"+ Add VPN Provider\")\n        inactiveColour: Colours.palette.m3primaryContainer\n        inactiveOnColour: Colours.palette.m3onPrimaryContainer\n\n        onClicked: {\n            vpnDialog.showProviderSelection();\n        }\n    }\n\n    ListView {\n        id: listView\n\n        Layout.fillWidth: true\n        Layout.preferredHeight: contentHeight\n\n        interactive: false\n        spacing: Appearance.spacing.smaller\n\n        model: ScriptModel {\n            values: Config.utilities.vpn.provider.map((provider, index) => {\n                const isObject = typeof provider === \"object\";\n                const name = isObject ? (provider.name || \"custom\") : String(provider);\n                const displayName = isObject ? (provider.displayName || name) : name;\n                const iface = isObject ? (provider.interface || \"\") : \"\";\n                const enabled = isObject ? (provider.enabled === true) : false;\n\n                return {\n                    index: index,\n                    name: name,\n                    displayName: displayName,\n                    interface: iface,\n                    provider: provider,\n                    enabled: enabled\n                };\n            })\n        }\n\n        delegate: Component {\n            StyledRect {\n                required property var modelData\n                required property int index\n\n                width: ListView.view ? ListView.view.width : undefined\n\n                color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0)\n                radius: Appearance.rounding.normal\n\n                StateLayer {\n                    function onClicked(): void {\n                        if (root.session && root.session.vpn) {\n                            root.session.vpn.active = modelData;\n                        }\n                    }\n                }\n\n                RowLayout {\n                    id: rowLayout\n\n                    anchors.left: parent.left\n                    anchors.right: parent.right\n                    anchors.verticalCenter: parent.verticalCenter\n                    anchors.margins: Appearance.padding.normal\n\n                    spacing: Appearance.spacing.normal\n\n                    StyledRect {\n                        implicitWidth: implicitHeight\n                        implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2\n\n                        radius: Appearance.rounding.normal\n                        color: modelData.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh\n\n                        MaterialIcon {\n                            id: icon\n\n                            anchors.centerIn: parent\n                            text: modelData.enabled && VPN.connected ? \"vpn_key\" : \"vpn_key_off\"\n                            font.pointSize: Appearance.font.size.large\n                            fill: modelData.enabled && VPN.connected ? 1 : 0\n                            color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface\n                        }\n                    }\n\n                    ColumnLayout {\n                        Layout.fillWidth: true\n\n                        spacing: 0\n\n                        StyledText {\n                            Layout.fillWidth: true\n                            elide: Text.ElideRight\n                            maximumLineCount: 1\n\n                            text: modelData.displayName || qsTr(\"Unknown\")\n                        }\n\n                        RowLayout {\n                            Layout.fillWidth: true\n                            spacing: Appearance.spacing.smaller\n\n                            StyledText {\n                                Layout.fillWidth: true\n                                text: {\n                                    if (modelData.enabled && VPN.connected)\n                                        return qsTr(\"Connected\");\n                                    if (modelData.enabled && VPN.connecting)\n                                        return qsTr(\"Connecting...\");\n                                    if (modelData.enabled)\n                                        return qsTr(\"Enabled\");\n                                    return qsTr(\"Disabled\");\n                                }\n                                color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline\n                                font.pointSize: Appearance.font.size.small\n                                font.weight: modelData.enabled && VPN.connected ? 500 : 400\n                                elide: Text.ElideRight\n                            }\n                        }\n                    }\n\n                    StyledRect {\n                        implicitWidth: implicitHeight\n                        implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2\n\n                        radius: Appearance.rounding.full\n                        color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0)\n\n                        StateLayer {\n                            function onClicked(): void {\n                                const clickedIndex = modelData.index;\n\n                                if (modelData.enabled) {\n                                    VPN.toggle();\n                                } else {\n                                    if (VPN.connected) {\n                                        root.pendingSwitchIndex = clickedIndex;\n                                        VPN.toggle();\n                                    } else {\n                                        const providers = [];\n                                        for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {\n                                            const p = Config.utilities.vpn.provider[i];\n                                            if (typeof p === \"object\") {\n                                                const newProvider = {\n                                                    name: p.name,\n                                                    displayName: p.displayName,\n                                                    interface: p.interface,\n                                                    enabled: (i === clickedIndex)\n                                                };\n                                                providers.push(newProvider);\n                                            } else {\n                                                providers.push(p);\n                                            }\n                                        }\n                                        Config.utilities.vpn.provider = providers;\n                                        Config.save();\n\n                                        Qt.callLater(function () {\n                                            VPN.toggle();\n                                        });\n                                    }\n                                }\n                            }\n\n                            enabled: !VPN.connecting\n                        }\n\n                        MaterialIcon {\n                            id: connectIcon\n\n                            anchors.centerIn: parent\n                            text: VPN.connected && modelData.enabled ? \"link_off\" : \"link\"\n                            color: VPN.connected && modelData.enabled ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface\n                        }\n                    }\n\n                    StyledRect {\n                        implicitWidth: implicitHeight\n                        implicitHeight: deleteIcon.implicitHeight + Appearance.padding.smaller * 2\n\n                        radius: Appearance.rounding.full\n                        color: \"transparent\"\n\n                        StateLayer {\n                            function onClicked(): void {\n                                const providers = [];\n                                for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {\n                                    if (i !== modelData.index) {\n                                        providers.push(Config.utilities.vpn.provider[i]);\n                                    }\n                                }\n                                Config.utilities.vpn.provider = providers;\n                                Config.save();\n                            }\n                        }\n\n                        MaterialIcon {\n                            id: deleteIcon\n\n                            anchors.centerIn: parent\n                            text: \"delete\"\n                            color: Colours.palette.m3onSurface\n                        }\n                    }\n                }\n\n                implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2\n            }\n        }\n    }\n\n    Popup {\n        id: vpnDialog\n\n        property string currentState: \"selection\"\n        property int editIndex: -1\n        property string providerName: \"\"\n        property string displayName: \"\"\n        property string interfaceName: \"\"\n\n        function showProviderSelection(): void {\n            currentState = \"selection\";\n            open();\n        }\n\n        function closeWithAnimation(): void {\n            close();\n        }\n\n        function showAddForm(providerType: string, defaultDisplayName: string): void {\n            editIndex = -1;\n            providerName = providerType;\n            displayName = defaultDisplayName;\n            interfaceName = \"\";\n\n            if (currentState === \"selection\") {\n                transitionToForm.start();\n            } else {\n                currentState = \"form\";\n                isClosing = false;\n                open();\n            }\n        }\n\n        function showEditForm(index: int): void {\n            const provider = Config.utilities.vpn.provider[index];\n            const isObject = typeof provider === \"object\";\n\n            editIndex = index;\n            providerName = isObject ? (provider.name || \"custom\") : String(provider);\n            displayName = isObject ? (provider.displayName || providerName) : providerName;\n            interfaceName = isObject ? (provider.interface || \"\") : \"\";\n\n            currentState = \"form\";\n            open();\n        }\n\n        parent: Overlay.overlay\n        x: Math.round((parent.width - width) / 2)\n        y: Math.round((parent.height - height) / 2)\n        implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2)\n        padding: Appearance.padding.large * 1.5\n\n        modal: true\n        closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside\n\n        opacity: 0\n        scale: 0.7\n\n        enter: Transition {\n            ParallelAnimation {\n                Anim {\n                    property: \"opacity\"\n                    from: 0\n                    to: 1\n                    duration: Appearance.anim.durations.normal\n                    easing.bezierCurve: Appearance.anim.curves.emphasized\n                }\n                Anim {\n                    property: \"scale\"\n                    from: 0.7\n                    to: 1\n                    duration: Appearance.anim.durations.normal\n                    easing.bezierCurve: Appearance.anim.curves.emphasized\n                }\n            }\n        }\n\n        exit: Transition {\n            ParallelAnimation {\n                Anim {\n                    property: \"opacity\"\n                    from: 1\n                    to: 0\n                    duration: Appearance.anim.durations.small\n                    easing.bezierCurve: Appearance.anim.curves.emphasized\n                }\n                Anim {\n                    property: \"scale\"\n                    from: 1\n                    to: 0.7\n                    duration: Appearance.anim.durations.small\n                    easing.bezierCurve: Appearance.anim.curves.emphasized\n                }\n            }\n        }\n\n        Overlay.modal: Rectangle {\n            color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity)\n        }\n\n        onClosed: {\n            currentState = \"selection\";\n        }\n\n        background: StyledRect {\n            color: Colours.palette.m3surfaceContainerHigh\n            radius: Appearance.rounding.large\n\n            Elevation {\n                anchors.fill: parent\n                radius: parent.radius\n                level: 3\n                z: -1\n            }\n\n            Behavior on implicitHeight {\n                Anim {\n                    duration: Appearance.anim.durations.normal\n                    easing.bezierCurve: Appearance.anim.curves.emphasized\n                }\n            }\n        }\n\n        contentItem: Item {\n            implicitHeight: vpnDialog.currentState === \"selection\" ? selectionContent.implicitHeight : formContent.implicitHeight\n\n            Behavior on implicitHeight {\n                Anim {\n                    duration: Appearance.anim.durations.normal\n                    easing.bezierCurve: Appearance.anim.curves.emphasized\n                }\n            }\n\n            ColumnLayout {\n                id: selectionContent\n\n                anchors.fill: parent\n                spacing: Appearance.spacing.normal\n                visible: vpnDialog.currentState === \"selection\"\n                opacity: vpnDialog.currentState === \"selection\" ? 1 : 0\n\n                Behavior on opacity {\n                    Anim {\n                        duration: Appearance.anim.durations.small\n                        easing.bezierCurve: Appearance.anim.curves.emphasized\n                    }\n                }\n\n                StyledText {\n                    text: qsTr(\"Add VPN Provider\")\n                    font.pointSize: Appearance.font.size.large\n                    font.weight: 500\n                }\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: qsTr(\"Choose a provider to add\")\n                    wrapMode: Text.WordWrap\n                    color: Colours.palette.m3outline\n                    font.pointSize: Appearance.font.size.small\n                }\n\n                TextButton {\n                    Layout.topMargin: Appearance.spacing.normal\n                    Layout.fillWidth: true\n                    text: qsTr(\"NetBird\")\n                    inactiveColour: Colours.tPalette.m3surfaceContainerHigh\n                    inactiveOnColour: Colours.palette.m3onSurface\n                    onClicked: {\n                        const providers = [];\n                        for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {\n                            providers.push(Config.utilities.vpn.provider[i]);\n                        }\n                        providers.push({\n                            name: \"netbird\",\n                            displayName: \"NetBird\",\n                            interface: \"wt0\"\n                        });\n                        Config.utilities.vpn.provider = providers;\n                        Config.save();\n                        vpnDialog.closeWithAnimation();\n                    }\n                }\n\n                TextButton {\n                    Layout.fillWidth: true\n                    text: qsTr(\"Tailscale\")\n                    inactiveColour: Colours.tPalette.m3surfaceContainerHigh\n                    inactiveOnColour: Colours.palette.m3onSurface\n                    onClicked: {\n                        const providers = [];\n                        for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {\n                            providers.push(Config.utilities.vpn.provider[i]);\n                        }\n                        providers.push({\n                            name: \"tailscale\",\n                            displayName: \"Tailscale\",\n                            interface: \"tailscale0\"\n                        });\n                        Config.utilities.vpn.provider = providers;\n                        Config.save();\n                        vpnDialog.closeWithAnimation();\n                    }\n                }\n\n                TextButton {\n                    Layout.fillWidth: true\n                    text: qsTr(\"Cloudflare WARP\")\n                    inactiveColour: Colours.tPalette.m3surfaceContainerHigh\n                    inactiveOnColour: Colours.palette.m3onSurface\n                    onClicked: {\n                        const providers = [];\n                        for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {\n                            providers.push(Config.utilities.vpn.provider[i]);\n                        }\n                        providers.push({\n                            name: \"warp\",\n                            displayName: \"Cloudflare WARP\",\n                            interface: \"CloudflareWARP\"\n                        });\n                        Config.utilities.vpn.provider = providers;\n                        Config.save();\n                        vpnDialog.closeWithAnimation();\n                    }\n                }\n\n                TextButton {\n                    Layout.fillWidth: true\n                    text: qsTr(\"WireGuard (Custom)\")\n                    inactiveColour: Colours.tPalette.m3surfaceContainerHigh\n                    inactiveOnColour: Colours.palette.m3onSurface\n                    onClicked: {\n                        vpnDialog.showAddForm(\"wireguard\", \"WireGuard\");\n                    }\n                }\n\n                TextButton {\n                    Layout.topMargin: Appearance.spacing.normal\n                    Layout.fillWidth: true\n                    text: qsTr(\"Cancel\")\n                    inactiveColour: Colours.palette.m3secondaryContainer\n                    inactiveOnColour: Colours.palette.m3onSecondaryContainer\n                    onClicked: vpnDialog.closeWithAnimation()\n                }\n            }\n\n            ColumnLayout {\n                id: formContent\n\n                anchors.fill: parent\n                spacing: Appearance.spacing.normal\n                visible: vpnDialog.currentState === \"form\"\n                opacity: vpnDialog.currentState === \"form\" ? 1 : 0\n\n                Behavior on opacity {\n                    Anim {\n                        duration: Appearance.anim.durations.small\n                        easing.bezierCurve: Appearance.anim.curves.emphasized\n                    }\n                }\n\n                StyledText {\n                    text: vpnDialog.editIndex >= 0 ? qsTr(\"Edit VPN Provider\") : qsTr(\"Add %1 VPN\").arg(vpnDialog.displayName)\n                    font.pointSize: Appearance.font.size.large\n                    font.weight: 500\n                }\n\n                ColumnLayout {\n                    Layout.fillWidth: true\n                    spacing: Appearance.spacing.smaller / 2\n\n                    StyledText {\n                        text: qsTr(\"Display Name\")\n                        font.pointSize: Appearance.font.size.small\n                        color: Colours.palette.m3onSurfaceVariant\n                    }\n\n                    StyledRect {\n                        Layout.fillWidth: true\n                        implicitHeight: 40\n                        color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)\n                        radius: Appearance.rounding.small\n                        border.width: 1\n                        border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)\n\n                        Behavior on color {\n                            CAnim {}\n                        }\n                        Behavior on border.color {\n                            CAnim {}\n                        }\n\n                        StyledTextField {\n                            id: displayNameField\n\n                            anchors.centerIn: parent\n                            width: parent.width - Appearance.padding.normal\n                            horizontalAlignment: TextInput.AlignLeft\n                            text: vpnDialog.displayName\n                            onTextChanged: vpnDialog.displayName = text\n                        }\n                    }\n                }\n\n                ColumnLayout {\n                    Layout.fillWidth: true\n                    spacing: Appearance.spacing.smaller / 2\n\n                    StyledText {\n                        text: qsTr(\"Interface (e.g., wg0, torguard)\")\n                        font.pointSize: Appearance.font.size.small\n                        color: Colours.palette.m3onSurfaceVariant\n                    }\n\n                    StyledRect {\n                        Layout.fillWidth: true\n                        implicitHeight: 40\n                        color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)\n                        radius: Appearance.rounding.small\n                        border.width: 1\n                        border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)\n\n                        Behavior on color {\n                            CAnim {}\n                        }\n                        Behavior on border.color {\n                            CAnim {}\n                        }\n\n                        StyledTextField {\n                            id: interfaceNameField\n\n                            anchors.centerIn: parent\n                            width: parent.width - Appearance.padding.normal\n                            horizontalAlignment: TextInput.AlignLeft\n                            text: vpnDialog.interfaceName\n                            onTextChanged: vpnDialog.interfaceName = text\n                        }\n                    }\n                }\n\n                RowLayout {\n                    Layout.topMargin: Appearance.spacing.normal\n                    Layout.fillWidth: true\n                    spacing: Appearance.spacing.normal\n\n                    TextButton {\n                        Layout.fillWidth: true\n                        text: qsTr(\"Cancel\")\n                        inactiveColour: Colours.tPalette.m3surfaceContainerHigh\n                        inactiveOnColour: Colours.palette.m3onSurface\n                        onClicked: vpnDialog.closeWithAnimation()\n                    }\n\n                    TextButton {\n                        Layout.fillWidth: true\n                        text: qsTr(\"Save\")\n                        enabled: vpnDialog.interfaceName.length > 0\n                        inactiveColour: Colours.palette.m3primaryContainer\n                        inactiveOnColour: Colours.palette.m3onPrimaryContainer\n\n                        onClicked: {\n                            const providers = [];\n                            const newProvider = {\n                                name: vpnDialog.providerName,\n                                displayName: vpnDialog.displayName || vpnDialog.interfaceName,\n                                interface: vpnDialog.interfaceName\n                            };\n\n                            if (vpnDialog.editIndex >= 0) {\n                                for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {\n                                    if (i === vpnDialog.editIndex) {\n                                        providers.push(newProvider);\n                                    } else {\n                                        providers.push(Config.utilities.vpn.provider[i]);\n                                    }\n                                }\n                            } else {\n                                for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {\n                                    providers.push(Config.utilities.vpn.provider[i]);\n                                }\n                                providers.push(newProvider);\n                            }\n\n                            Config.utilities.vpn.provider = providers;\n                            Config.save();\n                            vpnDialog.closeWithAnimation();\n                        }\n                    }\n                }\n            }\n        }\n\n        SequentialAnimation {\n            id: transitionToForm\n\n            ParallelAnimation {\n                Anim {\n                    target: selectionContent\n                    property: \"opacity\"\n                    to: 0\n                    duration: Appearance.anim.durations.small\n                    easing.bezierCurve: Appearance.anim.curves.emphasized\n                }\n            }\n\n            ScriptAction {\n                script: {\n                    vpnDialog.currentState = \"form\";\n                }\n            }\n\n            ParallelAnimation {\n                Anim {\n                    target: formContent\n                    property: \"opacity\"\n                    to: 1\n                    duration: Appearance.anim.durations.small\n                    easing.bezierCurve: Appearance.anim.curves.emphasized\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/VpnSettings.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property Session session\n\n    spacing: Appearance.spacing.normal\n\n    SettingsHeader {\n        icon: \"vpn_key\"\n        title: qsTr(\"VPN Settings\")\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"General\")\n        description: qsTr(\"VPN configuration\")\n    }\n\n    SectionContainer {\n        ToggleRow {\n            label: qsTr(\"VPN enabled\")\n            checked: Config.utilities.vpn.enabled\n            toggle.onToggled: {\n                Config.utilities.vpn.enabled = checked;\n                Config.save();\n            }\n        }\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"Providers\")\n        description: qsTr(\"Manage VPN providers\")\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.normal\n\n        ListView {\n            Layout.fillWidth: true\n            Layout.preferredHeight: contentHeight\n\n            interactive: false\n            spacing: Appearance.spacing.smaller\n\n            model: ScriptModel {\n                values: Config.utilities.vpn.provider.map((provider, index) => {\n                    const isObject = typeof provider === \"object\";\n                    const name = isObject ? (provider.name || \"custom\") : String(provider);\n                    const displayName = isObject ? (provider.displayName || name) : name;\n                    const iface = isObject ? (provider.interface || \"\") : \"\";\n\n                    return {\n                        index: index,\n                        name: name,\n                        displayName: displayName,\n                        interface: iface,\n                        provider: provider,\n                        isActive: index === 0\n                    };\n                })\n            }\n\n            delegate: Component {\n                StyledRect {\n                    required property var modelData\n                    required property int index\n\n                    width: ListView.view ? ListView.view.width : undefined\n                    color: Colours.tPalette.m3surfaceContainerHigh\n                    radius: Appearance.rounding.normal\n\n                    RowLayout {\n                        anchors.left: parent.left\n                        anchors.right: parent.right\n                        anchors.verticalCenter: parent.verticalCenter\n                        anchors.margins: Appearance.padding.normal\n                        spacing: Appearance.spacing.normal\n\n                        MaterialIcon {\n                            text: modelData.isActive ? \"vpn_key\" : \"vpn_key_off\"\n                            font.pointSize: Appearance.font.size.large\n                            color: modelData.isActive ? Colours.palette.m3primary : Colours.palette.m3outline\n                        }\n\n                        ColumnLayout {\n                            Layout.fillWidth: true\n                            spacing: 0\n\n                            StyledText {\n                                text: modelData.displayName\n                                font.weight: modelData.isActive ? 500 : 400\n                            }\n\n                            StyledText {\n                                text: qsTr(\"%1 • %2\").arg(modelData.name).arg(modelData.interface || qsTr(\"No interface\"))\n                                font.pointSize: Appearance.font.size.small\n                                color: Colours.palette.m3outline\n                            }\n                        }\n\n                        IconButton {\n                            icon: modelData.isActive ? \"arrow_downward\" : \"arrow_upward\"\n                            visible: !modelData.isActive || Config.utilities.vpn.provider.length > 1\n                            onClicked: {\n                                if (modelData.isActive && index < Config.utilities.vpn.provider.length - 1) {\n                                    // Move down\n                                    const providers = [...Config.utilities.vpn.provider];\n                                    const temp = providers[index];\n                                    providers[index] = providers[index + 1];\n                                    providers[index + 1] = temp;\n                                    Config.utilities.vpn.provider = providers;\n                                    Config.save();\n                                } else if (!modelData.isActive) {\n                                    // Make active (move to top)\n                                    const providers = [...Config.utilities.vpn.provider];\n                                    const provider = providers.splice(index, 1)[0];\n                                    providers.unshift(provider);\n                                    Config.utilities.vpn.provider = providers;\n                                    Config.save();\n                                }\n                            }\n                        }\n\n                        IconButton {\n                            icon: \"delete\"\n                            onClicked: {\n                                const providers = [...Config.utilities.vpn.provider];\n                                providers.splice(index, 1);\n                                Config.utilities.vpn.provider = providers;\n                                Config.save();\n                            }\n                        }\n                    }\n\n                    implicitHeight: 60\n                }\n            }\n        }\n\n        TextButton {\n            Layout.fillWidth: true\n            Layout.topMargin: Appearance.spacing.normal\n            text: qsTr(\"+ Add Provider\")\n            inactiveColour: Colours.palette.m3primaryContainer\n            inactiveOnColour: Colours.palette.m3onPrimaryContainer\n\n            onClicked: {\n                addProviderDialog.open();\n            }\n        }\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"Quick Add\")\n        description: qsTr(\"Add common VPN providers\")\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.smaller\n\n        TextButton {\n            Layout.fillWidth: true\n            text: qsTr(\"+ Add NetBird\")\n            inactiveColour: Colours.tPalette.m3surfaceContainerHigh\n            inactiveOnColour: Colours.palette.m3onSurface\n\n            onClicked: {\n                const providers = [...Config.utilities.vpn.provider];\n                providers.push({\n                    name: \"netbird\",\n                    displayName: \"NetBird\",\n                    interface: \"wt0\"\n                });\n                Config.utilities.vpn.provider = providers;\n                Config.save();\n            }\n        }\n\n        TextButton {\n            Layout.fillWidth: true\n            text: qsTr(\"+ Add Tailscale\")\n            inactiveColour: Colours.tPalette.m3surfaceContainerHigh\n            inactiveOnColour: Colours.palette.m3onSurface\n\n            onClicked: {\n                const providers = [...Config.utilities.vpn.provider];\n                providers.push({\n                    name: \"tailscale\",\n                    displayName: \"Tailscale\",\n                    interface: \"tailscale0\"\n                });\n                Config.utilities.vpn.provider = providers;\n                Config.save();\n            }\n        }\n\n        TextButton {\n            Layout.fillWidth: true\n            text: qsTr(\"+ Add Cloudflare WARP\")\n            inactiveColour: Colours.tPalette.m3surfaceContainerHigh\n            inactiveOnColour: Colours.palette.m3onSurface\n\n            onClicked: {\n                const providers = [...Config.utilities.vpn.provider];\n                providers.push({\n                    name: \"warp\",\n                    displayName: \"Cloudflare WARP\",\n                    interface: \"CloudflareWARP\"\n                });\n                Config.utilities.vpn.provider = providers;\n                Config.save();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/WirelessDetails.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport qs.utils\nimport QtQuick\nimport QtQuick.Layouts\n\nDeviceDetails {\n    id: root\n\n    required property Session session\n    readonly property var network: root.session.network.active\n\n    function checkSavedProfile(): void {\n        if (network && network.ssid) {\n            Nmcli.loadSavedConnections(() => {});\n        }\n    }\n\n    function updateDeviceDetails(): void {\n        if (network && network.ssid) {\n            const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);\n            if (isActive) {\n                Nmcli.getWirelessDeviceDetails(\"\");\n            } else {\n                Nmcli.wirelessDeviceDetails = null;\n            }\n        } else {\n            Nmcli.wirelessDeviceDetails = null;\n        }\n    }\n\n    device: network\n\n    Component.onCompleted: {\n        updateDeviceDetails();\n        checkSavedProfile();\n    }\n\n    onNetworkChanged: {\n        connectionUpdateTimer.stop();\n        if (network && network.ssid) {\n            connectionUpdateTimer.start();\n        }\n        updateDeviceDetails();\n        checkSavedProfile();\n    }\n\n    headerComponent: Component {\n        ConnectionHeader {\n            icon: root.network?.isSecure ? \"lock\" : \"wifi\"\n            title: root.network?.ssid ?? qsTr(\"Unknown\")\n        }\n    }\n\n    sections: [\n        Component {\n            ColumnLayout {\n                spacing: Appearance.spacing.normal\n\n                SectionHeader {\n                    title: qsTr(\"Connection status\")\n                    description: qsTr(\"Connection settings for this network\")\n                }\n\n                SectionContainer {\n                    ToggleRow {\n                        label: qsTr(\"Connected\")\n                        checked: root.network?.active ?? false\n                        toggle.onToggled: {\n                            if (checked) {\n                                NetworkConnection.handleConnect(root.network, root.session, null);\n                            } else {\n                                Nmcli.disconnectFromNetwork();\n                            }\n                        }\n                    }\n\n                    TextButton {\n                        Layout.fillWidth: true\n                        Layout.topMargin: Appearance.spacing.normal\n                        Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2\n                        visible: {\n                            if (!root.network || !root.network.ssid) {\n                                return false;\n                            }\n                            return Nmcli.hasSavedProfile(root.network.ssid);\n                        }\n                        inactiveColour: Colours.palette.m3secondaryContainer\n                        inactiveOnColour: Colours.palette.m3onSecondaryContainer\n                        text: qsTr(\"Forget Network\")\n\n                        onClicked: {\n                            if (root.network && root.network.ssid) {\n                                if (root.network.active) {\n                                    Nmcli.disconnectFromNetwork();\n                                }\n                                Nmcli.forgetNetwork(root.network.ssid);\n                            }\n                        }\n                    }\n                }\n            }\n        },\n        Component {\n            ColumnLayout {\n                spacing: Appearance.spacing.normal\n\n                SectionHeader {\n                    title: qsTr(\"Network properties\")\n                    description: qsTr(\"Additional information\")\n                }\n\n                SectionContainer {\n                    contentSpacing: Appearance.spacing.small / 2\n\n                    PropertyRow {\n                        label: qsTr(\"SSID\")\n                        value: root.network?.ssid ?? qsTr(\"Unknown\")\n                    }\n\n                    PropertyRow {\n                        showTopMargin: true\n                        label: qsTr(\"BSSID\")\n                        value: root.network?.bssid ?? qsTr(\"Unknown\")\n                    }\n\n                    PropertyRow {\n                        showTopMargin: true\n                        label: qsTr(\"Signal strength\")\n                        value: root.network ? qsTr(\"%1%\").arg(root.network.strength) : qsTr(\"N/A\")\n                    }\n\n                    PropertyRow {\n                        showTopMargin: true\n                        label: qsTr(\"Frequency\")\n                        value: root.network ? qsTr(\"%1 MHz\").arg(root.network.frequency) : qsTr(\"N/A\")\n                    }\n\n                    PropertyRow {\n                        showTopMargin: true\n                        label: qsTr(\"Security\")\n                        value: root.network ? (root.network.isSecure ? root.network.security : qsTr(\"Open\")) : qsTr(\"N/A\")\n                    }\n                }\n            }\n        },\n        Component {\n            ColumnLayout {\n                spacing: Appearance.spacing.normal\n\n                SectionHeader {\n                    title: qsTr(\"Connection information\")\n                    description: qsTr(\"Network connection details\")\n                }\n\n                SectionContainer {\n                    ConnectionInfoSection {\n                        deviceDetails: Nmcli.wirelessDeviceDetails\n                    }\n                }\n            }\n        }\n    ]\n\n    Connections {\n        function onActiveChanged() {\n            root.updateDeviceDetails();\n        }\n        function onWirelessDeviceDetailsChanged() {\n            if (root.network && root.network.ssid) {\n                const isActive = root.network.active || (Nmcli.active && Nmcli.active.ssid === root.network.ssid);\n                if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) {\n                    connectionUpdateTimer.stop();\n                }\n            }\n        }\n\n        target: Nmcli\n    }\n\n    Timer {\n        id: connectionUpdateTimer\n\n        interval: 500\n        repeat: true\n        running: root.network && root.network.ssid\n        onTriggered: {\n            if (root.network) {\n                const isActive = root.network.active || (Nmcli.active && Nmcli.active.ssid === root.network.ssid);\n                if (isActive) {\n                    if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) {\n                        Nmcli.getWirelessDeviceDetails(\"\", () => {});\n                    } else {\n                        connectionUpdateTimer.stop();\n                    }\n                } else {\n                    if (Nmcli.wirelessDeviceDetails !== null) {\n                        Nmcli.wirelessDeviceDetails = null;\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/WirelessList.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nDeviceList {\n    id: root\n\n    required property Session session\n\n    function checkSavedProfileForNetwork(ssid: string): void {\n        if (ssid && ssid.length > 0) {\n            Nmcli.loadSavedConnections(() => {});\n        }\n    }\n\n    title: qsTr(\"Networks (%1)\").arg(Nmcli.networks.length)\n    description: qsTr(\"All available WiFi networks\")\n    activeItem: session.network.active\n\n    titleSuffix: Component {\n        StyledText {\n            visible: Nmcli.scanning\n            text: qsTr(\"Scanning...\")\n            color: Colours.palette.m3primary\n            font.pointSize: Appearance.font.size.small\n        }\n    }\n\n    model: ScriptModel {\n        values: [...Nmcli.networks].sort((a, b) => {\n            if (a.active !== b.active)\n                return b.active - a.active;\n            return b.strength - a.strength;\n        })\n    }\n\n    headerComponent: Component {\n        RowLayout {\n            spacing: Appearance.spacing.smaller\n\n            StyledText {\n                text: qsTr(\"Settings\")\n                font.pointSize: Appearance.font.size.large\n                font.weight: 500\n            }\n\n            Item {\n                Layout.fillWidth: true\n            }\n\n            ToggleButton {\n                toggled: Nmcli.wifiEnabled\n                icon: \"wifi\"\n                accent: \"Tertiary\"\n                iconSize: Appearance.font.size.normal\n                horizontalPadding: Appearance.padding.normal\n                verticalPadding: Appearance.padding.smaller\n\n                onClicked: {\n                    Nmcli.toggleWifi(null);\n                }\n            }\n\n            ToggleButton {\n                toggled: Nmcli.scanning\n                icon: \"wifi_find\"\n                accent: \"Secondary\"\n                iconSize: Appearance.font.size.normal\n                horizontalPadding: Appearance.padding.normal\n                verticalPadding: Appearance.padding.smaller\n\n                onClicked: {\n                    Nmcli.rescanWifi();\n                }\n            }\n\n            ToggleButton {\n                toggled: !root.session.network.active\n                icon: \"settings\"\n                accent: \"Primary\"\n                iconSize: Appearance.font.size.normal\n                horizontalPadding: Appearance.padding.normal\n                verticalPadding: Appearance.padding.smaller\n\n                onClicked: {\n                    if (root.session.network.active)\n                        root.session.network.active = null;\n                    else {\n                        root.session.network.active = root.view.model.get(0)?.modelData ?? null;\n                    }\n                }\n            }\n        }\n    }\n\n    delegate: Component {\n        StyledRect {\n            id: networkDelegate\n\n            required property var modelData\n\n            width: ListView.view ? ListView.view.width : undefined\n\n            color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === networkDelegate.modelData ? Colours.tPalette.m3surfaceContainer.a : 0)\n            radius: Appearance.rounding.normal\n\n            StateLayer {\n                function onClicked(): void {\n                    root.session.network.active = networkDelegate.modelData;\n                    if (networkDelegate.modelData && networkDelegate.modelData.ssid) {\n                        root.checkSavedProfileForNetwork(networkDelegate.modelData.ssid);\n                    }\n                }\n            }\n\n            RowLayout {\n                id: rowLayout\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.verticalCenter: parent.verticalCenter\n                anchors.margins: Appearance.padding.normal\n\n                spacing: Appearance.spacing.normal\n\n                StyledRect {\n                    implicitWidth: implicitHeight\n                    implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2\n\n                    radius: Appearance.rounding.normal\n                    color: networkDelegate.modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh\n\n                    MaterialIcon {\n                        id: icon\n\n                        anchors.centerIn: parent\n                        text: Icons.getNetworkIcon(networkDelegate.modelData.strength, networkDelegate.modelData.isSecure)\n                        font.pointSize: Appearance.font.size.large\n                        fill: networkDelegate.modelData.active ? 1 : 0\n                        color: networkDelegate.modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface\n                    }\n                }\n\n                ColumnLayout {\n                    Layout.fillWidth: true\n\n                    spacing: 0\n\n                    StyledText {\n                        Layout.fillWidth: true\n                        elide: Text.ElideRight\n                        maximumLineCount: 1\n\n                        text: networkDelegate.modelData.ssid || qsTr(\"Unknown\")\n                    }\n\n                    RowLayout {\n                        Layout.fillWidth: true\n                        spacing: Appearance.spacing.smaller\n\n                        StyledText {\n                            Layout.fillWidth: true\n                            text: {\n                                if (networkDelegate.modelData.active)\n                                    return qsTr(\"Connected\");\n                                if (networkDelegate.modelData.isSecure && networkDelegate.modelData.security && networkDelegate.modelData.security.length > 0) {\n                                    return networkDelegate.modelData.security;\n                                }\n                                if (networkDelegate.modelData.isSecure)\n                                    return qsTr(\"Secured\");\n                                return qsTr(\"Open\");\n                            }\n                            color: networkDelegate.modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline\n                            font.pointSize: Appearance.font.size.small\n                            font.weight: networkDelegate.modelData.active ? 500 : 400\n                            elide: Text.ElideRight\n                        }\n                    }\n                }\n\n                StyledRect {\n                    implicitWidth: implicitHeight\n                    implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2\n\n                    radius: Appearance.rounding.full\n                    color: Qt.alpha(Colours.palette.m3primaryContainer, networkDelegate.modelData.active ? 1 : 0)\n\n                    StateLayer {\n                        function onClicked(): void {\n                            if (networkDelegate.modelData.active) {\n                                Nmcli.disconnectFromNetwork();\n                            } else {\n                                NetworkConnection.handleConnect(networkDelegate.modelData, root.session, null);\n                            }\n                        }\n                    }\n\n                    MaterialIcon {\n                        id: connectIcon\n\n                        anchors.centerIn: parent\n                        text: networkDelegate.modelData.active ? \"link_off\" : \"link\"\n                        color: networkDelegate.modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface\n                    }\n                }\n            }\n\n            implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2\n        }\n    }\n\n    onItemSelected: function (item) {\n        session.network.active = item;\n        if (item && item.ssid) {\n            checkSavedProfileForNetwork(item.ssid);\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/WirelessPane.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components.containers\nimport QtQuick\n\nSplitPaneWithDetails {\n    id: root\n\n    required property Session session\n\n    anchors.fill: parent\n\n    activeItem: session.network.active\n    paneIdGenerator: function (item) {\n        return item ? (item.ssid || item.bssid || \"\") : \"\";\n    }\n\n    leftContent: Component {\n        WirelessList {\n            session: root.session\n        }\n    }\n\n    rightDetailsComponent: Component {\n        WirelessDetails {\n            session: root.session\n        }\n    }\n\n    rightSettingsComponent: Component {\n        StyledFlickable {\n            flickableDirection: Flickable.VerticalFlick\n            contentHeight: settingsInner.height\n            clip: true\n\n            WirelessSettings {\n                id: settingsInner\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                session: root.session\n            }\n        }\n    }\n\n    overlayComponent: Component {\n        WirelessPasswordDialog {\n            anchors.fill: parent\n            session: root.session\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/WirelessPasswordDialog.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property Session session\n\n    readonly property var network: {\n        if (session.network.pendingNetwork) {\n            return session.network.pendingNetwork;\n        }\n        if (session.network.active) {\n            return session.network.active;\n        }\n        return null;\n    }\n\n    property bool isClosing: false\n\n    function checkConnectionStatus(): void {\n        if (!root.visible || !connectButton.connecting) {\n            return;\n        }\n\n        const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();\n\n        if (isConnected) {\n            connectionSuccessTimer.start();\n            return;\n        }\n\n        if (Nmcli.pendingConnection === null && connectButton.connecting) {\n            if (connectionMonitor.repeatCount > 10) {\n                connectionMonitor.stop();\n                connectButton.connecting = false;\n                connectButton.hasError = true;\n                connectButton.enabled = true;\n                connectButton.text = qsTr(\"Connect\");\n                passwordContainer.passwordBuffer = \"\";\n                if (root.network && root.network.ssid) {\n                    Nmcli.forgetNetwork(root.network.ssid);\n                }\n            }\n        }\n    }\n\n    function closeDialog(): void {\n        if (isClosing) {\n            return;\n        }\n\n        isClosing = true;\n        passwordContainer.passwordBuffer = \"\";\n        connectButton.connecting = false;\n        connectButton.hasError = false;\n        connectButton.text = qsTr(\"Connect\");\n        connectionMonitor.stop();\n    }\n\n    visible: session.network.showPasswordDialog || isClosing\n    enabled: session.network.showPasswordDialog && !isClosing\n    focus: enabled\n\n    Keys.onEscapePressed: closeDialog()\n\n    Rectangle {\n        anchors.fill: parent\n        color: Qt.rgba(0, 0, 0, 0.5)\n        opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0\n\n        Behavior on opacity {\n            Anim {}\n        }\n\n        MouseArea {\n            anchors.fill: parent\n            onClicked: root.closeDialog()\n        }\n    }\n\n    StyledRect {\n        id: dialog\n\n        anchors.centerIn: parent\n\n        implicitWidth: 400\n        implicitHeight: content.implicitHeight + Appearance.padding.large * 2\n\n        radius: Appearance.rounding.normal\n        color: Colours.tPalette.m3surface\n        opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0\n        scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7\n\n        Behavior on opacity {\n            Anim {}\n        }\n\n        Behavior on scale {\n            Anim {}\n        }\n\n        ParallelAnimation {\n            running: root.isClosing\n            onFinished: {\n                if (root.isClosing) {\n                    root.session.network.showPasswordDialog = false;\n                    root.isClosing = false;\n                }\n            }\n\n            Anim {\n                target: dialog\n                property: \"opacity\"\n                to: 0\n            }\n            Anim {\n                target: dialog\n                property: \"scale\"\n                to: 0.7\n            }\n        }\n\n        Keys.onEscapePressed: root.closeDialog()\n\n        ColumnLayout {\n            id: content\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.margins: Appearance.padding.large\n\n            spacing: Appearance.spacing.normal\n\n            MaterialIcon {\n                Layout.alignment: Qt.AlignHCenter\n                text: \"lock\"\n                font.pointSize: Appearance.font.size.extraLarge * 2\n            }\n\n            StyledText {\n                Layout.alignment: Qt.AlignHCenter\n                text: qsTr(\"Enter password\")\n                font.pointSize: Appearance.font.size.large\n                font.weight: 500\n            }\n\n            StyledText {\n                Layout.alignment: Qt.AlignHCenter\n                text: root.network ? qsTr(\"Network: %1\").arg(root.network.ssid) : \"\"\n                color: Colours.palette.m3outline\n                font.pointSize: Appearance.font.size.small\n            }\n\n            StyledText {\n                id: statusText\n\n                Layout.alignment: Qt.AlignHCenter\n                Layout.topMargin: Appearance.spacing.small\n                visible: connectButton.connecting || connectButton.hasError\n                text: {\n                    if (connectButton.hasError) {\n                        return qsTr(\"Connection failed. Please check your password and try again.\");\n                    }\n                    if (connectButton.connecting) {\n                        return qsTr(\"Connecting...\");\n                    }\n                    return \"\";\n                }\n                color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.small\n                font.weight: 400\n                wrapMode: Text.WordWrap\n                Layout.maximumWidth: parent.width - Appearance.padding.large * 2\n            }\n\n            Item {\n                id: passwordContainer\n\n                property string passwordBuffer: \"\"\n\n                Layout.topMargin: Appearance.spacing.large\n                Layout.fillWidth: true\n                implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2)\n                focus: true\n                Keys.onPressed: event => {\n                    if (!activeFocus) {\n                        forceActiveFocus();\n                    }\n\n                    if (connectButton.hasError && event.text && event.text.length > 0) {\n                        connectButton.hasError = false;\n                    }\n\n                    if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {\n                        if (connectButton.enabled) {\n                            connectButton.clicked();\n                        }\n                        event.accepted = true;\n                    } else if (event.key === Qt.Key_Backspace) {\n                        if (event.modifiers & Qt.ControlModifier) {\n                            passwordBuffer = \"\";\n                        } else {\n                            passwordBuffer = passwordBuffer.slice(0, -1);\n                        }\n                        event.accepted = true;\n                    } else if (event.text && event.text.length > 0) {\n                        passwordBuffer += event.text;\n                        event.accepted = true;\n                    }\n                }\n\n                Connections {\n                    function onShowPasswordDialogChanged(): void {\n                        if (root.session.network.showPasswordDialog) {\n                            Qt.callLater(() => {\n                                passwordContainer.forceActiveFocus();\n                                passwordContainer.passwordBuffer = \"\";\n                                connectButton.hasError = false;\n                            });\n                        }\n                    }\n\n                    target: root.session.network\n                }\n\n                Connections {\n                    function onVisibleChanged(): void {\n                        if (root.visible) {\n                            Qt.callLater(() => {\n                                passwordContainer.forceActiveFocus();\n                            });\n                        }\n                    }\n\n                    target: root\n                }\n\n                StyledRect {\n                    anchors.fill: parent\n                    radius: Appearance.rounding.normal\n                    color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer\n                    border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0)\n                    border.color: {\n                        if (connectButton.hasError) {\n                            return Colours.palette.m3error;\n                        }\n                        if (passwordContainer.activeFocus) {\n                            return Colours.palette.m3primary;\n                        }\n                        return root.visible ? Colours.palette.m3outline : \"transparent\";\n                    }\n\n                    Behavior on border.color {\n                        CAnim {}\n                    }\n\n                    Behavior on border.width {\n                        CAnim {}\n                    }\n\n                    Behavior on color {\n                        CAnim {}\n                    }\n                }\n\n                StateLayer {\n                    function onClicked(): void {\n                        passwordContainer.forceActiveFocus();\n                    }\n\n                    hoverEnabled: false\n                    cursorShape: Qt.IBeamCursor\n                }\n\n                StyledText {\n                    id: placeholder\n\n                    anchors.centerIn: parent\n                    text: qsTr(\"Password\")\n                    color: Colours.palette.m3outline\n                    font.pointSize: Appearance.font.size.normal\n                    font.family: Appearance.font.family.mono\n                    opacity: passwordContainer.passwordBuffer ? 0 : 1\n\n                    Behavior on opacity {\n                        Anim {}\n                    }\n                }\n\n                ListView {\n                    id: charList\n\n                    readonly property int fullWidth: count * (implicitHeight + spacing) - spacing\n\n                    anchors.centerIn: parent\n                    implicitWidth: fullWidth\n                    implicitHeight: Appearance.font.size.normal\n\n                    orientation: Qt.Horizontal\n                    spacing: Appearance.spacing.small / 2\n                    interactive: false\n\n                    model: ScriptModel {\n                        values: passwordContainer.passwordBuffer.split(\"\")\n                    }\n\n                    delegate: StyledRect {\n                        id: ch\n\n                        implicitWidth: implicitHeight\n                        implicitHeight: charList.implicitHeight\n\n                        color: Colours.palette.m3onSurface\n                        radius: Appearance.rounding.small / 2\n\n                        opacity: 0\n                        scale: 0\n                        Component.onCompleted: {\n                            opacity = 1;\n                            scale = 1;\n                        }\n                        ListView.onRemove: removeAnim.start()\n\n                        SequentialAnimation {\n                            id: removeAnim\n\n                            PropertyAction {\n                                target: ch\n                                property: \"ListView.delayRemove\"\n                                value: true\n                            }\n                            ParallelAnimation {\n                                Anim {\n                                    target: ch\n                                    property: \"opacity\"\n                                    to: 0\n                                }\n                                Anim {\n                                    target: ch\n                                    property: \"scale\"\n                                    to: 0.5\n                                }\n                            }\n                            PropertyAction {\n                                target: ch\n                                property: \"ListView.delayRemove\"\n                                value: false\n                            }\n                        }\n\n                        Behavior on opacity {\n                            Anim {}\n                        }\n\n                        Behavior on scale {\n                            Anim {\n                                duration: Appearance.anim.durations.expressiveFastSpatial\n                                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                            }\n                        }\n                    }\n\n                    Behavior on implicitWidth {\n                        Anim {}\n                    }\n                }\n            }\n\n            RowLayout {\n                Layout.topMargin: Appearance.spacing.normal\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.normal\n\n                TextButton {\n                    id: cancelButton\n\n                    Layout.fillWidth: true\n                    Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2\n                    inactiveColour: Colours.palette.m3secondaryContainer\n                    inactiveOnColour: Colours.palette.m3onSecondaryContainer\n                    text: qsTr(\"Cancel\")\n\n                    onClicked: root.closeDialog()\n                }\n\n                TextButton {\n                    id: connectButton\n\n                    property bool connecting: false\n                    property bool hasError: false\n\n                    Layout.fillWidth: true\n                    Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2\n                    inactiveColour: Colours.palette.m3primary\n                    inactiveOnColour: Colours.palette.m3onPrimary\n                    text: qsTr(\"Connect\")\n                    enabled: passwordContainer.passwordBuffer.length > 0 && !connecting\n\n                    onClicked: {\n                        if (!root.network || connecting) {\n                            return;\n                        }\n\n                        const password = passwordContainer.passwordBuffer;\n                        if (!password || password.length === 0) {\n                            return;\n                        }\n\n                        hasError = false;\n                        connecting = true;\n                        enabled = false;\n                        text = qsTr(\"Connecting...\");\n\n                        NetworkConnection.connectWithPassword(root.network, password, result => {\n                            if (result && result.success) {} else if (result && result.needsPassword) {\n                                connectionMonitor.stop();\n                                connecting = false;\n                                hasError = true;\n                                enabled = true;\n                                text = qsTr(\"Connect\");\n                                passwordContainer.passwordBuffer = \"\";\n                                if (root.network && root.network.ssid) {\n                                    Nmcli.forgetNetwork(root.network.ssid);\n                                }\n                            } else {\n                                connectionMonitor.stop();\n                                connecting = false;\n                                hasError = true;\n                                enabled = true;\n                                text = qsTr(\"Connect\");\n                                passwordContainer.passwordBuffer = \"\";\n                                if (root.network && root.network.ssid) {\n                                    Nmcli.forgetNetwork(root.network.ssid);\n                                }\n                            }\n                        });\n\n                        connectionMonitor.start();\n                    }\n                }\n            }\n        }\n    }\n\n    Timer {\n        id: connectionMonitor\n\n        property int repeatCount: 0\n\n        interval: 1000\n        repeat: true\n        triggeredOnStart: false\n        onTriggered: {\n            repeatCount++;\n            root.checkConnectionStatus();\n        }\n\n        onRunningChanged: {\n            if (!running) {\n                repeatCount = 0;\n            }\n        }\n    }\n\n    Timer {\n        id: connectionSuccessTimer\n\n        interval: 500\n        onTriggered: {\n            if (root.visible && Nmcli.active && Nmcli.active.ssid) {\n                const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();\n                if (stillConnected) {\n                    connectionMonitor.stop();\n                    connectButton.connecting = false;\n                    connectButton.text = qsTr(\"Connect\");\n                    root.closeDialog();\n                }\n            }\n        }\n    }\n\n    Connections {\n        function onActiveChanged() {\n            if (root.visible) {\n                root.checkConnectionStatus();\n            }\n        }\n        function onConnectionFailed(ssid: string) {\n            if (root.visible && root.network && root.network.ssid === ssid && connectButton.connecting) {\n                connectionMonitor.stop();\n                connectButton.connecting = false;\n                connectButton.hasError = true;\n                connectButton.enabled = true;\n                connectButton.text = qsTr(\"Connect\");\n                passwordContainer.passwordBuffer = \"\";\n                Nmcli.forgetNetwork(ssid);\n            }\n        }\n\n        target: Nmcli\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/network/WirelessSettings.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property Session session\n\n    spacing: Appearance.spacing.normal\n\n    SettingsHeader {\n        icon: \"wifi\"\n        title: qsTr(\"Network settings\")\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"WiFi status\")\n        description: qsTr(\"General WiFi settings\")\n    }\n\n    SectionContainer {\n        ToggleRow {\n            label: qsTr(\"WiFi enabled\")\n            checked: Nmcli.wifiEnabled\n            toggle.onToggled: {\n                Nmcli.enableWifi(checked);\n            }\n        }\n    }\n\n    SectionHeader {\n        Layout.topMargin: Appearance.spacing.large\n        title: qsTr(\"Network information\")\n        description: qsTr(\"Current network connection\")\n    }\n\n    SectionContainer {\n        contentSpacing: Appearance.spacing.small / 2\n\n        PropertyRow {\n            label: qsTr(\"Connected network\")\n            value: Nmcli.active ? Nmcli.active.ssid : qsTr(\"Not connected\")\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            label: qsTr(\"Signal strength\")\n            value: Nmcli.active ? qsTr(\"%1%\").arg(Nmcli.active.strength) : qsTr(\"N/A\")\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            label: qsTr(\"Security\")\n            value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr(\"Secured\") : qsTr(\"Open\")) : qsTr(\"N/A\")\n        }\n\n        PropertyRow {\n            showTopMargin: true\n            label: qsTr(\"Frequency\")\n            value: Nmcli.active ? qsTr(\"%1 MHz\").arg(Nmcli.active.frequency) : qsTr(\"N/A\")\n        }\n    }\n}\n"
  },
  {
    "path": "modules/controlcenter/state/BluetoothState.qml",
    "content": "import Quickshell.Bluetooth\nimport QtQuick\n\nQtObject {\n    id: root\n\n    property BluetoothDevice active: null\n    property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter\n    property bool editingAdapterName: false\n    property bool fabMenuOpen: false\n    property bool editingDeviceName: false\n}\n"
  },
  {
    "path": "modules/controlcenter/state/EthernetState.qml",
    "content": "import QtQuick\n\nQtObject {\n    id: root\n\n    property var active: null\n}\n"
  },
  {
    "path": "modules/controlcenter/state/LauncherState.qml",
    "content": "import QtQuick\n\nQtObject {\n    id: root\n\n    property var active: null\n}\n"
  },
  {
    "path": "modules/controlcenter/state/NetworkState.qml",
    "content": "import QtQuick\n\nQtObject {\n    id: root\n\n    property var active: null\n    property bool showPasswordDialog: false\n    property var pendingNetwork: null\n}\n"
  },
  {
    "path": "modules/controlcenter/state/VpnState.qml",
    "content": "import QtQuick\n\nQtObject {\n    property var active: null\n}\n"
  },
  {
    "path": "modules/controlcenter/taskbar/TaskbarPane.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"..\"\nimport \"../components\"\nimport qs.components\nimport qs.components.controls\nimport qs.components.effects\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property Session session\n\n    property bool activeWindowCompact: Config.bar.activeWindow.compact ?? false\n    property bool activeWindowInverted: Config.bar.activeWindow.inverted ?? false\n    property bool clockShowIcon: Config.bar.clock.showIcon ?? true\n    property bool clockBackground: Config.bar.clock.background ?? false\n    property bool clockShowDate: Config.bar.clock.showDate ?? false\n    property bool persistent: Config.bar.persistent ?? true\n    property bool showOnHover: Config.bar.showOnHover ?? true\n    property int dragThreshold: Config.bar.dragThreshold ?? 20\n    property bool showAudio: Config.bar.status.showAudio ?? true\n    property bool showMicrophone: Config.bar.status.showMicrophone ?? true\n    property bool showKbLayout: Config.bar.status.showKbLayout ?? false\n    property bool showNetwork: Config.bar.status.showNetwork ?? true\n    property bool showWifi: Config.bar.status.showWifi ?? true\n    property bool showBluetooth: Config.bar.status.showBluetooth ?? true\n    property bool showBattery: Config.bar.status.showBattery ?? true\n    property bool showLockStatus: Config.bar.status.showLockStatus ?? true\n    property bool trayBackground: Config.bar.tray.background ?? false\n    property bool trayCompact: Config.bar.tray.compact ?? false\n    property bool trayRecolour: Config.bar.tray.recolour ?? false\n    property int workspacesShown: Config.bar.workspaces.shown ?? 5\n    property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true\n    property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false\n    property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false\n    property int workspacesMaxWindowIcons: Config.bar.workspaces.maxWindowIcons ?? 0\n    property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true\n    property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true\n    property bool scrollVolume: Config.bar.scrollActions.volume ?? true\n    property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true\n    property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true\n    property bool popoutTray: Config.bar.popouts.tray ?? true\n    property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true\n    property list<string> monitorNames: Hypr.monitorNames()\n    property list<string> excludedScreens: Config.bar.excludedScreens ?? []\n\n    function saveConfig(entryIndex, entryEnabled) {\n        Config.bar.activeWindow.compact = root.activeWindowCompact;\n        Config.bar.activeWindow.inverted = root.activeWindowInverted;\n        Config.bar.clock.background = root.clockBackground;\n        Config.bar.clock.showDate = root.clockShowDate;\n        Config.bar.clock.showIcon = root.clockShowIcon;\n        Config.bar.persistent = root.persistent;\n        Config.bar.showOnHover = root.showOnHover;\n        Config.bar.dragThreshold = root.dragThreshold;\n        Config.bar.status.showAudio = root.showAudio;\n        Config.bar.status.showMicrophone = root.showMicrophone;\n        Config.bar.status.showKbLayout = root.showKbLayout;\n        Config.bar.status.showNetwork = root.showNetwork;\n        Config.bar.status.showWifi = root.showWifi;\n        Config.bar.status.showBluetooth = root.showBluetooth;\n        Config.bar.status.showBattery = root.showBattery;\n        Config.bar.status.showLockStatus = root.showLockStatus;\n        Config.bar.tray.background = root.trayBackground;\n        Config.bar.tray.compact = root.trayCompact;\n        Config.bar.tray.recolour = root.trayRecolour;\n        Config.bar.workspaces.shown = root.workspacesShown;\n        Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator;\n        Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg;\n        Config.bar.workspaces.showWindows = root.workspacesShowWindows;\n        Config.bar.workspaces.maxWindowIcons = root.workspacesMaxWindowIcons;\n        Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor;\n        Config.bar.scrollActions.workspaces = root.scrollWorkspaces;\n        Config.bar.scrollActions.volume = root.scrollVolume;\n        Config.bar.scrollActions.brightness = root.scrollBrightness;\n        Config.bar.popouts.activeWindow = root.popoutActiveWindow;\n        Config.bar.popouts.tray = root.popoutTray;\n        Config.bar.popouts.statusIcons = root.popoutStatusIcons;\n        Config.bar.excludedScreens = root.excludedScreens;\n\n        const entries = [];\n        for (let i = 0; i < entriesModel.count; i++) {\n            const entry = entriesModel.get(i);\n            let enabled = entry.enabled;\n            if (entryIndex !== undefined && i === entryIndex) {\n                enabled = entryEnabled;\n            }\n            entries.push({\n                id: entry.id,\n                enabled: enabled\n            });\n        }\n        Config.bar.entries = entries;\n        Config.save();\n    }\n\n    anchors.fill: parent\n\n    Component.onCompleted: {\n        if (Config.bar.entries) {\n            entriesModel.clear();\n            for (let i = 0; i < Config.bar.entries.length; i++) {\n                const entry = Config.bar.entries[i];\n                entriesModel.append({\n                    id: entry.id,\n                    enabled: entry.enabled !== false\n                });\n            }\n        }\n    }\n\n    ListModel {\n        id: entriesModel\n    }\n\n    ClippingRectangle {\n        id: taskbarClippingRect\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.normal\n        anchors.leftMargin: 0\n        anchors.rightMargin: Appearance.padding.normal\n\n        radius: taskbarBorder.innerRadius\n        color: \"transparent\"\n\n        Loader {\n            id: taskbarLoader\n\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.large + Appearance.padding.normal\n            anchors.leftMargin: Appearance.padding.large\n            anchors.rightMargin: Appearance.padding.large\n\n            asynchronous: true\n            sourceComponent: taskbarContentComponent\n        }\n    }\n\n    InnerBorder {\n        id: taskbarBorder\n\n        leftThickness: 0\n        rightThickness: Appearance.padding.normal\n    }\n\n    Component {\n        id: taskbarContentComponent\n\n        StyledFlickable {\n            id: sidebarFlickable\n\n            flickableDirection: Flickable.VerticalFlick\n            contentHeight: sidebarLayout.height\n\n            StyledScrollBar.vertical: StyledScrollBar {\n                flickable: sidebarFlickable\n            }\n\n            ColumnLayout {\n                id: sidebarLayout\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.top: parent.top\n\n                spacing: Appearance.spacing.normal\n\n                RowLayout {\n                    spacing: Appearance.spacing.smaller\n\n                    StyledText {\n                        text: qsTr(\"Taskbar\")\n                        font.pointSize: Appearance.font.size.large\n                        font.weight: 500\n                    }\n                }\n\n                SectionContainer {\n                    Layout.fillWidth: true\n                    alignTop: true\n\n                    StyledText {\n                        text: qsTr(\"Status Icons\")\n                        font.pointSize: Appearance.font.size.normal\n                    }\n\n                    ConnectedButtonGroup {\n                        rootItem: root\n\n                        options: [\n                            {\n                                label: qsTr(\"Speakers\"),\n                                propertyName: \"showAudio\",\n                                onToggled: function (checked) {\n                                    root.showAudio = checked;\n                                    root.saveConfig();\n                                }\n                            },\n                            {\n                                label: qsTr(\"Microphone\"),\n                                propertyName: \"showMicrophone\",\n                                onToggled: function (checked) {\n                                    root.showMicrophone = checked;\n                                    root.saveConfig();\n                                }\n                            },\n                            {\n                                label: qsTr(\"Keyboard\"),\n                                propertyName: \"showKbLayout\",\n                                onToggled: function (checked) {\n                                    root.showKbLayout = checked;\n                                    root.saveConfig();\n                                }\n                            },\n                            {\n                                label: qsTr(\"Network\"),\n                                propertyName: \"showNetwork\",\n                                onToggled: function (checked) {\n                                    root.showNetwork = checked;\n                                    root.saveConfig();\n                                }\n                            },\n                            {\n                                label: qsTr(\"Wifi\"),\n                                propertyName: \"showWifi\",\n                                onToggled: function (checked) {\n                                    root.showWifi = checked;\n                                    root.saveConfig();\n                                }\n                            },\n                            {\n                                label: qsTr(\"Bluetooth\"),\n                                propertyName: \"showBluetooth\",\n                                onToggled: function (checked) {\n                                    root.showBluetooth = checked;\n                                    root.saveConfig();\n                                }\n                            },\n                            {\n                                label: qsTr(\"Battery\"),\n                                propertyName: \"showBattery\",\n                                onToggled: function (checked) {\n                                    root.showBattery = checked;\n                                    root.saveConfig();\n                                }\n                            },\n                            {\n                                label: qsTr(\"Capslock\"),\n                                propertyName: \"showLockStatus\",\n                                onToggled: function (checked) {\n                                    root.showLockStatus = checked;\n                                    root.saveConfig();\n                                }\n                            }\n                        ]\n                    }\n                }\n\n                RowLayout {\n                    id: mainRowLayout\n\n                    Layout.fillWidth: true\n                    spacing: Appearance.spacing.normal\n\n                    ColumnLayout {\n                        id: leftColumnLayout\n\n                        Layout.fillWidth: true\n                        Layout.alignment: Qt.AlignTop\n                        spacing: Appearance.spacing.normal\n\n                        SectionContainer {\n                            Layout.fillWidth: true\n                            alignTop: true\n\n                            StyledText {\n                                text: qsTr(\"Workspaces\")\n                                font.pointSize: Appearance.font.size.normal\n                            }\n\n                            StyledRect {\n                                Layout.fillWidth: true\n                                implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2\n                                radius: Appearance.rounding.normal\n                                color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n\n                                Behavior on implicitHeight {\n                                    Anim {}\n                                }\n\n                                RowLayout {\n                                    id: workspacesShownRow\n\n                                    anchors.left: parent.left\n                                    anchors.right: parent.right\n                                    anchors.verticalCenter: parent.verticalCenter\n                                    anchors.margins: Appearance.padding.large\n                                    spacing: Appearance.spacing.normal\n\n                                    StyledText {\n                                        Layout.fillWidth: true\n                                        text: qsTr(\"Shown\")\n                                    }\n\n                                    CustomSpinBox {\n                                        min: 1\n                                        max: 20\n                                        value: root.workspacesShown\n                                        onValueModified: value => {\n                                            root.workspacesShown = value;\n                                            root.saveConfig();\n                                        }\n                                    }\n                                }\n                            }\n\n                            StyledRect {\n                                Layout.fillWidth: true\n                                implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2\n                                radius: Appearance.rounding.normal\n                                color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n\n                                Behavior on implicitHeight {\n                                    Anim {}\n                                }\n\n                                RowLayout {\n                                    id: workspacesActiveIndicatorRow\n\n                                    anchors.left: parent.left\n                                    anchors.right: parent.right\n                                    anchors.verticalCenter: parent.verticalCenter\n                                    anchors.margins: Appearance.padding.large\n                                    spacing: Appearance.spacing.normal\n\n                                    StyledText {\n                                        Layout.fillWidth: true\n                                        text: qsTr(\"Active indicator\")\n                                    }\n\n                                    StyledSwitch {\n                                        checked: root.workspacesActiveIndicator\n                                        onToggled: {\n                                            root.workspacesActiveIndicator = checked;\n                                            root.saveConfig();\n                                        }\n                                    }\n                                }\n                            }\n\n                            StyledRect {\n                                Layout.fillWidth: true\n                                implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2\n                                radius: Appearance.rounding.normal\n                                color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n\n                                Behavior on implicitHeight {\n                                    Anim {}\n                                }\n\n                                RowLayout {\n                                    id: workspacesOccupiedBgRow\n\n                                    anchors.left: parent.left\n                                    anchors.right: parent.right\n                                    anchors.verticalCenter: parent.verticalCenter\n                                    anchors.margins: Appearance.padding.large\n                                    spacing: Appearance.spacing.normal\n\n                                    StyledText {\n                                        Layout.fillWidth: true\n                                        text: qsTr(\"Occupied background\")\n                                    }\n\n                                    StyledSwitch {\n                                        checked: root.workspacesOccupiedBg\n                                        onToggled: {\n                                            root.workspacesOccupiedBg = checked;\n                                            root.saveConfig();\n                                        }\n                                    }\n                                }\n                            }\n\n                            StyledRect {\n                                Layout.fillWidth: true\n                                implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2\n                                radius: Appearance.rounding.normal\n                                color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n\n                                Behavior on implicitHeight {\n                                    Anim {}\n                                }\n\n                                RowLayout {\n                                    id: workspacesShowWindowsRow\n\n                                    anchors.left: parent.left\n                                    anchors.right: parent.right\n                                    anchors.verticalCenter: parent.verticalCenter\n                                    anchors.margins: Appearance.padding.large\n                                    spacing: Appearance.spacing.normal\n\n                                    StyledText {\n                                        Layout.fillWidth: true\n                                        text: qsTr(\"Show windows\")\n                                    }\n\n                                    StyledSwitch {\n                                        checked: root.workspacesShowWindows\n                                        onToggled: {\n                                            root.workspacesShowWindows = checked;\n                                            root.saveConfig();\n                                        }\n                                    }\n                                }\n                            }\n\n                            StyledRect {\n                                Layout.fillWidth: true\n                                implicitHeight: workspacesMaxWindowIconsRow.implicitHeight + Appearance.padding.large * 2\n                                radius: Appearance.rounding.normal\n                                color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n\n                                Behavior on implicitHeight {\n                                    Anim {}\n                                }\n\n                                RowLayout {\n                                    id: workspacesMaxWindowIconsRow\n\n                                    anchors.left: parent.left\n                                    anchors.right: parent.right\n                                    anchors.verticalCenter: parent.verticalCenter\n                                    anchors.margins: Appearance.padding.large\n                                    spacing: Appearance.spacing.normal\n\n                                    StyledText {\n                                        Layout.fillWidth: true\n                                        text: qsTr(\"Max window icons\")\n                                    }\n\n                                    CustomSpinBox {\n                                        min: 0\n                                        max: 20\n                                        value: root.workspacesMaxWindowIcons\n                                        onValueModified: value => {\n                                            root.workspacesMaxWindowIcons = value;\n                                            root.saveConfig();\n                                        }\n                                    }\n                                }\n                            }\n\n                            StyledRect {\n                                Layout.fillWidth: true\n                                implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2\n                                radius: Appearance.rounding.normal\n                                color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n\n                                Behavior on implicitHeight {\n                                    Anim {}\n                                }\n\n                                RowLayout {\n                                    id: workspacesPerMonitorRow\n\n                                    anchors.left: parent.left\n                                    anchors.right: parent.right\n                                    anchors.verticalCenter: parent.verticalCenter\n                                    anchors.margins: Appearance.padding.large\n                                    spacing: Appearance.spacing.normal\n\n                                    StyledText {\n                                        Layout.fillWidth: true\n                                        text: qsTr(\"Per monitor workspaces\")\n                                    }\n\n                                    StyledSwitch {\n                                        checked: root.workspacesPerMonitor\n                                        onToggled: {\n                                            root.workspacesPerMonitor = checked;\n                                            root.saveConfig();\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        SectionContainer {\n                            Layout.fillWidth: true\n                            alignTop: true\n\n                            StyledText {\n                                text: qsTr(\"Scroll Actions\")\n                                font.pointSize: Appearance.font.size.normal\n                            }\n\n                            ConnectedButtonGroup {\n                                rootItem: root\n\n                                options: [\n                                    {\n                                        label: qsTr(\"Workspaces\"),\n                                        propertyName: \"scrollWorkspaces\",\n                                        onToggled: function (checked) {\n                                            root.scrollWorkspaces = checked;\n                                            root.saveConfig();\n                                        }\n                                    },\n                                    {\n                                        label: qsTr(\"Volume\"),\n                                        propertyName: \"scrollVolume\",\n                                        onToggled: function (checked) {\n                                            root.scrollVolume = checked;\n                                            root.saveConfig();\n                                        }\n                                    },\n                                    {\n                                        label: qsTr(\"Brightness\"),\n                                        propertyName: \"scrollBrightness\",\n                                        onToggled: function (checked) {\n                                            root.scrollBrightness = checked;\n                                            root.saveConfig();\n                                        }\n                                    }\n                                ]\n                            }\n                        }\n                    }\n\n                    ColumnLayout {\n                        id: middleColumnLayout\n\n                        Layout.fillWidth: true\n                        Layout.alignment: Qt.AlignTop\n                        spacing: Appearance.spacing.normal\n\n                        SectionContainer {\n                            Layout.fillWidth: true\n                            alignTop: true\n\n                            StyledText {\n                                text: qsTr(\"Clock\")\n                                font.pointSize: Appearance.font.size.normal\n                            }\n\n                            SwitchRow {\n                                label: qsTr(\"Background\")\n                                checked: root.clockBackground\n                                onToggled: checked => {\n                                    root.clockBackground = checked;\n                                    root.saveConfig();\n                                }\n                            }\n\n                            SwitchRow {\n                                label: qsTr(\"Show date\")\n                                checked: root.clockShowDate\n                                onToggled: checked => {\n                                    root.clockShowDate = checked;\n                                    root.saveConfig();\n                                }\n                            }\n\n                            SwitchRow {\n                                label: qsTr(\"Show clock icon\")\n                                checked: root.clockShowIcon\n                                onToggled: checked => {\n                                    root.clockShowIcon = checked;\n                                    root.saveConfig();\n                                }\n                            }\n                        }\n\n                        SectionContainer {\n                            Layout.fillWidth: true\n                            alignTop: true\n\n                            StyledText {\n                                text: qsTr(\"Bar Behavior\")\n                                font.pointSize: Appearance.font.size.normal\n                            }\n\n                            SwitchRow {\n                                label: qsTr(\"Persistent\")\n                                checked: root.persistent\n                                onToggled: checked => {\n                                    root.persistent = checked;\n                                    root.saveConfig();\n                                }\n                            }\n\n                            SwitchRow {\n                                label: qsTr(\"Show on hover\")\n                                checked: root.showOnHover\n                                onToggled: checked => {\n                                    root.showOnHover = checked;\n                                    root.saveConfig();\n                                }\n                            }\n\n                            SectionContainer {\n                                contentSpacing: Appearance.spacing.normal\n\n                                SliderInput {\n                                    Layout.fillWidth: true\n\n                                    label: qsTr(\"Drag threshold\")\n                                    value: root.dragThreshold\n                                    from: 0\n                                    to: 100\n                                    suffix: \"px\"\n                                    validator: IntValidator {\n                                        bottom: 0\n                                        top: 100\n                                    }\n                                    formatValueFunction: val => Math.round(val).toString()\n                                    parseValueFunction: text => parseInt(text)\n\n                                    onValueModified: newValue => {\n                                        root.dragThreshold = Math.round(newValue);\n                                        root.saveConfig();\n                                    }\n                                }\n                            }\n                        }\n\n                        SectionContainer {\n                            Layout.fillWidth: true\n                            alignTop: true\n\n                            StyledText {\n                                text: qsTr(\"Active window\")\n                                font.pointSize: Appearance.font.size.normal\n                            }\n\n                            SwitchRow {\n                                label: qsTr(\"Compact\")\n                                checked: root.activeWindowCompact\n                                onToggled: checked => {\n                                    root.activeWindowCompact = checked;\n                                    root.saveConfig();\n                                }\n                            }\n\n                            SwitchRow {\n                                label: qsTr(\"Inverted\")\n                                checked: root.activeWindowInverted\n                                onToggled: checked => {\n                                    root.activeWindowInverted = checked;\n                                    root.saveConfig();\n                                }\n                            }\n                        }\n                    }\n\n                    ColumnLayout {\n                        id: rightColumnLayout\n\n                        Layout.fillWidth: true\n                        Layout.alignment: Qt.AlignTop\n                        spacing: Appearance.spacing.normal\n\n                        SectionContainer {\n                            Layout.fillWidth: true\n                            alignTop: true\n\n                            StyledText {\n                                text: qsTr(\"Popouts\")\n                                font.pointSize: Appearance.font.size.normal\n                            }\n\n                            SwitchRow {\n                                label: qsTr(\"Active window\")\n                                checked: root.popoutActiveWindow\n                                onToggled: checked => {\n                                    root.popoutActiveWindow = checked;\n                                    root.saveConfig();\n                                }\n                            }\n\n                            SwitchRow {\n                                label: qsTr(\"Tray\")\n                                checked: root.popoutTray\n                                onToggled: checked => {\n                                    root.popoutTray = checked;\n                                    root.saveConfig();\n                                }\n                            }\n\n                            SwitchRow {\n                                label: qsTr(\"Status icons\")\n                                checked: root.popoutStatusIcons\n                                onToggled: checked => {\n                                    root.popoutStatusIcons = checked;\n                                    root.saveConfig();\n                                }\n                            }\n                        }\n\n                        SectionContainer {\n                            Layout.fillWidth: true\n                            alignTop: true\n\n                            StyledText {\n                                text: qsTr(\"Tray Settings\")\n                                font.pointSize: Appearance.font.size.normal\n                            }\n\n                            ConnectedButtonGroup {\n                                rootItem: root\n\n                                options: [\n                                    {\n                                        label: qsTr(\"Background\"),\n                                        propertyName: \"trayBackground\",\n                                        onToggled: function (checked) {\n                                            root.trayBackground = checked;\n                                            root.saveConfig();\n                                        }\n                                    },\n                                    {\n                                        label: qsTr(\"Compact\"),\n                                        propertyName: \"trayCompact\",\n                                        onToggled: function (checked) {\n                                            root.trayCompact = checked;\n                                            root.saveConfig();\n                                        }\n                                    },\n                                    {\n                                        label: qsTr(\"Recolour\"),\n                                        propertyName: \"trayRecolour\",\n                                        onToggled: function (checked) {\n                                            root.trayRecolour = checked;\n                                            root.saveConfig();\n                                        }\n                                    }\n                                ]\n                            }\n                        }\n\n                        SectionContainer {\n                            Layout.fillWidth: true\n                            alignTop: true\n\n                            StyledText {\n                                text: qsTr(\"Monitors\")\n                                font.pointSize: Appearance.font.size.normal\n                            }\n\n                            ConnectedButtonGroup {\n                                rootItem: root\n                                // max 3 options per line\n                                rows: Math.ceil(root.monitorNames.length / 3)\n\n                                options: root.monitorNames.map(e => ({\n                                            label: qsTr(e),\n                                            propertyName: `monitor${e}`,\n                                            onToggled: function (_) {\n                                                // if the given monitor is in the excluded list, it should be added back\n                                                let addedBack = excludedScreens.includes(e);\n                                                if (addedBack) {\n                                                    const index = excludedScreens.indexOf(e);\n                                                    if (index !== -1) {\n                                                        excludedScreens.splice(index, 1);\n                                                    }\n                                                } else {\n                                                    if (!excludedScreens.includes(e)) {\n                                                        excludedScreens.push(e);\n                                                    }\n                                                }\n                                                root.saveConfig();\n                                            },\n                                            state: !Strings.testRegexList(root.excludedScreens, e)\n                                        }))\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/Background.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Shapes\n\nShapePath {\n    id: root\n\n    required property Wrapper wrapper\n    readonly property real rounding: Config.border.rounding\n    readonly property bool flatten: wrapper.height < rounding * 2\n    readonly property real roundingY: flatten ? wrapper.height / 2 : rounding\n\n    strokeWidth: -1\n    fillColor: Colours.palette.m3surface\n\n    PathArc {\n        relativeX: root.rounding\n        relativeY: root.roundingY\n        radiusX: root.rounding\n        radiusY: Math.min(root.rounding, root.wrapper.height)\n    }\n\n    PathLine {\n        relativeX: 0\n        relativeY: root.wrapper.height - root.roundingY * 2\n    }\n\n    PathArc {\n        relativeX: root.rounding\n        relativeY: root.roundingY\n        radiusX: root.rounding\n        radiusY: Math.min(root.rounding, root.wrapper.height)\n        direction: PathArc.Counterclockwise\n    }\n\n    PathLine {\n        relativeX: root.wrapper.width - root.rounding * 2\n        relativeY: 0\n    }\n\n    PathArc {\n        relativeX: root.rounding\n        relativeY: -root.roundingY\n        radiusX: root.rounding\n        radiusY: Math.min(root.rounding, root.wrapper.height)\n        direction: PathArc.Counterclockwise\n    }\n\n    PathLine {\n        relativeX: 0\n        relativeY: -(root.wrapper.height - root.roundingY * 2)\n    }\n\n    PathArc {\n        relativeX: root.rounding\n        relativeY: -root.roundingY\n        radiusX: root.rounding\n        radiusY: Math.min(root.rounding, root.wrapper.height)\n    }\n\n    Behavior on fillColor {\n        CAnim {}\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/Content.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.filedialog\nimport qs.config\nimport Quickshell\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property DrawerVisibilities visibilities\n    readonly property bool needsKeyboard: {\n        const count = repeater.count;\n        for (let i = 0; i < count; i++) {\n            const item = repeater.itemAt(i) as Loader;\n            if (item?.sourceComponent === mediaComponent && (item?.item as MediaWrapper)?.needsKeyboard)\n                return true;\n        }\n        return false;\n    }\n    required property DashboardState state\n    required property FileDialog facePicker\n\n    readonly property var dashboardTabs: {\n        const allTabs = [\n            {\n                component: dashComponent,\n                iconName: \"dashboard\",\n                text: qsTr(\"Dashboard\"),\n                enabled: Config.dashboard.showDashboard\n            },\n            {\n                component: mediaComponent,\n                iconName: \"queue_music\",\n                text: qsTr(\"Media\"),\n                enabled: Config.dashboard.showMedia\n            },\n            {\n                component: performanceComponent,\n                iconName: \"speed\",\n                text: qsTr(\"Performance\"),\n                enabled: Config.dashboard.showPerformance && (Config.dashboard.performance.showCpu || Config.dashboard.performance.showGpu || Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork || Config.dashboard.performance.showBattery)\n            },\n            {\n                component: weatherComponent,\n                iconName: \"cloud\",\n                text: qsTr(\"Weather\"),\n                enabled: Config.dashboard.showWeather\n            }\n        ];\n        return allTabs.filter(tab => tab.enabled);\n    }\n\n    readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2\n    readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2\n\n    implicitWidth: nonAnimWidth\n    implicitHeight: nonAnimHeight\n\n    Tabs {\n        id: tabs\n\n        anchors.top: parent.top\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.topMargin: Appearance.padding.normal\n        anchors.margins: Appearance.padding.large\n\n        nonAnimWidth: root.nonAnimWidth - anchors.margins * 2\n        state: root.state\n        tabs: root.dashboardTabs\n    }\n\n    ClippingRectangle {\n        id: viewWrapper\n\n        anchors.top: tabs.bottom\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.bottom: parent.bottom\n        anchors.margins: Appearance.padding.large\n\n        radius: Appearance.rounding.normal\n        color: \"transparent\"\n\n        Flickable {\n            id: view\n\n            readonly property int currentIndex: root.state.currentTab\n            readonly property Item currentItem: {\n                repeater.count; // Trigger update on count change\n                return repeater.itemAt(currentIndex);\n            }\n\n            anchors.fill: parent\n\n            flickableDirection: Flickable.HorizontalFlick\n\n            implicitWidth: currentItem?.implicitWidth ?? 0\n            implicitHeight: currentItem?.implicitHeight ?? 0\n\n            contentX: currentItem?.x ?? 0\n            contentWidth: row.implicitWidth\n            contentHeight: row.implicitHeight\n\n            onContentXChanged: {\n                if (!moving || !currentItem)\n                    return;\n\n                const x = contentX - currentItem.x;\n                if (x > currentItem.implicitWidth / 2)\n                    root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1);\n                else if (x < -currentItem.implicitWidth / 2)\n                    root.state.currentTab = Math.max(root.state.currentTab - 1, 0);\n            }\n\n            onDragEnded: {\n                if (!currentItem)\n                    return;\n\n                const x = contentX - currentItem.x;\n                if (x > currentItem.implicitWidth / 10)\n                    root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1);\n                else if (x < -currentItem.implicitWidth / 10)\n                    root.state.currentTab = Math.max(root.state.currentTab - 1, 0);\n                else\n                    contentX = Qt.binding(() => currentItem?.x ?? 0);\n            }\n\n            RowLayout {\n                id: row\n\n                Repeater {\n                    id: repeater\n\n                    model: ScriptModel {\n                        values: root.dashboardTabs\n                    }\n\n                    delegate: Loader {\n                        id: paneLoader\n\n                        required property int index\n                        required property var modelData\n\n                        Layout.alignment: Qt.AlignTop\n\n                        sourceComponent: modelData.component\n\n                        Component.onCompleted: active = Qt.binding(() => {\n                            if (index === view.currentIndex)\n                                return true;\n                            const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);\n                            const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);\n                            return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);\n                        })\n                    }\n                }\n            }\n\n            Component {\n                id: dashComponent\n\n                Dash {\n                    visibilities: root.visibilities\n                    state: root.state\n                    facePicker: root.facePicker\n                }\n            }\n\n            Component {\n                id: mediaComponent\n\n                MediaWrapper {\n                    visibilities: root.visibilities\n                }\n            }\n\n            Component {\n                id: performanceComponent\n\n                Performance {}\n            }\n\n            Component {\n                id: weatherComponent\n\n                Weather {}\n            }\n\n            Behavior on contentX {\n                Anim {}\n            }\n        }\n    }\n\n    Behavior on implicitWidth {\n        Anim {\n            duration: Appearance.anim.durations.large\n            easing.bezierCurve: Appearance.anim.curves.emphasized\n        }\n    }\n\n    Behavior on implicitHeight {\n        Anim {\n            duration: Appearance.anim.durations.large\n            easing.bezierCurve: Appearance.anim.curves.emphasized\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/Dash.qml",
    "content": "import qs.components\nimport qs.components.filedialog\nimport qs.services\nimport qs.config\nimport \"dash\"\nimport QtQuick.Layouts\n\nGridLayout {\n    id: root\n\n    required property DrawerVisibilities visibilities\n    required property DashboardState state\n    required property FileDialog facePicker\n\n    rowSpacing: Appearance.spacing.normal\n    columnSpacing: Appearance.spacing.normal\n\n    Rect {\n        Layout.column: 2\n        Layout.columnSpan: 3\n        Layout.preferredWidth: user.implicitWidth\n        Layout.preferredHeight: user.implicitHeight\n\n        radius: Appearance.rounding.large\n\n        User {\n            id: user\n\n            visibilities: root.visibilities\n            state: root.state\n            facePicker: root.facePicker\n        }\n    }\n\n    Rect {\n        Layout.row: 0\n        Layout.columnSpan: 2\n        Layout.preferredWidth: Config.dashboard.sizes.weatherWidth\n        Layout.fillHeight: true\n\n        radius: Appearance.rounding.large * 1.5\n\n        Weather {}\n    }\n\n    Rect {\n        Layout.row: 1\n        Layout.preferredWidth: dateTime.implicitWidth\n        Layout.fillHeight: true\n\n        radius: Appearance.rounding.normal\n\n        DateTime {\n            id: dateTime\n        }\n    }\n\n    Rect {\n        Layout.row: 1\n        Layout.column: 1\n        Layout.columnSpan: 3\n        Layout.fillWidth: true\n        Layout.preferredHeight: calendar.implicitHeight\n\n        radius: Appearance.rounding.large\n\n        Calendar {\n            id: calendar\n\n            state: root.state\n        }\n    }\n\n    Rect {\n        Layout.row: 1\n        Layout.column: 4\n        Layout.preferredWidth: resources.implicitWidth\n        Layout.fillHeight: true\n\n        radius: Appearance.rounding.normal\n\n        Resources {\n            id: resources\n        }\n    }\n\n    Rect {\n        Layout.row: 0\n        Layout.column: 5\n        Layout.rowSpan: 2\n        Layout.preferredWidth: media.implicitWidth\n        Layout.fillHeight: true\n\n        radius: Appearance.rounding.large * 2\n\n        Media {\n            id: media\n        }\n    }\n\n    component Rect: StyledRect {\n        color: Colours.tPalette.m3surfaceContainer\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/LyricMenu.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property real contentHeight\n\n    function searchCandidates(title, artist) {\n        LyricsService.currentRequestId++;\n        LyricsService.fetchNetEaseCandidates(title, artist, LyricsService.currentRequestId);\n    }\n\n    implicitHeight: contentHeight\n\n    radius: Appearance.rounding.large\n    color: Colours.tPalette.m3surfaceContainer\n\n    Loader {\n        asynchronous: true\n        anchors.fill: parent\n        active: root.height > 0\n\n        sourceComponent: ColumnLayout {\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.large\n            spacing: Appearance.spacing.normal\n\n            // Header: icon, backend name, refresh, toggle\n            RowLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.padding.small\n\n                MaterialIcon {\n                    text: \"lyrics\"\n                    fill: 1\n                    color: Colours.palette.m3primary\n                    font.pointSize: Appearance.spacing.large\n                }\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: LyricsService.backend\n                    font.pointSize: Appearance.font.size.normal\n                    color: Colours.palette.m3secondary\n                    elide: Text.ElideRight\n                }\n\n                IconButton {\n                    icon: \"refresh\"\n                    type: IconButton.Text\n                    onClicked: LyricsService.loadLyrics()\n                }\n\n                StyledSwitch {\n                    checked: LyricsService.lyricsVisible\n                    onToggled: LyricsService.toggleVisibility()\n                }\n            }\n\n            StyledText {\n                Layout.fillWidth: true\n                text: \"Fetched Candidates:\"\n                color: Colours.palette.m3outline\n                font.pointSize: Appearance.font.size.small\n                elide: Text.ElideRight\n            }\n\n            // Candidates list\n            ListView {\n                id: candidatesView\n\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n\n                visible: LyricsService.candidatesModel.count > 0\n                model: LyricsService.candidatesModel\n                clip: true\n                spacing: Appearance.spacing.small\n\n                opacity: visible ? 1 : 0\n                // Behavior on opacity {\n                //     NumberAnimation { duration: Appearance.anim.durations.normal }\n                // }\n\n                delegate: Item {\n                    id: delegateRoot\n\n                    required property real id\n                    required property string title\n                    required property string artist\n                    property bool hovered: false\n                    property bool pressed: false\n\n                    width: ListView.view.width * 0.98\n                    height: 70\n                    anchors.horizontalCenter: parent?.horizontalCenter\n                    scale: hovered ? 1.02 : 1.0\n\n                    Behavior on scale {\n                        NumberAnimation {\n                            duration: Appearance.anim.durations.small\n                            easing.type: Easing.OutCubic\n                        }\n                    }\n\n                    Rectangle {\n                        id: background\n\n                        anchors.fill: parent\n                        radius: Appearance.rounding.small\n\n                        color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03)\n\n                        border.width: delegateRoot.hovered ? 1 : 0\n                        border.color: Colours.palette.m3primary\n\n                        Behavior on color {\n                            ColorAnimation {\n                                duration: Appearance.anim.durations.small\n                            }\n                        }\n                        Behavior on border.width {\n                            NumberAnimation {\n                                duration: Appearance.anim.durations.small\n                            }\n                        }\n                    }\n\n                    MouseArea {\n                        anchors.fill: parent\n                        hoverEnabled: true\n                        cursorShape: Qt.PointingHandCursor\n\n                        onEntered: delegateRoot.hovered = true\n                        onExited: delegateRoot.hovered = false\n                        onPressed: delegateRoot.pressed = true\n                        onReleased: delegateRoot.pressed = false\n                        onClicked: LyricsService.selectCandidate(delegateRoot.id)\n                    }\n\n                    Row {\n                        anchors.fill: parent\n                        anchors.margins: Appearance.padding.normal\n                        spacing: Appearance.spacing.small\n\n                        // Active indicator bar\n                        Rectangle {\n                            width: 4\n                            height: parent.height * 0.6\n                            radius: 2\n                            anchors.verticalCenter: parent.verticalCenter\n                            color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : \"transparent\"\n\n                            Behavior on color {\n                                ColorAnimation {\n                                    duration: Appearance.anim.durations.small\n                                }\n                            }\n                        }\n\n                        Column {\n                            anchors.verticalCenter: parent.verticalCenter\n                            width: parent.width - 30\n                            spacing: 4\n\n                            Text {\n                                text: delegateRoot.title\n                                font.pointSize: Appearance.font.size.normal\n                                font.bold: true\n                                color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface\n                                width: parent.width\n                                elide: Text.ElideRight\n\n                                Behavior on color {\n                                    ColorAnimation {\n                                        duration: Appearance.anim.durations.small\n                                    }\n                                }\n                            }\n\n                            Text {\n                                text: delegateRoot.artist\n                                font.pointSize: Appearance.font.size.small\n                                color: Colours.palette.m3onSurfaceVariant\n                                elide: Text.ElideRight\n                            }\n                        }\n                    }\n                }\n            }\n\n            Item {\n                Layout.fillHeight: true\n                visible: LyricsService.candidatesModel.count == 0\n            }\n\n            // Manual search\n            ColumnLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.padding.small\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: \"Manual Search\"\n                    font.pointSize: Appearance.font.size.small\n                    color: Colours.palette.m3onSurfaceVariant\n                    elide: Text.ElideRight\n                }\n\n                RowLayout {\n                    Layout.fillWidth: true\n                    spacing: Appearance.padding.small\n\n                    StyledInputField {\n                        id: searchTitle\n\n                        Layout.fillWidth: true\n                        horizontalAlignment: TextInput.AlignLeft\n\n                        Binding {\n                            target: searchTitle\n                            property: \"text\"\n                            value: (Players.active?.trackTitle ?? qsTr(\"title\")) || qsTr(\"title\")\n                        }\n                    }\n\n                    StyledInputField {\n                        id: searchArtist\n\n                        Layout.fillWidth: true\n                        horizontalAlignment: TextInput.AlignLeft\n\n                        Binding {\n                            target: searchArtist\n                            property: \"text\"\n                            value: (Players.active?.trackArtist ?? qsTr(\"artist\")) || qsTr(\"artist\")\n                        }\n                    }\n\n                    IconButton {\n                        icon: \"search\"\n                        onClicked: root.searchCandidates(searchTitle.text, searchArtist.text)\n                    }\n                }\n            }\n\n            // Offset controls\n            RowLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.padding.small\n\n                MaterialIcon {\n                    text: \"contrast_square\"\n                    font.pointSize: Appearance.font.size.large\n                    color: Colours.palette.m3secondary\n                }\n\n                StyledText {\n                    text: \"Offset\"\n                    color: Colours.palette.m3outline\n                    font.pointSize: Appearance.font.size.normal\n                }\n\n                Item {\n                    Layout.fillWidth: true\n                }\n\n                IconButton {\n                    icon: \"remove\"\n                    type: IconButton.Text\n                    onClicked: {\n                        LyricsService.offset = parseFloat((LyricsService.offset - 0.1).toFixed(1));\n                        LyricsService.savePrefs();\n                    }\n                }\n\n                TextInput {\n                    id: offsetInput\n\n                    horizontalAlignment: TextInput.AlignHCenter\n                    color: Colours.palette.m3secondary\n                    font.pointSize: Appearance.font.size.normal\n                    selectByMouse: true\n                    text: (LyricsService.offset >= 0 ? \"+\" : \"\") + LyricsService.offset.toFixed(1) + \"s\"\n                    onEditingFinished: {\n                        let cleaned = offsetInput.text.replace(/[+s]/g, \"\").trim();\n                        let val = parseFloat(cleaned);\n                        if (!isNaN(val)) {\n                            LyricsService.offset = parseFloat(val.toFixed(1));\n                            LyricsService.savePrefs();\n                        } else {\n                            offsetInput.text = (LyricsService.offset >= 0 ? \"+\" : \"\") + LyricsService.offset.toFixed(1) + \"s\";\n                        }\n                    }\n\n                    Binding {\n                        target: offsetInput\n                        property: \"text\"\n                        value: (LyricsService.offset >= 0 ? \"+\" : \"\") + LyricsService.offset.toFixed(1) + \"s\"\n                        when: !offsetInput.activeFocus\n                    }\n\n                    Connections {\n                        function onCurrentRequestIdChanged() {\n                            offsetInput.focus = false;\n                        }\n\n                        target: LyricsService\n                    }\n                }\n\n                IconButton {\n                    icon: \"add\"\n                    type: IconButton.Text\n                    onClicked: {\n                        LyricsService.offset = parseFloat((LyricsService.offset + 0.1).toFixed(1));\n                        LyricsService.savePrefs();\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/LyricsView.qml",
    "content": "import qs.components\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\nimport QtQuick.Effects\n\nStyledListView {\n    id: root\n\n    readonly property bool lyricsActuallyVisible: LyricsService.lyricsVisible && LyricsService.model.count != 0\n\n    clip: true\n    model: LyricsService.model\n    currentIndex: LyricsService.currentIndex\n    visible: lyricsActuallyVisible || hideTimer.running\n    preferredHighlightBegin: height / 2 - 30\n    preferredHighlightEnd: height / 2 + 30\n    highlightRangeMode: ListView.ApplyRange\n    highlightFollowsCurrentItem: true\n    highlightMoveDuration: LyricsService.isManualSeeking ? 0 : Appearance.anim.durations.normal\n    layer.enabled: true\n    layer.effect: ShaderEffect {\n        required property Item source\n        property real fadeMargin: 0.5\n\n        fragmentShader: Quickshell.shellPath(\"assets/shaders/fade.frag.qsb\")\n    }\n    onLyricsActuallyVisibleChanged: {\n        if (!lyricsActuallyVisible)\n            hideTimer.restart();\n    }\n    onModelChanged: {\n        if (model && model.count > 0) {\n            Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center));\n        }\n    }\n    delegate: Item {\n        id: delegateRoot\n\n        required property string lyricLine\n        required property real time\n        required property int index\n        readonly property bool hasContent: lyricLine && lyricLine.trim().length > 0\n        property bool isCurrent: ListView.isCurrentItem\n\n        width: ListView.view.width\n        height: hasContent ? (lyricText.contentHeight + Appearance.spacing.large) : 0\n\n        MultiEffect {\n            id: effect\n\n            anchors.fill: lyricText\n            source: lyricText\n            scale: lyricText.scale\n            enabled: delegateRoot.isCurrent\n            visible: delegateRoot.isCurrent\n\n            blurEnabled: true\n            blur: 0.4\n\n            shadowEnabled: true\n            shadowColor: Colours.palette.m3primary\n            shadowOpacity: 0.5\n            shadowBlur: 0.6\n            shadowHorizontalOffset: 0\n            shadowVerticalOffset: 0\n\n            autoPaddingEnabled: true\n        }\n\n        MouseArea {\n            anchors.fill: parent\n            cursorShape: Qt.PointingHandCursor\n            onClicked: LyricsService.jumpTo(delegateRoot.index, delegateRoot.time)\n        }\n\n        Text {\n            id: lyricText\n\n            text: delegateRoot.lyricLine ? delegateRoot.lyricLine.replace(/\\u00A0/g, \" \") : \"\"\n            width: parent.width * 0.85\n            anchors.centerIn: parent\n            horizontalAlignment: Text.AlignHCenter\n            wrapMode: Text.WordWrap\n            font.pointSize: Appearance.font.size.normal\n            color: delegateRoot.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant\n            font.bold: delegateRoot.isCurrent\n            scale: delegateRoot.isCurrent ? 1.15 : 1.0\n\n            Behavior on color {\n                CAnim {\n                    duration: Appearance.anim.durations.small\n                }\n            }\n            Behavior on scale {\n                Anim {\n                    duration: Appearance.anim.durations.small\n                }\n            }\n        }\n    }\n\n    Timer {\n        id: hideTimer\n\n        interval: 300  // long enough to bridge the track switch gap\n        running: false\n        repeat: false\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/Media.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.utils\nimport qs.config\nimport Caelestia.Services\nimport Quickshell\nimport Quickshell.Services.Mpris\nimport QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Shapes\n\nItem {\n    id: root\n\n    required property DrawerVisibilities visibilities\n    readonly property bool needsKeyboard: lyricMenuOpen\n\n    readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2\n    readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight\n\n    property bool lyricMenuOpen: false\n    property bool lyricsShowing: LyricsService.lyricsVisible && LyricsService.model.count != 0\n    property bool lyricsShowingDebounced: false\n\n    property real playerProgress: {\n        const active = Players.active;\n        return active?.length ? active.position / active.length : 0;\n    }\n\n    function lengthStr(length: int): string {\n        if (length < 0)\n            return \"-1:-1\";\n\n        const hours = Math.floor(length / 3600);\n        const mins = Math.floor((length % 3600) / 60);\n        const secs = Math.floor(length % 60).toString().padStart(2, \"0\");\n\n        if (hours > 0)\n            return `${hours}:${mins.toString().padStart(2, \"0\")}:${secs}`;\n        return `${mins}:${secs}`;\n    }\n\n    onLyricsShowingChanged: {\n        if (lyricsShowing) {\n            lyricsHideDelay.stop();\n            lyricsShowingDebounced = true;\n        } else {\n            lyricsHideDelay.restart();\n        }\n    }\n\n    implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2\n    implicitHeight: nonAnimHeight\n\n    Behavior on implicitHeight {\n        Anim {}\n    }\n\n    Behavior on playerProgress {\n        Anim {\n            duration: Appearance.anim.durations.large\n        }\n    }\n\n    Timer {\n        running: Players.active?.isPlaying ?? false\n        interval: Config.dashboard.mediaUpdateInterval\n        triggeredOnStart: true\n        repeat: true\n        onTriggered: {\n            if (!Players.active)\n                return;\n            LyricsService.updatePosition();\n            Players.active?.positionChanged();\n        }\n    }\n\n    Timer {\n        id: lyricsHideDelay\n\n        interval: 300\n        repeat: false\n    }\n\n    Connections {\n        function onTriggered() {\n            root.lyricsShowingDebounced = false;\n        }\n\n        target: lyricsHideDelay\n    }\n\n    ServiceRef {\n        service: Audio.cava\n    }\n\n    ServiceRef {\n        service: Audio.beatTracker\n    }\n\n    Shape {\n        id: visualiser\n\n        readonly property real centerX: width / 2\n        readonly property real centerY: height / 2\n        readonly property real innerX: cover.implicitWidth / 2 + Appearance.spacing.small\n        readonly property real innerY: cover.implicitHeight / 2 + Appearance.spacing.small\n        property color colour: Colours.palette.m3primary\n\n        anchors.fill: cover\n        anchors.margins: -Config.dashboard.sizes.mediaVisualiserSize\n\n        asynchronous: true\n        preferredRendererType: Shape.CurveRenderer\n        data: visualiserBars.instances\n    }\n\n    Variants {\n        id: visualiserBars\n\n        model: Array.from({\n            length: Config.services.visualiserBars\n        }, (_, i) => i)\n\n        ShapePath {\n            id: visualiserBar\n\n            required property int modelData\n            readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData]))\n\n            readonly property real angle: modelData * 2 * Math.PI / Config.services.visualiserBars\n            readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize\n            readonly property real cos: Math.cos(angle)\n            readonly property real sin: Math.sin(angle)\n\n            capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap\n            strokeWidth: 360 / Config.services.visualiserBars - Appearance.spacing.small / 4\n            strokeColor: Colours.palette.m3primary\n\n            startX: visualiser.centerX + (visualiser.innerX + strokeWidth / 2) * cos\n            startY: visualiser.centerY + (visualiser.innerY + strokeWidth / 2) * sin\n\n            PathLine {\n                x: visualiser.centerX + (visualiser.innerX + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.cos\n                y: visualiser.centerY + (visualiser.innerY + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.sin\n            }\n\n            Behavior on strokeColor {\n                CAnim {}\n            }\n        }\n    }\n\n    StyledClippingRect {\n        id: cover\n\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.left: parent.left\n        anchors.leftMargin: Appearance.padding.large + Config.dashboard.sizes.mediaVisualiserSize\n\n        implicitWidth: Config.dashboard.sizes.mediaCoverArtSize\n        implicitHeight: Config.dashboard.sizes.mediaCoverArtSize\n\n        color: Colours.tPalette.m3surfaceContainerHigh\n        radius: Infinity\n\n        MaterialIcon {\n            anchors.centerIn: parent\n\n            grade: 200\n            text: \"art_track\"\n            color: Colours.palette.m3onSurfaceVariant\n            font.pointSize: (parent.width * 0.4) || 1\n        }\n\n        Image {\n            id: image\n\n            anchors.fill: parent\n\n            source: Players.active?.trackArtUrl ?? \"\" // qmllint disable incompatible-type\n            asynchronous: true\n            fillMode: Image.PreserveAspectCrop\n            sourceSize.width: width\n            sourceSize.height: height\n\n            MouseArea {\n                anchors.fill: parent\n                onClicked: {\n                    LyricsService.toggleVisibility();\n                }\n            }\n        }\n    }\n\n    ColumnLayout {\n        id: details\n\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.left: visualiser.right\n        anchors.leftMargin: Appearance.spacing.normal\n\n        spacing: Appearance.spacing.small\n\n        StyledText {\n            id: title\n\n            Layout.fillWidth: true\n            Layout.maximumWidth: parent.implicitWidth\n\n            animate: true\n            horizontalAlignment: Text.AlignHCenter\n            text: (Players.active?.trackTitle ?? qsTr(\"No media\")) || qsTr(\"Unknown title\")\n            color: Players.active ? Colours.palette.m3primary : Colours.palette.m3onSurface\n            font.pointSize: Appearance.font.size.normal\n            elide: Text.ElideRight\n        }\n\n        StyledText {\n            id: album\n\n            Layout.fillWidth: true\n            Layout.maximumWidth: parent.implicitWidth\n\n            animate: true\n            horizontalAlignment: Text.AlignHCenter\n            visible: !!Players.active\n            text: Players.active?.trackAlbum || qsTr(\"Unknown album\")\n            color: Colours.palette.m3outline\n            font.pointSize: Appearance.font.size.small\n            elide: Text.ElideRight\n        }\n\n        StyledText {\n            id: artist\n\n            Layout.fillWidth: true\n            Layout.maximumWidth: parent.implicitWidth\n\n            animate: true\n            horizontalAlignment: Text.AlignHCenter\n            text: (Players.active?.trackArtist ?? qsTr(\"Play some music for stuff to show up here!\")) || qsTr(\"Unknown artist\")\n            color: Players.active ? Colours.palette.m3secondary : Colours.palette.m3outline\n            elide: Text.ElideRight\n            wrapMode: Players.active ? Text.NoWrap : Text.WordWrap\n        }\n\n        LyricsView {\n            id: lyricsViewInDetails\n\n            Layout.fillWidth: true\n            Layout.preferredHeight: 200\n        }\n\n        RowLayout {\n            id: controls\n\n            Layout.alignment: Qt.AlignHCenter\n            Layout.topMargin: Appearance.spacing.small\n            Layout.bottomMargin: Appearance.spacing.smaller\n\n            spacing: Appearance.spacing.small\n\n            PlayerControl {\n                type: IconButton.Text\n                icon: Players.active?.shuffle ? \"shuffle_on\" : \"shuffle\"\n                font.pointSize: Math.round(Appearance.font.size.large)\n                disabled: !Players.active?.shuffleSupported\n                onClicked: Players.active.shuffle = !Players.active?.shuffle\n            }\n\n            PlayerControl {\n                type: IconButton.Text\n                icon: \"skip_previous\"\n                font.pointSize: Math.round(Appearance.font.size.large * 1.5)\n                disabled: !Players.active?.canGoPrevious\n                onClicked: Players.active?.previous()\n            }\n\n            PlayerControl {\n                icon: Players.active?.isPlaying ? \"pause\" : \"play_arrow\"\n                label.animate: true\n                toggle: true\n                padding: Appearance.padding.small / 2\n                checked: Players.active?.isPlaying ?? false\n                font.pointSize: Math.round(Appearance.font.size.large * 1.5)\n                disabled: !Players.active?.canTogglePlaying\n                onClicked: Players.active?.togglePlaying()\n            }\n\n            PlayerControl {\n                type: IconButton.Text\n                icon: \"skip_next\"\n                font.pointSize: Math.round(Appearance.font.size.large * 1.5)\n                disabled: !Players.active?.canGoNext\n                onClicked: Players.active?.next()\n            }\n\n            PlayerControl {\n                type: IconButton.Text\n                icon: \"lyrics\"\n                font.pointSize: Math.round(Appearance.font.size.large)\n                onClicked: root.lyricMenuOpen = !root.lyricMenuOpen\n            }\n        }\n\n        StyledSlider {\n            id: slider\n\n            enabled: !!Players.active\n            implicitWidth: 280\n            implicitHeight: Appearance.padding.normal * 3\n\n            onMoved: {\n                const active = Players.active;\n                if (active?.canSeek && active?.positionSupported)\n                    active.position = value * active.length;\n            }\n\n            Binding {\n                target: slider\n                property: \"value\"\n                value: root.playerProgress\n                when: !slider.pressed\n            }\n\n            CustomMouseArea {\n                function onWheel(event: WheelEvent) {\n                    const active = Players.active;\n                    if (!active?.canSeek || !active?.positionSupported)\n                        return;\n\n                    event.accepted = true;\n                    const delta = event.angleDelta.y > 0 ? 10 : -10;    // Time 10 seconds\n                    Qt.callLater(() => {\n                        active.position = Math.max(0, Math.min(active.length, active.position + delta));\n                    });\n                }\n\n                anchors.fill: parent\n                acceptedButtons: Qt.NoButton\n            }\n        }\n\n        Item {\n            Layout.fillWidth: true\n            implicitHeight: Math.max(position.implicitHeight, length.implicitHeight)\n\n            StyledText {\n                id: position\n\n                anchors.left: parent.left\n\n                text: root.lengthStr(Players.active?.position ?? -1)\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.small\n            }\n\n            StyledText {\n                id: length\n\n                anchors.right: parent.right\n\n                text: root.lengthStr(Players.active?.length ?? -1)\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.small\n            }\n        }\n    }\n\n    ColumnLayout {\n        id: leftSection\n\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.verticalCenterOffset: playerChanger.parent == leftSection ? -playerChanger.height : 0\n        anchors.left: details.right\n        anchors.leftMargin: Appearance.spacing.normal\n\n        visible: lyricMenu.height === 0 || opacity > 0\n        opacity: lyricMenu.height === 0 ? 1 : 0\n\n        Behavior on opacity {\n            NumberAnimation {\n                duration: Appearance.anim.durations.normal\n                easing.type: Easing.OutCubic\n            }\n        }\n\n        Item {\n            id: bongocat\n\n            implicitWidth: visualiser.width\n            implicitHeight: visualiser.height\n\n            AnimatedImage {\n                anchors.centerIn: parent\n\n                width: visualiser.width * 0.75\n                height: visualiser.height * 0.75\n\n                playing: Players.active?.isPlaying ?? false\n                speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type\n                source: Paths.absolutePath(Config.paths.mediaGif)\n                asynchronous: true\n                fillMode: AnimatedImage.PreserveAspectFit\n            }\n        }\n    }\n\n    LyricMenu {\n        id: lyricMenu\n\n        anchors.top: parent.top\n        anchors.left: details.right\n        anchors.right: parent.right\n        anchors.leftMargin: Appearance.spacing.normal\n\n        contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Appearance.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight\n\n        visible: root.lyricMenuOpen || height > 0\n        height: root.lyricMenuOpen ? implicitHeight : 0\n        clip: true\n\n        Behavior on height {\n            NumberAnimation {\n                duration: Appearance.anim.durations.normal\n                easing.type: Easing.OutCubic\n            }\n        }\n    }\n\n    RowLayout {\n        id: playerChanger\n\n        parent: !root.lyricsShowingDebounced ? details : leftSection\n        Layout.alignment: Qt.AlignHCenter\n        spacing: Appearance.spacing.small\n\n        PlayerControl {\n            type: IconButton.Text\n            icon: \"move_up\"\n            inactiveOnColour: Colours.palette.m3secondary\n            padding: Appearance.padding.small\n            font.pointSize: Appearance.font.size.large\n            disabled: !Players.active?.canRaise\n            onClicked: {\n                Players.active?.raise();\n                root.visibilities.dashboard = false;\n            }\n        }\n\n        SplitButton {\n            id: playerSelector\n\n            disabled: !Players.list.length\n            active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null\n            menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData\n\n            menuItems: playerList.instances\n            fallbackIcon: \"music_off\"\n            fallbackText: qsTr(\"No players\")\n\n            label.Layout.maximumWidth: slider.implicitWidth * 0.28\n            label.elide: Text.ElideRight\n\n            stateLayer.disabled: true\n            menuOnTop: true\n\n            Variants {\n                id: playerList\n\n                model: Players.list\n\n                PlayerItem {}\n            }\n        }\n\n        PlayerControl {\n            type: IconButton.Text\n            icon: \"delete\"\n            inactiveOnColour: Colours.palette.m3error\n            padding: Appearance.padding.small\n            font.pointSize: Appearance.font.size.large\n            disabled: !Players.active?.canQuit\n            onClicked: Players.active?.quit()\n        }\n    }\n\n    component PlayerItem: MenuItem {\n        required property MprisPlayer modelData\n\n        icon: modelData === Players.active ? \"check\" : \"\"\n        text: Players.getIdentity(modelData)\n        activeIcon: \"animated_images\"\n    }\n\n    component PlayerControl: IconButton {\n        Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0)\n        radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : implicitHeight / 2\n        radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial\n        radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n\n        Behavior on Layout.preferredWidth {\n            Anim {\n                duration: Appearance.anim.durations.expressiveFastSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/MediaWrapper.qml",
    "content": "import QtQuick\n\nItem {\n    property alias visibilities: media.visibilities\n    readonly property alias needsKeyboard: media.needsKeyboard\n\n    implicitWidth: media.implicitWidth\n    implicitHeight: media.nonAnimHeight\n\n    Media {\n        id: media\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/Performance.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\nimport Quickshell.Services.UPower\nimport Caelestia.Internal\nimport qs.components\nimport qs.components.misc\nimport qs.config\nimport qs.services\n\nItem {\n    id: root\n\n    readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2\n\n    function displayTemp(temp: real): string {\n        return `${Math.ceil(Config.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheitPerformance ? \"F\" : \"C\"}`;\n    }\n\n    implicitWidth: Math.max(minWidth, content.implicitWidth)\n    implicitHeight: placeholder.visible ? placeholder.height : content.implicitHeight\n\n    StyledRect {\n        id: placeholder\n\n        anchors.centerIn: parent\n        width: 400\n        height: 350\n        radius: Appearance.rounding.large\n        color: Colours.tPalette.m3surfaceContainer\n        visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && SystemUsage.gpuType !== \"NONE\") && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery)\n\n        ColumnLayout {\n            anchors.centerIn: parent\n            spacing: Appearance.spacing.normal\n\n            MaterialIcon {\n                Layout.alignment: Qt.AlignHCenter\n                text: \"tune\"\n                font.pointSize: Appearance.font.size.extraLarge * 2\n                color: Colours.palette.m3onSurfaceVariant\n            }\n\n            StyledText {\n                Layout.alignment: Qt.AlignHCenter\n                text: qsTr(\"No widgets enabled\")\n                font.pointSize: Appearance.font.size.large\n                color: Colours.palette.m3onSurface\n            }\n\n            StyledText {\n                Layout.alignment: Qt.AlignHCenter\n                text: qsTr(\"Enable widgets in dashboard settings\")\n                font.pointSize: Appearance.font.size.small\n                color: Colours.palette.m3onSurfaceVariant\n            }\n        }\n    }\n\n    RowLayout {\n        id: content\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        spacing: Appearance.spacing.normal\n        visible: !placeholder.visible\n\n        Ref {\n            service: SystemUsage\n        }\n\n        ColumnLayout {\n            id: mainColumn\n\n            Layout.fillWidth: true\n            spacing: Appearance.spacing.normal\n\n            RowLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.normal\n                visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== \"NONE\")\n\n                HeroCard {\n                    Layout.fillWidth: true\n                    Layout.minimumWidth: 400\n                    Layout.preferredHeight: 150\n                    visible: Config.dashboard.performance.showCpu\n                    icon: \"memory\"\n                    title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr(\"CPU\")\n                    mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%`\n                    mainLabel: qsTr(\"Usage\")\n                    secondaryValue: root.displayTemp(SystemUsage.cpuTemp)\n                    secondaryLabel: qsTr(\"Temp\")\n                    usage: SystemUsage.cpuPerc\n                    temperature: SystemUsage.cpuTemp\n                    accentColor: Colours.palette.m3primary\n                }\n\n                HeroCard {\n                    Layout.fillWidth: true\n                    Layout.minimumWidth: 400\n                    Layout.preferredHeight: 150\n                    visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== \"NONE\"\n                    icon: \"desktop_windows\"\n                    title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr(\"GPU\")\n                    mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%`\n                    mainLabel: qsTr(\"Usage\")\n                    secondaryValue: root.displayTemp(SystemUsage.gpuTemp)\n                    secondaryLabel: qsTr(\"Temp\")\n                    usage: SystemUsage.gpuPerc\n                    temperature: SystemUsage.gpuTemp\n                    accentColor: Colours.palette.m3secondary\n                }\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.normal\n                visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork\n\n                GaugeCard {\n                    Layout.minimumWidth: 250\n                    Layout.preferredHeight: 220\n                    Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork\n                    icon: \"memory_alt\"\n                    title: qsTr(\"Memory\")\n                    percentage: SystemUsage.memPerc\n                    subtitle: {\n                        const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed);\n                        const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal);\n                        return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;\n                    }\n                    accentColor: Colours.palette.m3tertiary\n                    visible: Config.dashboard.performance.showMemory\n                }\n\n                StorageGaugeCard {\n                    Layout.minimumWidth: 250\n                    Layout.preferredHeight: 220\n                    Layout.fillWidth: !Config.dashboard.performance.showNetwork\n                    visible: Config.dashboard.performance.showStorage\n                }\n\n                NetworkCard {\n                    Layout.fillWidth: true\n                    Layout.minimumWidth: 200\n                    Layout.preferredHeight: 220\n                    visible: Config.dashboard.performance.showNetwork\n                }\n            }\n        }\n\n        BatteryTank {\n            Layout.preferredWidth: 120\n            Layout.preferredHeight: mainColumn.implicitHeight\n            visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery\n        }\n    }\n\n    component BatteryTank: StyledClippingRect {\n        id: batteryTank\n\n        property real percentage: UPower.displayDevice.percentage\n        property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging\n        property color accentColor: Colours.palette.m3primary\n        property real animatedPercentage: 0\n\n        color: Colours.tPalette.m3surfaceContainer\n        radius: Appearance.rounding.large\n        Component.onCompleted: animatedPercentage = percentage\n        onPercentageChanged: animatedPercentage = percentage\n\n        // Background Fill\n        StyledRect {\n            anchors.left: parent.left\n            anchors.right: parent.right\n            anchors.bottom: parent.bottom\n            height: parent.height * batteryTank.animatedPercentage\n            color: Qt.alpha(batteryTank.accentColor, 0.15)\n        }\n\n        ColumnLayout {\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.large\n            spacing: Appearance.spacing.small\n\n            // Header Section\n            ColumnLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.small\n\n                MaterialIcon {\n                    text: {\n                        if (!UPower.displayDevice.isLaptopBattery) {\n                            if (PowerProfiles.profile === PowerProfile.PowerSaver)\n                                return \"energy_savings_leaf\";\n\n                            if (PowerProfiles.profile === PowerProfile.Performance)\n                                return \"rocket_launch\";\n\n                            return \"balance\";\n                        }\n                        if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)\n                            return \"battery_full\";\n\n                        const perc = UPower.displayDevice.percentage;\n                        const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state);\n                        if (perc >= 0.99)\n                            return \"battery_full\";\n\n                        let level = Math.floor(perc * 7);\n                        if (charging && (level === 4 || level === 1))\n                            level--;\n\n                        return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`;\n                    }\n                    font.pointSize: Appearance.font.size.large\n                    color: batteryTank.accentColor\n                }\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: qsTr(\"Battery\")\n                    font.pointSize: Appearance.font.size.normal\n                    color: Colours.palette.m3onSurface\n                }\n            }\n\n            Item {\n                Layout.fillHeight: true\n            }\n\n            // Bottom Info Section\n            ColumnLayout {\n                Layout.fillWidth: true\n                spacing: -4\n\n                StyledText {\n                    Layout.alignment: Qt.AlignRight\n                    text: `${Math.round(batteryTank.percentage * 100)}%`\n                    font.pointSize: Appearance.font.size.extraLarge\n                    font.weight: Font.Medium\n                    color: batteryTank.accentColor\n                }\n\n                StyledText {\n                    Layout.alignment: Qt.AlignRight\n                    text: {\n                        if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)\n                            return qsTr(\"Full\");\n\n                        if (batteryTank.isCharging)\n                            return qsTr(\"Charging\");\n\n                        const s = UPower.displayDevice.timeToEmpty;\n                        if (s === 0)\n                            return qsTr(\"...\");\n\n                        const hr = Math.floor(s / 3600);\n                        const min = Math.floor((s % 3600) / 60);\n                        if (hr > 0)\n                            return `${hr}h ${min}m`;\n\n                        return `${min}m`;\n                    }\n                    font.pointSize: Appearance.font.size.smaller\n                    color: Colours.palette.m3onSurfaceVariant\n                }\n            }\n        }\n\n        Behavior on animatedPercentage {\n            Anim {\n                duration: Appearance.anim.durations.large\n            }\n        }\n    }\n\n    component CardHeader: RowLayout {\n        property string icon\n        property string title\n        property color accentColor: Colours.palette.m3primary\n\n        Layout.fillWidth: true\n        spacing: Appearance.spacing.small\n\n        MaterialIcon {\n            text: parent.icon\n            fill: 1\n            color: parent.accentColor\n            font.pointSize: Appearance.spacing.large\n        }\n\n        StyledText {\n            Layout.fillWidth: true\n            text: parent.title\n            font.pointSize: Appearance.font.size.normal\n            elide: Text.ElideRight\n        }\n    }\n\n    component ProgressBar: StyledRect {\n        id: progressBar\n\n        property real value: 0\n        property color fgColor: Colours.palette.m3primary\n        property color bgColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)\n        property real animatedValue: 0\n\n        color: bgColor\n        radius: Appearance.rounding.full\n        Component.onCompleted: animatedValue = value\n        onValueChanged: animatedValue = value\n\n        StyledRect {\n            anchors.left: parent.left\n            anchors.top: parent.top\n            anchors.bottom: parent.bottom\n            width: parent.width * progressBar.animatedValue\n            color: progressBar.fgColor\n            radius: Appearance.rounding.full\n        }\n\n        Behavior on animatedValue {\n            Anim {\n                duration: Appearance.anim.durations.large\n            }\n        }\n    }\n\n    component HeroCard: StyledClippingRect {\n        id: heroCard\n\n        property string icon\n        property string title\n        property string mainValue\n        property string mainLabel\n        property string secondaryValue\n        property string secondaryLabel\n        property real usage: 0\n        property real temperature: 0\n        property color accentColor: Colours.palette.m3primary\n        readonly property real maxTemp: 100\n        readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp))\n        property real animatedUsage: 0\n        property real animatedTemp: 0\n\n        color: Colours.tPalette.m3surfaceContainer\n        radius: Appearance.rounding.large\n        Component.onCompleted: {\n            animatedUsage = usage;\n            animatedTemp = tempProgress;\n        }\n        onUsageChanged: animatedUsage = usage\n        onTempProgressChanged: animatedTemp = tempProgress\n\n        StyledRect {\n            anchors.left: parent.left\n            anchors.top: parent.top\n            anchors.bottom: parent.bottom\n            width: parent.width * heroCard.animatedUsage\n            color: Qt.alpha(heroCard.accentColor, 0.15)\n        }\n\n        ColumnLayout {\n            anchors.fill: parent\n            anchors.leftMargin: Appearance.padding.large\n            anchors.rightMargin: Appearance.padding.large\n            anchors.topMargin: Appearance.padding.normal\n            anchors.bottomMargin: Appearance.padding.normal\n            spacing: Appearance.spacing.small\n\n            CardHeader {\n                icon: heroCard.icon\n                title: heroCard.title\n                accentColor: heroCard.accentColor\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n                spacing: Appearance.spacing.normal\n\n                Column {\n                    Layout.alignment: Qt.AlignBottom\n                    Layout.fillWidth: true\n                    spacing: Appearance.spacing.small\n\n                    Row {\n                        spacing: Appearance.spacing.small\n\n                        StyledText {\n                            text: heroCard.secondaryValue\n                            font.pointSize: Appearance.font.size.normal\n                            font.weight: Font.Medium\n                        }\n\n                        StyledText {\n                            text: heroCard.secondaryLabel\n                            font.pointSize: Appearance.font.size.small\n                            color: Colours.palette.m3onSurfaceVariant\n                            anchors.baseline: parent.children[0].baseline\n                        }\n                    }\n\n                    ProgressBar {\n                        width: parent.width * 0.5\n                        height: 6\n                        value: heroCard.tempProgress\n                        fgColor: heroCard.accentColor\n                        bgColor: Qt.alpha(heroCard.accentColor, 0.2)\n                    }\n                }\n\n                Item {\n                    Layout.fillWidth: true\n                }\n            }\n        }\n\n        Column {\n            anchors.right: parent.right\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.margins: Appearance.padding.large\n            anchors.rightMargin: 32\n            spacing: 0\n\n            StyledText {\n                anchors.right: parent.right\n                text: heroCard.mainLabel\n                font.pointSize: Appearance.font.size.normal\n                color: Colours.palette.m3onSurfaceVariant\n            }\n\n            StyledText {\n                anchors.right: parent.right\n                text: heroCard.mainValue\n                font.pointSize: Appearance.font.size.extraLarge\n                font.weight: Font.Medium\n                color: heroCard.accentColor\n            }\n        }\n\n        Behavior on animatedUsage {\n            Anim {\n                duration: Appearance.anim.durations.large\n            }\n        }\n\n        Behavior on animatedTemp {\n            Anim {\n                duration: Appearance.anim.durations.large\n            }\n        }\n    }\n\n    component GaugeCard: StyledRect {\n        id: gaugeCard\n\n        property string icon\n        property string title\n        property real percentage: 0\n        property string subtitle\n        property color accentColor: Colours.palette.m3primary\n        readonly property real arcStartAngle: 0.75 * Math.PI\n        readonly property real arcSweep: 1.5 * Math.PI\n        property real animatedPercentage: 0\n\n        color: Colours.tPalette.m3surfaceContainer\n        radius: Appearance.rounding.large\n        clip: true\n        Component.onCompleted: animatedPercentage = percentage\n        onPercentageChanged: animatedPercentage = percentage\n\n        ColumnLayout {\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.large\n            spacing: Appearance.spacing.smaller\n\n            CardHeader {\n                icon: gaugeCard.icon\n                title: gaugeCard.title\n                accentColor: gaugeCard.accentColor\n            }\n\n            Item {\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n\n                ArcGauge {\n                    anchors.centerIn: parent\n                    width: Math.min(parent.width, parent.height)\n                    height: width\n                    percentage: gaugeCard.animatedPercentage\n                    accentColor: gaugeCard.accentColor\n                    trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)\n                    startAngle: gaugeCard.arcStartAngle\n                    sweepAngle: gaugeCard.arcSweep\n                }\n\n                StyledText {\n                    anchors.centerIn: parent\n                    text: `${Math.round(gaugeCard.percentage * 100)}%`\n                    font.pointSize: Appearance.font.size.extraLarge\n                    font.weight: Font.Medium\n                    color: gaugeCard.accentColor\n                }\n            }\n\n            StyledText {\n                Layout.alignment: Qt.AlignHCenter\n                text: gaugeCard.subtitle\n                font.pointSize: Appearance.font.size.smaller\n                color: Colours.palette.m3onSurfaceVariant\n            }\n        }\n\n        Behavior on animatedPercentage {\n            Anim {\n                duration: Appearance.anim.durations.large\n            }\n        }\n    }\n\n    component StorageGaugeCard: StyledRect {\n        id: storageGaugeCard\n\n        property int currentDiskIndex: 0\n        readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null\n        property int diskCount: 0\n        readonly property real arcStartAngle: 0.75 * Math.PI\n        readonly property real arcSweep: 1.5 * Math.PI\n        property real animatedPercentage: 0\n        property color accentColor: Colours.palette.m3secondary\n\n        color: Colours.tPalette.m3surfaceContainer\n        radius: Appearance.rounding.large\n        clip: true\n        Component.onCompleted: {\n            diskCount = SystemUsage.disks.length;\n            if (currentDisk)\n                animatedPercentage = currentDisk.perc;\n        }\n        onCurrentDiskChanged: {\n            if (currentDisk)\n                animatedPercentage = currentDisk.perc;\n        }\n\n        // Update diskCount and animatedPercentage when disks data changes\n        Connections {\n            function onDisksChanged() {\n                if (SystemUsage.disks.length !== storageGaugeCard.diskCount)\n                    storageGaugeCard.diskCount = SystemUsage.disks.length;\n\n                // Update animated percentage when disk data refreshes\n                if (storageGaugeCard.currentDisk)\n                    storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc;\n            }\n\n            target: SystemUsage\n        }\n\n        MouseArea {\n            anchors.fill: parent\n            onWheel: wheel => {\n                if (wheel.angleDelta.y > 0)\n                    storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount;\n                else if (wheel.angleDelta.y < 0)\n                    storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount;\n            }\n        }\n\n        ColumnLayout {\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.large\n            spacing: Appearance.spacing.smaller\n\n            CardHeader {\n                icon: \"hard_disk\"\n                title: {\n                    const base = qsTr(\"Storage\");\n                    if (!storageGaugeCard.currentDisk)\n                        return base;\n\n                    return `${base} - ${storageGaugeCard.currentDisk.mount}`;\n                }\n                accentColor: storageGaugeCard.accentColor\n\n                // Scroll hint icon\n                MaterialIcon {\n                    text: \"unfold_more\"\n                    color: Colours.palette.m3onSurfaceVariant\n                    font.pointSize: Appearance.font.size.normal\n                    visible: storageGaugeCard.diskCount > 1\n                    opacity: 0.7\n                    ToolTip.visible: hintHover.hovered\n                    ToolTip.text: qsTr(\"Scroll to switch disks\")\n                    ToolTip.delay: 500\n\n                    HoverHandler {\n                        id: hintHover\n                    }\n                }\n            }\n\n            Item {\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n\n                ArcGauge {\n                    anchors.centerIn: parent\n                    width: Math.min(parent.width, parent.height)\n                    height: width\n                    percentage: storageGaugeCard.animatedPercentage\n                    accentColor: storageGaugeCard.accentColor\n                    trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)\n                    startAngle: storageGaugeCard.arcStartAngle\n                    sweepAngle: storageGaugeCard.arcSweep\n                }\n\n                StyledText {\n                    anchors.centerIn: parent\n                    text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : \"—\"\n                    font.pointSize: Appearance.font.size.extraLarge\n                    font.weight: Font.Medium\n                    color: storageGaugeCard.accentColor\n                }\n            }\n\n            StyledText {\n                Layout.alignment: Qt.AlignHCenter\n                text: {\n                    if (!storageGaugeCard.currentDisk)\n                        return \"—\";\n\n                    const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used);\n                    const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total);\n                    return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;\n                }\n                font.pointSize: Appearance.font.size.smaller\n                color: Colours.palette.m3onSurfaceVariant\n            }\n        }\n\n        Behavior on animatedPercentage {\n            Anim {\n                duration: Appearance.anim.durations.large\n            }\n        }\n    }\n\n    component NetworkCard: StyledRect {\n        id: networkCard\n\n        property color accentColor: Colours.palette.m3primary\n\n        color: Colours.tPalette.m3surfaceContainer\n        radius: Appearance.rounding.large\n        clip: true\n\n        Ref {\n            service: NetworkUsage\n        }\n\n        ColumnLayout {\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.large\n            spacing: Appearance.spacing.small\n\n            CardHeader {\n                icon: \"swap_vert\"\n                title: qsTr(\"Network\")\n                accentColor: networkCard.accentColor\n            }\n\n            // Sparkline graph\n            Item {\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n\n                SparklineItem {\n                    id: sparkline\n\n                    property real targetMax: 1024\n                    property real smoothMax: targetMax\n\n                    anchors.fill: parent\n                    line1: NetworkUsage.uploadBuffer\n                    line1Color: Colours.palette.m3secondary\n                    line1FillAlpha: 0.15\n                    line2: NetworkUsage.downloadBuffer\n                    line2Color: Colours.palette.m3tertiary\n                    line2FillAlpha: 0.2\n                    maxValue: smoothMax\n                    historyLength: NetworkUsage.historyLength\n\n                    Connections {\n                        function onValuesChanged(): void {\n                            sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);\n                            slideAnim.restart();\n                        }\n\n                        target: NetworkUsage.downloadBuffer\n                    }\n\n                    NumberAnimation {\n                        id: slideAnim\n\n                        target: sparkline\n                        property: \"slideProgress\"\n                        from: 0\n                        to: 1\n                        duration: Config.dashboard.resourceUpdateInterval\n                    }\n\n                    Behavior on smoothMax {\n                        Anim {\n                            duration: Appearance.anim.durations.large\n                        }\n                    }\n                }\n\n                // \"No data\" placeholder\n                StyledText {\n                    anchors.centerIn: parent\n                    text: qsTr(\"Collecting data...\")\n                    font.pointSize: Appearance.font.size.small\n                    color: Colours.palette.m3onSurfaceVariant\n                    visible: NetworkUsage.downloadBuffer.count < 2\n                    opacity: 0.6\n                }\n            }\n\n            // Download row\n            RowLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.normal\n\n                MaterialIcon {\n                    text: \"download\"\n                    color: Colours.palette.m3tertiary\n                    font.pointSize: Appearance.font.size.normal\n                }\n\n                StyledText {\n                    text: qsTr(\"Download\")\n                    font.pointSize: Appearance.font.size.small\n                    color: Colours.palette.m3onSurfaceVariant\n                }\n\n                Item {\n                    Layout.fillWidth: true\n                }\n\n                StyledText {\n                    text: {\n                        const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0);\n                        return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : \"0.0 B/s\";\n                    }\n                    font.pointSize: Appearance.font.size.normal\n                    font.weight: Font.Medium\n                    color: Colours.palette.m3tertiary\n                }\n            }\n\n            // Upload row\n            RowLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.normal\n\n                MaterialIcon {\n                    text: \"upload\"\n                    color: Colours.palette.m3secondary\n                    font.pointSize: Appearance.font.size.normal\n                }\n\n                StyledText {\n                    text: qsTr(\"Upload\")\n                    font.pointSize: Appearance.font.size.small\n                    color: Colours.palette.m3onSurfaceVariant\n                }\n\n                Item {\n                    Layout.fillWidth: true\n                }\n\n                StyledText {\n                    text: {\n                        const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0);\n                        return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : \"0.0 B/s\";\n                    }\n                    font.pointSize: Appearance.font.size.normal\n                    font.weight: Font.Medium\n                    color: Colours.palette.m3secondary\n                }\n            }\n\n            // Session totals\n            RowLayout {\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.normal\n\n                MaterialIcon {\n                    text: \"history\"\n                    color: Colours.palette.m3onSurfaceVariant\n                    font.pointSize: Appearance.font.size.normal\n                }\n\n                StyledText {\n                    text: qsTr(\"Total\")\n                    font.pointSize: Appearance.font.size.small\n                    color: Colours.palette.m3onSurfaceVariant\n                }\n\n                Item {\n                    Layout.fillWidth: true\n                }\n\n                StyledText {\n                    text: {\n                        const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0);\n                        const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0);\n                        return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : \"↓0.0B ↑0.0B\";\n                    }\n                    font.pointSize: Appearance.font.size.small\n                    color: Colours.palette.m3onSurfaceVariant\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/Tabs.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport Quickshell\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Controls\n\nItem {\n    id: root\n\n    required property real nonAnimWidth\n    required property DashboardState state\n    required property var tabs\n\n    readonly property alias count: bar.count\n\n    implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight\n\n    TabBar {\n        id: bar\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.top: parent.top\n\n        currentIndex: root.state.currentTab\n        background: null\n\n        onCurrentIndexChanged: root.state.currentTab = currentIndex\n\n        Repeater {\n            model: ScriptModel {\n                values: root.tabs\n            }\n\n            delegate: Tab {\n                required property var modelData\n\n                iconName: modelData.iconName\n                text: modelData.text\n            }\n        }\n    }\n\n    Item {\n        id: indicator\n\n        anchors.top: bar.bottom\n        anchors.topMargin: 5\n\n        implicitWidth: {\n            const tab = bar.currentItem;\n            if (tab)\n                return tab.implicitWidth;\n            const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count;\n            return width;\n        }\n        implicitHeight: 3\n\n        x: {\n            const tab = bar.currentItem;\n            const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count;\n            const tabWidth = tab?.implicitWidth ?? width;\n            return width * bar.currentIndex + (width - tabWidth) / 2;\n        }\n\n        clip: true\n\n        StyledRect {\n            anchors.top: parent.top\n            anchors.left: parent.left\n            anchors.right: parent.right\n            implicitHeight: parent.implicitHeight * 2\n\n            color: Colours.palette.m3primary\n            radius: Appearance.rounding.full\n        }\n\n        Behavior on x {\n            Anim {}\n        }\n\n        Behavior on implicitWidth {\n            Anim {}\n        }\n    }\n\n    StyledRect {\n        id: separator\n\n        anchors.top: indicator.bottom\n        anchors.left: parent.left\n        anchors.right: parent.right\n\n        implicitHeight: 1\n        color: Colours.palette.m3outlineVariant\n    }\n\n    component Tab: TabButton {\n        id: tab\n\n        required property string iconName\n        readonly property bool current: TabBar.tabBar.currentItem === this\n\n        background: null\n\n        contentItem: CustomMouseArea {\n            id: mouse\n\n            function onWheel(event: WheelEvent): void {\n                if (event.angleDelta.y < 0)\n                    root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1);\n                else if (event.angleDelta.y > 0)\n                    root.state.currentTab = Math.max(root.state.currentTab - 1, 0);\n            }\n\n            implicitWidth: Math.max(icon.width, label.width)\n            implicitHeight: icon.height + label.height\n\n            cursorShape: Qt.PointingHandCursor\n\n            onPressed: event => {\n                root.state.currentTab = tab.TabBar.index;\n\n                const stateY = stateWrapper.y;\n                rippleAnim.x = event.x;\n                rippleAnim.y = event.y - stateY;\n\n                const dist = (ox, oy) => ox * ox + oy * oy;\n                rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y + stateY), dist(event.x, stateWrapper.height - event.y), dist(width - event.x, event.y + stateY), dist(width - event.x, stateWrapper.height - event.y)));\n\n                rippleAnim.restart();\n            }\n\n            SequentialAnimation {\n                id: rippleAnim\n\n                property real x\n                property real y\n                property real radius\n\n                PropertyAction {\n                    target: ripple\n                    property: \"x\"\n                    value: rippleAnim.x\n                }\n                PropertyAction {\n                    target: ripple\n                    property: \"y\"\n                    value: rippleAnim.y\n                }\n                PropertyAction {\n                    target: ripple\n                    property: \"opacity\"\n                    value: 0.08\n                }\n                Anim {\n                    target: ripple\n                    properties: \"implicitWidth,implicitHeight\"\n                    from: 0\n                    to: rippleAnim.radius * 2\n                    duration: Appearance.anim.durations.normal\n                    easing.bezierCurve: Appearance.anim.curves.standardDecel\n                }\n                Anim {\n                    target: ripple\n                    property: \"opacity\"\n                    to: 0\n                    duration: Appearance.anim.durations.normal\n                    easing.type: Easing.BezierSpline\n                    easing.bezierCurve: Appearance.anim.curves.standard\n                }\n            }\n\n            ClippingRectangle {\n                id: stateWrapper\n\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.verticalCenter: parent.verticalCenter\n                implicitHeight: parent.height + Config.dashboard.sizes.tabIndicatorSpacing * 2\n\n                color: \"transparent\"\n                radius: Appearance.rounding.small\n\n                StyledRect {\n                    id: stateLayer\n\n                    anchors.fill: parent\n\n                    color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface\n                    opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0\n\n                    Behavior on opacity {\n                        Anim {}\n                    }\n                }\n\n                StyledRect {\n                    id: ripple\n\n                    radius: Appearance.rounding.full\n                    color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface\n                    opacity: 0\n\n                    transform: Translate {\n                        x: -ripple.width / 2\n                        y: -ripple.height / 2\n                    }\n                }\n            }\n\n            MaterialIcon {\n                id: icon\n\n                anchors.horizontalCenter: parent.horizontalCenter\n                anchors.bottom: label.top\n\n                text: tab.iconName\n                color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant\n                fill: tab.current ? 1 : 0\n                font.pointSize: Appearance.font.size.large\n\n                Behavior on fill {\n                    Anim {}\n                }\n            }\n\n            StyledText {\n                id: label\n\n                anchors.horizontalCenter: parent.horizontalCenter\n                anchors.bottom: parent.bottom\n\n                text: tab.text\n                color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/Weather.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null\n\n    implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840\n    implicitHeight: layout.implicitHeight\n    Component.onCompleted: Weather.reload()\n\n    ColumnLayout {\n        id: layout\n\n        anchors.fill: parent\n        spacing: Appearance.spacing.smaller\n\n        RowLayout {\n            Layout.leftMargin: Appearance.padding.large\n            Layout.rightMargin: Appearance.padding.large\n            Layout.fillWidth: true\n\n            Column {\n                spacing: Appearance.spacing.small / 2\n\n                StyledText {\n                    text: Weather.city || qsTr(\"Loading...\")\n                    font.pointSize: Appearance.font.size.extraLarge\n                    font.weight: 600\n                    color: Colours.palette.m3onSurface\n                }\n\n                StyledText {\n                    text: new Date().toLocaleDateString(Qt.locale(), \"dddd, MMMM d\")\n                    font.pointSize: Appearance.font.size.small\n                    color: Colours.palette.m3onSurfaceVariant\n                }\n            }\n\n            Item {\n                Layout.fillWidth: true\n            }\n\n            Row {\n                spacing: Appearance.spacing.large\n\n                WeatherStat {\n                    icon: \"wb_twilight\"\n                    label: \"Sunrise\"\n                    value: Weather.sunrise\n                    colour: Colours.palette.m3tertiary\n                }\n\n                WeatherStat {\n                    icon: \"bedtime\"\n                    label: \"Sunset\"\n                    value: Weather.sunset\n                    colour: Colours.palette.m3tertiary\n                }\n            }\n        }\n\n        StyledRect {\n            Layout.fillWidth: true\n            implicitHeight: bigInfoRow.implicitHeight + Appearance.padding.small * 2\n\n            radius: Appearance.rounding.large * 2\n            color: Colours.tPalette.m3surfaceContainer\n\n            RowLayout {\n                id: bigInfoRow\n\n                anchors.centerIn: parent\n                spacing: Appearance.spacing.large\n\n                MaterialIcon {\n                    Layout.alignment: Qt.AlignVCenter\n                    text: Weather.icon\n                    font.pointSize: Appearance.font.size.extraLarge * 3\n                    color: Colours.palette.m3secondary\n                    animate: true\n                }\n\n                ColumnLayout {\n                    Layout.alignment: Qt.AlignVCenter\n                    spacing: -Appearance.spacing.small\n\n                    StyledText {\n                        text: Weather.temp\n                        font.pointSize: Appearance.font.size.extraLarge * 2\n                        font.weight: 500\n                        color: Colours.palette.m3primary\n                    }\n\n                    StyledText {\n                        Layout.leftMargin: Appearance.padding.small\n                        text: Weather.description\n                        font.pointSize: Appearance.font.size.normal\n                        color: Colours.palette.m3onSurfaceVariant\n                    }\n                }\n            }\n        }\n\n        RowLayout {\n            Layout.fillWidth: true\n            spacing: Appearance.spacing.smaller\n\n            DetailCard {\n                icon: \"water_drop\"\n                label: \"Humidity\"\n                value: Weather.humidity + \"%\"\n                colour: Colours.palette.m3secondary\n            }\n            DetailCard {\n                icon: \"thermostat\"\n                label: \"Feels Like\"\n                value: Weather.feelsLike\n                colour: Colours.palette.m3primary\n            }\n            DetailCard {\n                icon: \"air\"\n                label: \"Wind\"\n                value: Weather.windSpeed ? Weather.windSpeed + \" km/h\" : \"--\"\n                colour: Colours.palette.m3tertiary\n            }\n        }\n\n        StyledText {\n            Layout.topMargin: Appearance.spacing.normal\n            Layout.leftMargin: Appearance.padding.normal\n            visible: forecastRepeater.count > 0\n            text: qsTr(\"7-Day Forecast\")\n            font.pointSize: Appearance.font.size.normal\n            font.weight: 600\n            color: Colours.palette.m3onSurface\n        }\n\n        RowLayout {\n            Layout.fillWidth: true\n            spacing: Appearance.spacing.smaller\n\n            Repeater {\n                id: forecastRepeater\n\n                model: Weather.forecast\n\n                StyledRect {\n                    id: forecastItem\n\n                    required property int index\n                    required property var modelData\n\n                    Layout.fillWidth: true\n                    implicitHeight: forecastItemColumn.implicitHeight + Appearance.padding.normal * 2\n\n                    radius: Appearance.rounding.normal\n                    color: Colours.tPalette.m3surfaceContainer\n\n                    ColumnLayout {\n                        id: forecastItemColumn\n\n                        anchors.centerIn: parent\n                        spacing: Appearance.spacing.small\n\n                        StyledText {\n                            Layout.alignment: Qt.AlignHCenter\n                            text: forecastItem.index === 0 ? qsTr(\"Today\") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), \"ddd\")\n                            font.pointSize: Appearance.font.size.normal\n                            font.weight: 600\n                            color: Colours.palette.m3primary\n                        }\n\n                        StyledText {\n                            Layout.topMargin: -Appearance.spacing.small / 2\n                            Layout.alignment: Qt.AlignHCenter\n                            text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), \"MMM d\")\n                            font.pointSize: Appearance.font.size.small\n                            opacity: 0.7\n                            color: Colours.palette.m3onSurfaceVariant\n                        }\n\n                        MaterialIcon {\n                            Layout.alignment: Qt.AlignHCenter\n                            text: forecastItem.modelData.icon\n                            font.pointSize: Appearance.font.size.extraLarge\n                            color: Colours.palette.m3secondary\n                        }\n\n                        StyledText {\n                            Layout.alignment: Qt.AlignHCenter\n                            text: Config.services.useFahrenheit ? forecastItem.modelData.maxTempF + \"°\" + \" / \" + forecastItem.modelData.minTempF + \"°\" : forecastItem.modelData.maxTempC + \"°\" + \" / \" + forecastItem.modelData.minTempC + \"°\"\n                            font.weight: 600\n                            color: Colours.palette.m3tertiary\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    component DetailCard: StyledRect {\n        id: detailRoot\n\n        property string icon\n        property string label\n        property string value\n        property color colour\n\n        Layout.fillWidth: true\n        Layout.preferredHeight: 60\n        radius: Appearance.rounding.small\n        color: Colours.tPalette.m3surfaceContainer\n\n        Row {\n            anchors.centerIn: parent\n            spacing: Appearance.spacing.normal\n\n            MaterialIcon {\n                text: detailRoot.icon\n                color: detailRoot.colour\n                font.pointSize: Appearance.font.size.large\n                anchors.verticalCenter: parent.verticalCenter\n            }\n\n            Column {\n                anchors.verticalCenter: parent.verticalCenter\n                spacing: 0\n\n                StyledText {\n                    text: detailRoot.label\n                    font.pointSize: Appearance.font.size.smaller\n                    opacity: 0.7\n                    horizontalAlignment: Text.AlignLeft\n                }\n                StyledText {\n                    text: detailRoot.value\n                    font.weight: 600\n                    horizontalAlignment: Text.AlignLeft\n                }\n            }\n        }\n    }\n\n    component WeatherStat: Row {\n        id: weatherStat\n\n        property string icon\n        property string label\n        property string value\n        property color colour\n\n        spacing: Appearance.spacing.small\n\n        MaterialIcon {\n            text: weatherStat.icon\n            font.pointSize: Appearance.font.size.extraLarge\n            color: weatherStat.colour\n        }\n\n        Column {\n            StyledText {\n                text: weatherStat.label\n                font.pointSize: Appearance.font.size.smaller\n                color: Colours.palette.m3onSurfaceVariant\n            }\n            StyledText {\n                text: weatherStat.value\n                font.pointSize: Appearance.font.size.small\n                font.weight: 600\n                color: Colours.palette.m3onSurface\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/Wrapper.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.filedialog\nimport qs.config\nimport qs.utils\nimport Caelestia\nimport Quickshell\nimport QtQuick\n\nItem {\n    id: root\n\n    required property DrawerVisibilities visibilities\n    readonly property bool needsKeyboard: (content.item as Content)?.needsKeyboard ?? false\n    readonly property DashboardState dashState: DashboardState {\n        reloadableId: \"dashboardState\"\n    }\n    readonly property FileDialog facePicker: FileDialog {\n        title: qsTr(\"Select a profile picture\")\n        filterLabel: qsTr(\"Image files\")\n        filters: Images.validImageExtensions\n        onAccepted: path => {\n            if (CUtils.copyFile(Qt.resolvedUrl(path), Qt.resolvedUrl(`${Paths.home}/.face`)))\n                Quickshell.execDetached([\"notify-send\", \"-a\", \"caelestia-shell\", \"-u\", \"low\", \"-h\", `STRING:image-path:${path}`, \"Profile picture changed\", `Profile picture changed to ${Paths.shortenHome(path)}`]);\n            else\n                Quickshell.execDetached([\"notify-send\", \"-a\", \"caelestia-shell\", \"-u\", \"critical\", \"Unable to change profile picture\", `Failed to change profile picture to ${Paths.shortenHome(path)}`]);\n        }\n    }\n\n    readonly property real nonAnimHeight: state === \"visible\" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0\n\n    visible: height > 0\n    implicitHeight: 0\n    implicitWidth: content.implicitWidth\n\n    onStateChanged: {\n        if (state === \"visible\" && timer.running) {\n            timer.triggered();\n            timer.stop();\n        }\n    }\n\n    states: State {\n        name: \"visible\"\n        when: root.visibilities.dashboard && Config.dashboard.enabled\n\n        PropertyChanges {\n            root.implicitHeight: content.implicitHeight\n        }\n    }\n\n    transitions: [\n        Transition {\n            from: \"\"\n            to: \"visible\"\n\n            Anim {\n                target: root\n                property: \"implicitHeight\"\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        },\n        Transition {\n            from: \"visible\"\n            to: \"\"\n\n            Anim {\n                target: root\n                property: \"implicitHeight\"\n                easing.bezierCurve: Appearance.anim.curves.emphasized\n            }\n        }\n    ]\n\n    Timer {\n        id: timer\n\n        running: true\n        interval: Appearance.anim.durations.extraLarge\n        onTriggered: {\n            content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible);\n            content.visible = true;\n        }\n    }\n\n    Loader {\n        id: content\n\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.bottom: parent.bottom\n\n        visible: false\n        active: true\n\n        sourceComponent: Content {\n            visibilities: root.visibilities\n            state: root.dashState\n            facePicker: root.facePicker\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/dash/Calendar.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.effects\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\n\nCustomMouseArea {\n    id: root\n\n    required property var state\n\n    readonly property int currMonth: state.currentDate.getMonth()\n    readonly property int currYear: state.currentDate.getFullYear()\n\n    function onWheel(event: WheelEvent): void {\n        if (event.angleDelta.y > 0)\n            root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1);\n        else if (event.angleDelta.y < 0)\n            root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1);\n    }\n\n    anchors.left: parent.left\n    anchors.right: parent.right\n    implicitHeight: inner.implicitHeight + inner.anchors.margins * 2\n\n    acceptedButtons: Qt.MiddleButton\n    onClicked: root.state.currentDate = new Date()\n\n    ColumnLayout {\n        id: inner\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.large\n        spacing: Appearance.spacing.small\n\n        RowLayout {\n            id: monthNavigationRow\n\n            Layout.fillWidth: true\n            spacing: Appearance.spacing.small\n\n            Item {\n                implicitWidth: implicitHeight\n                implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2\n\n                StateLayer {\n                    id: prevMonthStateLayer\n\n                    function onClicked(): void {\n                        root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1);\n                    }\n\n                    radius: Appearance.rounding.full\n                }\n\n                MaterialIcon {\n                    id: prevMonthText\n\n                    anchors.centerIn: parent\n                    text: \"chevron_left\"\n                    color: Colours.palette.m3tertiary\n                    font.pointSize: Appearance.font.size.normal\n                    font.weight: 700\n                }\n            }\n\n            Item {\n                Layout.fillWidth: true\n\n                implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2\n                implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2\n\n                StateLayer {\n                    function onClicked(): void {\n                        root.state.currentDate = new Date();\n                    }\n\n                    anchors.fill: monthYearDisplay\n                    anchors.margins: -Appearance.padding.small\n                    anchors.leftMargin: -Appearance.padding.normal\n                    anchors.rightMargin: -Appearance.padding.normal\n\n                    radius: Appearance.rounding.full\n                    disabled: {\n                        const now = new Date();\n                        return root.currMonth === now.getMonth() && root.currYear === now.getFullYear();\n                    }\n                }\n\n                StyledText {\n                    id: monthYearDisplay\n\n                    anchors.centerIn: parent\n                    text: grid.title\n                    color: Colours.palette.m3primary\n                    font.pointSize: Appearance.font.size.normal\n                    font.weight: 500\n                    font.capitalization: Font.Capitalize\n                }\n            }\n\n            Item {\n                implicitWidth: implicitHeight\n                implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2\n\n                StateLayer {\n                    id: nextMonthStateLayer\n\n                    function onClicked(): void {\n                        root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1);\n                    }\n\n                    radius: Appearance.rounding.full\n                }\n\n                MaterialIcon {\n                    id: nextMonthText\n\n                    anchors.centerIn: parent\n                    text: \"chevron_right\"\n                    color: Colours.palette.m3tertiary\n                    font.pointSize: Appearance.font.size.normal\n                    font.weight: 700\n                }\n            }\n        }\n\n        DayOfWeekRow {\n            id: daysRow\n\n            Layout.fillWidth: true\n            locale: grid.locale\n\n            delegate: StyledText {\n                required property var model\n\n                horizontalAlignment: Text.AlignHCenter\n                text: model.shortName\n                font.weight: 500\n                color: (model.day === 0 || model.day === 6) ? Colours.palette.m3secondary : Colours.palette.m3onSurfaceVariant\n            }\n        }\n\n        Item {\n            Layout.fillWidth: true\n            implicitHeight: grid.implicitHeight\n\n            MonthGrid {\n                id: grid\n\n                month: root.currMonth\n                year: root.currYear\n\n                anchors.fill: parent\n\n                spacing: 3\n                locale: Qt.locale()\n\n                delegate: Item {\n                    id: dayItem\n\n                    required property var model\n\n                    implicitWidth: implicitHeight\n                    implicitHeight: text.implicitHeight + Appearance.padding.small * 2\n\n                    StyledText {\n                        id: text\n\n                        anchors.centerIn: parent\n\n                        horizontalAlignment: Text.AlignHCenter\n                        text: grid.locale.toString(dayItem.model.day)\n                        color: {\n                            const dayOfWeek = dayItem.model.date.getUTCDay();\n                            if (dayOfWeek === 0 || dayOfWeek === 6)\n                                return Colours.palette.m3secondary;\n\n                            return Colours.palette.m3onSurfaceVariant;\n                        }\n                        opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4\n                        font.pointSize: Appearance.font.size.normal\n                        font.weight: 500\n                    }\n                }\n            }\n\n            StyledRect {\n                id: todayIndicator\n\n                readonly property Item todayItem: grid.contentItem.children.find(c => c.model.today) ?? null\n                property Item today\n\n                onTodayItemChanged: {\n                    if (todayItem)\n                        today = todayItem;\n                }\n\n                x: today ? today.x + (today.width - implicitWidth) / 2 : 0\n                y: today?.y ?? 0\n\n                implicitWidth: today?.implicitWidth ?? 0\n                implicitHeight: today?.implicitHeight ?? 0\n\n                clip: true\n                radius: Appearance.rounding.full\n                color: Colours.palette.m3primary\n\n                opacity: todayItem ? 1 : 0\n                scale: todayItem ? 1 : 0.7\n\n                Colouriser {\n                    x: -todayIndicator.x\n                    y: -todayIndicator.y\n\n                    implicitWidth: grid.width\n                    implicitHeight: grid.height\n\n                    source: grid\n                    sourceColor: Colours.palette.m3onSurface\n                    colorizationColor: Colours.palette.m3onPrimary\n                }\n\n                Behavior on opacity {\n                    Anim {}\n                }\n\n                Behavior on scale {\n                    Anim {}\n                }\n\n                Behavior on x {\n                    Anim {\n                        duration: Appearance.anim.durations.expressiveDefaultSpatial\n                        easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                    }\n                }\n\n                Behavior on y {\n                    Anim {\n                        duration: Appearance.anim.durations.expressiveDefaultSpatial\n                        easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/dash/DateTime.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    anchors.top: parent.top\n    anchors.bottom: parent.bottom\n    implicitWidth: Config.dashboard.sizes.dateTimeWidth\n\n    ColumnLayout {\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.verticalCenter: parent.verticalCenter\n        spacing: 0\n\n        StyledText {\n            Layout.bottomMargin: -(font.pointSize * 0.4)\n            Layout.alignment: Qt.AlignHCenter\n            text: Time.hourStr\n            color: Colours.palette.m3secondary\n            font.pointSize: Appearance.font.size.extraLarge\n            font.family: Appearance.font.family.clock\n            font.weight: 600\n        }\n\n        StyledText {\n            Layout.alignment: Qt.AlignHCenter\n            text: \"•••\"\n            color: Colours.palette.m3primary\n            font.pointSize: Appearance.font.size.extraLarge * 0.9\n            font.family: Appearance.font.family.clock\n        }\n\n        StyledText {\n            Layout.topMargin: -(font.pointSize * 0.4)\n            Layout.alignment: Qt.AlignHCenter\n            text: Time.minuteStr\n            color: Colours.palette.m3secondary\n            font.pointSize: Appearance.font.size.extraLarge\n            font.family: Appearance.font.family.clock\n            font.weight: 600\n        }\n\n        Loader {\n            asynchronous: true\n            Layout.alignment: Qt.AlignHCenter\n\n            active: Config.services.useTwelveHourClock\n            visible: active\n\n            sourceComponent: StyledText {\n                text: Time.amPmStr\n                color: Colours.palette.m3primary\n                font.pointSize: Appearance.font.size.large\n                font.family: Appearance.font.family.clock\n                font.weight: 600\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/dash/Media.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Caelestia.Services\nimport QtQuick\nimport QtQuick.Shapes\n\nItem {\n    id: root\n\n    property real playerProgress: {\n        const active = Players.active;\n        return active?.length ? active.position / active.length : 0;\n    }\n\n    anchors.top: parent.top\n    anchors.bottom: parent.bottom\n    implicitWidth: Config.dashboard.sizes.mediaWidth\n\n    Behavior on playerProgress {\n        Anim {\n            duration: Appearance.anim.durations.large\n        }\n    }\n\n    Timer {\n        running: Players.active?.isPlaying ?? false\n        interval: Config.dashboard.mediaUpdateInterval\n        triggeredOnStart: true\n        repeat: true\n        onTriggered: Players.active?.positionChanged()\n    }\n\n    ServiceRef {\n        service: Audio.beatTracker\n    }\n\n    Shape {\n        preferredRendererType: Shape.CurveRenderer\n\n        ShapePath {\n            fillColor: \"transparent\"\n            strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)\n            strokeWidth: Config.dashboard.sizes.mediaProgressThickness\n            capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap\n\n            PathAngleArc {\n                centerX: cover.x + cover.width / 2\n                centerY: cover.y + cover.height / 2\n                radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small\n                radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small\n                startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2\n                sweepAngle: Config.dashboard.sizes.mediaProgressSweep\n            }\n\n            Behavior on strokeColor {\n                CAnim {}\n            }\n        }\n\n        ShapePath {\n            fillColor: \"transparent\"\n            strokeColor: Colours.palette.m3primary\n            strokeWidth: Config.dashboard.sizes.mediaProgressThickness\n            capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap\n\n            PathAngleArc {\n                centerX: cover.x + cover.width / 2\n                centerY: cover.y + cover.height / 2\n                radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small\n                radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small\n                startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2\n                sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress\n            }\n\n            Behavior on strokeColor {\n                CAnim {}\n            }\n        }\n    }\n\n    StyledClippingRect {\n        id: cover\n\n        anchors.top: parent.top\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.margins: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small\n\n        implicitHeight: width\n        color: Colours.tPalette.m3surfaceContainerHigh\n        radius: Infinity\n\n        MaterialIcon {\n            anchors.centerIn: parent\n\n            grade: 200\n            text: \"art_track\"\n            color: Colours.palette.m3onSurfaceVariant\n            font.pointSize: (parent.width * 0.4) || 1\n        }\n\n        Image {\n            id: image\n\n            anchors.fill: parent\n\n            source: Players.active?.trackArtUrl ?? \"\" // qmllint disable incompatible-type\n            asynchronous: true\n            fillMode: Image.PreserveAspectCrop\n            sourceSize.width: width\n            sourceSize.height: height\n        }\n    }\n\n    StyledText {\n        id: title\n\n        anchors.top: cover.bottom\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.topMargin: Appearance.spacing.normal\n\n        animate: true\n        horizontalAlignment: Text.AlignHCenter\n        text: (Players.active?.trackTitle ?? qsTr(\"No media\")) || qsTr(\"Unknown title\")\n        color: Colours.palette.m3primary\n        font.pointSize: Appearance.font.size.normal\n\n        width: parent.implicitWidth - Appearance.padding.large * 2\n        elide: Text.ElideRight\n    }\n\n    StyledText {\n        id: album\n\n        anchors.top: title.bottom\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.topMargin: Appearance.spacing.small\n\n        animate: true\n        horizontalAlignment: Text.AlignHCenter\n        text: (Players.active?.trackAlbum ?? qsTr(\"No media\")) || qsTr(\"Unknown album\")\n        color: Colours.palette.m3outline\n        font.pointSize: Appearance.font.size.small\n\n        width: parent.implicitWidth - Appearance.padding.large * 2\n        elide: Text.ElideRight\n    }\n\n    StyledText {\n        id: artist\n\n        anchors.top: album.bottom\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.topMargin: Appearance.spacing.small\n\n        animate: true\n        horizontalAlignment: Text.AlignHCenter\n        text: (Players.active?.trackArtist ?? qsTr(\"No media\")) || qsTr(\"Unknown artist\")\n        color: Colours.palette.m3secondary\n\n        width: parent.implicitWidth - Appearance.padding.large * 2\n        elide: Text.ElideRight\n    }\n\n    Row {\n        id: controls\n\n        anchors.top: artist.bottom\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.topMargin: Appearance.spacing.smaller\n\n        spacing: Appearance.spacing.small\n\n        Control {\n            function onClicked(): void {\n                Players.active?.previous();\n            }\n\n            icon: \"skip_previous\"\n            canUse: Players.active?.canGoPrevious ?? false\n        }\n\n        Control {\n            function onClicked(): void {\n                Players.active?.togglePlaying();\n            }\n\n            icon: Players.active?.isPlaying ? \"pause\" : \"play_arrow\"\n            canUse: Players.active?.canTogglePlaying ?? false\n        }\n\n        Control {\n            function onClicked(): void {\n                Players.active?.next();\n            }\n\n            icon: \"skip_next\"\n            canUse: Players.active?.canGoNext ?? false\n        }\n    }\n\n    AnimatedImage {\n        id: bongocat\n\n        anchors.top: controls.bottom\n        anchors.bottom: parent.bottom\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.topMargin: Appearance.spacing.small\n        anchors.bottomMargin: Appearance.padding.large\n        anchors.margins: Appearance.padding.large * 2\n\n        playing: Players.active?.isPlaying ?? false\n        speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment\n        source: Paths.absolutePath(Config.paths.mediaGif)\n        asynchronous: true\n        fillMode: AnimatedImage.PreserveAspectFit\n    }\n\n    component Control: StyledRect {\n        id: control\n\n        required property string icon\n        required property bool canUse\n\n        function onClicked(): void {\n        }\n\n        implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small\n        implicitHeight: implicitWidth\n\n        StateLayer {\n            function onClicked(): void {\n                control.onClicked();\n            }\n\n            disabled: !control.canUse\n            radius: Appearance.rounding.full\n        }\n\n        MaterialIcon {\n            id: icon\n\n            anchors.centerIn: parent\n            anchors.verticalCenterOffset: font.pointSize * 0.05\n\n            animate: true\n            text: control.icon\n            color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline\n            font.pointSize: Appearance.font.size.large\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/dash/Resources.qml",
    "content": "import qs.components\nimport qs.components.misc\nimport qs.services\nimport qs.config\nimport QtQuick\n\nRow {\n    id: root\n\n    anchors.top: parent.top\n    anchors.bottom: parent.bottom\n\n    padding: Appearance.padding.large\n    spacing: Appearance.spacing.normal\n\n    Ref {\n        service: SystemUsage\n    }\n\n    Resource {\n        icon: \"memory\"\n        value: SystemUsage.cpuPerc\n        colour: Colours.palette.m3primary\n    }\n\n    Resource {\n        icon: \"memory_alt\"\n        value: SystemUsage.memPerc\n        colour: Colours.palette.m3secondary\n    }\n\n    Resource {\n        icon: \"hard_disk\"\n        value: SystemUsage.storagePerc\n        colour: Colours.palette.m3tertiary\n    }\n\n    component Resource: Item {\n        id: res\n\n        required property string icon\n        required property real value\n        required property color colour\n\n        anchors.top: parent.top\n        anchors.bottom: parent.bottom\n        anchors.margins: Appearance.padding.large\n        implicitWidth: icon.implicitWidth\n\n        StyledRect {\n            anchors.horizontalCenter: parent.horizontalCenter\n            anchors.top: parent.top\n            anchors.bottom: icon.top\n            anchors.bottomMargin: Appearance.spacing.small\n\n            implicitWidth: Config.dashboard.sizes.resourceProgessThickness\n\n            color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)\n            radius: Appearance.rounding.full\n\n            StyledRect {\n                anchors.left: parent.left\n                anchors.right: parent.right\n                anchors.bottom: parent.bottom\n                implicitHeight: res.value * parent.height\n\n                color: res.colour\n                radius: Appearance.rounding.full\n            }\n        }\n\n        MaterialIcon {\n            id: icon\n\n            anchors.bottom: parent.bottom\n\n            text: res.icon\n            color: res.colour\n        }\n\n        Behavior on value {\n            Anim {\n                duration: Appearance.anim.durations.large\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/dash/User.qml",
    "content": "import qs.components\nimport qs.components.effects\nimport qs.components.images\nimport qs.components.filedialog\nimport qs.services\nimport qs.config\nimport qs.utils\nimport QtQuick\n\nRow {\n    id: root\n\n    required property DrawerVisibilities visibilities\n    required property DashboardState state\n    required property FileDialog facePicker\n\n    padding: Appearance.padding.large\n    spacing: Appearance.spacing.normal\n\n    StyledClippingRect {\n        implicitWidth: info.implicitHeight\n        implicitHeight: info.implicitHeight\n\n        radius: Appearance.rounding.large\n        color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)\n\n        MaterialIcon {\n            anchors.centerIn: parent\n\n            text: \"person\"\n            fill: 1\n            grade: 200\n            font.pointSize: Math.floor(info.implicitHeight / 2) || 1\n            visible: pfp.status !== Image.Ready\n        }\n\n        CachingImage {\n            id: pfp\n\n            anchors.fill: parent\n            path: `${Paths.home}/.face`\n        }\n\n        MouseArea {\n            anchors.fill: parent\n            hoverEnabled: true\n\n            StyledRect {\n                anchors.fill: parent\n\n                color: Qt.alpha(Colours.palette.m3scrim, 0.5)\n                opacity: parent.containsMouse ? 1 : 0\n\n                Behavior on opacity {\n                    Anim {\n                        duration: Appearance.anim.durations.expressiveFastSpatial\n                    }\n                }\n            }\n\n            StyledRect {\n                anchors.centerIn: parent\n\n                implicitWidth: selectIcon.implicitHeight + Appearance.padding.small * 2\n                implicitHeight: selectIcon.implicitHeight + Appearance.padding.small * 2\n\n                radius: Appearance.rounding.normal\n                color: Colours.palette.m3primary\n                scale: parent.containsMouse ? 1 : 0.5\n                opacity: parent.containsMouse ? 1 : 0\n\n                StateLayer {\n                    function onClicked(): void {\n                        root.visibilities.launcher = false;\n                        root.facePicker.open();\n                    }\n\n                    color: Colours.palette.m3onPrimary\n                }\n\n                MaterialIcon {\n                    id: selectIcon\n\n                    anchors.centerIn: parent\n                    anchors.horizontalCenterOffset: -font.pointSize * 0.02\n\n                    text: \"frame_person\"\n                    color: Colours.palette.m3onPrimary\n                    font.pointSize: Appearance.font.size.extraLarge\n                }\n\n                Behavior on scale {\n                    Anim {\n                        duration: Appearance.anim.durations.expressiveFastSpatial\n                        easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                    }\n                }\n\n                Behavior on opacity {\n                    Anim {\n                        duration: Appearance.anim.durations.expressiveFastSpatial\n                    }\n                }\n            }\n        }\n    }\n\n    Column {\n        id: info\n\n        anchors.verticalCenter: parent.verticalCenter\n        spacing: Appearance.spacing.normal\n\n        Item {\n            id: line\n\n            implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin\n            implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight)\n\n            ColouredIcon {\n                id: icon\n\n                anchors.left: parent.left\n                anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2\n\n                source: SysInfo.osLogo\n                implicitSize: Math.floor(Appearance.font.size.normal * 1.34)\n                colour: Colours.palette.m3primary\n            }\n\n            StyledText {\n                id: text\n\n                anchors.verticalCenter: icon.verticalCenter\n                anchors.left: icon.right\n                anchors.leftMargin: icon.anchors.leftMargin\n                text: `:  ${SysInfo.osPrettyName || SysInfo.osName}`\n                font.pointSize: Appearance.font.size.normal\n\n                width: Config.dashboard.sizes.infoWidth\n                elide: Text.ElideRight\n            }\n        }\n\n        InfoLine {\n            icon: \"select_window_2\"\n            text: SysInfo.wm\n            colour: Colours.palette.m3secondary\n        }\n\n        InfoLine {\n            id: uptime\n\n            icon: \"timer\"\n            text: qsTr(\"up %1\").arg(SysInfo.uptime)\n            colour: Colours.palette.m3tertiary\n        }\n    }\n\n    component InfoLine: Item {\n        id: line\n\n        required property string icon\n        required property string text\n        required property color colour\n\n        implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin\n        implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight)\n\n        MaterialIcon {\n            id: icon\n\n            anchors.left: parent.left\n            anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2\n\n            fill: 1\n            text: line.icon\n            color: line.colour\n            font.pointSize: Appearance.font.size.normal\n        }\n\n        StyledText {\n            id: text\n\n            anchors.verticalCenter: icon.verticalCenter\n            anchors.left: icon.right\n            anchors.leftMargin: icon.anchors.leftMargin\n            text: `:  ${line.text}`\n            font.pointSize: Appearance.font.size.normal\n\n            width: Config.dashboard.sizes.infoWidth\n            elide: Text.ElideRight\n        }\n    }\n}\n"
  },
  {
    "path": "modules/dashboard/dash/Weather.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    anchors.centerIn: parent\n\n    implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin\n\n    Component.onCompleted: Weather.reload()\n\n    MaterialIcon {\n        id: icon\n\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.left: parent.left\n\n        animate: true\n        text: Weather.icon\n        color: Colours.palette.m3secondary\n        font.pointSize: Appearance.font.size.extraLarge * 2\n    }\n\n    Column {\n        id: info\n\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.left: icon.right\n        anchors.leftMargin: Appearance.spacing.large\n\n        spacing: Appearance.spacing.small\n\n        StyledText {\n            anchors.horizontalCenter: parent.horizontalCenter\n\n            animate: true\n            text: Weather.temp\n            color: Colours.palette.m3primary\n            font.pointSize: Appearance.font.size.extraLarge\n            font.weight: 500\n        }\n\n        StyledText {\n            anchors.horizontalCenter: parent.horizontalCenter\n\n            animate: true\n            text: Weather.description\n\n            elide: Text.ElideRight\n            width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Appearance.padding.large * 2)\n        }\n    }\n}\n"
  },
  {
    "path": "modules/drawers/Backgrounds.qml",
    "content": "import qs.config\nimport qs.modules.osd as Osd\nimport qs.modules.notifications as Notifications\nimport qs.modules.session as Session\nimport qs.modules.launcher as Launcher\nimport qs.modules.dashboard as Dashboard\nimport qs.modules.bar.popouts as BarPopouts\nimport qs.modules.utilities as Utilities\nimport qs.modules.sidebar as Sidebar\nimport QtQuick\nimport QtQuick.Shapes\n\nShape {\n    id: root\n\n    required property Panels panels\n    required property Item bar\n\n    anchors.fill: parent\n    anchors.margins: Config.border.thickness\n    anchors.leftMargin: bar.implicitWidth\n    preferredRendererType: Shape.CurveRenderer\n\n    Osd.Background {\n        wrapper: root.panels.osd\n\n        startX: root.width - root.panels.session.width - root.panels.sidebar.width\n        startY: (root.height - wrapper.height) / 2 - rounding\n    }\n\n    Notifications.Background {\n        wrapper: root.panels.notifications\n        sidebar: sidebar\n\n        startX: root.width\n        startY: 0\n    }\n\n    Session.Background {\n        wrapper: root.panels.session\n\n        startX: root.width - root.panels.sidebar.width\n        startY: (root.height - wrapper.height) / 2 - rounding\n    }\n\n    Launcher.Background {\n        wrapper: root.panels.launcher\n\n        startX: (root.width - wrapper.width) / 2 - rounding\n        startY: root.height\n    }\n\n    Dashboard.Background {\n        wrapper: root.panels.dashboard\n\n        startX: (root.width - wrapper.width) / 2 - rounding\n        startY: 0\n    }\n\n    BarPopouts.Background {\n        wrapper: root.panels.popouts\n        invertBottomRounding: wrapper.y + wrapper.height + 1 >= root.height\n\n        startX: wrapper.x\n        startY: wrapper.y - rounding * sideRounding\n    }\n\n    Utilities.Background {\n        wrapper: root.panels.utilities\n        sidebar: sidebar\n\n        startX: root.width\n        startY: root.height\n    }\n\n    Sidebar.Background {\n        id: sidebar\n\n        wrapper: root.panels.sidebar\n        panels: root.panels\n\n        startX: root.width\n        startY: root.panels.notifications.height\n    }\n}\n"
  },
  {
    "path": "modules/drawers/Border.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Effects\n\nItem {\n    id: root\n\n    required property Item bar\n\n    anchors.fill: parent\n\n    StyledRect {\n        anchors.fill: parent\n        color: Colours.palette.m3surface\n\n        layer.enabled: true\n        layer.effect: MultiEffect {\n            maskSource: mask\n            maskEnabled: true\n            maskInverted: true\n            maskThresholdMin: 0.5\n            maskSpreadAtMin: 1\n        }\n    }\n\n    Item {\n        id: mask\n\n        anchors.fill: parent\n        layer.enabled: true\n        visible: false\n\n        Rectangle {\n            anchors.fill: parent\n            anchors.margins: Config.border.thickness\n            anchors.leftMargin: root.bar.implicitWidth\n            radius: Config.border.rounding\n        }\n    }\n}\n"
  },
  {
    "path": "modules/drawers/Drawers.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport qs.utils\nimport qs.modules.bar\nimport Quickshell\nimport Quickshell.Wayland\nimport Quickshell.Hyprland\nimport QtQuick\nimport QtQuick.Controls\nimport QtQuick.Effects\n\nVariants {\n    model: Screens.screens\n\n    Scope {\n        id: scope\n\n        required property ShellScreen modelData\n        readonly property bool barDisabled: Strings.testRegexList(Config.bar.excludedScreens, modelData.name)\n\n        Exclusions {\n            screen: scope.modelData\n            bar: bar\n        }\n\n        StyledWindow {\n            id: win\n\n            readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false\n            readonly property int dragMaskPadding: {\n                if (focusGrab.active || panels.popouts.isDetached)\n                    return 0;\n\n                const mon = Hypr.monitorFor(screen);\n                if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject.windows > 0)\n                    return 0;\n\n                const thresholds = [];\n                for (const panel of [\"dashboard\", \"launcher\", \"session\", \"sidebar\"])\n                    if (Config[panel].enabled)\n                        thresholds.push(Config[panel].dragThreshold);\n                return Math.max(...thresholds);\n            }\n\n            onHasFullscreenChanged: {\n                visibilities.launcher = false;\n                visibilities.session = false;\n                visibilities.dashboard = false;\n            }\n\n            screen: scope.modelData\n            name: \"drawers\"\n            WlrLayershell.exclusionMode: ExclusionMode.Ignore\n            WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None\n\n            mask: Region {\n                x: bar.clampedWidth + win.dragMaskPadding\n                y: Config.border.clampedThickness + win.dragMaskPadding\n                width: win.width - bar.clampedWidth - Config.border.clampedThickness - win.dragMaskPadding * 2\n                height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2\n                intersection: Intersection.Xor\n\n                regions: regions.instances\n            }\n\n            anchors.top: true\n            anchors.bottom: true\n            anchors.left: true\n            anchors.right: true\n\n            Variants {\n                id: regions\n\n                model: panels.children\n\n                Region {\n                    required property Item modelData\n\n                    x: modelData.x + bar.implicitWidth\n                    y: modelData.y + Config.border.thickness\n                    width: modelData.width\n                    height: modelData.height\n                    intersection: Intersection.Subtract\n                }\n            }\n\n            HyprlandFocusGrab {\n                id: focusGrab\n\n                active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith(\"traymenu\") && (panels.popouts.current as StackView)?.depth > 1)\n                windows: [win]\n                onCleared: {\n                    visibilities.launcher = false;\n                    visibilities.session = false;\n                    visibilities.sidebar = false;\n                    visibilities.dashboard = false;\n                    panels.popouts.hasCurrent = false;\n                    bar.closeTray();\n                }\n            }\n\n            StyledRect {\n                anchors.fill: parent\n                opacity: visibilities.session && Config.session.enabled ? 0.5 : 0\n                color: Colours.palette.m3scrim\n\n                Behavior on opacity {\n                    Anim {}\n                }\n            }\n\n            Item {\n                anchors.fill: parent\n                opacity: Colours.transparency.enabled ? Colours.transparency.base : 1\n                layer.enabled: true\n                layer.effect: MultiEffect {\n                    shadowEnabled: true\n                    blurMax: 15\n                    shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7)\n                }\n\n                Border {\n                    bar: bar\n                }\n\n                Backgrounds {\n                    panels: panels\n                    bar: bar\n                }\n            }\n\n            DrawerVisibilities {\n                id: visibilities\n\n                Component.onCompleted: Visibilities.load(scope.modelData, this)\n            }\n\n            Interactions {\n                screen: scope.modelData\n                popouts: panels.popouts\n                visibilities: visibilities\n                panels: panels\n                bar: bar\n\n                Panels {\n                    id: panels\n\n                    screen: scope.modelData\n                    visibilities: visibilities\n                    bar: bar\n                }\n\n                BarWrapper {\n                    id: bar\n\n                    anchors.top: parent.top\n                    anchors.bottom: parent.bottom\n\n                    screen: scope.modelData\n                    visibilities: visibilities\n                    popouts: panels.popouts\n\n                    disabled: scope.barDisabled\n\n                    Component.onCompleted: Visibilities.bars.set(scope.modelData, this)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/drawers/Exclusions.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components.containers\nimport qs.config\nimport qs.modules.bar as Bar\nimport Quickshell\nimport QtQuick\n\nScope {\n    id: root\n\n    required property ShellScreen screen\n    required property Bar.BarWrapper bar\n\n    ExclusionZone {\n        anchors.left: true\n        exclusiveZone: root.bar.exclusiveZone\n    }\n\n    ExclusionZone {\n        anchors.top: true\n    }\n\n    ExclusionZone {\n        anchors.right: true\n    }\n\n    ExclusionZone {\n        anchors.bottom: true\n    }\n\n    component ExclusionZone: StyledWindow {\n        screen: root.screen\n        name: \"border-exclusion\"\n        exclusiveZone: Config.border.thickness\n        mask: Region {}\n        implicitWidth: 1\n        implicitHeight: 1\n    }\n}\n"
  },
  {
    "path": "modules/drawers/Interactions.qml",
    "content": "import qs.components\nimport qs.components.controls\nimport qs.config\nimport qs.modules.bar as Bar\nimport qs.modules.bar.popouts as BarPopouts\nimport Quickshell\nimport QtQuick\nimport QtQuick.Controls\n\nCustomMouseArea {\n    id: root\n\n    required property ShellScreen screen\n    required property BarPopouts.Wrapper popouts\n    required property DrawerVisibilities visibilities\n    required property Panels panels\n    required property Bar.BarWrapper bar\n\n    property point dragStart\n    property bool dashboardShortcutActive\n    property bool osdShortcutActive\n    property bool utilitiesShortcutActive\n\n    function withinPanelHeight(panel: Item, x: real, y: real): bool {\n        const panelY = Config.border.thickness + panel.y;\n        return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding;\n    }\n\n    function withinPanelWidth(panel: Item, x: real, y: real): bool {\n        const panelX = bar.implicitWidth + panel.x;\n        return x >= panelX - Config.border.rounding && x <= panelX + panel.width + Config.border.rounding;\n    }\n\n    function inLeftPanel(panel: Item, x: real, y: real): bool {\n        return x < bar.implicitWidth + panel.x + panel.width && withinPanelHeight(panel, x, y);\n    }\n\n    function inRightPanel(panel: Item, x: real, y: real): bool {\n        return x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panel.x) && withinPanelHeight(panel, x, y);\n    }\n\n    function inTopPanel(panel: Item, x: real, y: real): bool {\n        return y < Math.max(Config.border.minThickness, Config.border.thickness + panel.height) + panel.y && withinPanelWidth(panel, x, y);\n    }\n\n    function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool {\n        return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panel.height) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y);\n    }\n\n    function onWheel(event: WheelEvent): void {\n        if (event.x < bar.implicitWidth) {\n            bar.handleWheel(event.y, event.angleDelta);\n        }\n    }\n\n    anchors.fill: parent\n    hoverEnabled: true\n\n    onPressed: event => dragStart = Qt.point(event.x, event.y)\n    onContainsMouseChanged: {\n        if (!containsMouse) {\n            // Only hide if not activated by shortcut\n            if (!osdShortcutActive) {\n                visibilities.osd = false;\n                root.panels.osd.hovered = false;\n            }\n\n            if (!dashboardShortcutActive)\n                visibilities.dashboard = false;\n\n            if (!utilitiesShortcutActive)\n                visibilities.utilities = false;\n\n            if (!popouts.currentName.startsWith(\"traymenu\") || ((popouts.current as StackView)?.depth ?? 0) <= 1) {\n                popouts.hasCurrent = false;\n                bar.closeTray();\n            }\n\n            if (Config.bar.showOnHover)\n                bar.isHovered = false;\n        }\n    }\n\n    onPositionChanged: event => {\n        if (popouts.isDetached)\n            return;\n\n        const x = event.x;\n        const y = event.y;\n        const dragX = x - dragStart.x;\n        const dragY = y - dragStart.y;\n\n        // Show bar in non-exclusive mode on hover\n        if (!visibilities.bar && Config.bar.showOnHover && x < bar.clampedWidth)\n            bar.isHovered = true;\n\n        // Show/hide bar on drag\n        if (pressed && dragStart.x < bar.clampedWidth) {\n            if (dragX > Config.bar.dragThreshold)\n                visibilities.bar = true;\n            else if (dragX < -Config.bar.dragThreshold)\n                visibilities.bar = false;\n        }\n\n        if (panels.sidebar.width === 0) {\n            // Show osd on hover\n            const showOsd = inRightPanel(panels.osd, x, y);\n\n            // Always update visibility based on hover if not in shortcut mode\n            if (!osdShortcutActive) {\n                visibilities.osd = showOsd;\n                root.panels.osd.hovered = showOsd;\n            } else if (showOsd) {\n                // If hovering over OSD area while in shortcut mode, transition to hover control\n                osdShortcutActive = false;\n                root.panels.osd.hovered = true;\n            }\n\n            const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x);\n\n            // Show/hide session on drag\n            if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) {\n                if (dragX < -Config.session.dragThreshold)\n                    visibilities.session = true;\n                else if (dragX > Config.session.dragThreshold)\n                    visibilities.session = false;\n\n                // Show sidebar on drag if in session area and session is nearly fully visible\n                if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold)\n                    visibilities.sidebar = true;\n            } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) {\n                // Show sidebar on drag if not in session area\n                visibilities.sidebar = true;\n            }\n        } else {\n            const outOfSidebar = x < width - panels.sidebar.width;\n            // Show osd on hover\n            const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y);\n\n            // Always update visibility based on hover if not in shortcut mode\n            if (!osdShortcutActive) {\n                visibilities.osd = showOsd;\n                root.panels.osd.hovered = showOsd;\n            } else if (showOsd) {\n                // If hovering over OSD area while in shortcut mode, transition to hover control\n                osdShortcutActive = false;\n                root.panels.osd.hovered = true;\n            }\n\n            // Show/hide session on drag\n            if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) {\n                if (dragX < -Config.session.dragThreshold)\n                    visibilities.session = true;\n                else if (dragX > Config.session.dragThreshold)\n                    visibilities.session = false;\n            }\n\n            // Hide sidebar on drag\n            if (pressed && inRightPanel(panels.sidebar, dragStart.x, 0) && dragX > Config.sidebar.dragThreshold)\n                visibilities.sidebar = false;\n        }\n\n        // Show launcher on hover, or show/hide on drag if hover is disabled\n        if (Config.launcher.showOnHover) {\n            if (!visibilities.launcher && inBottomPanel(panels.launcher, x, y))\n                visibilities.launcher = true;\n        } else if (pressed && inBottomPanel(panels.launcher, dragStart.x, dragStart.y) && withinPanelWidth(panels.launcher, x, y)) {\n            if (dragY < -Config.launcher.dragThreshold)\n                visibilities.launcher = true;\n            else if (dragY > Config.launcher.dragThreshold)\n                visibilities.launcher = false;\n        }\n\n        // Show dashboard on hover\n        const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboard, x, y);\n\n        // Always update visibility based on hover if not in shortcut mode\n        if (!dashboardShortcutActive) {\n            visibilities.dashboard = showDashboard;\n        } else if (showDashboard) {\n            // If hovering over dashboard area while in shortcut mode, transition to hover control\n            dashboardShortcutActive = false;\n        }\n\n        // Show/hide dashboard on drag (for touchscreen devices)\n        if (pressed && inTopPanel(panels.dashboard, dragStart.x, dragStart.y) && withinPanelWidth(panels.dashboard, x, y)) {\n            if (dragY > Config.dashboard.dragThreshold)\n                visibilities.dashboard = true;\n            else if (dragY < -Config.dashboard.dragThreshold)\n                visibilities.dashboard = false;\n        }\n\n        // Show utilities on hover\n        const showUtilities = inBottomPanel(panels.utilities, x, y, true);\n\n        // Always update visibility based on hover if not in shortcut mode\n        if (!utilitiesShortcutActive) {\n            visibilities.utilities = showUtilities;\n        } else if (showUtilities) {\n            // If hovering over utilities area while in shortcut mode, transition to hover control\n            utilitiesShortcutActive = false;\n        }\n\n        // Show popouts on hover\n        if (x < bar.implicitWidth) {\n            bar.checkPopout(y);\n        } else if ((!popouts.currentName.startsWith(\"traymenu\") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) {\n            popouts.hasCurrent = false;\n            bar.closeTray();\n        }\n    }\n\n    // Monitor individual visibility changes\n    Connections {\n        function onLauncherChanged() {\n            // If launcher is hidden, clear shortcut flags for dashboard and OSD\n            if (!root.visibilities.launcher) {\n                root.dashboardShortcutActive = false;\n                root.osdShortcutActive = false;\n                root.utilitiesShortcutActive = false;\n\n                // Also hide dashboard and OSD if they're not being hovered\n                const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY);\n                const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);\n\n                if (!inDashboardArea) {\n                    root.visibilities.dashboard = false;\n                }\n                if (!inOsdArea) {\n                    root.visibilities.osd = false;\n                    root.panels.osd.hovered = false;\n                }\n            }\n        }\n\n        function onDashboardChanged() {\n            if (root.visibilities.dashboard) {\n                // Dashboard became visible, immediately check if this should be shortcut mode\n                const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY);\n                if (!inDashboardArea) {\n                    root.dashboardShortcutActive = true;\n                }\n            } else {\n                // Dashboard hidden, clear shortcut flag\n                root.dashboardShortcutActive = false;\n            }\n        }\n\n        function onOsdChanged() {\n            if (root.visibilities.osd) {\n                // OSD became visible, immediately check if this should be shortcut mode\n                const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);\n                if (!inOsdArea) {\n                    root.osdShortcutActive = true;\n                }\n            } else {\n                // OSD hidden, clear shortcut flag\n                root.osdShortcutActive = false;\n            }\n        }\n\n        function onUtilitiesChanged() {\n            if (root.visibilities.utilities) {\n                // Utilities became visible, immediately check if this should be shortcut mode\n                const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY);\n                if (!inUtilitiesArea) {\n                    root.utilitiesShortcutActive = true;\n                }\n            } else {\n                // Utilities hidden, clear shortcut flag\n                root.utilitiesShortcutActive = false;\n            }\n        }\n\n        target: root.visibilities\n    }\n}\n"
  },
  {
    "path": "modules/drawers/Panels.qml",
    "content": "import qs.components\nimport qs.config\nimport qs.modules.osd as Osd\nimport qs.modules.notifications as Notifications\nimport qs.modules.session as Session\nimport qs.modules.launcher as Launcher\nimport qs.modules.dashboard as Dashboard\nimport qs.modules.bar as Bar\nimport qs.modules.bar.popouts as BarPopouts\nimport qs.modules.utilities as Utilities\nimport qs.modules.utilities.toasts as Toasts\nimport qs.modules.sidebar as Sidebar\nimport Quickshell\nimport QtQuick\n\nItem {\n    id: root\n\n    required property ShellScreen screen\n    required property DrawerVisibilities visibilities\n    required property Bar.BarWrapper bar\n\n    readonly property alias osd: osd\n    readonly property alias notifications: notifications\n    readonly property alias session: session\n    readonly property alias launcher: launcher\n    readonly property alias dashboard: dashboard\n    readonly property alias popouts: popouts\n    readonly property alias utilities: utilities\n    readonly property alias toasts: toasts\n    readonly property alias sidebar: sidebar\n\n    anchors.fill: parent\n    anchors.margins: Config.border.thickness\n    anchors.leftMargin: bar.implicitWidth\n\n    Osd.Wrapper {\n        id: osd\n\n        clip: session.width > 0 || sidebar.width > 0\n        screen: root.screen\n        visibilities: root.visibilities\n\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.right: parent.right\n        anchors.rightMargin: session.width + sidebar.width\n    }\n\n    Notifications.Wrapper {\n        id: notifications\n\n        visibilities: root.visibilities\n        sidebarPanel: sidebar\n        osdPanel: osd\n        sessionPanel: session\n\n        anchors.top: parent.top\n        anchors.right: parent.right\n    }\n\n    Session.Wrapper {\n        id: session\n\n        clip: sidebar.width > 0\n        visibilities: root.visibilities\n        panels: root\n\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.right: parent.right\n        anchors.rightMargin: sidebar.width\n    }\n\n    Launcher.Wrapper {\n        id: launcher\n\n        screen: root.screen\n        visibilities: root.visibilities\n        panels: root\n\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.bottom: parent.bottom\n    }\n\n    Dashboard.Wrapper {\n        id: dashboard\n\n        visibilities: root.visibilities\n\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.top: parent.top\n    }\n\n    BarPopouts.Wrapper {\n        id: popouts\n\n        screen: root.screen\n\n        x: isDetached ? (root.width - nonAnimWidth) / 2 : 0\n        y: {\n            if (isDetached)\n                return (root.height - nonAnimHeight) / 2;\n\n            const off = currentCenter - Config.border.thickness - nonAnimHeight / 2;\n            const diff = root.height - Math.floor(off + nonAnimHeight);\n            if (diff < 0)\n                return off + diff;\n            return Math.max(off, 0);\n        }\n    }\n\n    Utilities.Wrapper {\n        id: utilities\n\n        visibilities: root.visibilities\n        sidebar: sidebar\n        popouts: popouts\n\n        anchors.bottom: parent.bottom\n        anchors.right: parent.right\n    }\n\n    Toasts.Toasts {\n        id: toasts\n\n        anchors.bottom: sidebar.visible ? parent.bottom : utilities.top\n        anchors.right: sidebar.left\n        anchors.margins: Appearance.padding.normal\n    }\n\n    Sidebar.Wrapper {\n        id: sidebar\n\n        visibilities: root.visibilities\n        panels: root\n\n        anchors.top: notifications.bottom\n        anchors.bottom: utilities.top\n        anchors.right: parent.right\n    }\n}\n"
  },
  {
    "path": "modules/launcher/AppList.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.modules.launcher.items\nimport qs.modules.launcher.services\nimport qs.components\nimport qs.components.controls\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\n\nStyledListView {\n    id: root\n\n    required property StyledTextField search\n    required property DrawerVisibilities visibilities\n\n    model: ScriptModel {\n        id: model\n\n        onValuesChanged: root.currentIndex = 0\n    }\n\n    spacing: Appearance.spacing.small\n    orientation: Qt.Vertical\n    implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxShown, count) - spacing\n\n    preferredHighlightBegin: 0\n    preferredHighlightEnd: height\n    highlightRangeMode: ListView.ApplyRange\n\n    highlightFollowsCurrentItem: false\n    highlight: StyledRect {\n        radius: Appearance.rounding.normal\n        color: Colours.palette.m3onSurface\n        opacity: 0.08\n\n        y: root.currentItem?.y ?? 0\n        implicitWidth: root.width\n        implicitHeight: root.currentItem?.implicitHeight ?? 0\n\n        Behavior on y {\n            Anim {\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        }\n    }\n\n    state: {\n        const text = search.text;\n        const prefix = Config.launcher.actionPrefix;\n        if (text.startsWith(prefix)) {\n            for (const action of [\"calc\", \"scheme\", \"variant\"])\n                if (text.startsWith(`${prefix}${action} `))\n                    return action;\n\n            return \"actions\";\n        }\n\n        return \"apps\";\n    }\n\n    onStateChanged: {\n        if (state === \"scheme\" || state === \"variant\")\n            Schemes.reload();\n    }\n\n    states: [\n        State {\n            name: \"apps\"\n\n            PropertyChanges {\n                model.values: Apps.search(search.text)\n                root.delegate: appItem\n            }\n        },\n        State {\n            name: \"actions\"\n\n            PropertyChanges {\n                model.values: Actions.query(search.text)\n                root.delegate: actionItem\n            }\n        },\n        State {\n            name: \"calc\"\n\n            PropertyChanges {\n                model.values: [0]\n                root.delegate: calcItem\n            }\n        },\n        State {\n            name: \"scheme\"\n\n            PropertyChanges {\n                model.values: Schemes.query(search.text)\n                root.delegate: schemeItem\n            }\n        },\n        State {\n            name: \"variant\"\n\n            PropertyChanges {\n                model.values: M3Variants.query(search.text)\n                root.delegate: variantItem\n            }\n        }\n    ]\n\n    transitions: Transition {\n        SequentialAnimation {\n            ParallelAnimation {\n                Anim {\n                    target: root\n                    property: \"opacity\"\n                    from: 1\n                    to: 0\n                    duration: Appearance.anim.durations.small\n                    easing.bezierCurve: Appearance.anim.curves.standardAccel\n                }\n                Anim {\n                    target: root\n                    property: \"scale\"\n                    from: 1\n                    to: 0.9\n                    duration: Appearance.anim.durations.small\n                    easing.bezierCurve: Appearance.anim.curves.standardAccel\n                }\n            }\n            PropertyAction {\n                targets: [model, root]\n                properties: \"values,delegate\"\n            }\n            ParallelAnimation {\n                Anim {\n                    target: root\n                    property: \"opacity\"\n                    from: 0\n                    to: 1\n                    duration: Appearance.anim.durations.small\n                    easing.bezierCurve: Appearance.anim.curves.standardDecel\n                }\n                Anim {\n                    target: root\n                    property: \"scale\"\n                    from: 0.9\n                    to: 1\n                    duration: Appearance.anim.durations.small\n                    easing.bezierCurve: Appearance.anim.curves.standardDecel\n                }\n            }\n            PropertyAction {\n                targets: [root.add, root.remove]\n                property: \"enabled\"\n                value: true\n            }\n        }\n    }\n\n    StyledScrollBar.vertical: StyledScrollBar {\n        flickable: root\n    }\n\n    add: Transition {\n        enabled: !root.state\n\n        Anim {\n            properties: \"opacity,scale\"\n            from: 0\n            to: 1\n        }\n    }\n\n    remove: Transition {\n        enabled: !root.state\n\n        Anim {\n            properties: \"opacity,scale\"\n            from: 1\n            to: 0\n        }\n    }\n\n    move: Transition {\n        Anim {\n            property: \"y\"\n        }\n        Anim {\n            properties: \"opacity,scale\"\n            to: 1\n        }\n    }\n\n    addDisplaced: Transition {\n        Anim {\n            property: \"y\"\n            duration: Appearance.anim.durations.small\n        }\n        Anim {\n            properties: \"opacity,scale\"\n            to: 1\n        }\n    }\n\n    displaced: Transition {\n        Anim {\n            property: \"y\"\n        }\n        Anim {\n            properties: \"opacity,scale\"\n            to: 1\n        }\n    }\n\n    Component {\n        id: appItem\n\n        AppItem {\n            visibilities: root.visibilities\n        }\n    }\n\n    Component {\n        id: actionItem\n\n        ActionItem {\n            list: root\n        }\n    }\n\n    Component {\n        id: calcItem\n\n        CalcItem {\n            list: root\n        }\n    }\n\n    Component {\n        id: schemeItem\n\n        SchemeItem {\n            list: root\n        }\n    }\n\n    Component {\n        id: variantItem\n\n        VariantItem {\n            list: root\n        }\n    }\n}\n"
  },
  {
    "path": "modules/launcher/Background.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Shapes\n\nShapePath {\n    id: root\n\n    required property Wrapper wrapper\n    readonly property real rounding: Config.border.rounding\n    readonly property bool flatten: wrapper.height < rounding * 2\n    readonly property real roundingY: flatten ? wrapper.height / 2 : rounding\n\n    strokeWidth: -1\n    fillColor: Colours.palette.m3surface\n\n    PathArc {\n        relativeX: root.rounding\n        relativeY: -root.roundingY\n        radiusX: root.rounding\n        radiusY: Math.min(root.rounding, root.wrapper.height)\n        direction: PathArc.Counterclockwise\n    }\n    PathLine {\n        relativeX: 0\n        relativeY: -(root.wrapper.height - root.roundingY * 2)\n    }\n    PathArc {\n        relativeX: root.rounding\n        relativeY: -root.roundingY\n        radiusX: root.rounding\n        radiusY: Math.min(root.rounding, root.wrapper.height)\n    }\n    PathLine {\n        relativeX: root.wrapper.width - root.rounding * 2\n        relativeY: 0\n    }\n    PathArc {\n        relativeX: root.rounding\n        relativeY: root.roundingY\n        radiusX: root.rounding\n        radiusY: Math.min(root.rounding, root.wrapper.height)\n    }\n    PathLine {\n        relativeX: 0\n        relativeY: root.wrapper.height - root.roundingY * 2\n    }\n    PathArc {\n        relativeX: root.rounding\n        relativeY: root.roundingY\n        radiusX: root.rounding\n        radiusY: Math.min(root.rounding, root.wrapper.height)\n        direction: PathArc.Counterclockwise\n    }\n\n    Behavior on fillColor {\n        CAnim {}\n    }\n}\n"
  },
  {
    "path": "modules/launcher/Content.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.modules.launcher.services\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    required property DrawerVisibilities visibilities\n    required property var panels\n    required property real maxHeight\n\n    readonly property int padding: Appearance.padding.large\n    readonly property int rounding: Appearance.rounding.large\n\n    implicitWidth: listWrapper.width + padding * 2\n    implicitHeight: searchWrapper.height + listWrapper.height + padding * 2\n\n    Item {\n        id: listWrapper\n\n        implicitWidth: list.width\n        implicitHeight: list.height + root.padding\n\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.bottom: searchWrapper.top\n        anchors.bottomMargin: root.padding\n\n        ContentList {\n            id: list\n\n            content: root\n            visibilities: root.visibilities\n            panels: root.panels\n            maxHeight: root.maxHeight - searchWrapper.implicitHeight - root.padding * 3\n            search: search\n            padding: root.padding\n            rounding: root.rounding\n        }\n    }\n\n    StyledRect {\n        id: searchWrapper\n\n        color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n        radius: Appearance.rounding.full\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.bottom: parent.bottom\n        anchors.margins: root.padding\n\n        implicitHeight: Math.max(searchIcon.implicitHeight, search.implicitHeight, clearIcon.implicitHeight)\n\n        MaterialIcon {\n            id: searchIcon\n\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.left: parent.left\n            anchors.leftMargin: root.padding\n\n            text: \"search\"\n            color: Colours.palette.m3onSurfaceVariant\n        }\n\n        StyledTextField {\n            id: search\n\n            anchors.left: searchIcon.right\n            anchors.right: clearIcon.left\n            anchors.leftMargin: Appearance.spacing.small\n            anchors.rightMargin: Appearance.spacing.small\n\n            topPadding: Appearance.padding.larger\n            bottomPadding: Appearance.padding.larger\n\n            placeholderText: qsTr(\"Type \\\"%1\\\" for commands\").arg(Config.launcher.actionPrefix)\n\n            onAccepted: {\n                const currentItem = list.currentList?.currentItem;\n                if (currentItem) {\n                    if (list.showWallpapers) {\n                        if (Colours.scheme === \"dynamic\" && currentItem.modelData.path !== Wallpapers.actualCurrent)\n                            Wallpapers.previewColourLock = true;\n                        Wallpapers.setWallpaper(currentItem.modelData.path);\n                        root.visibilities.launcher = false;\n                    } else if (text.startsWith(Config.launcher.actionPrefix)) {\n                        if (text.startsWith(`${Config.launcher.actionPrefix}calc `))\n                            currentItem.onClicked();\n                        else\n                            currentItem.modelData.onClicked(list.currentList);\n                    } else {\n                        Apps.launch(currentItem.modelData);\n                        root.visibilities.launcher = false;\n                    }\n                }\n            }\n\n            Keys.onUpPressed: list.currentList?.decrementCurrentIndex()\n            Keys.onDownPressed: list.currentList?.incrementCurrentIndex()\n\n            Keys.onEscapePressed: root.visibilities.launcher = false\n\n            Keys.onPressed: event => {\n                if (!Config.launcher.vimKeybinds)\n                    return;\n\n                if (event.modifiers & Qt.ControlModifier) {\n                    if (event.key === Qt.Key_J) {\n                        list.currentList?.incrementCurrentIndex();\n                        event.accepted = true;\n                    } else if (event.key === Qt.Key_K) {\n                        list.currentList?.decrementCurrentIndex();\n                        event.accepted = true;\n                    }\n                } else if (event.key === Qt.Key_Tab) {\n                    list.currentList?.incrementCurrentIndex();\n                    event.accepted = true;\n                } else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {\n                    list.currentList?.decrementCurrentIndex();\n                    event.accepted = true;\n                }\n            }\n\n            Component.onCompleted: forceActiveFocus()\n\n            Connections {\n                function onLauncherChanged(): void {\n                    if (!root.visibilities.launcher)\n                        search.text = \"\";\n                }\n\n                function onSessionChanged(): void {\n                    if (!root.visibilities.session)\n                        search.forceActiveFocus();\n                }\n\n                target: root.visibilities\n            }\n        }\n\n        MaterialIcon {\n            id: clearIcon\n\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.right: parent.right\n            anchors.rightMargin: root.padding\n\n            width: search.text ? implicitWidth : implicitWidth / 2\n            opacity: {\n                if (!search.text)\n                    return 0;\n                if (mouse.pressed)\n                    return 0.7;\n                if (mouse.containsMouse)\n                    return 0.8;\n                return 1;\n            }\n\n            text: \"close\"\n            color: Colours.palette.m3onSurfaceVariant\n\n            MouseArea {\n                id: mouse\n\n                anchors.fill: parent\n                hoverEnabled: true\n                cursorShape: search.text ? Qt.PointingHandCursor : undefined\n\n                onClicked: search.text = \"\"\n            }\n\n            Behavior on width {\n                Anim {\n                    duration: Appearance.anim.durations.small\n                }\n            }\n\n            Behavior on opacity {\n                Anim {\n                    duration: Appearance.anim.durations.small\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/launcher/ContentList.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport qs.utils\nimport QtQuick\n\nItem {\n    id: root\n\n    required property var content\n    required property DrawerVisibilities visibilities\n    required property var panels\n    required property real maxHeight\n    required property StyledTextField search\n    required property int padding\n    required property int rounding\n\n    readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `)\n    readonly property var currentList: showWallpapers ? wallpaperList.item : appList.item // Can be either ListView or PathView, so can't type properly\n\n    anchors.horizontalCenter: parent.horizontalCenter\n    anchors.bottom: parent.bottom\n\n    clip: true\n    state: showWallpapers ? \"wallpapers\" : \"apps\"\n\n    states: [\n        State {\n            name: \"apps\"\n\n            PropertyChanges {\n                root.implicitWidth: Config.launcher.sizes.itemWidth\n                root.implicitHeight: Math.min(root.maxHeight, appList.implicitHeight > 0 ? appList.implicitHeight : empty.implicitHeight)\n                appList.active: true\n            }\n\n            AnchorChanges {\n                anchors.left: root.parent.left\n                anchors.right: root.parent.right\n            }\n        },\n        State {\n            name: \"wallpapers\"\n\n            PropertyChanges {\n                root.implicitWidth: Math.max(Config.launcher.sizes.itemWidth * 1.2, wallpaperList.implicitWidth)\n                root.implicitHeight: Config.launcher.sizes.wallpaperHeight\n                wallpaperList.active: true\n            }\n        }\n    ]\n\n    Behavior on state {\n        SequentialAnimation {\n            Anim {\n                target: root\n                property: \"opacity\"\n                from: 1\n                to: 0\n                duration: Appearance.anim.durations.small\n            }\n            PropertyAction {}\n            Anim {\n                target: root\n                property: \"opacity\"\n                from: 0\n                to: 1\n                duration: Appearance.anim.durations.small\n            }\n        }\n    }\n\n    Loader {\n        id: appList\n\n        active: false\n\n        anchors.fill: parent\n\n        sourceComponent: AppList {\n            search: root.search\n            visibilities: root.visibilities\n        }\n    }\n\n    Loader {\n        id: wallpaperList\n\n        asynchronous: true\n        active: false\n\n        anchors.top: parent.top\n        anchors.bottom: parent.bottom\n        anchors.horizontalCenter: parent.horizontalCenter\n\n        sourceComponent: WallpaperList {\n            search: root.search\n            visibilities: root.visibilities\n            panels: root.panels\n            content: root.content\n        }\n    }\n\n    Row {\n        id: empty\n\n        opacity: root.currentList?.count === 0 ? 1 : 0\n        scale: root.currentList?.count === 0 ? 1 : 0.5\n\n        spacing: Appearance.spacing.normal\n        padding: Appearance.padding.large\n\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.verticalCenter: parent.verticalCenter\n\n        MaterialIcon {\n            text: root.state === \"wallpapers\" ? \"wallpaper_slideshow\" : \"manage_search\"\n            color: Colours.palette.m3onSurfaceVariant\n            font.pointSize: Appearance.font.size.extraLarge\n\n            anchors.verticalCenter: parent.verticalCenter\n        }\n\n        Column {\n            anchors.verticalCenter: parent.verticalCenter\n\n            StyledText {\n                text: root.state === \"wallpapers\" ? qsTr(\"No wallpapers found\") : qsTr(\"No results\")\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.larger\n                font.weight: 500\n            }\n\n            StyledText {\n                text: root.state === \"wallpapers\" && Wallpapers.list.length === 0 ? qsTr(\"Try putting some wallpapers in %1\").arg(Paths.shortenHome(Paths.wallsdir)) : qsTr(\"Try searching for something else\")\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.normal\n            }\n        }\n\n        Behavior on opacity {\n            Anim {}\n        }\n\n        Behavior on scale {\n            Anim {}\n        }\n    }\n\n    Behavior on implicitWidth {\n        enabled: root.visibilities.launcher\n\n        Anim {\n            duration: Appearance.anim.durations.large\n            easing.bezierCurve: Appearance.anim.curves.emphasizedDecel\n        }\n    }\n\n    Behavior on implicitHeight {\n        enabled: root.visibilities.launcher\n\n        Anim {\n            duration: Appearance.anim.durations.large\n            easing.bezierCurve: Appearance.anim.curves.emphasizedDecel\n        }\n    }\n}\n"
  },
  {
    "path": "modules/launcher/WallpaperList.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport \"items\"\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\n\nPathView {\n    id: root\n\n    required property StyledTextField search\n    required property var visibilities\n    required property var panels\n    required property var content\n\n    readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2\n\n    readonly property int numItems: {\n        const screen = (QsWindow.window as QsWindow)?.screen;\n        if (!screen)\n            return 0;\n\n        // Screen width - 4x outer rounding - 2x max side thickness (cause centered)\n        const barMargins = Math.max(Config.border.thickness, panels.bar.implicitWidth);\n        let outerMargins = 0;\n        if (panels.popouts.hasCurrent && panels.popouts.currentCenter + panels.popouts.nonAnimHeight / 2 > screen.height - content.implicitHeight - Config.border.thickness * 2)\n            outerMargins = panels.popouts.nonAnimWidth;\n        if ((visibilities.utilities || visibilities.sidebar) && panels.utilities.implicitWidth > outerMargins)\n            outerMargins = panels.utilities.implicitWidth;\n        const maxWidth = screen.width - Config.border.rounding * 4 - (barMargins + outerMargins) * 2;\n\n        if (maxWidth <= 0)\n            return 0;\n\n        const maxItemsOnScreen = Math.floor(maxWidth / itemWidth);\n        const visible = Math.min(maxItemsOnScreen, Config.launcher.maxWallpapers, scriptModel.values.length);\n\n        if (visible === 2)\n            return 1;\n        if (visible > 1 && visible % 2 === 0)\n            return visible - 1;\n        return visible;\n    }\n\n    model: ScriptModel {\n        id: scriptModel\n\n        readonly property string search: root.search.text.split(\" \").slice(1).join(\" \")\n\n        values: Wallpapers.query(search)\n        onValuesChanged: root.currentIndex = search ? 0 : values.findIndex(w => w.path === Wallpapers.actualCurrent)\n    }\n\n    Component.onCompleted: currentIndex = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent)\n    Component.onDestruction: Wallpapers.stopPreview()\n\n    onCurrentItemChanged: {\n        if (currentItem)\n            Wallpapers.preview((currentItem as WallpaperItem).modelData.path);\n    }\n\n    implicitWidth: Math.min(numItems, count) * itemWidth\n    pathItemCount: numItems\n    cacheItemCount: 4\n\n    snapMode: PathView.SnapToItem\n    preferredHighlightBegin: 0.5\n    preferredHighlightEnd: 0.5\n    highlightRangeMode: PathView.StrictlyEnforceRange\n\n    delegate: WallpaperItem {\n        visibilities: root.visibilities\n    }\n\n    path: Path {\n        startY: root.height / 2\n\n        PathAttribute {\n            name: \"z\"\n            value: 0\n        }\n        PathLine {\n            x: root.width / 2\n            relativeY: 0\n        }\n        PathAttribute {\n            name: \"z\"\n            value: 1\n        }\n        PathLine {\n            x: root.width\n            relativeY: 0\n        }\n    }\n}\n"
  },
  {
    "path": "modules/launcher/Wrapper.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.config\nimport Quickshell\nimport QtQuick\n\nItem {\n    id: root\n\n    required property ShellScreen screen\n    required property DrawerVisibilities visibilities\n    required property var panels\n\n    readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled\n    property int contentHeight\n\n    readonly property real maxHeight: {\n        let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large;\n        if (visibilities.dashboard)\n            max -= panels.dashboard.nonAnimHeight;\n        return max;\n    }\n\n    onMaxHeightChanged: timer.start()\n\n    visible: height > 0\n    implicitHeight: 0\n    implicitWidth: content.implicitWidth\n\n    onShouldBeActiveChanged: {\n        if (shouldBeActive) {\n            timer.stop();\n            hideAnim.stop();\n            showAnim.start();\n        } else {\n            showAnim.stop();\n            hideAnim.start();\n        }\n    }\n\n    SequentialAnimation {\n        id: showAnim\n\n        Anim {\n            target: root\n            property: \"implicitHeight\"\n            to: root.contentHeight\n            duration: Appearance.anim.durations.expressiveDefaultSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n        }\n        ScriptAction {\n            script: root.implicitHeight = Qt.binding(() => content.implicitHeight)\n        }\n    }\n\n    SequentialAnimation {\n        id: hideAnim\n\n        ScriptAction {\n            script: root.implicitHeight = root.implicitHeight\n        }\n        Anim {\n            target: root\n            property: \"implicitHeight\"\n            to: 0\n            easing.bezierCurve: Appearance.anim.curves.emphasized\n        }\n    }\n\n    Connections {\n        function onEnabledChanged(): void {\n            timer.start();\n        }\n\n        function onMaxShownChanged(): void {\n            timer.start();\n        }\n\n        target: Config.launcher\n    }\n\n    Connections {\n        function onValuesChanged(): void {\n            if (DesktopEntries.applications.values.length < Config.launcher.maxShown)\n                timer.start();\n        }\n\n        target: DesktopEntries.applications\n    }\n\n    Timer {\n        id: timer\n\n        interval: Appearance.anim.durations.extraLarge\n        onRunningChanged: {\n            if (running && !root.shouldBeActive) {\n                content.visible = false;\n                content.active = true;\n            } else {\n                root.contentHeight = Math.min(root.maxHeight, content.implicitHeight);\n                content.active = Qt.binding(() => root.shouldBeActive || root.visible);\n                content.visible = true;\n                if (showAnim.running) {\n                    showAnim.stop();\n                    showAnim.start();\n                }\n            }\n        }\n    }\n\n    Loader {\n        id: content\n\n        anchors.top: parent.top\n        anchors.horizontalCenter: parent.horizontalCenter\n\n        visible: false\n        active: false\n        Component.onCompleted: timer.start()\n\n        sourceComponent: Content {\n            visibilities: root.visibilities\n            panels: root.panels\n            maxHeight: root.maxHeight\n\n            Component.onCompleted: root.contentHeight = implicitHeight\n        }\n    }\n}\n"
  },
  {
    "path": "modules/launcher/items/ActionItem.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    required property var modelData\n    required property var list\n\n    implicitHeight: Config.launcher.sizes.itemHeight\n\n    anchors.left: parent?.left\n    anchors.right: parent?.right\n\n    StateLayer {\n        function onClicked(): void {\n            root.modelData?.onClicked(root.list);\n        }\n\n        radius: Appearance.rounding.normal\n    }\n\n    Item {\n        anchors.fill: parent\n        anchors.leftMargin: Appearance.padding.larger\n        anchors.rightMargin: Appearance.padding.larger\n        anchors.margins: Appearance.padding.smaller\n\n        MaterialIcon {\n            id: icon\n\n            text: root.modelData?.icon ?? \"\"\n            font.pointSize: Appearance.font.size.extraLarge\n\n            anchors.verticalCenter: parent.verticalCenter\n        }\n\n        Item {\n            anchors.left: icon.right\n            anchors.leftMargin: Appearance.spacing.normal\n            anchors.verticalCenter: icon.verticalCenter\n\n            implicitWidth: parent.width - icon.width\n            implicitHeight: name.implicitHeight + desc.implicitHeight\n\n            StyledText {\n                id: name\n\n                text: root.modelData?.name ?? \"\"\n                font.pointSize: Appearance.font.size.normal\n            }\n\n            StyledText {\n                id: desc\n\n                text: root.modelData?.desc ?? \"\"\n                font.pointSize: Appearance.font.size.small\n                color: Colours.palette.m3outline\n\n                elide: Text.ElideRight\n                width: root.width - icon.width - Appearance.rounding.normal * 2\n\n                anchors.top: name.bottom\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/launcher/items/AppItem.qml",
    "content": "import qs.modules.launcher.services\nimport qs.components\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport Quickshell.Widgets\nimport QtQuick\n\nItem {\n    id: root\n\n    required property DesktopEntry modelData\n    required property DrawerVisibilities visibilities\n\n    implicitHeight: Config.launcher.sizes.itemHeight\n\n    anchors.left: parent?.left\n    anchors.right: parent?.right\n\n    StateLayer {\n        function onClicked(): void {\n            Apps.launch(root.modelData);\n            root.visibilities.launcher = false;\n        }\n\n        radius: Appearance.rounding.normal\n    }\n\n    Item {\n        anchors.fill: parent\n        anchors.leftMargin: Appearance.padding.larger\n        anchors.rightMargin: Appearance.padding.larger\n        anchors.margins: Appearance.padding.smaller\n\n        IconImage {\n            id: icon\n\n            asynchronous: true\n            source: Quickshell.iconPath(root.modelData?.icon, \"image-missing\")\n            implicitSize: parent.height * 0.8\n\n            anchors.verticalCenter: parent.verticalCenter\n        }\n\n        Item {\n            anchors.left: icon.right\n            anchors.leftMargin: Appearance.spacing.normal\n            anchors.verticalCenter: icon.verticalCenter\n\n            implicitWidth: parent.width - icon.width - favouriteIcon.width\n            implicitHeight: name.implicitHeight + comment.implicitHeight\n\n            StyledText {\n                id: name\n\n                text: root.modelData?.name ?? \"\"\n                font.pointSize: Appearance.font.size.normal\n            }\n\n            StyledText {\n                id: comment\n\n                text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? \"\"\n                font.pointSize: Appearance.font.size.small\n                color: Colours.palette.m3outline\n\n                elide: Text.ElideRight\n                width: root.width - icon.width - favouriteIcon.width - Appearance.rounding.normal * 2\n\n                anchors.top: name.bottom\n            }\n        }\n\n        Loader {\n            id: favouriteIcon\n\n            asynchronous: true\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.right: parent.right\n            active: root.modelData && Strings.testRegexList(Config.launcher.favouriteApps, root.modelData.id)\n\n            sourceComponent: MaterialIcon {\n                text: \"favorite\"\n                fill: 1\n                color: Colours.palette.m3primary\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/launcher/items/CalcItem.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport Caelestia\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property var list\n    readonly property string math: list.search.text.slice(`${Config.launcher.actionPrefix}calc `.length)\n\n    function onClicked(): void {\n        Quickshell.execDetached([\"wl-copy\", Qalculator.rawResult]);\n        root.list.visibilities.launcher = false;\n    }\n\n    onMathChanged: {\n        if (math.length > 0)\n            Qalculator.evalAsync(math);\n    }\n\n    implicitHeight: Config.launcher.sizes.itemHeight\n\n    anchors.left: parent?.left\n    anchors.right: parent?.right\n\n    StateLayer {\n        function onClicked(): void {\n            root.onClicked();\n        }\n\n        radius: Appearance.rounding.normal\n    }\n\n    RowLayout {\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.margins: Appearance.padding.larger\n\n        spacing: Appearance.spacing.normal\n\n        MaterialIcon {\n            text: \"function\"\n            font.pointSize: Appearance.font.size.extraLarge\n            Layout.alignment: Qt.AlignVCenter\n        }\n\n        StyledText {\n            id: result\n\n            color: {\n                if (text.includes(\"error: \") || text.includes(\"warning: \"))\n                    return Colours.palette.m3error;\n                if (!root.math)\n                    return Colours.palette.m3onSurfaceVariant;\n                return Colours.palette.m3onSurface;\n            }\n\n            text: root.math.length > 0 ? (Qalculator.result || qsTr(\"Calculating...\")) : qsTr(\"Type an expression to calculate\")\n            elide: Text.ElideLeft\n\n            Layout.fillWidth: true\n            Layout.alignment: Qt.AlignVCenter\n        }\n\n        StyledRect {\n            color: Colours.palette.m3tertiary\n            radius: Appearance.rounding.normal\n            clip: true\n\n            implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Appearance.padding.normal * 2\n            implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Appearance.padding.small * 2\n\n            Layout.alignment: Qt.AlignVCenter\n\n            StateLayer {\n                id: stateLayer\n\n                function onClicked(): void {\n                    Quickshell.execDetached([\"app2unit\", \"--\", ...Config.general.apps.terminal, \"fish\", \"-C\", `exec qalc -i '${root.math}'`]);\n                    root.list.visibilities.launcher = false;\n                }\n\n                color: Colours.palette.m3onTertiary\n            }\n\n            StyledText {\n                id: label\n\n                anchors.verticalCenter: parent.verticalCenter\n                anchors.right: icon.left\n                anchors.rightMargin: Appearance.spacing.small\n\n                text: qsTr(\"Open in calculator\")\n                color: Colours.palette.m3onTertiary\n                font.pointSize: Appearance.font.size.normal\n\n                opacity: stateLayer.containsMouse ? 1 : 0\n\n                Behavior on opacity {\n                    Anim {}\n                }\n            }\n\n            MaterialIcon {\n                id: icon\n\n                anchors.verticalCenter: parent.verticalCenter\n                anchors.right: parent.right\n                anchors.rightMargin: Appearance.padding.normal\n\n                text: \"open_in_new\"\n                color: Colours.palette.m3onTertiary\n                font.pointSize: Appearance.font.size.large\n            }\n\n            Behavior on implicitWidth {\n                Anim {\n                    easing.bezierCurve: Appearance.anim.curves.emphasized\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/launcher/items/SchemeItem.qml",
    "content": "import qs.modules.launcher.services\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    required property Schemes.Scheme modelData\n    required property var list\n\n    implicitHeight: Config.launcher.sizes.itemHeight\n\n    anchors.left: parent?.left\n    anchors.right: parent?.right\n\n    StateLayer {\n        function onClicked(): void {\n            root.modelData?.onClicked(root.list);\n        }\n\n        radius: Appearance.rounding.normal\n    }\n\n    Item {\n        anchors.fill: parent\n        anchors.leftMargin: Appearance.padding.larger\n        anchors.rightMargin: Appearance.padding.larger\n        anchors.margins: Appearance.padding.smaller\n\n        StyledRect {\n            id: preview\n\n            anchors.verticalCenter: parent.verticalCenter\n\n            border.width: 1\n            border.color: Qt.alpha(`#${root.modelData?.colours?.outline}`, 0.5)\n\n            color: `#${root.modelData?.colours?.surface}`\n            radius: Appearance.rounding.full\n            implicitWidth: parent.height * 0.8\n            implicitHeight: parent.height * 0.8\n\n            Item {\n                anchors.top: parent.top\n                anchors.bottom: parent.bottom\n                anchors.right: parent.right\n\n                implicitWidth: parent.implicitWidth / 2\n                clip: true\n\n                StyledRect {\n                    anchors.top: parent.top\n                    anchors.bottom: parent.bottom\n                    anchors.right: parent.right\n\n                    implicitWidth: preview.implicitWidth\n                    color: `#${root.modelData?.colours?.primary}`\n                    radius: Appearance.rounding.full\n                }\n            }\n        }\n\n        Column {\n            anchors.left: preview.right\n            anchors.leftMargin: Appearance.spacing.normal\n            anchors.verticalCenter: parent.verticalCenter\n\n            width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0)\n            spacing: 0\n\n            StyledText {\n                text: root.modelData?.flavour ?? \"\"\n                font.pointSize: Appearance.font.size.normal\n            }\n\n            StyledText {\n                text: root.modelData?.name ?? \"\"\n                font.pointSize: Appearance.font.size.small\n                color: Colours.palette.m3outline\n\n                elide: Text.ElideRight\n                anchors.left: parent.left\n                anchors.right: parent.right\n            }\n        }\n\n        Loader {\n            id: current\n\n            asynchronous: true\n            anchors.right: parent.right\n            anchors.verticalCenter: parent.verticalCenter\n\n            active: `${root.modelData?.name} ${root.modelData?.flavour}` === Schemes.currentScheme\n\n            sourceComponent: MaterialIcon {\n                text: \"check\"\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.large\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/launcher/items/VariantItem.qml",
    "content": "import qs.modules.launcher.services\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    required property M3Variants.Variant modelData\n    required property var list\n\n    implicitHeight: Config.launcher.sizes.itemHeight\n\n    anchors.left: parent?.left\n    anchors.right: parent?.right\n\n    StateLayer {\n        function onClicked(): void {\n            root.modelData?.onClicked(root.list);\n        }\n\n        radius: Appearance.rounding.normal\n    }\n\n    Item {\n        anchors.fill: parent\n        anchors.leftMargin: Appearance.padding.larger\n        anchors.rightMargin: Appearance.padding.larger\n        anchors.margins: Appearance.padding.smaller\n\n        MaterialIcon {\n            id: icon\n\n            text: root.modelData?.icon ?? \"\"\n            font.pointSize: Appearance.font.size.extraLarge\n\n            anchors.verticalCenter: parent.verticalCenter\n        }\n\n        Column {\n            anchors.left: icon.right\n            anchors.leftMargin: Appearance.spacing.larger\n            anchors.verticalCenter: icon.verticalCenter\n\n            width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0)\n            spacing: 0\n\n            StyledText {\n                text: root.modelData?.name ?? \"\"\n                font.pointSize: Appearance.font.size.normal\n            }\n\n            StyledText {\n                text: root.modelData?.description ?? \"\"\n                font.pointSize: Appearance.font.size.small\n                color: Colours.palette.m3outline\n\n                elide: Text.ElideRight\n                anchors.left: parent.left\n                anchors.right: parent.right\n            }\n        }\n\n        Loader {\n            id: current\n\n            asynchronous: true\n            anchors.right: parent.right\n            anchors.verticalCenter: parent.verticalCenter\n\n            active: root.modelData?.variant === Schemes.currentVariant\n\n            sourceComponent: MaterialIcon {\n                text: \"check\"\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.large\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/launcher/items/WallpaperItem.qml",
    "content": "import qs.components\nimport qs.components.effects\nimport qs.components.images\nimport qs.services\nimport qs.config\nimport Caelestia.Models\nimport QtQuick\n\nItem {\n    id: root\n\n    required property FileSystemEntry modelData\n    required property DrawerVisibilities visibilities\n\n    scale: 0.5\n    opacity: 0\n    z: PathView.z ?? 0 // qmllint disable missing-property\n\n    Component.onCompleted: {\n        scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0);\n        opacity = Qt.binding(() => PathView.onPath ? 1 : 0);\n    }\n\n    implicitWidth: image.width + Appearance.padding.larger * 2\n    implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal\n\n    StateLayer {\n        function onClicked(): void {\n            Wallpapers.setWallpaper(root.modelData.path);\n            root.visibilities.launcher = false;\n        }\n\n        radius: Appearance.rounding.normal\n    }\n\n    Elevation {\n        anchors.fill: image\n        radius: image.radius\n        opacity: root.PathView.isCurrentItem ? 1 : 0\n        level: 4\n\n        Behavior on opacity {\n            Anim {}\n        }\n    }\n\n    StyledClippingRect {\n        id: image\n\n        anchors.horizontalCenter: parent.horizontalCenter\n        y: Appearance.padding.large\n        color: Colours.tPalette.m3surfaceContainer\n        radius: Appearance.rounding.normal\n\n        implicitWidth: Config.launcher.sizes.wallpaperWidth\n        implicitHeight: implicitWidth / 16 * 9\n\n        MaterialIcon {\n            anchors.centerIn: parent\n            text: \"image\"\n            color: Colours.tPalette.m3outline\n            font.pointSize: Appearance.font.size.extraLarge * 2\n            font.weight: 600\n        }\n\n        CachingImage {\n            path: root.modelData.path\n            smooth: !root.PathView.view.moving\n            cache: true\n\n            anchors.fill: parent\n        }\n    }\n\n    StyledText {\n        id: label\n\n        anchors.top: image.bottom\n        anchors.topMargin: Appearance.spacing.small / 2\n        anchors.horizontalCenter: parent.horizontalCenter\n\n        width: image.width - Appearance.padding.normal * 2\n        horizontalAlignment: Text.AlignHCenter\n        elide: Text.ElideRight\n        renderType: Text.QtRendering\n        text: root.modelData.relativePath\n        font.pointSize: Appearance.font.size.normal\n    }\n\n    Behavior on scale {\n        Anim {}\n    }\n\n    Behavior on opacity {\n        Anim {}\n    }\n}\n"
  },
  {
    "path": "modules/launcher/services/Actions.qml",
    "content": "pragma Singleton\n\nimport \"..\"\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport QtQuick\n\nSearcher {\n    id: root\n\n    function transformSearch(search: string): string {\n        return search.slice(Config.launcher.actionPrefix.length);\n    }\n\n    list: variants.instances\n    useFuzzy: Config.launcher.useFuzzy.actions\n\n    Variants {\n        id: variants\n\n        model: Config.launcher.actions.filter(a => (a.enabled ?? true) && (Config.launcher.enableDangerousActions || !(a.dangerous ?? false)))\n\n        Action {}\n    }\n\n    component Action: QtObject {\n        required property var modelData\n        readonly property string name: modelData.name ?? qsTr(\"Unnamed\")\n        readonly property string desc: modelData.description ?? qsTr(\"No description\")\n        readonly property string icon: modelData.icon ?? \"help_outline\"\n        readonly property list<string> command: modelData.command ?? []\n        readonly property bool enabled: modelData.enabled ?? true\n        readonly property bool dangerous: modelData.dangerous ?? false\n\n        function onClicked(list: AppList): void {\n            if (command.length === 0)\n                return;\n\n            if (command[0] === \"autocomplete\" && command.length > 1) {\n                list.search.text = `${Config.launcher.actionPrefix}${command[1]} `;\n            } else if (command[0] === \"setMode\" && command.length > 1) {\n                list.visibilities.launcher = false;\n                Colours.setMode(command[1]);\n            } else {\n                list.visibilities.launcher = false;\n                Quickshell.execDetached(command);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/launcher/services/Apps.qml",
    "content": "pragma Singleton\n\nimport qs.config\nimport qs.utils\nimport Caelestia\nimport Quickshell\n\nSearcher {\n    id: root\n\n    function launch(entry: DesktopEntry): void {\n        appDb.incrementFrequency(entry.id);\n\n        if (entry.runInTerminal)\n            Quickshell.execDetached({\n                command: [\"app2unit\", \"--\", ...Config.general.apps.terminal, `${Quickshell.shellDir}/assets/wrap_term_launch.sh`, ...entry.command],\n                workingDirectory: entry.workingDirectory\n            });\n        else\n            Quickshell.execDetached({\n                command: [\"app2unit\", \"--\", ...entry.command],\n                workingDirectory: entry.workingDirectory\n            });\n    }\n\n    function search(search: string): list<var> {\n        const prefix = Config.launcher.specialPrefix;\n\n        if (search.startsWith(`${prefix}i `)) {\n            keys = [\"id\", \"name\"];\n            weights = [0.9, 0.1];\n        } else if (search.startsWith(`${prefix}c `)) {\n            keys = [\"categories\", \"name\"];\n            weights = [0.9, 0.1];\n        } else if (search.startsWith(`${prefix}d `)) {\n            keys = [\"comment\", \"name\"];\n            weights = [0.9, 0.1];\n        } else if (search.startsWith(`${prefix}e `)) {\n            keys = [\"execString\", \"name\"];\n            weights = [0.9, 0.1];\n        } else if (search.startsWith(`${prefix}w `)) {\n            keys = [\"startupClass\", \"name\"];\n            weights = [0.9, 0.1];\n        } else if (search.startsWith(`${prefix}g `)) {\n            keys = [\"genericName\", \"name\"];\n            weights = [0.9, 0.1];\n        } else if (search.startsWith(`${prefix}k `)) {\n            keys = [\"keywords\", \"name\"];\n            weights = [0.9, 0.1];\n        } else {\n            keys = [\"name\"];\n            weights = [1];\n\n            if (!search.startsWith(`${prefix}t `))\n                return query(search).map(e => e.entry);\n        }\n\n        const results = query(search.slice(prefix.length + 2)).map(e => e.entry);\n        if (search.startsWith(`${prefix}t `))\n            return results.filter(a => a.runInTerminal);\n        return results;\n    }\n\n    function selector(item: var): string {\n        return keys.map(k => item[k]).join(\" \");\n    }\n\n    list: appDb.apps\n    useFuzzy: Config.launcher.useFuzzy.apps\n\n    AppDb {\n        id: appDb\n\n        path: `${Paths.state}/apps.sqlite`\n        favouriteApps: Config.launcher.favouriteApps\n        entries: DesktopEntries.applications.values.filter(a => !Strings.testRegexList(Config.launcher.hiddenApps, a.id))\n    }\n}\n"
  },
  {
    "path": "modules/launcher/services/M3Variants.qml",
    "content": "pragma Singleton\n\nimport \"..\"\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport QtQuick\n\nSearcher {\n    id: root\n\n    function transformSearch(search: string): string {\n        return search.slice(`${Config.launcher.actionPrefix}variant `.length);\n    }\n\n    list: [\n        Variant {\n            variant: \"vibrant\"\n            icon: \"sentiment_very_dissatisfied\"\n            name: qsTr(\"Vibrant\")\n            description: qsTr(\"A high chroma palette. The primary palette's chroma is at maximum.\")\n        },\n        Variant {\n            variant: \"tonalspot\"\n            icon: \"android\"\n            name: qsTr(\"Tonal Spot\")\n            description: qsTr(\"Default for Material theme colours. A pastel palette with a low chroma.\")\n        },\n        Variant {\n            variant: \"expressive\"\n            icon: \"compare_arrows\"\n            name: qsTr(\"Expressive\")\n            description: qsTr(\"A medium chroma palette. The primary palette's hue is different from the seed colour, for variety.\")\n        },\n        Variant {\n            variant: \"fidelity\"\n            icon: \"compare\"\n            name: qsTr(\"Fidelity\")\n            description: qsTr(\"Matches the seed colour, even if the seed colour is very bright (high chroma).\")\n        },\n        Variant {\n            variant: \"content\"\n            icon: \"sentiment_calm\"\n            name: qsTr(\"Content\")\n            description: qsTr(\"Almost identical to fidelity.\")\n        },\n        Variant {\n            variant: \"fruitsalad\"\n            icon: \"nutrition\"\n            name: qsTr(\"Fruit Salad\")\n            description: qsTr(\"A playful theme - the seed colour's hue does not appear in the theme.\")\n        },\n        Variant {\n            variant: \"rainbow\"\n            icon: \"looks\"\n            name: qsTr(\"Rainbow\")\n            description: qsTr(\"A playful theme - the seed colour's hue does not appear in the theme.\")\n        },\n        Variant {\n            variant: \"neutral\"\n            icon: \"contrast\"\n            name: qsTr(\"Neutral\")\n            description: qsTr(\"Close to grayscale, a hint of chroma.\")\n        },\n        Variant {\n            variant: \"monochrome\"\n            icon: \"filter_b_and_w\"\n            name: qsTr(\"Monochrome\")\n            description: qsTr(\"All colours are grayscale, no chroma.\")\n        }\n    ]\n    useFuzzy: Config.launcher.useFuzzy.variants\n\n    component Variant: QtObject {\n        required property string variant\n        required property string icon\n        required property string name\n        required property string description\n\n        function onClicked(list: AppList): void {\n            list.visibilities.launcher = false;\n            Quickshell.execDetached([\"caelestia\", \"scheme\", \"set\", \"-v\", variant]);\n        }\n    }\n}\n"
  },
  {
    "path": "modules/launcher/services/Schemes.qml",
    "content": "pragma Singleton\n\nimport \"..\"\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport Quickshell.Io\nimport QtQuick\n\nSearcher {\n    id: root\n\n    property string currentScheme\n    property string currentVariant\n\n    function transformSearch(search: string): string {\n        return search.slice(`${Config.launcher.actionPrefix}scheme `.length);\n    }\n\n    function selector(item: var): string {\n        return `${item.name} ${item.flavour}`;\n    }\n\n    function reload(): void {\n        getCurrent.running = true;\n    }\n\n    list: schemes.instances\n    useFuzzy: Config.launcher.useFuzzy.schemes\n    keys: [\"name\", \"flavour\"]\n    weights: [0.9, 0.1]\n\n    Variants {\n        id: schemes\n\n        Scheme {}\n    }\n\n    Process {\n        id: getSchemes\n\n        running: true\n        command: [\"caelestia\", \"scheme\", \"list\"]\n        stdout: StdioCollector {\n            onStreamFinished: {\n                const schemeData = JSON.parse(text);\n                const list = Object.entries(schemeData).map(([name, f]) => Object.entries(f).map(([flavour, colours]) => ({\n                                name,\n                                flavour,\n                                colours\n                            })));\n\n                const flat = [];\n                for (const s of list)\n                    for (const f of s)\n                        flat.push(f);\n\n                schemes.model = flat.sort((a, b) => String(a.name + a.flavour).localeCompare((b.name + b.flavour)));\n            }\n        }\n    }\n\n    Process {\n        id: getCurrent\n\n        running: true\n        command: [\"caelestia\", \"scheme\", \"get\", \"-nfv\"]\n        stdout: StdioCollector {\n            onStreamFinished: {\n                const [name, flavour, variant] = text.trim().split(\"\\n\");\n                root.currentScheme = `${name} ${flavour}`;\n                root.currentVariant = variant;\n            }\n        }\n    }\n\n    component Scheme: QtObject {\n        required property var modelData\n        readonly property string name: modelData.name\n        readonly property string flavour: modelData.flavour\n        readonly property var colours: modelData.colours\n\n        function onClicked(list: AppList): void {\n            list.visibilities.launcher = false;\n            Quickshell.execDetached([\"caelestia\", \"scheme\", \"set\", \"-n\", name, \"-f\", flavour]);\n        }\n    }\n}\n"
  },
  {
    "path": "modules/lock/Center.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.components.images\nimport qs.services\nimport qs.config\nimport qs.utils\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property var lock\n    readonly property real centerScale: Math.min(1, (lock.screen?.height ?? 1440) / 1440)\n    readonly property int centerWidth: Config.lock.sizes.centerWidth * centerScale\n\n    Layout.preferredWidth: centerWidth\n    Layout.fillWidth: false\n    Layout.fillHeight: true\n\n    spacing: Appearance.spacing.large * 2\n\n    RowLayout {\n        Layout.alignment: Qt.AlignHCenter\n        spacing: Appearance.spacing.small\n\n        StyledText {\n            Layout.alignment: Qt.AlignVCenter\n            text: Time.hourStr\n            color: Colours.palette.m3secondary\n            font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale)\n            font.family: Appearance.font.family.clock\n            font.bold: true\n        }\n\n        StyledText {\n            Layout.alignment: Qt.AlignVCenter\n            text: \":\"\n            color: Colours.palette.m3primary\n            font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale)\n            font.family: Appearance.font.family.clock\n            font.bold: true\n        }\n\n        StyledText {\n            Layout.alignment: Qt.AlignVCenter\n            text: Time.minuteStr\n            color: Colours.palette.m3secondary\n            font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale)\n            font.family: Appearance.font.family.clock\n            font.bold: true\n        }\n\n        Loader {\n            asynchronous: true\n            Layout.leftMargin: Appearance.spacing.small\n            Layout.alignment: Qt.AlignVCenter\n\n            active: Config.services.useTwelveHourClock\n            visible: active\n\n            sourceComponent: StyledText {\n                text: Time.amPmStr\n                color: Colours.palette.m3primary\n                font.pointSize: Math.floor(Appearance.font.size.extraLarge * 2 * root.centerScale)\n                font.family: Appearance.font.family.clock\n                font.bold: true\n            }\n        }\n    }\n\n    StyledText {\n        Layout.alignment: Qt.AlignHCenter\n        Layout.topMargin: -Appearance.padding.large * 2\n\n        text: Time.format(\"dddd, d MMMM yyyy\")\n        color: Colours.palette.m3tertiary\n        font.pointSize: Math.floor(Appearance.font.size.extraLarge * root.centerScale)\n        font.family: Appearance.font.family.mono\n        font.bold: true\n    }\n\n    StyledClippingRect {\n        Layout.topMargin: Appearance.spacing.large * 2\n        Layout.alignment: Qt.AlignHCenter\n\n        implicitWidth: root.centerWidth / 2\n        implicitHeight: root.centerWidth / 2\n\n        color: Colours.tPalette.m3surfaceContainer\n        radius: Appearance.rounding.full\n\n        MaterialIcon {\n            anchors.centerIn: parent\n\n            text: \"person\"\n            color: Colours.palette.m3onSurfaceVariant\n            font.pointSize: Math.floor(root.centerWidth / 4)\n            visible: pfp.status !== Image.Ready\n        }\n\n        CachingImage {\n            id: pfp\n\n            anchors.fill: parent\n            path: `${Paths.home}/.face`\n        }\n    }\n\n    StyledRect {\n        Layout.alignment: Qt.AlignHCenter\n\n        implicitWidth: root.centerWidth * 0.8\n        implicitHeight: input.implicitHeight + Appearance.padding.small * 2\n\n        color: Colours.tPalette.m3surfaceContainer\n        radius: Appearance.rounding.full\n\n        focus: true\n        onActiveFocusChanged: {\n            if (!activeFocus)\n                forceActiveFocus();\n        }\n\n        Keys.onPressed: event => {\n            if (root.lock.unlocking)\n                return;\n\n            if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return)\n                inputField.placeholder.animate = false;\n\n            root.lock.pam.handleKey(event);\n        }\n\n        StateLayer {\n            function onClicked(): void {\n                parent.forceActiveFocus();\n            }\n\n            hoverEnabled: false\n            cursorShape: Qt.IBeamCursor\n        }\n\n        RowLayout {\n            id: input\n\n            anchors.fill: parent\n            anchors.margins: Appearance.padding.small\n            spacing: Appearance.spacing.normal\n\n            Item {\n                implicitWidth: implicitHeight\n                implicitHeight: fprintIcon.implicitHeight + Appearance.padding.small * 2\n\n                MaterialIcon {\n                    id: fprintIcon\n\n                    anchors.centerIn: parent\n                    animate: true\n                    text: {\n                        if (root.lock.pam.fprint.tries >= Config.lock.maxFprintTries)\n                            return \"fingerprint_off\";\n                        if (root.lock.pam.fprint.active)\n                            return \"fingerprint\";\n                        return \"lock\";\n                    }\n                    color: root.lock.pam.fprint.tries >= Config.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface\n                    opacity: root.lock.pam.passwd.active ? 0 : 1\n\n                    Behavior on opacity {\n                        Anim {}\n                    }\n                }\n\n                CircularIndicator {\n                    anchors.fill: parent\n                    running: root.lock.pam.passwd.active\n                }\n            }\n\n            InputField {\n                id: inputField\n\n                pam: root.lock.pam\n            }\n\n            StyledRect {\n                implicitWidth: implicitHeight\n                implicitHeight: enterIcon.implicitHeight + Appearance.padding.small * 2\n\n                color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)\n                radius: Appearance.rounding.full\n\n                StateLayer {\n                    function onClicked(): void {\n                        root.lock.pam.passwd.start();\n                    }\n\n                    color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n                }\n\n                MaterialIcon {\n                    id: enterIcon\n\n                    anchors.centerIn: parent\n                    text: \"arrow_forward\"\n                    color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface\n                    font.weight: 500\n                }\n            }\n        }\n    }\n\n    Item {\n        Layout.fillWidth: true\n        Layout.topMargin: -Appearance.spacing.large\n\n        implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight)\n\n        Behavior on implicitHeight {\n            Anim {}\n        }\n\n        StyledText {\n            id: stateMessage\n\n            readonly property string msg: {\n                if (Hypr.kbLayout !== Hypr.defaultKbLayout) {\n                    if (Hypr.capsLock && Hypr.numLock)\n                        return qsTr(\"Caps lock and Num lock are ON.\\nKeyboard layout: %1\").arg(Hypr.kbLayoutFull);\n                    if (Hypr.capsLock)\n                        return qsTr(\"Caps lock is ON. Kb layout: %1\").arg(Hypr.kbLayoutFull);\n                    if (Hypr.numLock)\n                        return qsTr(\"Num lock is ON. Kb layout: %1\").arg(Hypr.kbLayoutFull);\n                    return qsTr(\"Keyboard layout: %1\").arg(Hypr.kbLayoutFull);\n                }\n\n                if (Hypr.capsLock && Hypr.numLock)\n                    return qsTr(\"Caps lock and Num lock are ON.\");\n                if (Hypr.capsLock)\n                    return qsTr(\"Caps lock is ON.\");\n                if (Hypr.numLock)\n                    return qsTr(\"Num lock is ON.\");\n\n                return \"\";\n            }\n\n            property bool shouldBeVisible\n\n            onMsgChanged: {\n                if (msg) {\n                    if (opacity > 0) {\n                        animate = true;\n                        text = msg;\n                        animate = false;\n                    } else {\n                        text = msg;\n                    }\n                    shouldBeVisible = true;\n                } else {\n                    shouldBeVisible = false;\n                }\n            }\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n\n            scale: shouldBeVisible && !message.msg ? 1 : 0.7\n            opacity: shouldBeVisible && !message.msg ? 1 : 0\n            color: Colours.palette.m3onSurfaceVariant\n            animateProp: \"opacity\"\n\n            font.family: Appearance.font.family.mono\n            horizontalAlignment: Qt.AlignHCenter\n            wrapMode: Text.WrapAtWordBoundaryOrAnywhere\n            lineHeight: 1.2\n\n            Behavior on scale {\n                Anim {}\n            }\n\n            Behavior on opacity {\n                Anim {}\n            }\n        }\n\n        StyledText {\n            id: message\n\n            readonly property Pam pam: root.lock.pam\n            readonly property string msg: {\n                if (pam.fprintState === \"error\")\n                    return qsTr(\"FP ERROR: %1\").arg(pam.fprint.message);\n                if (pam.state === \"error\")\n                    return qsTr(\"PW ERROR: %1\").arg(pam.passwd.message);\n\n                if (pam.lockMessage)\n                    return pam.lockMessage;\n\n                if (pam.state === \"max\" && pam.fprintState === \"max\")\n                    return qsTr(\"Maximum password and fingerprint attempts reached.\");\n                if (pam.state === \"max\") {\n                    if (pam.fprint.available)\n                        return qsTr(\"Maximum password attempts reached. Please use fingerprint.\");\n                    return qsTr(\"Maximum password attempts reached.\");\n                }\n                if (pam.fprintState === \"max\")\n                    return qsTr(\"Maximum fingerprint attempts reached. Please use password.\");\n\n                if (pam.state === \"fail\") {\n                    if (pam.fprint.available)\n                        return qsTr(\"Incorrect password. Please try again or use fingerprint.\");\n                    return qsTr(\"Incorrect password. Please try again.\");\n                }\n                if (pam.fprintState === \"fail\")\n                    return qsTr(\"Fingerprint not recognized (%1/%2). Please try again or use password.\").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries);\n\n                return \"\";\n            }\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n\n            scale: 0.7\n            opacity: 0\n            color: Colours.palette.m3error\n\n            font.pointSize: Appearance.font.size.small\n            font.family: Appearance.font.family.mono\n            horizontalAlignment: Qt.AlignHCenter\n            wrapMode: Text.WrapAtWordBoundaryOrAnywhere\n\n            onMsgChanged: {\n                if (msg) {\n                    if (opacity > 0) {\n                        animate = true;\n                        text = msg;\n                        animate = false;\n\n                        exitAnim.stop();\n                        if (scale < 1)\n                            appearAnim.restart();\n                        else\n                            flashAnim.restart();\n                    } else {\n                        text = msg;\n                        exitAnim.stop();\n                        appearAnim.restart();\n                    }\n                } else {\n                    appearAnim.stop();\n                    flashAnim.stop();\n                    exitAnim.start();\n                }\n            }\n\n            Connections {\n                function onFlashMsg(): void {\n                    exitAnim.stop();\n                    if (message.scale < 1)\n                        appearAnim.restart();\n                    else\n                        flashAnim.restart();\n                }\n\n                target: root.lock.pam\n            }\n\n            Anim {\n                id: appearAnim\n\n                target: message\n                properties: \"scale,opacity\"\n                to: 1\n                onFinished: flashAnim.restart()\n            }\n\n            SequentialAnimation {\n                id: flashAnim\n\n                loops: 2\n\n                FlashAnim {\n                    to: 0.3\n                }\n                FlashAnim {\n                    to: 1\n                }\n            }\n\n            ParallelAnimation {\n                id: exitAnim\n\n                Anim {\n                    target: message\n                    property: \"scale\"\n                    to: 0.7\n                    duration: Appearance.anim.durations.large\n                }\n                Anim {\n                    target: message\n                    property: \"opacity\"\n                    to: 0\n                    duration: Appearance.anim.durations.large\n                }\n            }\n        }\n    }\n\n    component FlashAnim: NumberAnimation {\n        target: message\n        property: \"opacity\"\n        duration: Appearance.anim.durations.small\n        easing.type: Easing.Linear\n    }\n}\n"
  },
  {
    "path": "modules/lock/Content.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nRowLayout {\n    id: root\n\n    required property var lock\n\n    spacing: Appearance.spacing.large * 2\n\n    ColumnLayout {\n        Layout.fillWidth: true\n        spacing: Appearance.spacing.normal\n\n        StyledRect {\n            Layout.fillWidth: true\n            implicitHeight: weather.implicitHeight\n\n            topLeftRadius: Appearance.rounding.large\n            radius: Appearance.rounding.small\n            color: Colours.tPalette.m3surfaceContainer\n\n            WeatherInfo {\n                id: weather\n\n                rootHeight: root.height\n            }\n        }\n\n        StyledRect {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            radius: Appearance.rounding.small\n            color: Colours.tPalette.m3surfaceContainer\n\n            Fetch {}\n        }\n\n        StyledClippingRect {\n            Layout.fillWidth: true\n            implicitHeight: media.implicitHeight\n\n            bottomLeftRadius: Appearance.rounding.large\n            radius: Appearance.rounding.small\n            color: Colours.tPalette.m3surfaceContainer\n\n            Media {\n                id: media\n\n                lock: root.lock\n            }\n        }\n    }\n\n    Center {\n        lock: root.lock\n    }\n\n    ColumnLayout {\n        Layout.fillWidth: true\n        spacing: Appearance.spacing.normal\n\n        StyledRect {\n            Layout.fillWidth: true\n            implicitHeight: resources.implicitHeight\n\n            topRightRadius: Appearance.rounding.large\n            radius: Appearance.rounding.small\n            color: Colours.tPalette.m3surfaceContainer\n\n            Resources {\n                id: resources\n            }\n        }\n\n        StyledRect {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            bottomRightRadius: Appearance.rounding.large\n            radius: Appearance.rounding.small\n            color: Colours.tPalette.m3surfaceContainer\n\n            NotifDock {\n                lock: root.lock\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/lock/Fetch.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell.Services.UPower\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    anchors.fill: parent\n    anchors.margins: Appearance.padding.large * 2\n    anchors.topMargin: Appearance.padding.large\n\n    spacing: Appearance.spacing.small\n\n    RowLayout {\n        Layout.fillWidth: true\n        Layout.fillHeight: false\n        spacing: Appearance.spacing.normal\n\n        StyledRect {\n            implicitWidth: prompt.implicitWidth + Appearance.padding.normal * 2\n            implicitHeight: prompt.implicitHeight + Appearance.padding.normal * 2\n\n            color: Colours.palette.m3primary\n            radius: Appearance.rounding.small\n\n            MonoText {\n                id: prompt\n\n                anchors.centerIn: parent\n                text: \">\"\n                font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal\n                color: Colours.palette.m3onPrimary\n            }\n        }\n\n        MonoText {\n            Layout.fillWidth: true\n            text: \"caelestiafetch.sh\"\n            font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal\n            elide: Text.ElideRight\n        }\n\n        WrappedLoader {\n            Layout.fillHeight: true\n            active: !iconLoader.active\n\n            sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon\n        }\n    }\n\n    RowLayout {\n        Layout.fillWidth: true\n        Layout.fillHeight: false\n        spacing: height * 0.15\n\n        WrappedLoader {\n            id: iconLoader\n\n            Layout.fillHeight: true\n            active: root.width > 320\n\n            sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon\n        }\n\n        ColumnLayout {\n            Layout.fillWidth: true\n            Layout.topMargin: Appearance.padding.normal\n            Layout.bottomMargin: Appearance.padding.normal\n            Layout.leftMargin: iconLoader.active ? 0 : width * 0.1\n            spacing: Appearance.spacing.normal\n\n            WrappedLoader {\n                Layout.fillWidth: true\n                active: !batLoader.active && root.height > 200\n\n                sourceComponent: FetchText {\n                    text: `OS  : ${SysInfo.osPrettyName || SysInfo.osName}`\n                }\n            }\n\n            WrappedLoader {\n                Layout.fillWidth: true\n                active: root.height > (batLoader.active ? 200 : 110)\n\n                sourceComponent: FetchText {\n                    text: `WM  : ${SysInfo.wm}`\n                }\n            }\n\n            WrappedLoader {\n                Layout.fillWidth: true\n                active: !batLoader.active || root.height > 110\n\n                sourceComponent: FetchText {\n                    text: `USER: ${SysInfo.user}`\n                }\n            }\n\n            FetchText {\n                text: `UP  : ${SysInfo.uptime}`\n            }\n\n            WrappedLoader {\n                id: batLoader\n\n                Layout.fillWidth: true\n                active: UPower.displayDevice.isLaptopBattery\n\n                sourceComponent: FetchText {\n                    text: `BATT: ${[UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state) ? \"(+) \" : \"\"}${Math.round(UPower.displayDevice.percentage * 100)}%`\n                }\n            }\n        }\n    }\n\n    WrappedLoader {\n        Layout.alignment: Qt.AlignHCenter\n        active: root.height > 180\n\n        sourceComponent: RowLayout {\n            spacing: Appearance.spacing.large\n\n            Repeater {\n                model: Math.max(0, Math.min(8, root.width / (Appearance.font.size.larger * 2 + Appearance.spacing.large)))\n\n                StyledRect {\n                    required property int index\n\n                    implicitWidth: implicitHeight\n                    implicitHeight: Appearance.font.size.larger * 2\n                    color: Colours.palette[`term${index}`]\n                    radius: Appearance.rounding.small\n                }\n            }\n        }\n    }\n\n    Component {\n        id: caelestiaLogo\n\n        Logo {\n            width: height\n        }\n    }\n\n    Component {\n        id: distroIcon\n\n        ColouredIcon {\n            source: SysInfo.osLogo\n            implicitSize: height\n            colour: Colours.palette.m3primary\n            layer.enabled: Config.lock.recolourLogo\n        }\n    }\n\n    component WrappedLoader: Loader {\n        asynchronous: true\n        visible: active\n    }\n\n    component FetchText: MonoText {\n        Layout.fillWidth: true\n        font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal\n        elide: Text.ElideRight\n    }\n\n    component MonoText: StyledText {\n        font.family: Appearance.font.family.mono\n    }\n}\n"
  },
  {
    "path": "modules/lock/InputField.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property Pam pam\n    readonly property alias placeholder: placeholder\n    property string buffer\n\n    Layout.fillWidth: true\n    Layout.fillHeight: true\n\n    clip: true\n\n    Connections {\n        function onBufferChanged(): void {\n            if (root.pam.buffer.length > root.buffer.length) {\n                charList.bindImWidth();\n            } else if (root.pam.buffer.length === 0) {\n                charList.implicitWidth = charList.implicitWidth;\n                placeholder.animate = true;\n            }\n\n            root.buffer = root.pam.buffer;\n        }\n\n        target: root.pam\n    }\n\n    StyledText {\n        id: placeholder\n\n        anchors.centerIn: parent\n\n        text: {\n            if (root.pam.passwd.active)\n                return qsTr(\"Loading...\");\n            if (root.pam.state === \"max\")\n                return qsTr(\"You have reached the maximum number of tries\");\n            return qsTr(\"Enter your password\");\n        }\n\n        animate: true\n        color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline\n        font.pointSize: Appearance.font.size.normal\n        font.family: Appearance.font.family.mono\n\n        opacity: root.buffer ? 0 : 1\n\n        Behavior on opacity {\n            Anim {}\n        }\n    }\n\n    ListView {\n        id: charList\n\n        readonly property int fullWidth: count * (implicitHeight + spacing) - spacing\n\n        function bindImWidth(): void {\n            imWidthBehavior.enabled = false;\n            implicitWidth = Qt.binding(() => fullWidth);\n            imWidthBehavior.enabled = true;\n        }\n\n        anchors.centerIn: parent\n        anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0\n\n        implicitWidth: fullWidth\n        implicitHeight: Appearance.font.size.normal\n\n        orientation: Qt.Horizontal\n        spacing: Appearance.spacing.small / 2\n        interactive: false\n\n        model: ScriptModel {\n            values: root.buffer.split(\"\")\n        }\n\n        delegate: StyledRect {\n            id: ch\n\n            implicitWidth: implicitHeight\n            implicitHeight: charList.implicitHeight\n\n            color: Colours.palette.m3onSurface\n            radius: Appearance.rounding.small / 2\n\n            opacity: 0\n            scale: 0\n            Component.onCompleted: {\n                opacity = 1;\n                scale = 1;\n            }\n            ListView.onRemove: removeAnim.start()\n\n            SequentialAnimation {\n                id: removeAnim\n\n                PropertyAction {\n                    target: ch\n                    property: \"ListView.delayRemove\"\n                    value: true\n                }\n                ParallelAnimation {\n                    Anim {\n                        target: ch\n                        property: \"opacity\"\n                        to: 0\n                    }\n                    Anim {\n                        target: ch\n                        property: \"scale\"\n                        to: 0.5\n                    }\n                }\n                PropertyAction {\n                    target: ch\n                    property: \"ListView.delayRemove\"\n                    value: false\n                }\n            }\n\n            Behavior on opacity {\n                Anim {}\n            }\n\n            Behavior on scale {\n                Anim {\n                    duration: Appearance.anim.durations.expressiveFastSpatial\n                    easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                }\n            }\n        }\n\n        Behavior on implicitWidth {\n            id: imWidthBehavior\n\n            Anim {}\n        }\n    }\n}\n"
  },
  {
    "path": "modules/lock/Lock.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components.misc\nimport Quickshell\nimport Quickshell.Io\nimport Quickshell.Wayland\n\nScope {\n    property alias lock: lock\n\n    WlSessionLock {\n        id: lock\n\n        signal unlock\n\n        LockSurface {\n            lock: lock\n            pam: pam\n        }\n    }\n\n    Pam {\n        id: pam\n\n        lock: lock\n    }\n\n    CustomShortcut {\n        name: \"lock\"\n        description: \"Lock the current session\"\n        onPressed: lock.locked = true\n    }\n\n    CustomShortcut {\n        name: \"unlock\"\n        description: \"Unlock the current session\"\n        onPressed: lock.unlock()\n    }\n\n    IpcHandler {\n        function lock(): void {\n            lock.locked = true;\n        }\n\n        function unlock(): void {\n            lock.unlock();\n        }\n\n        function isLocked(): bool {\n            return lock.locked;\n        }\n\n        target: \"lock\"\n    }\n}\n"
  },
  {
    "path": "modules/lock/LockSurface.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Quickshell.Wayland\nimport QtQuick\nimport QtQuick.Effects\n\nWlSessionLockSurface {\n    id: root\n\n    required property WlSessionLock lock\n    required property Pam pam\n\n    readonly property alias unlocking: unlockAnim.running\n\n    color: \"transparent\"\n\n    Connections {\n        function onUnlock(): void {\n            unlockAnim.start();\n        }\n\n        target: root.lock\n    }\n\n    SequentialAnimation {\n        id: unlockAnim\n\n        ParallelAnimation {\n            Anim {\n                target: lockContent\n                properties: \"implicitWidth,implicitHeight\"\n                to: lockContent.size\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n            Anim {\n                target: lockBg\n                property: \"radius\"\n                to: lockContent.radius\n            }\n            Anim {\n                target: content\n                property: \"scale\"\n                to: 0\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n            Anim {\n                target: content\n                property: \"opacity\"\n                to: 0\n                duration: Appearance.anim.durations.small\n            }\n            Anim {\n                target: lockIcon\n                property: \"opacity\"\n                to: 1\n                duration: Appearance.anim.durations.large\n            }\n            Anim {\n                target: background\n                property: \"opacity\"\n                to: 0\n                duration: Appearance.anim.durations.large\n            }\n            SequentialAnimation {\n                PauseAnimation {\n                    duration: Appearance.anim.durations.small\n                }\n                Anim {\n                    target: lockContent\n                    property: \"opacity\"\n                    to: 0\n                }\n            }\n        }\n        PropertyAction {\n            target: root.lock\n            property: \"locked\"\n            value: false\n        }\n    }\n\n    ParallelAnimation {\n        id: initAnim\n\n        running: true\n\n        Anim {\n            target: background\n            property: \"opacity\"\n            to: 1\n            duration: Appearance.anim.durations.large\n        }\n        SequentialAnimation {\n            ParallelAnimation {\n                Anim {\n                    target: lockContent\n                    property: \"scale\"\n                    to: 1\n                    duration: Appearance.anim.durations.expressiveFastSpatial\n                    easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                }\n                Anim {\n                    target: lockContent\n                    property: \"rotation\"\n                    to: 360\n                    duration: Appearance.anim.durations.expressiveFastSpatial\n                    easing.bezierCurve: Appearance.anim.curves.standardAccel\n                }\n            }\n            ParallelAnimation {\n                Anim {\n                    target: lockIcon\n                    property: \"rotation\"\n                    to: 360\n                    easing.bezierCurve: Appearance.anim.curves.standardDecel\n                }\n                Anim {\n                    target: lockIcon\n                    property: \"opacity\"\n                    to: 0\n                }\n                Anim {\n                    target: content\n                    property: \"opacity\"\n                    to: 1\n                }\n                Anim {\n                    target: content\n                    property: \"scale\"\n                    to: 1\n                    duration: Appearance.anim.durations.expressiveDefaultSpatial\n                    easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                }\n                Anim {\n                    target: lockBg\n                    property: \"radius\"\n                    to: Appearance.rounding.large * 1.5\n                }\n                Anim {\n                    target: lockContent\n                    property: \"implicitWidth\"\n                    to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio\n                    duration: Appearance.anim.durations.expressiveDefaultSpatial\n                    easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                }\n                Anim {\n                    target: lockContent\n                    property: \"implicitHeight\"\n                    to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult\n                    duration: Appearance.anim.durations.expressiveDefaultSpatial\n                    easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                }\n            }\n        }\n    }\n\n    ScreencopyView {\n        id: background\n\n        anchors.fill: parent\n        captureSource: root.screen\n        opacity: 0\n\n        layer.enabled: true\n        layer.effect: MultiEffect {\n            autoPaddingEnabled: false\n            blurEnabled: true\n            blur: 1\n            blurMax: 64\n            blurMultiplier: 1\n        }\n    }\n\n    Item {\n        id: lockContent\n\n        readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4\n        readonly property int radius: size / 4 * Appearance.rounding.scale\n\n        anchors.centerIn: parent\n        implicitWidth: size\n        implicitHeight: size\n\n        rotation: 180\n        scale: 0\n\n        StyledRect {\n            id: lockBg\n\n            anchors.fill: parent\n            color: Colours.palette.m3surface\n            radius: parent.radius\n            opacity: Colours.transparency.enabled ? Colours.transparency.base : 1\n\n            layer.enabled: true\n            layer.effect: MultiEffect {\n                shadowEnabled: true\n                blurMax: 15\n                shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7)\n            }\n        }\n\n        MaterialIcon {\n            id: lockIcon\n\n            anchors.centerIn: parent\n            text: \"lock\"\n            font.pointSize: Appearance.font.size.extraLarge * 4\n            font.bold: true\n            rotation: 180\n        }\n\n        Content {\n            id: content\n\n            anchors.centerIn: parent\n            width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Appearance.padding.large * 2\n            height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Appearance.padding.large * 2\n\n            lock: root\n            opacity: 0\n            scale: 0\n        }\n    }\n}\n"
  },
  {
    "path": "modules/lock/Media.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property var lock\n\n    anchors.left: parent.left\n    anchors.right: parent.right\n    implicitHeight: layout.implicitHeight\n\n    Image {\n        anchors.fill: parent\n        source: Players.active?.trackArtUrl ?? \"\"\n\n        asynchronous: true\n        fillMode: Image.PreserveAspectCrop\n        sourceSize.width: width\n        sourceSize.height: height\n\n        layer.enabled: true\n        layer.effect: OpacityMask {\n            maskSource: mask\n        }\n\n        opacity: status === Image.Ready ? 1 : 0\n\n        Behavior on opacity {\n            Anim {\n                duration: Appearance.anim.durations.extraLarge\n            }\n        }\n    }\n\n    Rectangle {\n        id: mask\n\n        anchors.fill: parent\n        layer.enabled: true\n        visible: false\n\n        gradient: Gradient {\n            orientation: Gradient.Horizontal\n\n            GradientStop {\n                position: 0\n                color: Qt.rgba(0, 0, 0, 0.5)\n            }\n            GradientStop {\n                position: 0.4\n                color: Qt.rgba(0, 0, 0, 0.2)\n            }\n            GradientStop {\n                position: 0.8\n                color: Qt.rgba(0, 0, 0, 0)\n            }\n        }\n    }\n\n    ColumnLayout {\n        id: layout\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.margins: Appearance.padding.large\n\n        StyledText {\n            Layout.topMargin: Appearance.padding.large\n            Layout.bottomMargin: Appearance.spacing.larger\n            text: qsTr(\"Now playing\")\n            color: Colours.palette.m3onSurfaceVariant\n            font.family: Appearance.font.family.mono\n            font.weight: 500\n        }\n\n        StyledText {\n            Layout.fillWidth: true\n            animate: true\n            text: Players.active?.trackArtist ?? qsTr(\"No media\")\n            color: Colours.palette.m3primary\n            horizontalAlignment: Text.AlignHCenter\n            font.pointSize: Appearance.font.size.large\n            font.family: Appearance.font.family.mono\n            font.weight: 600\n            elide: Text.ElideRight\n        }\n\n        StyledText {\n            Layout.fillWidth: true\n            animate: true\n            text: Players.active?.trackTitle ?? qsTr(\"No media\")\n            horizontalAlignment: Text.AlignHCenter\n            font.pointSize: Appearance.font.size.larger\n            font.family: Appearance.font.family.mono\n            elide: Text.ElideRight\n        }\n\n        RowLayout {\n            Layout.alignment: Qt.AlignHCenter\n            Layout.topMargin: Appearance.spacing.large * 1.2\n            Layout.bottomMargin: Appearance.padding.large\n\n            spacing: Appearance.spacing.large\n\n            PlayerControl {\n                function onClicked(): void {\n                    if (Players.active?.canGoPrevious)\n                        Players.active.previous();\n                }\n\n                icon: \"skip_previous\"\n            }\n\n            PlayerControl {\n                function onClicked(): void {\n                    if (Players.active?.canTogglePlaying)\n                        Players.active.togglePlaying();\n                }\n\n                animate: true\n                icon: active ? \"pause\" : \"play_arrow\"\n                colour: \"Primary\"\n                level: active ? 2 : 1\n                active: Players.active?.isPlaying ?? false\n            }\n\n            PlayerControl {\n                function onClicked(): void {\n                    if (Players.active?.canGoNext)\n                        Players.active.next();\n                }\n\n                icon: \"skip_next\"\n            }\n        }\n    }\n\n    component PlayerControl: StyledRect {\n        id: control\n\n        property alias animate: controlIcon.animate\n        property alias icon: controlIcon.text\n        property bool active\n        property string colour: \"Secondary\"\n        property int level: 1\n\n        function onClicked(): void {\n        }\n\n        Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0)\n        implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2\n        implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2\n\n        color: active ? Colours.palette[`m3${colour.toLowerCase()}`] : Colours.palette[`m3${colour.toLowerCase()}Container`]\n        radius: active || controlState.pressed ? Appearance.rounding.normal : Math.min(implicitWidth, implicitHeight) / 2 * Math.min(1, Appearance.rounding.scale)\n\n        Elevation {\n            anchors.fill: parent\n            radius: parent.radius\n            z: -1\n            level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level\n        }\n\n        StateLayer {\n            id: controlState\n\n            function onClicked(): void {\n                control.onClicked();\n            }\n\n            color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`]\n        }\n\n        MaterialIcon {\n            id: controlIcon\n\n            anchors.centerIn: parent\n            color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`]\n            font.pointSize: Appearance.font.size.large\n            fill: control.active ? 1 : 0\n\n            Behavior on fill {\n                Anim {}\n            }\n        }\n\n        Behavior on Layout.preferredWidth {\n            Anim {\n                duration: Appearance.anim.durations.expressiveFastSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n            }\n        }\n\n        Behavior on radius {\n            Anim {\n                duration: Appearance.anim.durations.expressiveFastSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/lock/NotifDock.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.containers\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport Quickshell\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property var lock\n\n    anchors.fill: parent\n    anchors.margins: Appearance.padding.large\n\n    spacing: Appearance.spacing.smaller\n\n    StyledText {\n        Layout.fillWidth: true\n        text: Notifs.list.length > 0 ? qsTr(\"%1 notification%2\").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? \"\" : \"s\") : qsTr(\"Notifications\")\n        color: Colours.palette.m3outline\n        font.family: Appearance.font.family.mono\n        font.weight: 500\n        elide: Text.ElideRight\n    }\n\n    ClippingRectangle {\n        id: clipRect\n\n        Layout.fillWidth: true\n        Layout.fillHeight: true\n\n        radius: Appearance.rounding.small\n        color: \"transparent\"\n\n        Loader {\n            asynchronous: true\n            anchors.centerIn: parent\n            active: opacity > 0\n            opacity: Notifs.list.length > 0 && !Config.lock.hideNotifs ? 0 : 1\n\n            sourceComponent: ColumnLayout {\n                spacing: Appearance.spacing.large\n\n                Image {\n                    asynchronous: true\n                    source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`)\n                    fillMode: Image.PreserveAspectFit\n                    sourceSize.width: clipRect.width * 0.8\n\n                    layer.enabled: true\n                    layer.effect: Colouriser {\n                        colorizationColor: Colours.palette.m3outlineVariant\n                        brightness: 1\n                    }\n                }\n\n                StyledText {\n                    Layout.alignment: Qt.AlignHCenter\n                    text: Config.lock.hideNotifs ? qsTr(\"Unlock for Notifications\") : qsTr(\"No Notifications\")\n                    color: Colours.palette.m3outlineVariant\n                    font.pointSize: Appearance.font.size.large\n                    font.family: Appearance.font.family.mono\n                    font.weight: 500\n                }\n            }\n\n            Behavior on opacity {\n                Anim {\n                    duration: Appearance.anim.durations.extraLarge\n                }\n            }\n        }\n\n        StyledListView {\n            anchors.fill: parent\n            visible: !Config.lock.hideNotifs\n            spacing: Appearance.spacing.small\n            clip: true\n\n            model: ScriptModel {\n                values: {\n                    const list = Notifs.notClosed.map(n => [n.appName, null]);\n                    return [...new Map(list).keys()];\n                }\n            }\n\n            delegate: NotifGroup {}\n\n            add: Transition {\n                Anim {\n                    property: \"opacity\"\n                    from: 0\n                    to: 1\n                }\n                Anim {\n                    property: \"scale\"\n                    from: 0\n                    to: 1\n                    duration: Appearance.anim.durations.expressiveDefaultSpatial\n                    easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                }\n            }\n\n            remove: Transition {\n                Anim {\n                    property: \"opacity\"\n                    to: 0\n                }\n                Anim {\n                    property: \"scale\"\n                    to: 0.6\n                }\n            }\n\n            move: Transition {\n                Anim {\n                    properties: \"opacity,scale\"\n                    to: 1\n                }\n                Anim {\n                    property: \"y\"\n                    duration: Appearance.anim.durations.expressiveDefaultSpatial\n                    easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                }\n            }\n\n            displaced: Transition {\n                Anim {\n                    properties: \"opacity,scale\"\n                    to: 1\n                }\n                Anim {\n                    property: \"y\"\n                    duration: Appearance.anim.durations.expressiveDefaultSpatial\n                    easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/lock/NotifGroup.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport Quickshell.Widgets\nimport Quickshell.Services.Notifications\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property string modelData\n\n    readonly property list<var> notifs: Notifs.list.filter(notif => notif.appName === modelData)\n    readonly property var props: {\n        let img = \"\";\n        let icon = \"\";\n        let hasCritical = false;\n        let hasNormal = false;\n        for (const n of notifs) {\n            if (!img && n.image.length > 0)\n                img = n.image;\n            if (!icon && n.appIcon.length > 0)\n                icon = n.appIcon;\n            if (n.urgency === NotificationUrgency.Critical)\n                hasCritical = true;\n            else if (n.urgency === NotificationUrgency.Normal)\n                hasNormal = true;\n        }\n        return {\n            img,\n            icon,\n            urgency: hasCritical ? \"critical\" : hasNormal ? \"normal\" : \"low\"\n        };\n    }\n    readonly property string image: props.img\n    readonly property string appIcon: props.icon\n    readonly property string urgency: props.urgency\n\n    property bool expanded\n\n    anchors.left: parent?.left\n    anchors.right: parent?.right\n    implicitHeight: content.implicitHeight + Appearance.padding.normal * 2\n\n    clip: true\n    radius: Appearance.rounding.normal\n    color: root.urgency === \"critical\" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)\n\n    RowLayout {\n        id: content\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.top: parent.top\n        anchors.margins: Appearance.padding.normal\n\n        spacing: Appearance.spacing.normal\n\n        Item {\n            Layout.alignment: Qt.AlignLeft | Qt.AlignTop\n            implicitWidth: Config.notifs.sizes.image\n            implicitHeight: Config.notifs.sizes.image\n\n            Component {\n                id: imageComp\n\n                Image {\n                    source: Qt.resolvedUrl(root.image)\n                    fillMode: Image.PreserveAspectCrop\n                    sourceSize.width: Config.notifs.sizes.image\n                    sourceSize.height: Config.notifs.sizes.image\n                    cache: false\n                    asynchronous: true\n                    width: Config.notifs.sizes.image\n                    height: Config.notifs.sizes.image\n                }\n            }\n\n            Component {\n                id: appIconComp\n\n                ColouredIcon {\n                    implicitSize: Math.round(Config.notifs.sizes.image * 0.6)\n                    source: Quickshell.iconPath(root.appIcon)\n                    colour: root.urgency === \"critical\" ? Colours.palette.m3onError : root.urgency === \"low\" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer\n                    layer.enabled: root.appIcon.endsWith(\"symbolic\")\n                }\n            }\n\n            Component {\n                id: materialIconComp\n\n                MaterialIcon {\n                    text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency)\n                    color: root.urgency === \"critical\" ? Colours.palette.m3onError : root.urgency === \"low\" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer\n                    font.pointSize: Appearance.font.size.large\n                }\n            }\n\n            ClippingRectangle {\n                anchors.fill: parent\n                color: root.urgency === \"critical\" ? Colours.palette.m3error : root.urgency === \"low\" ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) : Colours.palette.m3secondaryContainer\n                radius: Appearance.rounding.full\n\n                Loader {\n                    asynchronous: true\n                    anchors.centerIn: parent\n                    sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp\n                }\n            }\n\n            Loader {\n                asynchronous: true\n                anchors.right: parent.right\n                anchors.bottom: parent.bottom\n                active: root.appIcon && root.image\n\n                sourceComponent: StyledRect {\n                    implicitWidth: Config.notifs.sizes.badge\n                    implicitHeight: Config.notifs.sizes.badge\n\n                    color: root.urgency === \"critical\" ? Colours.palette.m3error : root.urgency === \"low\" ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3secondaryContainer\n                    radius: Appearance.rounding.full\n\n                    ColouredIcon {\n                        anchors.centerIn: parent\n                        implicitSize: Math.round(Config.notifs.sizes.badge * 0.6)\n                        source: Quickshell.iconPath(root.appIcon)\n                        colour: root.urgency === \"critical\" ? Colours.palette.m3onError : root.urgency === \"low\" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer\n                        layer.enabled: root.appIcon.endsWith(\"symbolic\")\n                    }\n                }\n            }\n        }\n\n        ColumnLayout {\n            Layout.topMargin: -Appearance.padding.small\n            Layout.bottomMargin: -Appearance.padding.small / 2 - (root.expanded ? 0 : spacing)\n            Layout.fillWidth: true\n            spacing: Math.round(Appearance.spacing.small / 2)\n\n            RowLayout {\n                Layout.bottomMargin: -parent.spacing\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.smaller\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: root.modelData\n                    color: Colours.palette.m3onSurfaceVariant\n                    font.pointSize: Appearance.font.size.small\n                    elide: Text.ElideRight\n                }\n\n                StyledText {\n                    animate: true\n                    text: root.notifs[0]?.timeStr ?? \"\"\n                    color: Colours.palette.m3outline\n                    font.pointSize: Appearance.font.size.small\n                }\n\n                StyledRect {\n                    implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2\n                    implicitHeight: groupCount.implicitHeight + Appearance.padding.small\n\n                    color: root.urgency === \"critical\" ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2)\n                    radius: Appearance.rounding.full\n\n                    opacity: root.notifs.length > Config.notifs.groupPreviewNum ? 1 : 0\n                    Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0\n\n                    StateLayer {\n                        function onClicked(): void {\n                            root.expanded = !root.expanded;\n                        }\n\n                        color: root.urgency === \"critical\" ? Colours.palette.m3onError : Colours.palette.m3onSurface\n                    }\n\n                    RowLayout {\n                        id: expandBtn\n\n                        anchors.centerIn: parent\n                        spacing: Appearance.spacing.small / 2\n\n                        StyledText {\n                            id: groupCount\n\n                            Layout.leftMargin: Appearance.padding.small / 2\n                            animate: true\n                            text: root.notifs.length\n                            color: root.urgency === \"critical\" ? Colours.palette.m3onError : Colours.palette.m3onSurface\n                            font.pointSize: Appearance.font.size.small\n                        }\n\n                        MaterialIcon {\n                            Layout.rightMargin: -Appearance.padding.small / 2\n                            animate: true\n                            text: root.expanded ? \"expand_less\" : \"expand_more\"\n                            color: root.urgency === \"critical\" ? Colours.palette.m3onError : Colours.palette.m3onSurface\n                        }\n                    }\n\n                    Behavior on opacity {\n                        Anim {}\n                    }\n\n                    Behavior on Layout.preferredWidth {\n                        Anim {}\n                    }\n                }\n            }\n\n            Repeater {\n                model: ScriptModel {\n                    values: root.notifs.slice(0, Config.notifs.groupPreviewNum)\n                }\n\n                NotifLine {\n                    id: notif\n\n                    ParallelAnimation {\n                        running: true\n\n                        Anim {\n                            target: notif\n                            property: \"opacity\"\n                            from: 0\n                            to: 1\n                        }\n                        Anim {\n                            target: notif\n                            property: \"scale\"\n                            from: 0.7\n                            to: 1\n                        }\n                        Anim {\n                            target: notif.Layout\n                            property: \"preferredHeight\"\n                            from: 0\n                            to: notif.implicitHeight\n                        }\n                    }\n\n                    ParallelAnimation {\n                        running: notif.modelData.closed\n                        onFinished: notif.modelData.unlock(notif)\n\n                        Anim {\n                            target: notif\n                            property: \"opacity\"\n                            to: 0\n                        }\n                        Anim {\n                            target: notif\n                            property: \"scale\"\n                            to: 0.7\n                        }\n                        Anim {\n                            target: notif.Layout\n                            property: \"preferredHeight\"\n                            to: 0\n                        }\n                    }\n                }\n            }\n\n            Loader {\n                asynchronous: true\n                Layout.fillWidth: true\n\n                opacity: root.expanded ? 1 : 0\n                Layout.preferredHeight: root.expanded ? implicitHeight : 0\n                active: opacity > 0\n\n                sourceComponent: ColumnLayout {\n                    Repeater {\n                        model: ScriptModel {\n                            values: root.notifs.slice(Config.notifs.groupPreviewNum)\n                        }\n\n                        NotifLine {}\n                    }\n                }\n\n                Behavior on opacity {\n                    Anim {}\n                }\n            }\n        }\n    }\n\n    Behavior on implicitHeight {\n        Anim {\n            duration: Appearance.anim.durations.expressiveDefaultSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n        }\n    }\n\n    component NotifLine: StyledText {\n        id: notifLine\n\n        required property NotifData modelData\n\n        Layout.fillWidth: true\n        textFormat: Text.MarkdownText\n        text: {\n            const summary = modelData.summary.replace(/\\n/g, \" \");\n            const body = modelData.body.replace(/\\n/g, \" \");\n            const colour = root.urgency === \"critical\" ? Colours.palette.m3secondary : Colours.palette.m3outline;\n\n            if (metrics.text === metrics.elidedText)\n                return `${summary} <span style='color:${colour}'>${body}</span>`;\n\n            const t = metrics.elidedText.length - 3;\n            if (t < summary.length)\n                return `${summary.slice(0, t)}...`;\n\n            return `${summary} <span style='color:${colour}'>${body.slice(0, t - summary.length)}...</span>`;\n        }\n        color: root.urgency === \"critical\" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n\n        Component.onCompleted: modelData.lock(this)\n        Component.onDestruction: modelData.unlock(this)\n\n        TextMetrics {\n            id: metrics\n\n            text: `${notifLine.modelData.summary} ${notifLine.modelData.body}`.replace(/\\n/g, \" \")\n            font.pointSize: notifLine.font.pointSize\n            font.family: notifLine.font.family\n            elideWidth: notifLine.width\n            elide: Text.ElideRight\n        }\n    }\n}\n"
  },
  {
    "path": "modules/lock/Pam.qml",
    "content": "import qs.config\nimport Quickshell\nimport Quickshell.Io\nimport Quickshell.Wayland\nimport Quickshell.Services.Pam\nimport QtQuick\n\nScope {\n    id: root\n\n    required property WlSessionLock lock\n\n    readonly property alias passwd: passwd\n    readonly property alias fprint: fprint\n    property string lockMessage\n    property string state\n    property string fprintState\n    property string buffer\n\n    signal flashMsg\n\n    function handleKey(event: KeyEvent): void {\n        if (passwd.active || state === \"max\")\n            return;\n\n        if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {\n            passwd.start();\n        } else if (event.key === Qt.Key_Backspace) {\n            if (event.modifiers & Qt.ControlModifier) {\n                buffer = \"\";\n            } else {\n                buffer = buffer.slice(0, -1);\n            }\n        } else if (\" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\\\|;:'\\\",<.>/?\".includes(event.text.toLowerCase())) {\n            // No illegal characters (you are insane if you use unicode in your password)\n            buffer += event.text;\n        }\n    }\n\n    PamContext {\n        id: passwd\n\n        config: \"passwd\"\n        configDirectory: Quickshell.shellDir + \"/assets/pam.d\"\n\n        onMessageChanged: {\n            if (message.startsWith(\"The account is locked\"))\n                root.lockMessage = message;\n            else if (root.lockMessage && message.endsWith(\" left to unlock)\"))\n                root.lockMessage += \"\\n\" + message;\n        }\n\n        onResponseRequiredChanged: {\n            if (!responseRequired)\n                return;\n\n            respond(root.buffer);\n            root.buffer = \"\";\n        }\n\n        onCompleted: res => {\n            if (res === PamResult.Success)\n                return root.lock.unlock();\n\n            if (res === PamResult.Error)\n                root.state = \"error\";\n            else if (res === PamResult.MaxTries)\n                root.state = \"max\";\n            else if (res === PamResult.Failed)\n                root.state = \"fail\";\n\n            root.flashMsg();\n            stateReset.restart();\n        }\n    }\n\n    PamContext {\n        id: fprint\n\n        property bool available\n        property int tries\n        property int errorTries\n\n        function checkAvail(): void {\n            if (!available || !Config.lock.enableFprint || !root.lock.secure) {\n                abort();\n                return;\n            }\n\n            tries = 0;\n            errorTries = 0;\n            start();\n        }\n\n        config: \"fprint\"\n        configDirectory: Quickshell.shellDir + \"/assets/pam.d\"\n\n        onCompleted: res => {\n            if (!available)\n                return;\n\n            if (res === PamResult.Success)\n                return root.lock.unlock();\n\n            if (res === PamResult.Error) {\n                root.fprintState = \"error\";\n                errorTries++;\n                if (errorTries < 5) {\n                    abort();\n                    errorRetry.restart();\n                }\n            } else if (res === PamResult.MaxTries) {\n                // Isn't actually the real max tries as pam only reports completed\n                // when max tries is reached.\n                tries++;\n                if (tries < Config.lock.maxFprintTries) {\n                    // Restart if not actually real max tries\n                    root.fprintState = \"fail\";\n                    start();\n                } else {\n                    root.fprintState = \"max\";\n                    abort();\n                }\n            }\n\n            root.flashMsg();\n            fprintStateReset.start();\n        }\n    }\n\n    Process {\n        id: availProc\n\n        command: [\"sh\", \"-c\", \"fprintd-list $USER\"]\n        onExited: code => {\n            fprint.available = code === 0;\n            fprint.checkAvail();\n        }\n    }\n\n    Timer {\n        id: errorRetry\n\n        interval: 800\n        onTriggered: fprint.start()\n    }\n\n    Timer {\n        id: stateReset\n\n        interval: 4000\n        onTriggered: {\n            if (root.state !== \"max\")\n                root.state = \"\";\n        }\n    }\n\n    Timer {\n        id: fprintStateReset\n\n        interval: 4000\n        onTriggered: {\n            root.fprintState = \"\";\n            fprint.errorTries = 0;\n        }\n    }\n\n    Connections {\n        function onSecureChanged(): void {\n            if (root.lock.secure) {\n                availProc.running = true;\n                root.buffer = \"\";\n                root.state = \"\";\n                root.fprintState = \"\";\n                root.lockMessage = \"\";\n            }\n        }\n\n        function onUnlock(): void {\n            fprint.abort();\n        }\n\n        target: root.lock\n    }\n\n    Connections {\n        function onEnableFprintChanged(): void {\n            fprint.checkAvail();\n        }\n\n        target: Config.lock\n    }\n}\n"
  },
  {
    "path": "modules/lock/Resources.qml",
    "content": "import qs.components\nimport qs.components.controls\nimport qs.components.misc\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nGridLayout {\n    id: root\n\n    anchors.left: parent.left\n    anchors.right: parent.right\n    anchors.margins: Appearance.padding.large\n\n    rowSpacing: Appearance.spacing.large\n    columnSpacing: Appearance.spacing.large\n    rows: 2\n    columns: 2\n\n    Ref {\n        service: SystemUsage\n    }\n\n    Resource {\n        Layout.topMargin: Appearance.padding.large\n        icon: \"memory\"\n        value: SystemUsage.cpuPerc\n        colour: Colours.palette.m3primary\n    }\n\n    Resource {\n        Layout.topMargin: Appearance.padding.large\n        icon: \"thermostat\"\n        value: Math.min(1, SystemUsage.cpuTemp / 90)\n        colour: Colours.palette.m3secondary\n    }\n\n    Resource {\n        Layout.bottomMargin: Appearance.padding.large\n        icon: \"memory_alt\"\n        value: SystemUsage.memPerc\n        colour: Colours.palette.m3secondary\n    }\n\n    Resource {\n        Layout.bottomMargin: Appearance.padding.large\n        icon: \"hard_disk\"\n        value: SystemUsage.storagePerc\n        colour: Colours.palette.m3tertiary\n    }\n\n    component Resource: StyledRect {\n        id: res\n\n        required property string icon\n        required property real value\n        required property color colour\n\n        Layout.fillWidth: true\n        implicitHeight: width\n\n        color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)\n        radius: Appearance.rounding.large\n\n        CircularProgress {\n            id: circ\n\n            anchors.fill: parent\n            value: res.value\n            padding: Appearance.padding.large * 3\n            fgColour: res.colour\n            bgColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 3)\n            strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal\n        }\n\n        MaterialIcon {\n            id: icon\n\n            anchors.centerIn: parent\n            text: res.icon\n            color: res.colour\n            font.pointSize: (circ.arcRadius * 0.7) || 1\n            font.weight: 600\n        }\n\n        Behavior on value {\n            Anim {\n                duration: Appearance.anim.durations.large\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/lock/WeatherInfo.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property int rootHeight\n\n    anchors.left: parent.left\n    anchors.right: parent.right\n    anchors.margins: Appearance.padding.large * 2\n\n    spacing: Appearance.spacing.small\n\n    Loader {\n        asynchronous: true\n        Layout.topMargin: Appearance.padding.large * 2\n        Layout.bottomMargin: -Appearance.padding.large\n        Layout.alignment: Qt.AlignHCenter\n\n        active: root.rootHeight > 610\n        visible: active\n\n        sourceComponent: StyledText {\n            text: qsTr(\"Weather\")\n            color: Colours.palette.m3primary\n            font.pointSize: Appearance.font.size.extraLarge\n            font.weight: 500\n        }\n    }\n\n    RowLayout {\n        Layout.fillWidth: true\n        spacing: Appearance.spacing.large\n\n        MaterialIcon {\n            animate: true\n            text: Weather.icon\n            color: Colours.palette.m3secondary\n            font.pointSize: Appearance.font.size.extraLarge * 2.5\n        }\n\n        ColumnLayout {\n            spacing: Appearance.spacing.small\n\n            StyledText {\n                Layout.fillWidth: true\n\n                animate: true\n                text: Weather.description\n                color: Colours.palette.m3secondary\n                font.pointSize: Appearance.font.size.large\n                font.weight: 500\n                elide: Text.ElideRight\n            }\n\n            StyledText {\n                Layout.fillWidth: true\n\n                animate: true\n                text: qsTr(\"Humidity: %1%\").arg(Weather.humidity)\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.normal\n                elide: Text.ElideRight\n            }\n        }\n\n        Loader {\n            asynchronous: true\n            Layout.rightMargin: Appearance.padding.smaller\n            active: root.width > 400\n            visible: active\n\n            sourceComponent: ColumnLayout {\n                spacing: Appearance.spacing.small\n\n                StyledText {\n                    Layout.fillWidth: true\n\n                    animate: true\n                    text: Weather.temp\n                    color: Colours.palette.m3primary\n                    horizontalAlignment: Text.AlignRight\n                    font.pointSize: Appearance.font.size.extraLarge\n                    font.weight: 500\n                    elide: Text.ElideLeft\n                }\n\n                StyledText {\n                    Layout.fillWidth: true\n\n                    animate: true\n                    text: qsTr(\"Feels like: %1\").arg(Weather.feelsLike)\n                    color: Colours.palette.m3outline\n                    horizontalAlignment: Text.AlignRight\n                    font.pointSize: Appearance.font.size.smaller\n                    elide: Text.ElideLeft\n                }\n            }\n        }\n    }\n\n    Loader {\n        id: forecastLoader\n\n        asynchronous: true\n        Layout.topMargin: Appearance.spacing.smaller\n        Layout.bottomMargin: Appearance.padding.large * 2\n        Layout.fillWidth: true\n\n        active: root.rootHeight > 820\n        visible: active\n\n        sourceComponent: RowLayout {\n            spacing: Appearance.spacing.large\n\n            Repeater {\n                model: {\n                    const forecast = Weather.hourlyForecast;\n                    const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5;\n                    if (!forecast)\n                        return Array.from({\n                            length: count\n                        }, () => null);\n\n                    return forecast.slice(0, count);\n                }\n\n                ColumnLayout {\n                    id: forecastHour\n\n                    required property var modelData\n\n                    Layout.fillWidth: true\n                    spacing: Appearance.spacing.small\n\n                    StyledText {\n                        Layout.fillWidth: true\n                        text: {\n                            const hour = forecastHour.modelData?.hour ?? 0;\n                            return hour > 12 ? `${(hour - 12).toString().padStart(2, \"0\")} PM` : `${hour.toString().padStart(2, \"0\")} AM`;\n                        }\n                        color: Colours.palette.m3outline\n                        horizontalAlignment: Text.AlignHCenter\n                        font.pointSize: Appearance.font.size.larger\n                    }\n\n                    MaterialIcon {\n                        Layout.alignment: Qt.AlignHCenter\n                        text: forecastHour.modelData?.icon ?? \"cloud_alert\"\n                        font.pointSize: Appearance.font.size.extraLarge * 1.5\n                        font.weight: 500\n                    }\n\n                    StyledText {\n                        Layout.alignment: Qt.AlignHCenter\n                        text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C`\n                        color: Colours.palette.m3secondary\n                        font.pointSize: Appearance.font.size.larger\n                    }\n                }\n            }\n        }\n    }\n\n    Timer {\n        running: true\n        triggeredOnStart: true\n        repeat: true\n        interval: 900000 // 15 minutes\n        onTriggered: Weather.reload()\n    }\n}\n"
  },
  {
    "path": "modules/notifications/Background.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Shapes\n\nShapePath {\n    id: root\n\n    required property Wrapper wrapper\n    required property var sidebar\n    readonly property real rounding: Config.border.rounding\n    readonly property bool flatten: wrapper.height < rounding * 2\n    readonly property real roundingY: flatten ? wrapper.height / 2 : rounding\n\n    strokeWidth: -1\n    fillColor: Colours.palette.m3surface\n\n    PathLine {\n        relativeX: -(root.wrapper.width + root.rounding)\n        relativeY: 0\n    }\n    PathArc {\n        relativeX: root.rounding\n        relativeY: root.roundingY\n        radiusX: root.rounding\n        radiusY: Math.min(root.rounding, root.wrapper.height)\n    }\n    PathLine {\n        relativeX: 0\n        relativeY: root.wrapper.height - root.roundingY * 2\n    }\n    PathArc {\n        relativeX: root.sidebar.notifsRoundingX\n        relativeY: root.roundingY\n        radiusX: root.sidebar.notifsRoundingX\n        radiusY: Math.min(root.rounding, root.wrapper.height)\n        direction: PathArc.Counterclockwise\n    }\n    PathLine {\n        relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width\n        relativeY: 0\n    }\n    PathArc {\n        relativeX: root.rounding\n        relativeY: root.rounding\n        radiusX: root.rounding\n        radiusY: root.rounding\n    }\n\n    Behavior on fillColor {\n        CAnim {}\n    }\n}\n"
  },
  {
    "path": "modules/notifications/Content.qml",
    "content": "import qs.components\nimport qs.components.containers\nimport qs.components.widgets\nimport qs.services\nimport qs.config\nimport Quickshell\nimport Quickshell.Widgets\nimport QtQuick\n\nItem {\n    id: root\n\n    required property DrawerVisibilities visibilities\n    required property Item osdPanel\n    required property Item sessionPanel\n    readonly property int padding: Appearance.padding.large\n\n    anchors.top: parent.top\n    anchors.bottom: parent.bottom\n    anchors.right: parent.right\n\n    implicitWidth: Config.notifs.sizes.width + padding * 2\n    implicitHeight: {\n        const count = list.count;\n        if (count === 0)\n            return 0;\n\n        let height = (count - 1) * Appearance.spacing.smaller;\n        for (let i = 0; i < count; i++)\n            height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;\n\n        if (visibilities.osd) {\n            const h = osdPanel.y - Config.border.rounding * 2 - padding * 2;\n            if (height > h)\n                height = h;\n        }\n\n        if (visibilities.session) {\n            const h = sessionPanel.y - Config.border.rounding * 2 - padding * 2;\n            if (height > h)\n                height = h;\n        }\n\n        return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2);\n    }\n\n    ClippingWrapperRectangle {\n        anchors.fill: parent\n        anchors.margins: root.padding\n\n        color: \"transparent\"\n        radius: Appearance.rounding.normal\n\n        StyledListView {\n            id: list\n\n            model: ScriptModel {\n                values: Notifs.popups.filter(n => !n.closed)\n            }\n\n            anchors.fill: parent\n\n            orientation: Qt.Vertical\n            spacing: 0\n            cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0\n\n            delegate: NotifWrapper {}\n\n            move: Transition {\n                Anim {\n                    property: \"y\"\n                }\n            }\n\n            displaced: Transition {\n                Anim {\n                    property: \"y\"\n                }\n            }\n\n            ExtraIndicator {\n                anchors.top: parent.top\n                extra: {\n                    const count = list.count;\n                    if (count === 0)\n                        return 0;\n\n                    const scrollY = list.contentY;\n\n                    let height = 0;\n                    for (let i = 0; i < count; i++) {\n                        height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller;\n\n                        if (height - Appearance.spacing.smaller >= scrollY)\n                            return i;\n                    }\n\n                    return count;\n                }\n            }\n\n            ExtraIndicator {\n                anchors.bottom: parent.bottom\n                extra: {\n                    const count = list.count;\n                    if (count === 0)\n                        return 0;\n\n                    const scrollY = list.contentHeight - (list.contentY + list.height);\n\n                    let height = 0;\n                    for (let i = count - 1; i >= 0; i--) {\n                        height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller;\n\n                        if (height - Appearance.spacing.smaller >= scrollY)\n                            return count - i - 1;\n                    }\n\n                    return 0;\n                }\n            }\n        }\n    }\n\n    Behavior on implicitHeight {\n        Anim {}\n    }\n\n    component NotifWrapper: Item {\n        id: wrapper\n\n        required property NotifData modelData\n        required property int index\n        readonly property alias nonAnimHeight: notif.nonAnimHeight\n        property int idx\n\n        onIndexChanged: {\n            if (index !== -1)\n                idx = index;\n        }\n\n        implicitWidth: notif.implicitWidth\n        implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller)\n\n        ListView.onRemove: removeAnim.start()\n\n        SequentialAnimation {\n            id: removeAnim\n\n            PropertyAction {\n                target: wrapper\n                property: \"ListView.delayRemove\"\n                value: true\n            }\n            PropertyAction {\n                target: wrapper\n                property: \"enabled\"\n                value: false\n            }\n            PropertyAction {\n                target: wrapper\n                property: \"implicitHeight\"\n                value: 0\n            }\n            PropertyAction {\n                target: wrapper\n                property: \"z\"\n                value: 1\n            }\n            Anim {\n                target: notif\n                property: \"x\"\n                to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2\n                duration: Appearance.anim.durations.normal\n                easing.bezierCurve: Appearance.anim.curves.emphasized\n            }\n            PropertyAction {\n                target: wrapper\n                property: \"ListView.delayRemove\"\n                value: false\n            }\n        }\n\n        ClippingRectangle {\n            anchors.top: parent.top\n            anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller\n\n            color: \"transparent\"\n            radius: notif.radius\n            implicitWidth: notif.implicitWidth\n            implicitHeight: notif.implicitHeight\n\n            Notification {\n                id: notif\n\n                modelData: wrapper.modelData\n            }\n        }\n    }\n\n    component Anim: NumberAnimation {\n        duration: Appearance.anim.durations.expressiveDefaultSpatial\n        easing.type: Easing.BezierSpline\n        easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n    }\n}\n"
  },
  {
    "path": "modules/notifications/Notification.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport Quickshell.Widgets\nimport Quickshell.Services.Notifications\nimport QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Shapes\n\nStyledRect {\n    id: root\n\n    required property NotifData modelData\n    readonly property bool hasImage: modelData.image.length > 0\n    readonly property bool hasAppIcon: modelData.appIcon.length > 0\n    readonly property int bodyTextFormat: /[<*_`#\\[\\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText\n    readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2\n    property bool expanded: Config.notifs.openExpanded\n\n    color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer\n    radius: Appearance.rounding.normal\n    implicitWidth: Config.notifs.sizes.width\n    implicitHeight: inner.implicitHeight\n\n    x: Config.notifs.sizes.width\n    Component.onCompleted: {\n        x = 0;\n        modelData.lock(this);\n    }\n    Component.onDestruction: modelData.unlock(this)\n\n    Behavior on x {\n        Anim {\n            easing.bezierCurve: Appearance.anim.curves.emphasizedDecel\n        }\n    }\n\n    MouseArea {\n        property int startY\n\n        anchors.fill: parent\n        hoverEnabled: true\n        cursorShape: root.expanded && body.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined\n        acceptedButtons: Qt.LeftButton | Qt.MiddleButton\n        preventStealing: true\n\n        onEntered: root.modelData.timer.stop()\n        onExited: {\n            if (!pressed)\n                root.modelData.timer.start();\n        }\n\n        drag.target: parent\n        drag.axis: Drag.XAxis\n\n        onPressed: event => {\n            root.modelData.timer.stop();\n            startY = event.y;\n            if (event.button === Qt.MiddleButton)\n                root.modelData.close();\n        }\n        onReleased: event => {\n            if (!containsMouse)\n                root.modelData.timer.start();\n\n            if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold)\n                root.x = 0;\n            else\n                root.modelData.popup = false;\n        }\n        onPositionChanged: event => {\n            if (pressed) {\n                const diffY = event.y - startY;\n                if (Math.abs(diffY) > Config.notifs.expandThreshold)\n                    root.expanded = diffY > 0;\n            }\n        }\n        onClicked: event => {\n            if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton)\n                return;\n\n            const actions = root.modelData.actions;\n            if (actions?.length === 1)\n                actions[0].invoke();\n        }\n\n        Item {\n            id: inner\n\n            anchors.left: parent.left\n            anchors.right: parent.right\n            anchors.top: parent.top\n            anchors.margins: Appearance.padding.normal\n\n            implicitHeight: root.nonAnimHeight\n\n            Behavior on implicitHeight {\n                Anim {\n                    duration: Appearance.anim.durations.expressiveDefaultSpatial\n                    easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                }\n            }\n\n            Loader {\n                id: image\n\n                asynchronous: true\n                active: root.hasImage\n\n                anchors.left: parent.left\n                anchors.top: parent.top\n                width: Config.notifs.sizes.image\n                height: Config.notifs.sizes.image\n                visible: root.hasImage || root.hasAppIcon\n\n                sourceComponent: ClippingRectangle {\n                    radius: Appearance.rounding.full\n                    implicitWidth: Config.notifs.sizes.image\n                    implicitHeight: Config.notifs.sizes.image\n\n                    Image {\n                        anchors.fill: parent\n                        source: Qt.resolvedUrl(root.modelData.image)\n                        fillMode: Image.PreserveAspectCrop\n                        sourceSize.width: Config.notifs.sizes.image\n                        sourceSize.height: Config.notifs.sizes.image\n                        cache: false\n                        asynchronous: true\n                    }\n                }\n            }\n\n            Loader {\n                id: appIcon\n\n                asynchronous: true\n                active: root.hasAppIcon || !root.hasImage\n\n                anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter\n                anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter\n                anchors.right: root.hasImage ? image.right : undefined\n                anchors.bottom: root.hasImage ? image.bottom : undefined\n\n                sourceComponent: StyledRect {\n                    radius: Appearance.rounding.full\n                    color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer\n                    implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image\n                    implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image\n\n                    Loader {\n                        id: icon\n\n                        asynchronous: true\n                        active: root.hasAppIcon\n\n                        anchors.centerIn: parent\n\n                        width: Math.round(parent.width * 0.6)\n                        height: Math.round(parent.width * 0.6)\n\n                        sourceComponent: ColouredIcon {\n                            anchors.fill: parent\n                            source: Quickshell.iconPath(root.modelData.appIcon)\n                            colour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer\n                            layer.enabled: root.modelData.appIcon.endsWith(\"symbolic\")\n                        }\n                    }\n\n                    Loader {\n                        asynchronous: true\n                        active: !root.hasAppIcon\n                        anchors.centerIn: parent\n                        anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02\n                        anchors.verticalCenterOffset: Appearance.font.size.large * 0.02\n\n                        sourceComponent: MaterialIcon {\n                            text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)\n\n                            color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer\n                            font.pointSize: Appearance.font.size.large\n                        }\n                    }\n                }\n            }\n\n            Shape {\n                id: progressIndicator\n\n                anchors.centerIn: appIcon\n                width: appIcon.implicitWidth + progressShape.strokeWidth * 2\n                height: appIcon.implicitHeight + progressShape.strokeWidth * 2\n                preferredRendererType: Shape.CurveRenderer\n\n                ShapePath {\n                    id: progressShape\n\n                    capStyle: ShapePath.RoundCap\n                    fillColor: \"transparent\"\n                    strokeWidth: 2\n                    strokeColor: Colours.palette.m3primary\n\n                    PathAngleArc {\n                        id: progressArc\n\n                        radiusX: progressIndicator.width / 2 - Appearance.padding.small / 2\n                        centerX: progressIndicator.width / 2\n                        radiusY: progressIndicator.height / 2 - Appearance.padding.small / 2\n                        centerY: progressIndicator.height / 2\n\n                        startAngle: -90\n                        sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360\n\n                        Behavior on sweepAngle {\n                            Anim {\n                                easing.bezierCurve: Appearance.anim.curves.emphasizedDecel\n                            }\n                        }\n                    }\n                }\n            }\n\n            StyledText {\n                id: appName\n\n                anchors.top: parent.top\n                anchors.left: image.right\n                anchors.leftMargin: Appearance.spacing.smaller\n\n                animate: true\n                text: appNameMetrics.elidedText\n                maximumLineCount: 1\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.small\n\n                opacity: root.expanded ? 1 : 0\n\n                Behavior on opacity {\n                    Anim {}\n                }\n            }\n\n            TextMetrics {\n                id: appNameMetrics\n\n                text: root.modelData.appName\n                font.family: appName.font.family\n                font.pointSize: appName.font.pointSize\n                elide: Text.ElideRight\n                elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3\n            }\n\n            StyledText {\n                id: summary\n\n                anchors.top: parent.top\n                anchors.left: image.right\n                anchors.leftMargin: Appearance.spacing.smaller\n\n                animate: true\n                text: summaryMetrics.elidedText\n                maximumLineCount: 1\n                height: implicitHeight\n\n                states: State {\n                    name: \"expanded\"\n                    when: root.expanded\n\n                    PropertyChanges {\n                        summary.maximumLineCount: undefined\n                    }\n\n                    AnchorChanges {\n                        target: summary\n                        anchors.top: appName.bottom\n                    }\n                }\n\n                transitions: Transition {\n                    PropertyAction {\n                        target: summary\n                        property: \"maximumLineCount\"\n                    }\n                    AnchorAnimation {\n                        duration: Appearance.anim.durations.normal\n                        easing.type: Easing.BezierSpline\n                        easing.bezierCurve: Appearance.anim.curves.standard\n                    }\n                }\n\n                Behavior on height {\n                    Anim {}\n                }\n            }\n\n            TextMetrics {\n                id: summaryMetrics\n\n                text: root.modelData.summary\n                font.family: summary.font.family\n                font.pointSize: summary.font.pointSize\n                elide: Text.ElideRight\n                elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3\n            }\n\n            StyledText {\n                id: timeSep\n\n                anchors.top: parent.top\n                anchors.left: summary.right\n                anchors.leftMargin: Appearance.spacing.small\n\n                text: \"•\"\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.small\n\n                states: State {\n                    name: \"expanded\"\n                    when: root.expanded\n\n                    AnchorChanges {\n                        target: timeSep\n                        anchors.left: appName.right\n                    }\n                }\n\n                transitions: Transition {\n                    AnchorAnimation {\n                        duration: Appearance.anim.durations.normal\n                        easing.type: Easing.BezierSpline\n                        easing.bezierCurve: Appearance.anim.curves.standard\n                    }\n                }\n            }\n\n            StyledText {\n                id: time\n\n                anchors.top: parent.top\n                anchors.left: timeSep.right\n                anchors.leftMargin: Appearance.spacing.small\n\n                animate: true\n                horizontalAlignment: Text.AlignLeft\n                text: root.modelData.timeStr\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.small\n            }\n\n            Item {\n                id: expandBtn\n\n                anchors.right: parent.right\n                anchors.top: parent.top\n\n                implicitWidth: expandIcon.height\n                implicitHeight: expandIcon.height\n\n                StateLayer {\n                    function onClicked() {\n                        root.expanded = !root.expanded;\n                    }\n\n                    radius: Appearance.rounding.full\n                    color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n                }\n\n                MaterialIcon {\n                    id: expandIcon\n\n                    anchors.centerIn: parent\n\n                    animate: true\n                    text: root.expanded ? \"expand_less\" : \"expand_more\"\n                    font.pointSize: Appearance.font.size.normal\n                }\n            }\n\n            StyledText {\n                id: bodyPreview\n\n                anchors.left: summary.left\n                anchors.right: expandBtn.left\n                anchors.top: summary.bottom\n                anchors.rightMargin: Appearance.spacing.small\n\n                animate: true\n                textFormat: root.bodyTextFormat\n                text: bodyPreviewMetrics.elidedText\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.small\n\n                opacity: root.expanded ? 0 : 1\n\n                Behavior on opacity {\n                    Anim {}\n                }\n            }\n\n            TextMetrics {\n                id: bodyPreviewMetrics\n\n                text: root.modelData.body\n                font.family: bodyPreview.font.family\n                font.pointSize: bodyPreview.font.pointSize\n                elide: Text.ElideRight\n                elideWidth: bodyPreview.width\n            }\n\n            StyledText {\n                id: body\n\n                anchors.left: summary.left\n                anchors.right: expandBtn.left\n                anchors.top: summary.bottom\n                anchors.rightMargin: Appearance.spacing.small\n\n                animate: true\n                textFormat: root.bodyTextFormat\n                text: root.modelData.body\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.small\n                wrapMode: Text.WrapAtWordBoundaryOrAnywhere\n                height: text ? implicitHeight : 0\n\n                onLinkActivated: link => {\n                    if (!root.expanded)\n                        return;\n\n                    Quickshell.execDetached([\"app2unit\", \"-O\", \"--\", link]);\n                    root.modelData.popup = false;\n                }\n\n                opacity: root.expanded ? 1 : 0\n\n                Behavior on opacity {\n                    Anim {}\n                }\n            }\n\n            RowLayout {\n                id: actions\n\n                anchors.horizontalCenter: parent.horizontalCenter\n                anchors.top: body.bottom\n                anchors.topMargin: Appearance.spacing.small\n\n                spacing: Appearance.spacing.smaller\n\n                opacity: root.expanded ? 1 : 0\n\n                Behavior on opacity {\n                    Anim {}\n                }\n\n                Action {\n                    modelData: QtObject {\n                        readonly property string text: qsTr(\"Close\")\n\n                        function invoke(): void {\n                            root.modelData.close();\n                        }\n                    }\n                }\n\n                Repeater {\n                    model: root.modelData.actions\n\n                    delegate: Component {\n                        Action {}\n                    }\n                }\n            }\n        }\n    }\n\n    component Action: StyledRect {\n        id: action\n\n        required property var modelData\n\n        radius: Appearance.rounding.full\n        color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)\n\n        Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2\n        Layout.preferredHeight: actionText.height + Appearance.padding.small * 2\n        implicitWidth: actionText.width + Appearance.padding.normal * 2\n        implicitHeight: actionText.height + Appearance.padding.small * 2\n\n        StateLayer {\n            function onClicked(): void {\n                action.modelData.invoke();\n            }\n\n            radius: Appearance.rounding.full\n            color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface\n        }\n\n        StyledText {\n            id: actionText\n\n            anchors.centerIn: parent\n            text: actionTextMetrics.elidedText\n            color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant\n            font.pointSize: Appearance.font.size.small\n        }\n\n        TextMetrics {\n            id: actionTextMetrics\n\n            text: action.modelData.text\n            font.family: actionText.font.family\n            font.pointSize: actionText.font.pointSize\n            elide: Text.ElideRight\n            elideWidth: {\n                const numActions = root.modelData.actions.length + 1;\n                return (inner.width - actions.spacing * (numActions - 1)) / numActions - Appearance.padding.normal * 2;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/notifications/Wrapper.qml",
    "content": "import qs.components\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    required property DrawerVisibilities visibilities\n    required property Item sidebarPanel\n    property alias osdPanel: content.osdPanel\n    property alias sessionPanel: content.sessionPanel\n\n    visible: height > 0\n    implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth)\n    implicitHeight: content.implicitHeight\n\n    states: State {\n        name: \"hidden\"\n        when: root.visibilities.sidebar && Config.sidebar.enabled\n\n        PropertyChanges {\n            root.implicitHeight: 0\n        }\n    }\n\n    transitions: Transition {\n        Anim {\n            target: root\n            property: \"implicitHeight\"\n            duration: Appearance.anim.durations.expressiveDefaultSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n        }\n    }\n\n    Content {\n        id: content\n\n        visibilities: root.visibilities\n    }\n}\n"
  },
  {
    "path": "modules/osd/Background.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Shapes\n\nShapePath {\n    id: root\n\n    required property Wrapper wrapper\n    readonly property real rounding: Config.border.rounding\n    readonly property bool flatten: wrapper.width < rounding * 2\n    readonly property real roundingX: flatten ? wrapper.width / 2 : rounding\n\n    strokeWidth: -1\n    fillColor: Colours.palette.m3surface\n\n    PathArc {\n        relativeX: -root.roundingX\n        relativeY: root.rounding\n        radiusX: Math.min(root.rounding, root.wrapper.width)\n        radiusY: root.rounding\n    }\n    PathLine {\n        relativeX: -(root.wrapper.width - root.roundingX * 2)\n        relativeY: 0\n    }\n    PathArc {\n        relativeX: -root.roundingX\n        relativeY: root.rounding\n        radiusX: Math.min(root.rounding, root.wrapper.width)\n        radiusY: root.rounding\n        direction: PathArc.Counterclockwise\n    }\n    PathLine {\n        relativeX: 0\n        relativeY: root.wrapper.height - root.rounding * 2\n    }\n    PathArc {\n        relativeX: root.roundingX\n        relativeY: root.rounding\n        radiusX: Math.min(root.rounding, root.wrapper.width)\n        radiusY: root.rounding\n        direction: PathArc.Counterclockwise\n    }\n    PathLine {\n        relativeX: root.wrapper.width - root.roundingX * 2\n        relativeY: 0\n    }\n    PathArc {\n        relativeX: root.roundingX\n        relativeY: root.rounding\n        radiusX: Math.min(root.rounding, root.wrapper.width)\n        radiusY: root.rounding\n    }\n\n    Behavior on fillColor {\n        CAnim {}\n    }\n}\n"
  },
  {
    "path": "modules/osd/Content.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport qs.utils\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property Brightness.Monitor monitor\n    required property DrawerVisibilities visibilities\n\n    required property real volume\n    required property bool muted\n    required property real sourceVolume\n    required property bool sourceMuted\n    required property real brightness\n\n    implicitWidth: layout.implicitWidth + Appearance.padding.large * 2\n    implicitHeight: layout.implicitHeight + Appearance.padding.large * 2\n\n    ColumnLayout {\n        id: layout\n\n        anchors.centerIn: parent\n        spacing: Appearance.spacing.normal\n\n        // Speaker volume\n        CustomMouseArea {\n            function onWheel(event: WheelEvent) {\n                if (event.angleDelta.y > 0)\n                    Audio.incrementVolume();\n                else if (event.angleDelta.y < 0)\n                    Audio.decrementVolume();\n            }\n\n            implicitWidth: Config.osd.sizes.sliderWidth\n            implicitHeight: Config.osd.sizes.sliderHeight\n\n            FilledSlider {\n                anchors.fill: parent\n\n                icon: Icons.getVolumeIcon(value, root.muted)\n                value: root.volume\n                to: Config.services.maxVolume\n                onMoved: Audio.setVolume(value)\n            }\n        }\n\n        // Microphone volume\n        WrappedLoader {\n            shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.visibilities.session)\n\n            sourceComponent: CustomMouseArea {\n                function onWheel(event: WheelEvent) {\n                    if (event.angleDelta.y > 0)\n                        Audio.incrementSourceVolume();\n                    else if (event.angleDelta.y < 0)\n                        Audio.decrementSourceVolume();\n                }\n\n                implicitWidth: Config.osd.sizes.sliderWidth\n                implicitHeight: Config.osd.sizes.sliderHeight\n\n                FilledSlider {\n                    anchors.fill: parent\n\n                    icon: Icons.getMicVolumeIcon(value, root.sourceMuted)\n                    value: root.sourceVolume\n                    to: Config.services.maxVolume\n                    onMoved: Audio.setSourceVolume(value)\n                }\n            }\n        }\n\n        // Brightness\n        WrappedLoader {\n            shouldBeActive: Config.osd.enableBrightness\n\n            sourceComponent: CustomMouseArea {\n                function onWheel(event: WheelEvent) {\n                    const monitor = root.monitor;\n                    if (!monitor)\n                        return;\n                    if (event.angleDelta.y > 0)\n                        monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement);\n                    else if (event.angleDelta.y < 0)\n                        monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement);\n                }\n\n                implicitWidth: Config.osd.sizes.sliderWidth\n                implicitHeight: Config.osd.sizes.sliderHeight\n\n                FilledSlider {\n                    anchors.fill: parent\n\n                    icon: `brightness_${(Math.round(value * 6) + 1)}`\n                    value: root.brightness\n                    onMoved: root.monitor?.setBrightness(value)\n                }\n            }\n        }\n    }\n\n    component WrappedLoader: Loader {\n        required property bool shouldBeActive\n\n        asynchronous: true\n        Layout.preferredHeight: shouldBeActive ? Config.osd.sizes.sliderHeight : 0\n        opacity: shouldBeActive ? 1 : 0\n        active: opacity > 0\n        visible: active\n\n        Behavior on Layout.preferredHeight {\n            Anim {\n                easing.bezierCurve: Appearance.anim.curves.emphasized\n            }\n        }\n\n        Behavior on opacity {\n            Anim {}\n        }\n    }\n}\n"
  },
  {
    "path": "modules/osd/Wrapper.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\n\nItem {\n    id: root\n\n    required property ShellScreen screen\n    required property DrawerVisibilities visibilities\n    property bool hovered\n    readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen)\n    readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled)\n\n    property real volume\n    property bool muted\n    property real sourceVolume\n    property bool sourceMuted\n    property real brightness\n\n    function show(): void {\n        visibilities.osd = true;\n        timer.restart();\n    }\n\n    Component.onCompleted: {\n        volume = Audio.volume;\n        muted = Audio.muted;\n        sourceVolume = Audio.sourceVolume;\n        sourceMuted = Audio.sourceMuted;\n        brightness = root.monitor?.brightness ?? 0;\n    }\n\n    visible: width > 0\n    implicitWidth: 0\n    implicitHeight: content.implicitHeight\n\n    states: State {\n        name: \"visible\"\n        when: root.shouldBeActive\n\n        PropertyChanges {\n            root.implicitWidth: content.implicitWidth\n        }\n    }\n\n    transitions: [\n        Transition {\n            from: \"\"\n            to: \"visible\"\n\n            Anim {\n                target: root\n                property: \"implicitWidth\"\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        },\n        Transition {\n            from: \"visible\"\n            to: \"\"\n\n            Anim {\n                target: root\n                property: \"implicitWidth\"\n                easing.bezierCurve: Appearance.anim.curves.emphasized\n            }\n        }\n    ]\n\n    Connections {\n        function onMutedChanged(): void {\n            root.show();\n            root.muted = Audio.muted;\n        }\n\n        function onVolumeChanged(): void {\n            root.show();\n            root.volume = Audio.volume;\n        }\n\n        function onSourceMutedChanged(): void {\n            root.show();\n            root.sourceMuted = Audio.sourceMuted;\n        }\n\n        function onSourceVolumeChanged(): void {\n            root.show();\n            root.sourceVolume = Audio.sourceVolume;\n        }\n\n        target: Audio\n    }\n\n    Connections {\n        function onBrightnessChanged(): void {\n            root.show();\n            root.brightness = root.monitor?.brightness ?? 0;\n        }\n\n        target: root.monitor\n    }\n\n    Timer {\n        id: timer\n\n        interval: Config.osd.hideDelay\n        onTriggered: {\n            if (!root.hovered)\n                root.visibilities.osd = false;\n        }\n    }\n\n    Loader {\n        id: content\n\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.left: parent.left\n\n        Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible)\n\n        sourceComponent: Content {\n            monitor: root.monitor\n            visibilities: root.visibilities\n            volume: root.volume\n            muted: root.muted\n            sourceVolume: root.sourceVolume\n            sourceMuted: root.sourceMuted\n            brightness: root.brightness\n        }\n    }\n}\n"
  },
  {
    "path": "modules/session/Background.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Shapes\n\nShapePath {\n    id: root\n\n    required property Wrapper wrapper\n    readonly property real rounding: Config.border.rounding\n    readonly property bool flatten: wrapper.width < rounding * 2\n    readonly property real roundingX: flatten ? wrapper.width / 2 : rounding\n\n    strokeWidth: -1\n    fillColor: Colours.palette.m3surface\n\n    PathArc {\n        relativeX: -root.roundingX\n        relativeY: root.rounding\n        radiusX: Math.min(root.rounding, root.wrapper.width)\n        radiusY: root.rounding\n    }\n    PathLine {\n        relativeX: -(root.wrapper.width - root.roundingX * 2)\n        relativeY: 0\n    }\n    PathArc {\n        relativeX: -root.roundingX\n        relativeY: root.rounding\n        radiusX: Math.min(root.rounding, root.wrapper.width)\n        radiusY: root.rounding\n        direction: PathArc.Counterclockwise\n    }\n    PathLine {\n        relativeX: 0\n        relativeY: root.wrapper.height - root.rounding * 2\n    }\n    PathArc {\n        relativeX: root.roundingX\n        relativeY: root.rounding\n        radiusX: Math.min(root.rounding, root.wrapper.width)\n        radiusY: root.rounding\n        direction: PathArc.Counterclockwise\n    }\n    PathLine {\n        relativeX: root.wrapper.width - root.roundingX * 2\n        relativeY: 0\n    }\n    PathArc {\n        relativeX: root.roundingX\n        relativeY: root.rounding\n        radiusX: Math.min(root.rounding, root.wrapper.width)\n        radiusY: root.rounding\n    }\n\n    Behavior on fillColor {\n        CAnim {}\n    }\n}\n"
  },
  {
    "path": "modules/session/Content.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport QtQuick\n\nColumn {\n    id: root\n\n    required property DrawerVisibilities visibilities\n\n    padding: Appearance.padding.large\n    spacing: Appearance.spacing.large\n\n    SessionButton {\n        id: logout\n\n        icon: Config.session.icons.logout\n        command: Config.session.commands.logout\n\n        KeyNavigation.down: shutdown\n\n        Component.onCompleted: forceActiveFocus()\n\n        Connections {\n            function onLauncherChanged(): void {\n                if (!root.visibilities.launcher)\n                    logout.forceActiveFocus();\n            }\n\n            target: root.visibilities\n        }\n    }\n\n    SessionButton {\n        id: shutdown\n\n        icon: Config.session.icons.shutdown\n        command: Config.session.commands.shutdown\n\n        KeyNavigation.up: logout\n        KeyNavigation.down: hibernate\n    }\n\n    AnimatedImage {\n        width: Config.session.sizes.button\n        height: Config.session.sizes.button\n        sourceSize.width: width\n        sourceSize.height: height\n\n        playing: visible\n        asynchronous: true\n        speed: Appearance.anim.sessionGifSpeed\n        source: Paths.absolutePath(Config.paths.sessionGif)\n    }\n\n    SessionButton {\n        id: hibernate\n\n        icon: Config.session.icons.hibernate\n        command: Config.session.commands.hibernate\n\n        KeyNavigation.up: shutdown\n        KeyNavigation.down: reboot\n    }\n\n    SessionButton {\n        id: reboot\n\n        icon: Config.session.icons.reboot\n        command: Config.session.commands.reboot\n\n        KeyNavigation.up: hibernate\n    }\n\n    component SessionButton: StyledRect {\n        id: button\n\n        required property string icon\n        required property list<string> command\n\n        implicitWidth: Config.session.sizes.button\n        implicitHeight: Config.session.sizes.button\n\n        radius: Appearance.rounding.large\n        color: button.activeFocus ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer\n\n        Keys.onEnterPressed: Quickshell.execDetached(button.command)\n        Keys.onReturnPressed: Quickshell.execDetached(button.command)\n        Keys.onEscapePressed: root.visibilities.session = false\n        Keys.onPressed: event => {\n            if (!Config.session.vimKeybinds)\n                return;\n\n            if (event.modifiers & Qt.ControlModifier) {\n                if (event.key === Qt.Key_J && KeyNavigation.down) {\n                    KeyNavigation.down.focus = true;\n                    event.accepted = true;\n                } else if (event.key === Qt.Key_K && KeyNavigation.up) {\n                    KeyNavigation.up.focus = true;\n                    event.accepted = true;\n                }\n            } else if (event.key === Qt.Key_Tab && KeyNavigation.down) {\n                KeyNavigation.down.focus = true;\n                event.accepted = true;\n            } else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {\n                if (KeyNavigation.up) {\n                    KeyNavigation.up.focus = true;\n                    event.accepted = true;\n                }\n            }\n        }\n\n        StateLayer {\n            function onClicked(): void {\n                Quickshell.execDetached(button.command);\n            }\n\n            radius: parent.radius\n            color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n        }\n\n        MaterialIcon {\n            anchors.centerIn: parent\n\n            text: button.icon\n            color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n            font.pointSize: Appearance.font.size.extraLarge\n            font.weight: 500\n        }\n    }\n}\n"
  },
  {
    "path": "modules/session/Wrapper.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    required property DrawerVisibilities visibilities\n    required property var panels\n    readonly property real nonAnimWidth: content.implicitWidth\n\n    visible: width > 0\n    implicitWidth: 0\n    implicitHeight: content.implicitHeight\n\n    states: State {\n        name: \"visible\"\n        when: root.visibilities.session && Config.session.enabled\n\n        PropertyChanges {\n            root.implicitWidth: root.nonAnimWidth\n        }\n    }\n\n    transitions: [\n        Transition {\n            from: \"\"\n            to: \"visible\"\n\n            Anim {\n                target: root\n                property: \"implicitWidth\"\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        },\n        Transition {\n            from: \"visible\"\n            to: \"\"\n\n            Anim {\n                target: root\n                property: \"implicitWidth\"\n                easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized\n            }\n        }\n    ]\n\n    Loader {\n        id: content\n\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.left: parent.left\n\n        Component.onCompleted: active = Qt.binding(() => (root.visibilities.session && Config.session.enabled) || root.visible)\n\n        sourceComponent: Content {\n            visibilities: root.visibilities\n        }\n    }\n}\n"
  },
  {
    "path": "modules/sidebar/Background.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Shapes\n\nShapePath {\n    id: root\n\n    required property Wrapper wrapper\n    required property var panels\n\n    readonly property real rounding: Config.border.rounding\n\n    readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width\n    readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding\n\n    readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width\n    readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding\n\n    strokeWidth: -1\n    fillColor: Colours.palette.m3surface\n\n    PathLine {\n        relativeX: -root.wrapper.width - root.notifsRoundingX\n        relativeY: 0\n    }\n    PathArc {\n        relativeX: root.notifsRoundingX\n        relativeY: root.rounding\n        radiusX: root.notifsRoundingX\n        radiusY: root.rounding\n    }\n    PathLine {\n        relativeX: 0\n        relativeY: root.wrapper.height - root.rounding * 2\n    }\n    PathArc {\n        relativeX: -root.utilsRoundingX\n        relativeY: root.rounding\n        radiusX: root.utilsRoundingX\n        radiusY: root.rounding\n    }\n    PathLine {\n        relativeX: root.wrapper.width + root.utilsRoundingX\n        relativeY: 0\n    }\n\n    Behavior on fillColor {\n        CAnim {}\n    }\n}\n"
  },
  {
    "path": "modules/sidebar/Content.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property Props props\n    required property DrawerVisibilities visibilities\n\n    ColumnLayout {\n        id: layout\n\n        anchors.fill: parent\n        spacing: Appearance.spacing.normal\n\n        StyledRect {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            radius: Appearance.rounding.normal\n            color: Colours.tPalette.m3surfaceContainerLow\n\n            NotifDock {\n                props: root.props\n                visibilities: root.visibilities\n            }\n        }\n\n        StyledRect {\n            Layout.topMargin: Appearance.padding.large - layout.spacing\n            Layout.fillWidth: true\n            implicitHeight: 1\n\n            color: Colours.tPalette.m3outlineVariant\n        }\n    }\n}\n"
  },
  {
    "path": "modules/sidebar/Notif.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property NotifData modelData\n    required property Props props\n    required property bool expanded\n    required property DrawerVisibilities visibilities\n\n    readonly property StyledText body: (expandedContent.item as ExpandedBody)?.body ?? null\n    readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Appearance.padding.normal * 2 : summaryHeightMetrics.height\n\n    implicitHeight: nonAnimHeight\n\n    radius: Appearance.rounding.small\n    color: {\n        const c = root.modelData.urgency === \"critical\" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);\n        return expanded ? c : Qt.alpha(c, 0);\n    }\n\n    states: State {\n        name: \"expanded\"\n        when: root.expanded\n\n        PropertyChanges {\n            summary.anchors.margins: Appearance.padding.normal\n            dummySummary.anchors.margins: Appearance.padding.normal\n            compactBody.anchors.margins: Appearance.padding.normal\n            timeStr.anchors.margins: Appearance.padding.normal\n            expandedContent.anchors.margins: Appearance.padding.normal\n            summary.width: root.width - Appearance.padding.normal * 2 - timeStr.implicitWidth - Appearance.spacing.small\n            summary.maximumLineCount: Number.MAX_SAFE_INTEGER\n        }\n    }\n\n    transitions: Transition {\n        Anim {\n            properties: \"margins,width,maximumLineCount\"\n        }\n    }\n\n    TextMetrics {\n        id: summaryHeightMetrics\n\n        font: summary.font\n        text: \" \" // Use this height to prevent weird characters from changing the line height\n    }\n\n    StyledText {\n        id: summary\n\n        anchors.top: parent.top\n        anchors.left: parent.left\n\n        width: parent.width\n        text: root.modelData.summary\n        color: root.modelData.urgency === \"critical\" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface\n        elide: Text.ElideRight\n        wrapMode: Text.WordWrap\n        maximumLineCount: 1\n    }\n\n    StyledText {\n        id: dummySummary\n\n        anchors.top: parent.top\n        anchors.left: parent.left\n\n        visible: false\n        text: root.modelData.summary\n    }\n\n    WrappedLoader {\n        id: compactBody\n\n        shouldBeActive: !root.expanded\n        anchors.top: parent.top\n        anchors.left: dummySummary.right\n        anchors.right: parent.right\n        anchors.leftMargin: Appearance.spacing.small\n\n        sourceComponent: StyledText {\n            text: root.modelData.body.replace(/\\n/g, \" \")\n            color: root.modelData.urgency === \"critical\" ? Colours.palette.m3secondary : Colours.palette.m3outline\n            elide: Text.ElideRight\n        }\n    }\n\n    WrappedLoader {\n        id: timeStr\n\n        shouldBeActive: root.expanded\n        anchors.top: parent.top\n        anchors.right: parent.right\n\n        sourceComponent: StyledText {\n            animate: true\n            text: root.modelData.timeStr\n            color: Colours.palette.m3outline\n            font.pointSize: Appearance.font.size.small\n        }\n    }\n\n    WrappedLoader {\n        id: expandedContent\n\n        shouldBeActive: root.expanded\n        anchors.top: summary.bottom\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.topMargin: Appearance.spacing.small / 2\n\n        sourceComponent: ExpandedBody {}\n    }\n\n    Behavior on implicitHeight {\n        Anim {\n            duration: Appearance.anim.durations.expressiveDefaultSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n        }\n    }\n\n    component ExpandedBody: ColumnLayout {\n        readonly property alias body: bodyText\n\n        spacing: Appearance.spacing.smaller\n\n        StyledText {\n            id: bodyText\n\n            Layout.fillWidth: true\n            textFormat: Text.MarkdownText\n            text: root.modelData.body.replace(/(.)\\n(?!\\n)/g, \"$1\\n\\n\") || qsTr(\"No body here! :/\")\n            color: root.modelData.urgency === \"critical\" ? Colours.palette.m3secondary : Colours.palette.m3outline\n            wrapMode: Text.WordWrap\n\n            onLinkActivated: link => {\n                Quickshell.execDetached([\"app2unit\", \"-O\", \"--\", link]);\n                root.visibilities.sidebar = false;\n            }\n        }\n\n        NotifActionList {\n            notif: root.modelData\n        }\n    }\n\n    component WrappedLoader: Loader {\n        required property bool shouldBeActive\n\n        asynchronous: true\n        opacity: shouldBeActive ? 1 : 0\n        active: opacity > 0\n\n        Behavior on opacity {\n            Anim {}\n        }\n    }\n}\n"
  },
  {
    "path": "modules/sidebar/NotifActionList.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.containers\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport Quickshell\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property NotifData notif\n\n    Layout.fillWidth: true\n    implicitHeight: flickable.contentHeight\n\n    layer.enabled: true\n    layer.smooth: true\n    layer.effect: OpacityMask {\n        maskSource: gradientMask\n    }\n\n    Item {\n        id: gradientMask\n\n        anchors.fill: parent\n        layer.enabled: true\n        visible: false\n\n        Rectangle {\n            anchors.fill: parent\n\n            gradient: Gradient {\n                orientation: Gradient.Horizontal\n\n                GradientStop {\n                    position: 0\n                    color: Qt.rgba(0, 0, 0, 0)\n                }\n                GradientStop {\n                    position: 0.1\n                    color: Qt.rgba(0, 0, 0, 1)\n                }\n                GradientStop {\n                    position: 0.9\n                    color: Qt.rgba(0, 0, 0, 1)\n                }\n                GradientStop {\n                    position: 1\n                    color: Qt.rgba(0, 0, 0, 0)\n                }\n            }\n        }\n\n        Rectangle {\n            anchors.top: parent.top\n            anchors.bottom: parent.bottom\n            anchors.left: parent.left\n\n            implicitWidth: parent.width / 2\n            opacity: flickable.contentX > 0 ? 0 : 1\n\n            Behavior on opacity {\n                Anim {}\n            }\n        }\n\n        Rectangle {\n            anchors.top: parent.top\n            anchors.bottom: parent.bottom\n            anchors.right: parent.right\n\n            implicitWidth: parent.width / 2\n            opacity: flickable.contentX < flickable.contentWidth - parent.width ? 0 : 1\n\n            Behavior on opacity {\n                Anim {}\n            }\n        }\n    }\n\n    StyledFlickable {\n        id: flickable\n\n        anchors.fill: parent\n        contentWidth: Math.max(width, actionList.implicitWidth)\n        contentHeight: actionList.implicitHeight\n\n        RowLayout {\n            id: actionList\n\n            anchors.fill: parent\n            spacing: Appearance.spacing.small\n\n            Repeater {\n                model: [\n                    {\n                        isClose: true\n                    },\n                    ...root.notif.actions,\n                    {\n                        isCopy: true\n                    }\n                ]\n\n                StyledRect {\n                    id: action\n\n                    required property var modelData\n\n                    Layout.fillWidth: true\n                    Layout.fillHeight: true\n                    implicitWidth: actionInner.implicitWidth + Appearance.padding.normal * 2\n                    implicitHeight: actionInner.implicitHeight + Appearance.padding.small * 2\n\n                    Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Appearance.padding.large : 0)\n                    radius: actionStateLayer.pressed ? Appearance.rounding.small / 2 : Appearance.rounding.small\n                    color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 4)\n\n                    Timer {\n                        id: copyTimer\n\n                        interval: 3000\n                        onTriggered: actionInner.item.text = \"content_copy\"\n                    }\n\n                    StateLayer {\n                        id: actionStateLayer\n\n                        function onClicked(): void {\n                            if (action.modelData.isClose) {\n                                root.notif.close();\n                            } else if (action.modelData.isCopy) {\n                                Quickshell.clipboardText = root.notif.body;\n                                actionInner.item.text = \"inventory\";\n                                copyTimer.start();\n                            } else if (action.modelData.invoke) {\n                                action.modelData.invoke();\n                            } else if (!root.notif.resident) {\n                                root.notif.close();\n                            }\n                        }\n                    }\n\n                    Loader {\n                        id: actionInner\n\n                        anchors.centerIn: parent\n                        sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif.hasActionIcons ? iconComp : textComp\n                    }\n\n                    Component {\n                        id: iconBtn\n\n                        MaterialIcon {\n                            animate: action.modelData.isCopy ?? false\n                            text: action.modelData.isCopy ? \"content_copy\" : \"close\"\n                            color: Colours.palette.m3onSurfaceVariant\n                        }\n                    }\n\n                    Component {\n                        id: iconComp\n\n                        IconImage {\n                            asynchronous: true\n                            source: Quickshell.iconPath(action.modelData.identifier)\n                        }\n                    }\n\n                    Component {\n                        id: textComp\n\n                        StyledText {\n                            text: action.modelData.text\n                            color: Colours.palette.m3onSurfaceVariant\n                        }\n                    }\n\n                    Behavior on Layout.preferredWidth {\n                        Anim {\n                            duration: Appearance.anim.durations.expressiveFastSpatial\n                            easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                        }\n                    }\n\n                    Behavior on radius {\n                        Anim {\n                            duration: Appearance.anim.durations.expressiveFastSpatial\n                            easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/sidebar/NotifDock.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.components.containers\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport Quickshell\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property Props props\n    required property DrawerVisibilities visibilities\n    readonly property int notifCount: Notifs.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0)\n\n    anchors.fill: parent\n    anchors.margins: Appearance.padding.normal\n\n    Component.onCompleted: Notifs.list.forEach(n => n.popup = false)\n\n    Item {\n        id: title\n\n        anchors.top: parent.top\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.margins: Appearance.padding.small\n\n        implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight)\n\n        StyledText {\n            id: count\n\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.left: parent.left\n            anchors.leftMargin: root.notifCount > 0 ? 0 : -width - titleText.anchors.leftMargin\n            opacity: root.notifCount > 0 ? 1 : 0\n\n            text: root.notifCount\n            color: Colours.palette.m3outline\n            font.pointSize: Appearance.font.size.normal\n            font.family: Appearance.font.family.mono\n            font.weight: 500\n\n            Behavior on anchors.leftMargin {\n                Anim {}\n            }\n\n            Behavior on opacity {\n                Anim {}\n            }\n        }\n\n        StyledText {\n            id: titleText\n\n            anchors.verticalCenter: parent.verticalCenter\n            anchors.left: count.right\n            anchors.right: parent.right\n            anchors.leftMargin: Appearance.spacing.small\n\n            text: root.notifCount > 0 ? qsTr(\"notification%1\").arg(root.notifCount === 1 ? \"\" : \"s\") : qsTr(\"Notifications\")\n            color: Colours.palette.m3outline\n            font.pointSize: Appearance.font.size.normal\n            font.family: Appearance.font.family.mono\n            font.weight: 500\n            elide: Text.ElideRight\n        }\n    }\n\n    ClippingRectangle {\n        id: clipRect\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.top: title.bottom\n        anchors.bottom: parent.bottom\n        anchors.topMargin: Appearance.spacing.smaller\n\n        radius: Appearance.rounding.small\n        color: \"transparent\"\n\n        Loader {\n            asynchronous: true\n            anchors.centerIn: parent\n            active: opacity > 0\n            opacity: root.notifCount > 0 ? 0 : 1\n\n            sourceComponent: ColumnLayout {\n                spacing: Appearance.spacing.large\n\n                Image {\n                    asynchronous: true\n                    source: Quickshell.shellPath(\"assets/dino.png\")\n                    fillMode: Image.PreserveAspectFit\n                    sourceSize.width: clipRect.width * 0.8\n\n                    layer.enabled: true\n                    layer.effect: Colouriser {\n                        colorizationColor: Colours.palette.m3outlineVariant\n                        brightness: 1\n                    }\n                }\n\n                StyledText {\n                    Layout.alignment: Qt.AlignHCenter\n                    text: qsTr(\"No Notifications\")\n                    color: Colours.palette.m3outlineVariant\n                    font.pointSize: Appearance.font.size.large\n                    font.family: Appearance.font.family.mono\n                    font.weight: 500\n                }\n            }\n\n            Behavior on opacity {\n                Anim {\n                    duration: Appearance.anim.durations.extraLarge\n                }\n            }\n        }\n\n        StyledFlickable {\n            id: view\n\n            anchors.fill: parent\n\n            flickableDirection: Flickable.VerticalFlick\n            contentWidth: width\n            contentHeight: notifList.implicitHeight\n\n            StyledScrollBar.vertical: StyledScrollBar {\n                flickable: view\n            }\n\n            NotifDockList {\n                id: notifList\n\n                props: root.props\n                visibilities: root.visibilities\n                container: view\n            }\n        }\n    }\n\n    Timer {\n        id: clearTimer\n\n        repeat: true\n        interval: 50\n        onTriggered: {\n            let next = null;\n            for (let i = 0; i < notifList.repeater.count; i++) {\n                next = notifList.repeater.itemAt(i);\n                if (!next?.closed) // qmllint disable missing-property\n                    break;\n            }\n            if (next) {\n                next.closeAll(); // qmllint disable missing-property\n            } else {\n                stop();\n            }\n        }\n    }\n\n    Loader {\n        asynchronous: true\n        anchors.right: parent.right\n        anchors.bottom: parent.bottom\n        anchors.margins: Appearance.padding.normal\n\n        scale: root.notifCount > 0 ? 1 : 0.5\n        opacity: root.notifCount > 0 ? 1 : 0\n        active: opacity > 0\n\n        sourceComponent: IconButton {\n            id: clearBtn\n\n            icon: \"clear_all\"\n            radius: Appearance.rounding.normal\n            padding: Appearance.padding.normal\n            font.pointSize: Math.round(Appearance.font.size.large * 1.2)\n            onClicked: clearTimer.start()\n\n            Elevation {\n                anchors.fill: parent\n                radius: parent.radius\n                z: -1\n                level: clearBtn.stateLayer.containsMouse ? 4 : 3\n            }\n        }\n\n        Behavior on scale {\n            Anim {\n                duration: Appearance.anim.durations.expressiveFastSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n            }\n        }\n\n        Behavior on opacity {\n            Anim {\n                duration: Appearance.anim.durations.expressiveFastSpatial\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/sidebar/NotifDockList.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Quickshell\nimport QtQuick\n\nItem {\n    id: root\n\n    required property Props props\n    required property Flickable container\n    required property DrawerVisibilities visibilities\n\n    readonly property alias repeater: repeater\n    readonly property int spacing: Appearance.spacing.small\n    property bool flag\n\n    anchors.left: parent.left\n    anchors.right: parent.right\n    implicitHeight: {\n        const item = repeater.itemAt(repeater.count - 1);\n        return item ? item.y + item.implicitHeight : 0;\n    }\n\n    Repeater {\n        id: repeater\n\n        model: ScriptModel {\n            values: {\n                const map = new Map();\n                for (const n of Notifs.notClosed)\n                    map.set(n.appName, null);\n                for (const n of Notifs.list)\n                    map.set(n.appName, null);\n                return [...map.keys()];\n            }\n            onValuesChanged: root.flagChanged()\n        }\n\n        delegate: NotifGroupDelegate {}\n    }\n\n    component NotifGroupDelegate: MouseArea {\n        id: notif\n\n        required property int index\n        required property string modelData\n\n        readonly property bool closed: notifInner.notifCount === 0\n        readonly property alias nonAnimHeight: notifInner.nonAnimHeight\n        property int startY\n\n        function closeAll(): void {\n            for (const n of Notifs.notClosed.filter(n => n.appName === modelData))\n                n.close();\n        }\n\n        y: {\n            root.flag; // Force update\n            let y = 0;\n            for (let i = 0; i < index; i++) {\n                const item = repeater.itemAt(i) as NotifGroupDelegate;\n                if (item && !item.closed)\n                    y += item.nonAnimHeight + root.spacing;\n            }\n            return y;\n        }\n\n        containmentMask: QtObject {\n            function contains(p: point): bool {\n                if (!root.container.contains(notif.mapToItem(root.container, p)))\n                    return false;\n                return notifInner.contains(p);\n            }\n        }\n\n        implicitWidth: root.width\n        implicitHeight: notifInner.implicitHeight\n\n        hoverEnabled: true\n        cursorShape: pressed ? Qt.ClosedHandCursor : undefined\n        acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton\n        preventStealing: true\n        enabled: !closed\n\n        drag.target: this\n        drag.axis: Drag.XAxis\n\n        onPressed: event => {\n            startY = event.y;\n            if (event.button === Qt.RightButton)\n                notifInner.toggleExpand(!notifInner.expanded);\n            else if (event.button === Qt.MiddleButton)\n                closeAll();\n        }\n        onPositionChanged: event => {\n            if (pressed) {\n                const diffY = event.y - startY;\n                if (Math.abs(diffY) > Config.notifs.expandThreshold)\n                    notifInner.toggleExpand(diffY > 0);\n            }\n        }\n        onReleased: event => {\n            if (Math.abs(x) < width * Config.notifs.clearThreshold)\n                x = 0;\n            else\n                closeAll();\n        }\n\n        ParallelAnimation {\n            running: true\n\n            Anim {\n                target: notif\n                property: \"opacity\"\n                from: 0\n                to: 1\n            }\n            Anim {\n                target: notif\n                property: \"scale\"\n                from: 0\n                to: 1\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        }\n\n        ParallelAnimation {\n            running: notif.closed\n\n            Anim {\n                target: notif\n                property: \"opacity\"\n                to: 0\n            }\n            Anim {\n                target: notif\n                property: \"scale\"\n                to: 0.6\n            }\n        }\n\n        NotifGroup {\n            id: notifInner\n\n            modelData: notif.modelData\n            props: root.props\n            container: root.container\n            visibilities: root.visibilities\n        }\n\n        Behavior on x {\n            Anim {\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        }\n\n        Behavior on y {\n            Anim {\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/sidebar/NotifGroup.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport Quickshell.Services.Notifications\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property string modelData\n    required property Props props\n    required property Flickable container\n    required property DrawerVisibilities visibilities\n\n    readonly property list<var> notifs: Notifs.list.filter(n => n.appName === modelData)\n    readonly property var groupProps: {\n        let count = 0;\n        let img = \"\";\n        let icon = \"\";\n        let hasCritical = false;\n        let hasNormal = false;\n        for (const n of notifs) {\n            if (!n.closed) {\n                count++;\n                if (!img && n.image.length > 0)\n                    img = n.image;\n                if (!icon && n.appIcon.length > 0)\n                    icon = n.appIcon;\n                if (n.urgency === NotificationUrgency.Critical)\n                    hasCritical = true;\n                else if (n.urgency === NotificationUrgency.Normal)\n                    hasNormal = true;\n            }\n        }\n        return {\n            count,\n            img,\n            icon,\n            urgency: hasCritical ? NotificationUrgency.Critical : hasNormal ? NotificationUrgency.Normal : NotificationUrgency.Low\n        };\n    }\n    readonly property int notifCount: groupProps.count\n    readonly property string image: groupProps.img\n    readonly property string appIcon: groupProps.icon\n    readonly property int urgency: groupProps.urgency\n\n    readonly property int nonAnimHeight: {\n        const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Appearance.spacing.small / 2) : 0);\n        const columnHeight = headerHeight + notifList.nonAnimHeight + column.Layout.topMargin + column.Layout.bottomMargin;\n        return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Appearance.padding.normal * 2);\n    }\n    readonly property bool expanded: props.expandedNotifs.includes(modelData)\n\n    function toggleExpand(expand: bool): void {\n        if (expand) {\n            if (!expanded)\n                props.expandedNotifs.push(modelData);\n        } else if (expanded) {\n            props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1);\n        }\n    }\n\n    Component.onDestruction: {\n        if (notifCount === 0 && expanded)\n            props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1);\n    }\n\n    anchors.left: parent?.left\n    anchors.right: parent?.right\n    implicitHeight: content.implicitHeight + Appearance.padding.normal * 2\n\n    clip: true\n    radius: Appearance.rounding.normal\n    color: Colours.layer(Colours.palette.m3surfaceContainer, 2)\n\n    RowLayout {\n        id: content\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.top: parent.top\n        anchors.margins: Appearance.padding.normal\n\n        spacing: Appearance.spacing.normal\n\n        Item {\n            Layout.alignment: Qt.AlignLeft | Qt.AlignTop\n            implicitWidth: Config.notifs.sizes.image\n            implicitHeight: Config.notifs.sizes.image\n\n            Component {\n                id: imageComp\n\n                Image {\n                    source: Qt.resolvedUrl(root.image)\n                    fillMode: Image.PreserveAspectCrop\n                    sourceSize.width: Config.notifs.sizes.image\n                    sourceSize.height: Config.notifs.sizes.image\n                    cache: false\n                    asynchronous: true\n                    width: Config.notifs.sizes.image\n                    height: Config.notifs.sizes.image\n                }\n            }\n\n            Component {\n                id: appIconComp\n\n                ColouredIcon {\n                    implicitSize: Math.round(Config.notifs.sizes.image * 0.6)\n                    source: Quickshell.iconPath(root.appIcon)\n                    colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer\n                    layer.enabled: root.appIcon.endsWith(\"symbolic\")\n                }\n            }\n\n            Component {\n                id: materialIconComp\n\n                MaterialIcon {\n                    text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency)\n                    color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer\n                    font.pointSize: Appearance.font.size.large\n                }\n            }\n\n            StyledClippingRect {\n                anchors.fill: parent\n                color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) : Colours.palette.m3secondaryContainer\n                radius: Appearance.rounding.full\n\n                Loader {\n                    asynchronous: true\n                    anchors.centerIn: parent\n                    sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp\n                }\n            }\n\n            Loader {\n                asynchronous: true\n                anchors.right: parent.right\n                anchors.bottom: parent.bottom\n                active: root.appIcon && root.image\n\n                sourceComponent: StyledRect {\n                    implicitWidth: Config.notifs.sizes.badge\n                    implicitHeight: Config.notifs.sizes.badge\n\n                    color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3secondaryContainer\n                    radius: Appearance.rounding.full\n\n                    ColouredIcon {\n                        anchors.centerIn: parent\n                        implicitSize: Math.round(Config.notifs.sizes.badge * 0.6)\n                        source: Quickshell.iconPath(root.appIcon)\n                        colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer\n                        layer.enabled: root.appIcon.endsWith(\"symbolic\")\n                    }\n                }\n            }\n        }\n\n        ColumnLayout {\n            id: column\n\n            Layout.topMargin: -Appearance.padding.small\n            Layout.bottomMargin: -Appearance.padding.small / 2\n            Layout.fillWidth: true\n            spacing: 0\n\n            RowLayout {\n                id: header\n\n                Layout.bottomMargin: root.expanded ? Math.round(Appearance.spacing.small / 2) : 0\n                Layout.fillWidth: true\n                spacing: Appearance.spacing.smaller\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: root.modelData\n                    color: Colours.palette.m3onSurfaceVariant\n                    font.pointSize: Appearance.font.size.small\n                    elide: Text.ElideRight\n                }\n\n                StyledText {\n                    animate: true\n                    text: root.notifs.find(n => !n.closed)?.timeStr ?? \"\"\n                    color: Colours.palette.m3outline\n                    font.pointSize: Appearance.font.size.small\n                }\n\n                StyledRect {\n                    implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2\n                    implicitHeight: groupCount.implicitHeight + Appearance.padding.small\n\n                    color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHigh, 3)\n                    radius: Appearance.rounding.full\n\n                    StateLayer {\n                        function onClicked(): void {\n                            root.toggleExpand(!root.expanded);\n                        }\n\n                        color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface\n                    }\n\n                    RowLayout {\n                        id: expandBtn\n\n                        anchors.centerIn: parent\n                        spacing: Appearance.spacing.small / 2\n\n                        StyledText {\n                            id: groupCount\n\n                            Layout.leftMargin: Appearance.padding.small / 2\n                            animate: true\n                            text: root.notifCount\n                            color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface\n                            font.pointSize: Appearance.font.size.small\n                        }\n\n                        MaterialIcon {\n                            Layout.rightMargin: -Appearance.padding.small / 2\n                            text: \"expand_more\"\n                            color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface\n                            rotation: root.expanded ? 180 : 0\n                            Layout.topMargin: root.expanded ? -Math.floor(Appearance.padding.smaller / 2) : 0\n\n                            Behavior on rotation {\n                                Anim {\n                                    duration: Appearance.anim.durations.expressiveDefaultSpatial\n                                    easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                                }\n                            }\n\n                            Behavior on Layout.topMargin {\n                                Anim {\n                                    duration: Appearance.anim.durations.expressiveDefaultSpatial\n                                    easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                                }\n                            }\n                        }\n                    }\n                }\n\n                Behavior on Layout.bottomMargin {\n                    Anim {}\n                }\n            }\n\n            NotifGroupList {\n                id: notifList\n\n                props: root.props\n                notifs: root.notifs\n                expanded: root.expanded\n                container: root.container\n                visibilities: root.visibilities\n                onRequestToggleExpand: expand => root.toggleExpand(expand)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/sidebar/NotifGroupList.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.config\nimport qs.services\nimport Quickshell\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property Props props\n    required property list<var> notifs\n    required property bool expanded\n    required property Flickable container\n    required property DrawerVisibilities visibilities\n\n    readonly property real nonAnimHeight: {\n        let h = -root.spacing;\n        for (let i = 0; i < repeater.count; i++) {\n            const item = repeater.itemAt(i) as NotifDelegate;\n            if (item && !item.modelData.closed && !item.previewHidden)\n                h += item.nonAnimHeight + root.spacing;\n        }\n        return h;\n    }\n\n    readonly property int spacing: Math.round(Appearance.spacing.small / 2)\n    property bool showAllNotifs\n    property bool flag\n\n    signal requestToggleExpand(expand: bool)\n\n    onExpandedChanged: {\n        if (expanded) {\n            clearTimer.stop();\n            showAllNotifs = true;\n        } else {\n            clearTimer.start();\n        }\n    }\n\n    Layout.fillWidth: true\n    implicitHeight: nonAnimHeight\n\n    Timer {\n        id: clearTimer\n\n        interval: Appearance.anim.durations.normal\n        onTriggered: root.showAllNotifs = false\n    }\n\n    Repeater {\n        id: repeater\n\n        model: ScriptModel {\n            values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1)\n            onValuesChanged: root.flagChanged()\n        }\n\n        delegate: NotifDelegate {}\n    }\n\n    Behavior on implicitHeight {\n        Anim {\n            duration: Appearance.anim.durations.expressiveDefaultSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n        }\n    }\n\n    component NotifDelegate: MouseArea {\n        id: notif\n\n        required property int index\n        required property NotifData modelData\n\n        readonly property alias nonAnimHeight: notifInner.nonAnimHeight\n        readonly property bool previewHidden: {\n            if (root.expanded)\n                return false;\n\n            let extraHidden = 0;\n            for (let i = 0; i < index; i++)\n                if (root.notifs[i].closed)\n                    extraHidden++;\n\n            return index >= Config.notifs.groupPreviewNum + extraHidden;\n        }\n        property int startY\n\n        y: {\n            root.flag; // Force update\n            let y = 0;\n            for (let i = 0; i < index; i++) {\n                const item = repeater.itemAt(i) as NotifDelegate;\n                if (item && !item.modelData.closed && !item.previewHidden)\n                    y += item.nonAnimHeight + root.spacing;\n            }\n            return y;\n        }\n\n        containmentMask: QtObject {\n            function contains(p: point): bool {\n                if (!root.container.contains(notif.mapToItem(root.container, p)))\n                    return false;\n                return notifInner.contains(p);\n            }\n        }\n\n        opacity: previewHidden ? 0 : 1\n        scale: previewHidden ? 0.7 : 1\n\n        implicitWidth: root.width\n        implicitHeight: notifInner.implicitHeight\n\n        hoverEnabled: true\n        cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined\n        acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton\n        preventStealing: !root.expanded\n        enabled: !modelData.closed\n\n        drag.target: this\n        drag.axis: Drag.XAxis\n\n        onPressed: event => {\n            startY = event.y;\n            if (event.button === Qt.RightButton)\n                root.requestToggleExpand(!root.expanded);\n            else if (event.button === Qt.MiddleButton)\n                modelData.close();\n        }\n        onPositionChanged: event => {\n            if (pressed && !root.expanded) {\n                const diffY = event.y - startY;\n                if (Math.abs(diffY) > Config.notifs.expandThreshold)\n                    root.requestToggleExpand(diffY > 0);\n            }\n        }\n        onReleased: event => {\n            if (Math.abs(x) < width * Config.notifs.clearThreshold)\n                x = 0;\n            else\n                modelData.close();\n        }\n\n        Component.onCompleted: modelData.lock(this)\n        Component.onDestruction: modelData.unlock(this)\n\n        ParallelAnimation {\n            Component.onCompleted: running = !notif.previewHidden\n\n            Anim {\n                target: notif\n                property: \"opacity\"\n                from: 0\n                to: 1\n            }\n            Anim {\n                target: notif\n                property: \"scale\"\n                from: 0.7\n                to: 1\n            }\n        }\n\n        ParallelAnimation {\n            running: notif.modelData.closed\n            onFinished: notif.modelData.unlock(notif)\n\n            Anim {\n                target: notif\n                property: \"opacity\"\n                to: 0\n            }\n            Anim {\n                target: notif\n                property: \"x\"\n                to: notif.x >= 0 ? notif.width : -notif.width\n            }\n        }\n\n        Notif {\n            id: notifInner\n\n            anchors.fill: parent\n            modelData: notif.modelData\n            props: root.props\n            expanded: root.expanded\n            visibilities: root.visibilities\n        }\n\n        Behavior on opacity {\n            Anim {}\n        }\n\n        Behavior on scale {\n            Anim {}\n        }\n\n        Behavior on x {\n            Anim {\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        }\n\n        Behavior on y {\n            Anim {\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/sidebar/Props.qml",
    "content": "import Quickshell\n\nPersistentProperties {\n    property list<string> expandedNotifs: []\n\n    reloadableId: \"sidebar\"\n}\n"
  },
  {
    "path": "modules/sidebar/Wrapper.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.config\nimport QtQuick\n\nItem {\n    id: root\n\n    required property DrawerVisibilities visibilities\n    required property var panels\n    readonly property Props props: Props {}\n\n    visible: width > 0\n    implicitWidth: 0\n\n    states: State {\n        name: \"visible\"\n        when: root.visibilities.sidebar && Config.sidebar.enabled\n\n        PropertyChanges {\n            root.implicitWidth: Config.sidebar.sizes.width\n        }\n    }\n\n    transitions: [\n        Transition {\n            from: \"\"\n            to: \"visible\"\n\n            Anim {\n                target: root\n                property: \"implicitWidth\"\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        },\n        Transition {\n            from: \"visible\"\n            to: \"\"\n\n            Anim {\n                target: root\n                property: \"implicitWidth\"\n                easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized\n            }\n        }\n    ]\n\n    Loader {\n        id: content\n\n        anchors.top: parent.top\n        anchors.bottom: parent.bottom\n        anchors.left: parent.left\n        anchors.margins: Appearance.padding.large\n        anchors.bottomMargin: 0\n\n        active: true\n        Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible)\n\n        sourceComponent: Content {\n            implicitWidth: Config.sidebar.sizes.width - Appearance.padding.large * 2\n            props: root.props\n            visibilities: root.visibilities\n        }\n    }\n}\n"
  },
  {
    "path": "modules/utilities/Background.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Shapes\n\nShapePath {\n    id: root\n\n    required property Wrapper wrapper\n    required property var sidebar\n    readonly property real rounding: Config.border.rounding\n    readonly property bool flatten: wrapper.height < rounding * 2\n    readonly property real roundingY: flatten ? wrapper.height / 2 : rounding\n\n    strokeWidth: -1\n    fillColor: Colours.palette.m3surface\n\n    PathLine {\n        relativeX: -(root.wrapper.width + root.rounding)\n        relativeY: 0\n    }\n    PathArc {\n        relativeX: root.rounding\n        relativeY: -root.roundingY\n        radiusX: root.rounding\n        radiusY: Math.min(root.rounding, root.wrapper.height)\n        direction: PathArc.Counterclockwise\n    }\n    PathLine {\n        relativeX: 0\n        relativeY: -(root.wrapper.height - root.roundingY * 2)\n    }\n    PathArc {\n        relativeX: root.sidebar.utilsRoundingX\n        relativeY: -root.roundingY\n        radiusX: root.sidebar.utilsRoundingX\n        radiusY: Math.min(root.rounding, root.wrapper.height)\n    }\n    PathLine {\n        relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.utilsRoundingX : root.wrapper.width\n        relativeY: 0\n    }\n    PathArc {\n        relativeX: root.rounding\n        relativeY: -root.rounding\n        radiusX: root.rounding\n        radiusY: root.rounding\n        direction: PathArc.Counterclockwise\n    }\n\n    Behavior on fillColor {\n        CAnim {}\n    }\n}\n"
  },
  {
    "path": "modules/utilities/Content.qml",
    "content": "import \"cards\"\nimport qs.components\nimport qs.config\nimport qs.modules.bar.popouts as BarPopouts\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property var props\n    required property DrawerVisibilities visibilities\n    required property BarPopouts.Wrapper popouts\n\n    implicitWidth: layout.implicitWidth\n    implicitHeight: layout.implicitHeight\n\n    ColumnLayout {\n        id: layout\n\n        anchors.fill: parent\n        spacing: Appearance.spacing.normal\n\n        IdleInhibit {}\n\n        Record {\n            props: root.props\n            visibilities: root.visibilities\n            z: 1\n        }\n\n        Toggles {\n            visibilities: root.visibilities\n            popouts: root.popouts\n        }\n    }\n\n    RecordingDeleteModal {\n        props: root.props\n    }\n}\n"
  },
  {
    "path": "modules/utilities/RecordingDeleteModal.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport Caelestia\nimport QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Shapes\n\nLoader {\n    id: root\n\n    required property var props\n\n    asynchronous: true\n    anchors.fill: parent\n\n    opacity: root.props.recordingConfirmDelete ? 1 : 0\n    active: opacity > 0\n\n    sourceComponent: MouseArea {\n        id: deleteConfirmation\n\n        property string path\n\n        Component.onCompleted: path = root.props.recordingConfirmDelete\n\n        hoverEnabled: true\n        onClicked: root.props.recordingConfirmDelete = \"\"\n\n        Item {\n            anchors.fill: parent\n            anchors.margins: -Appearance.padding.large\n            anchors.rightMargin: -Appearance.padding.large - Config.border.thickness\n            anchors.bottomMargin: -Appearance.padding.large - Config.border.thickness\n            opacity: 0.5\n\n            StyledRect {\n                anchors.fill: parent\n                topLeftRadius: Config.border.rounding\n                color: Colours.palette.m3scrim\n            }\n\n            Shape {\n                id: shape\n\n                anchors.fill: parent\n                preferredRendererType: Shape.CurveRenderer\n                asynchronous: true\n\n                ShapePath {\n                    startX: -Config.border.rounding * 2\n                    startY: shape.height - Config.border.thickness\n                    strokeWidth: 0\n                    fillGradient: LinearGradient {\n                        orientation: LinearGradient.Horizontal\n                        x1: -Config.border.rounding * 2\n\n                        GradientStop {\n                            position: 0\n                            color: Qt.alpha(Colours.palette.m3scrim, 0)\n                        }\n                        GradientStop {\n                            position: 1\n                            color: Colours.palette.m3scrim\n                        }\n                    }\n\n                    PathLine {\n                        relativeX: Config.border.rounding\n                        relativeY: 0\n                    }\n                    PathArc {\n                        relativeY: -Config.border.rounding\n                        radiusX: Config.border.rounding\n                        radiusY: Config.border.rounding\n                        direction: PathArc.Counterclockwise\n                    }\n                    PathLine {\n                        relativeX: 0\n                        relativeY: Config.border.rounding + Config.border.thickness\n                    }\n                    PathLine {\n                        relativeX: -Config.border.rounding * 2\n                        relativeY: 0\n                    }\n                }\n\n                ShapePath {\n                    startX: shape.width - Config.border.rounding - Config.border.thickness\n                    strokeWidth: 0\n                    fillGradient: LinearGradient {\n                        orientation: LinearGradient.Vertical\n                        y1: -Config.border.rounding * 2\n\n                        GradientStop {\n                            position: 0\n                            color: Qt.alpha(Colours.palette.m3scrim, 0)\n                        }\n                        GradientStop {\n                            position: 1\n                            color: Colours.palette.m3scrim\n                        }\n                    }\n\n                    PathArc {\n                        relativeX: Config.border.rounding\n                        relativeY: -Config.border.rounding\n                        radiusX: Config.border.rounding\n                        radiusY: Config.border.rounding\n                        direction: PathArc.Counterclockwise\n                    }\n                    PathLine {\n                        relativeX: 0\n                        relativeY: -Config.border.rounding\n                    }\n                    PathLine {\n                        relativeX: Config.border.thickness\n                        relativeY: 0\n                    }\n                    PathLine {\n                        relativeX: 0\n                    }\n                }\n            }\n        }\n\n        StyledRect {\n            anchors.centerIn: parent\n            radius: Appearance.rounding.large\n            color: Colours.palette.m3surfaceContainerHigh\n\n            scale: 0\n            Component.onCompleted: scale = Qt.binding(() => root.props.recordingConfirmDelete ? 1 : 0)\n\n            width: Math.min(parent.width - Appearance.padding.large * 2, implicitWidth)\n            implicitWidth: deleteConfirmationLayout.implicitWidth + Appearance.padding.large * 3\n            implicitHeight: deleteConfirmationLayout.implicitHeight + Appearance.padding.large * 3\n\n            MouseArea {\n                anchors.fill: parent\n            }\n\n            Elevation {\n                anchors.fill: parent\n                radius: parent.radius\n                z: -1\n                level: 3\n            }\n\n            ColumnLayout {\n                id: deleteConfirmationLayout\n\n                anchors.fill: parent\n                anchors.margins: Appearance.padding.large * 1.5\n                spacing: Appearance.spacing.normal\n\n                StyledText {\n                    text: qsTr(\"Delete recording?\")\n                    font.pointSize: Appearance.font.size.large\n                }\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: qsTr(\"Recording '%1' will be permanently deleted.\").arg(deleteConfirmation.path)\n                    color: Colours.palette.m3onSurfaceVariant\n                    font.pointSize: Appearance.font.size.small\n                    wrapMode: Text.WrapAtWordBoundaryOrAnywhere\n                }\n\n                RowLayout {\n                    Layout.topMargin: Appearance.spacing.normal\n                    Layout.alignment: Qt.AlignRight\n                    spacing: Appearance.spacing.normal\n\n                    TextButton {\n                        text: qsTr(\"Cancel\")\n                        type: TextButton.Text\n                        onClicked: root.props.recordingConfirmDelete = \"\"\n                    }\n\n                    TextButton {\n                        text: qsTr(\"Delete\")\n                        type: TextButton.Text\n                        onClicked: {\n                            CUtils.deleteFile(Qt.resolvedUrl(root.props.recordingConfirmDelete));\n                            root.props.recordingConfirmDelete = \"\";\n                        }\n                    }\n                }\n            }\n\n            Behavior on scale {\n                Anim {\n                    duration: Appearance.anim.durations.expressiveDefaultSpatial\n                    easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n                }\n            }\n        }\n    }\n\n    Behavior on opacity {\n        Anim {}\n    }\n}\n"
  },
  {
    "path": "modules/utilities/Wrapper.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.config\nimport Quickshell\nimport QtQuick\n\nItem {\n    id: root\n\n    required property DrawerVisibilities visibilities\n    required property Item sidebar\n    required property Item popouts\n\n    readonly property PersistentProperties props: PersistentProperties {\n        property bool recordingListExpanded: false\n        property string recordingConfirmDelete\n        property string recordingMode\n\n        reloadableId: \"utilities\"\n    }\n    readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled))\n\n    visible: height > 0\n    implicitHeight: 0\n    implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width\n\n    onStateChanged: {\n        if (state === \"visible\" && timer.running) {\n            timer.triggered();\n            timer.stop();\n        }\n    }\n\n    states: State {\n        name: \"visible\"\n        when: root.shouldBeActive\n\n        PropertyChanges {\n            root.implicitHeight: content.implicitHeight + Appearance.padding.large * 2\n        }\n    }\n\n    transitions: [\n        Transition {\n            from: \"\"\n            to: \"visible\"\n\n            Anim {\n                target: root\n                property: \"implicitHeight\"\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        },\n        Transition {\n            from: \"visible\"\n            to: \"\"\n\n            Anim {\n                target: root\n                property: \"implicitHeight\"\n                easing.bezierCurve: Appearance.anim.curves.emphasized\n            }\n        }\n    ]\n\n    Timer {\n        id: timer\n\n        running: true\n        interval: Appearance.anim.durations.extraLarge\n        onTriggered: {\n            content.active = Qt.binding(() => root.shouldBeActive || root.visible);\n            content.visible = true;\n        }\n    }\n\n    Loader {\n        id: content\n\n        asynchronous: true\n        anchors.top: parent.top\n        anchors.left: parent.left\n        anchors.margins: Appearance.padding.large\n\n        visible: false\n        active: true\n\n        sourceComponent: Content {\n            implicitWidth: root.implicitWidth - Appearance.padding.large * 2\n            props: root.props\n            visibilities: root.visibilities\n            popouts: root.popouts\n        }\n    }\n}\n"
  },
  {
    "path": "modules/utilities/cards/IdleInhibit.qml",
    "content": "import qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    Layout.fillWidth: true\n    implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Appearance.padding.large * 2\n\n    radius: Appearance.rounding.normal\n    color: Colours.tPalette.m3surfaceContainer\n    clip: true\n\n    RowLayout {\n        id: layout\n\n        anchors.top: parent.top\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.margins: Appearance.padding.large\n        spacing: Appearance.spacing.normal\n\n        StyledRect {\n            implicitWidth: implicitHeight\n            implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2\n\n            radius: Appearance.rounding.full\n            color: IdleInhibitor.enabled ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer\n\n            MaterialIcon {\n                id: icon\n\n                anchors.centerIn: parent\n                text: \"coffee\"\n                color: IdleInhibitor.enabled ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer\n                font.pointSize: Appearance.font.size.large\n            }\n        }\n\n        ColumnLayout {\n            Layout.fillWidth: true\n            spacing: 0\n\n            StyledText {\n                Layout.fillWidth: true\n                text: qsTr(\"Keep Awake\")\n                font.pointSize: Appearance.font.size.normal\n                elide: Text.ElideRight\n            }\n\n            StyledText {\n                Layout.fillWidth: true\n                text: IdleInhibitor.enabled ? qsTr(\"Preventing sleep mode\") : qsTr(\"Normal power management\")\n                color: Colours.palette.m3onSurfaceVariant\n                font.pointSize: Appearance.font.size.small\n                elide: Text.ElideRight\n            }\n        }\n\n        StyledSwitch {\n            checked: IdleInhibitor.enabled\n            onToggled: IdleInhibitor.enabled = checked\n        }\n    }\n\n    Loader {\n        id: activeChip\n\n        asynchronous: true\n        anchors.bottom: parent.bottom\n        anchors.left: parent.left\n        anchors.topMargin: Appearance.spacing.larger\n        anchors.bottomMargin: IdleInhibitor.enabled ? Appearance.padding.large : -implicitHeight\n        anchors.leftMargin: Appearance.padding.large\n\n        opacity: IdleInhibitor.enabled ? 1 : 0\n        scale: IdleInhibitor.enabled ? 1 : 0.5\n\n        Component.onCompleted: active = Qt.binding(() => opacity > 0)\n\n        sourceComponent: StyledRect {\n            implicitWidth: activeText.implicitWidth + Appearance.padding.normal * 2\n            implicitHeight: activeText.implicitHeight + Appearance.padding.small * 2\n\n            radius: Appearance.rounding.full\n            color: Colours.palette.m3primary\n\n            StyledText {\n                id: activeText\n\n                anchors.centerIn: parent\n                text: qsTr(\"Active since %1\").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? \"hh:mm a\" : \"hh:mm\"))\n                color: Colours.palette.m3onPrimary\n                font.pointSize: Math.round(Appearance.font.size.small * 0.9)\n            }\n        }\n\n        Behavior on anchors.bottomMargin {\n            Anim {\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        }\n\n        Behavior on opacity {\n            Anim {\n                duration: Appearance.anim.durations.small\n            }\n        }\n\n        Behavior on scale {\n            Anim {}\n        }\n    }\n\n    Behavior on implicitHeight {\n        Anim {\n            duration: Appearance.anim.durations.expressiveDefaultSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n        }\n    }\n}\n"
  },
  {
    "path": "modules/utilities/cards/Record.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property var props\n    required property DrawerVisibilities visibilities\n\n    Layout.fillWidth: true\n    implicitHeight: layout.implicitHeight + layout.anchors.margins * 2\n\n    radius: Appearance.rounding.normal\n    color: Colours.tPalette.m3surfaceContainer\n\n    ColumnLayout {\n        id: layout\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.large\n        spacing: Appearance.spacing.normal\n\n        RowLayout {\n            spacing: Appearance.spacing.normal\n            z: 1\n\n            StyledRect {\n                implicitWidth: implicitHeight\n                implicitHeight: {\n                    const h = icon.implicitHeight + Appearance.padding.smaller * 2;\n                    return h - (h % 2);\n                }\n\n                radius: Appearance.rounding.full\n                color: Recorder.running ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer\n\n                MaterialIcon {\n                    id: icon\n\n                    anchors.centerIn: parent\n                    anchors.horizontalCenterOffset: -0.5\n                    anchors.verticalCenterOffset: 1.5\n                    text: \"screen_record\"\n                    color: Recorder.running ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer\n                    font.pointSize: Appearance.font.size.large\n                }\n            }\n\n            ColumnLayout {\n                Layout.fillWidth: true\n                spacing: 0\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: qsTr(\"Screen Recorder\")\n                    font.pointSize: Appearance.font.size.normal\n                    elide: Text.ElideRight\n                }\n\n                StyledText {\n                    Layout.fillWidth: true\n                    text: Recorder.paused ? qsTr(\"Recording paused\") : Recorder.running ? qsTr(\"Recording running\") : qsTr(\"Recording off\")\n                    color: Colours.palette.m3onSurfaceVariant\n                    font.pointSize: Appearance.font.size.small\n                    elide: Text.ElideRight\n                }\n            }\n\n            SplitButton {\n                disabled: Recorder.running\n                active: menuItems.find(m => root.props.recordingMode === m.icon + m.text) ?? menuItems[0]\n                menu.onItemSelected: item => root.props.recordingMode = item.icon + item.text\n\n                menuItems: [\n                    MenuItem {\n                        icon: \"fullscreen\"\n                        text: qsTr(\"Record fullscreen\")\n                        activeText: qsTr(\"Fullscreen\")\n                        onClicked: Recorder.start()\n                    },\n                    MenuItem {\n                        icon: \"screenshot_region\"\n                        text: qsTr(\"Record region\")\n                        activeText: qsTr(\"Region\")\n                        onClicked: Recorder.start([\"-r\"])\n                    },\n                    MenuItem {\n                        icon: \"select_to_speak\"\n                        text: qsTr(\"Record fullscreen with sound\")\n                        activeText: qsTr(\"Fullscreen\")\n                        onClicked: Recorder.start([\"-s\"])\n                    },\n                    MenuItem {\n                        icon: \"volume_up\"\n                        text: qsTr(\"Record region with sound\")\n                        activeText: qsTr(\"Region\")\n                        onClicked: Recorder.start([\"-sr\"])\n                    }\n                ]\n            }\n        }\n\n        Loader {\n            id: listOrControls\n\n            property bool running: Recorder.running\n\n            asynchronous: true\n            Layout.fillWidth: true\n            Layout.preferredHeight: implicitHeight\n            sourceComponent: running ? recordingControls : recordingList\n\n            Behavior on Layout.preferredHeight {\n                id: locHeightAnim\n\n                enabled: false\n\n                Anim {}\n            }\n\n            Behavior on running {\n                SequentialAnimation {\n                    ParallelAnimation {\n                        Anim {\n                            target: listOrControls\n                            property: \"scale\"\n                            to: 0.7\n                            duration: Appearance.anim.durations.small\n                            easing.bezierCurve: Appearance.anim.curves.standardAccel\n                        }\n                        Anim {\n                            target: listOrControls\n                            property: \"opacity\"\n                            to: 0\n                            duration: Appearance.anim.durations.small\n                            easing.bezierCurve: Appearance.anim.curves.standardAccel\n                        }\n                    }\n                    PropertyAction {\n                        target: locHeightAnim\n                        property: \"enabled\"\n                        value: true\n                    }\n                    PropertyAction {}\n                    PropertyAction {\n                        target: locHeightAnim\n                        property: \"enabled\"\n                        value: false\n                    }\n                    ParallelAnimation {\n                        Anim {\n                            target: listOrControls\n                            property: \"scale\"\n                            to: 1\n                            duration: Appearance.anim.durations.small\n                            easing.bezierCurve: Appearance.anim.curves.standardDecel\n                        }\n                        Anim {\n                            target: listOrControls\n                            property: \"opacity\"\n                            to: 1\n                            duration: Appearance.anim.durations.small\n                            easing.bezierCurve: Appearance.anim.curves.standardDecel\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    Component {\n        id: recordingList\n\n        RecordingList {\n            props: root.props\n            visibilities: root.visibilities\n        }\n    }\n\n    Component {\n        id: recordingControls\n\n        RowLayout {\n            spacing: Appearance.spacing.normal\n\n            StyledRect {\n                radius: Appearance.rounding.full\n                color: Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error\n\n                implicitWidth: recText.implicitWidth + Appearance.padding.normal * 2\n                implicitHeight: recText.implicitHeight + Appearance.padding.smaller * 2\n\n                StyledText {\n                    id: recText\n\n                    anchors.centerIn: parent\n                    animate: true\n                    text: Recorder.paused ? \"PAUSED\" : \"REC\"\n                    color: Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError\n                    font.family: Appearance.font.family.mono\n                }\n\n                Behavior on implicitWidth {\n                    Anim {}\n                }\n\n                SequentialAnimation on opacity {\n                    running: !Recorder.paused\n                    alwaysRunToEnd: true\n                    loops: Animation.Infinite\n\n                    Anim {\n                        from: 1\n                        to: 0\n                        duration: Appearance.anim.durations.large\n                        easing.bezierCurve: Appearance.anim.curves.emphasizedAccel\n                    }\n                    Anim {\n                        from: 0\n                        to: 1\n                        duration: Appearance.anim.durations.extraLarge\n                        easing.bezierCurve: Appearance.anim.curves.emphasizedDecel\n                    }\n                }\n            }\n\n            StyledText {\n                text: {\n                    const elapsed = Recorder.elapsed;\n\n                    const hours = Math.floor(elapsed / 3600);\n                    const mins = Math.floor((elapsed % 3600) / 60);\n                    const secs = Math.floor(elapsed % 60).toString().padStart(2, \"0\");\n\n                    let time;\n                    if (hours > 0)\n                        time = `${hours}:${mins.toString().padStart(2, \"0\")}:${secs}`;\n                    else\n                        time = `${mins}:${secs}`;\n\n                    return qsTr(\"Recording for %1\").arg(time);\n                }\n                font.pointSize: Appearance.font.size.normal\n            }\n\n            Item {\n                Layout.fillWidth: true\n            }\n\n            IconButton {\n                label.animate: true\n                icon: Recorder.paused ? \"play_arrow\" : \"pause\"\n                toggle: true\n                checked: Recorder.paused\n                type: IconButton.Tonal\n                font.pointSize: Appearance.font.size.large\n                onClicked: {\n                    Recorder.togglePause();\n                    internalChecked = Recorder.paused;\n                }\n            }\n\n            IconButton {\n                icon: \"stop\"\n                inactiveColour: Colours.palette.m3error\n                inactiveOnColour: Colours.palette.m3onError\n                font.pointSize: Appearance.font.size.large\n                onClicked: Recorder.stop()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/utilities/cards/RecordingList.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.components.containers\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Caelestia.Models\nimport Quickshell\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property var props\n    required property DrawerVisibilities visibilities\n\n    spacing: 0\n\n    WrapperMouseArea {\n        Layout.fillWidth: true\n\n        cursorShape: Qt.PointingHandCursor\n        onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded\n\n        RowLayout {\n            spacing: Appearance.spacing.smaller\n\n            MaterialIcon {\n                Layout.alignment: Qt.AlignVCenter\n                text: \"list\"\n                font.pointSize: Appearance.font.size.large\n            }\n\n            StyledText {\n                Layout.alignment: Qt.AlignVCenter\n                Layout.fillWidth: true\n                text: qsTr(\"Recordings\")\n                font.pointSize: Appearance.font.size.normal\n            }\n\n            IconButton {\n                icon: root.props.recordingListExpanded ? \"unfold_less\" : \"unfold_more\"\n                type: IconButton.Text\n                label.animate: true\n                onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded\n            }\n        }\n    }\n\n    StyledListView {\n        id: list\n\n        model: FileSystemModel {\n            path: Paths.recsdir\n            nameFilters: [\"recording_*.mp4\"]\n            sortReverse: true\n        }\n\n        Layout.fillWidth: true\n        Layout.rightMargin: -Appearance.spacing.small\n        implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3)\n        clip: true\n\n        StyledScrollBar.vertical: StyledScrollBar {\n            flickable: list\n        }\n\n        delegate: RowLayout {\n            id: recording\n\n            required property FileSystemEntry modelData\n            property string baseName\n\n            anchors.left: list.contentItem.left\n            anchors.right: list.contentItem.right\n            anchors.rightMargin: Appearance.spacing.small\n            spacing: Appearance.spacing.small / 2\n\n            Component.onCompleted: baseName = modelData.baseName\n\n            StyledText {\n                Layout.fillWidth: true\n                Layout.rightMargin: Appearance.spacing.small / 2\n                text: {\n                    const time = recording.baseName;\n                    const matches = time.match(/^recording_(\\d{4})(\\d{2})(\\d{2})_(\\d{2})-(\\d{2})-(\\d{2})/);\n                    if (!matches)\n                        return time;\n                    const date = new Date(...matches.slice(1));\n                    date.setMonth(date.getMonth() - 1); // Woe (months start from 0)\n                    return qsTr(\"Recording at %1\").arg(Qt.formatDateTime(date, Qt.locale()));\n                }\n                color: Colours.palette.m3onSurfaceVariant\n                elide: Text.ElideRight\n            }\n\n            IconButton {\n                icon: \"play_arrow\"\n                type: IconButton.Text\n                onClicked: {\n                    root.visibilities.utilities = false;\n                    root.visibilities.sidebar = false;\n                    Quickshell.execDetached([\"app2unit\", \"--\", ...Config.general.apps.playback, recording.modelData.path]);\n                }\n            }\n\n            IconButton {\n                icon: \"folder\"\n                type: IconButton.Text\n                onClicked: {\n                    root.visibilities.utilities = false;\n                    root.visibilities.sidebar = false;\n                    Quickshell.execDetached([\"app2unit\", \"--\", ...Config.general.apps.explorer, recording.modelData.path]);\n                }\n            }\n\n            IconButton {\n                icon: \"delete_forever\"\n                type: IconButton.Text\n                label.color: Colours.palette.m3error\n                stateLayer.color: Colours.palette.m3error\n                onClicked: root.props.recordingConfirmDelete = recording.modelData.path\n            }\n        }\n\n        add: Transition {\n            Anim {\n                property: \"opacity\"\n                from: 0\n                to: 1\n            }\n            Anim {\n                property: \"scale\"\n                from: 0.5\n                to: 1\n            }\n        }\n\n        remove: Transition {\n            Anim {\n                property: \"opacity\"\n                to: 0\n            }\n            Anim {\n                property: \"scale\"\n                to: 0.5\n            }\n        }\n\n        displaced: Transition {\n            Anim {\n                properties: \"opacity,scale\"\n                to: 1\n            }\n            Anim {\n                property: \"y\"\n            }\n        }\n\n        Loader {\n            asynchronous: true\n            anchors.centerIn: parent\n\n            opacity: list.count === 0 ? 1 : 0\n            active: opacity > 0\n\n            sourceComponent: ColumnLayout {\n                spacing: Appearance.spacing.small\n\n                MaterialIcon {\n                    Layout.alignment: Qt.AlignHCenter\n                    text: \"scan_delete\"\n                    color: Colours.palette.m3outline\n                    font.pointSize: Appearance.font.size.extraLarge\n\n                    opacity: root.props.recordingListExpanded ? 1 : 0\n                    scale: root.props.recordingListExpanded ? 1 : 0\n                    Layout.preferredHeight: root.props.recordingListExpanded ? implicitHeight : 0\n\n                    Behavior on opacity {\n                        Anim {}\n                    }\n\n                    Behavior on scale {\n                        Anim {}\n                    }\n\n                    Behavior on Layout.preferredHeight {\n                        Anim {}\n                    }\n                }\n\n                RowLayout {\n                    spacing: Appearance.spacing.smaller\n\n                    MaterialIcon {\n                        Layout.alignment: Qt.AlignHCenter\n                        text: \"scan_delete\"\n                        color: Colours.palette.m3outline\n\n                        opacity: !root.props.recordingListExpanded ? 1 : 0\n                        scale: !root.props.recordingListExpanded ? 1 : 0\n                        Layout.preferredWidth: !root.props.recordingListExpanded ? implicitWidth : 0\n\n                        Behavior on opacity {\n                            Anim {}\n                        }\n\n                        Behavior on scale {\n                            Anim {}\n                        }\n\n                        Behavior on Layout.preferredWidth {\n                            Anim {}\n                        }\n                    }\n\n                    StyledText {\n                        text: qsTr(\"No recordings found\")\n                        color: Colours.palette.m3outline\n                    }\n                }\n            }\n\n            Behavior on opacity {\n                Anim {}\n            }\n        }\n\n        Behavior on implicitHeight {\n            Anim {\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/utilities/cards/Toggles.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.components.controls\nimport qs.services\nimport qs.config\nimport qs.modules.bar.popouts as BarPopouts\nimport Quickshell.Bluetooth\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property DrawerVisibilities visibilities\n    required property BarPopouts.Wrapper popouts\n\n    readonly property var quickToggles: {\n        const seenIds = new Set();\n\n        return Config.utilities.quickToggles.filter(item => {\n            if (!item.enabled)\n                return false;\n\n            if (seenIds.has(item.id)) {\n                return false;\n            }\n\n            if (item.id === \"vpn\") {\n                return Config.utilities.vpn.provider.some(p => typeof p === \"object\" ? (p.enabled === true) : false);\n            }\n\n            seenIds.add(item.id);\n            return true;\n        });\n    }\n    readonly property int splitIndex: Math.ceil(quickToggles.length / 2)\n    readonly property bool needExtraRow: quickToggles.length > 6\n\n    Layout.fillWidth: true\n    implicitHeight: layout.implicitHeight + Appearance.padding.large * 2\n\n    radius: Appearance.rounding.normal\n    color: Colours.tPalette.m3surfaceContainer\n\n    ColumnLayout {\n        id: layout\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.large\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            text: qsTr(\"Quick Toggles\")\n            font.pointSize: Appearance.font.size.normal\n        }\n\n        QuickToggleRow {\n            rowModel: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles\n        }\n\n        QuickToggleRow {\n            visible: root.needExtraRow\n            rowModel: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : []\n        }\n    }\n\n    component QuickToggleRow: RowLayout {\n        property var rowModel: []\n\n        Layout.fillWidth: true\n        spacing: Appearance.spacing.small\n\n        Repeater {\n            model: parent.rowModel\n\n            delegate: DelegateChooser {\n                role: \"id\"\n\n                DelegateChoice {\n                    roleValue: \"wifi\"\n                    delegate: Toggle {\n                        icon: \"wifi\"\n                        checked: Nmcli.wifiEnabled\n                        onClicked: Nmcli.toggleWifi()\n                    }\n                }\n                DelegateChoice {\n                    roleValue: \"bluetooth\"\n                    delegate: Toggle {\n                        icon: \"bluetooth\"\n                        checked: Bluetooth.defaultAdapter?.enabled ?? false\n                        onClicked: {\n                            const adapter = Bluetooth.defaultAdapter;\n                            if (adapter)\n                                adapter.enabled = !adapter.enabled;\n                        }\n                    }\n                }\n                DelegateChoice {\n                    roleValue: \"mic\"\n                    delegate: Toggle {\n                        icon: \"mic\"\n                        checked: !Audio.sourceMuted\n                        onClicked: {\n                            const audio = Audio.source?.audio;\n                            if (audio)\n                                audio.muted = !audio.muted;\n                        }\n                    }\n                }\n                DelegateChoice {\n                    roleValue: \"settings\"\n                    delegate: Toggle {\n                        icon: \"settings\"\n                        inactiveOnColour: Colours.palette.m3onSurfaceVariant\n                        toggle: false\n                        onClicked: {\n                            root.visibilities.utilities = false;\n                            root.popouts.detach(\"network\");\n                        }\n                    }\n                }\n                DelegateChoice {\n                    roleValue: \"gameMode\"\n                    delegate: Toggle {\n                        icon: \"gamepad\"\n                        checked: GameMode.enabled\n                        onClicked: GameMode.enabled = !GameMode.enabled\n                    }\n                }\n                DelegateChoice {\n                    roleValue: \"dnd\"\n                    delegate: Toggle {\n                        icon: \"notifications_off\"\n                        checked: Notifs.dnd\n                        onClicked: Notifs.dnd = !Notifs.dnd\n                    }\n                }\n                DelegateChoice {\n                    roleValue: \"vpn\"\n                    delegate: Toggle {\n                        icon: \"vpn_key\"\n                        checked: VPN.connected\n                        enabled: !VPN.connecting\n                        onClicked: VPN.toggle()\n                    }\n                }\n            }\n        }\n    }\n\n    component Toggle: IconButton {\n        Layout.fillWidth: true\n        Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0)\n        radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal\n        inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2)\n        toggle: true\n        radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial\n        radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n\n        Behavior on Layout.preferredWidth {\n            Anim {\n                duration: Appearance.anim.durations.expressiveFastSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/utilities/toasts/ToastItem.qml",
    "content": "import qs.components\nimport qs.components.effects\nimport qs.services\nimport qs.config\nimport Caelestia\nimport QtQuick\nimport QtQuick.Layouts\n\nStyledRect {\n    id: root\n\n    required property Toast modelData\n\n    anchors.left: parent.left\n    anchors.right: parent.right\n    implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2\n\n    radius: Appearance.rounding.normal\n    color: {\n        if (root.modelData.type === Toast.Success)\n            return Colours.palette.m3successContainer;\n        if (root.modelData.type === Toast.Warning)\n            return Colours.palette.m3secondary;\n        if (root.modelData.type === Toast.Error)\n            return Colours.palette.m3errorContainer;\n        return Colours.palette.m3surface;\n    }\n\n    border.width: 1\n    border.color: {\n        let colour = Colours.palette.m3outlineVariant;\n        if (root.modelData.type === Toast.Success)\n            colour = Colours.palette.m3success;\n        if (root.modelData.type === Toast.Warning)\n            colour = Colours.palette.m3secondaryContainer;\n        if (root.modelData.type === Toast.Error)\n            colour = Colours.palette.m3error;\n        return Qt.alpha(colour, 0.3);\n    }\n\n    Elevation {\n        anchors.fill: parent\n        radius: parent.radius\n        opacity: parent.opacity\n        z: -1\n        level: 3\n    }\n\n    RowLayout {\n        id: layout\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.smaller\n        anchors.leftMargin: Appearance.padding.normal\n        anchors.rightMargin: Appearance.padding.normal\n        spacing: Appearance.spacing.normal\n\n        StyledRect {\n            radius: Appearance.rounding.normal\n            color: {\n                if (root.modelData.type === Toast.Success)\n                    return Colours.palette.m3success;\n                if (root.modelData.type === Toast.Warning)\n                    return Colours.palette.m3secondaryContainer;\n                if (root.modelData.type === Toast.Error)\n                    return Colours.palette.m3error;\n                return Colours.palette.m3surfaceContainerHigh;\n            }\n\n            implicitWidth: implicitHeight\n            implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2\n\n            MaterialIcon {\n                id: icon\n\n                anchors.centerIn: parent\n                text: root.modelData.icon\n                color: {\n                    if (root.modelData.type === Toast.Success)\n                        return Colours.palette.m3onSuccess;\n                    if (root.modelData.type === Toast.Warning)\n                        return Colours.palette.m3onSecondaryContainer;\n                    if (root.modelData.type === Toast.Error)\n                        return Colours.palette.m3onError;\n                    return Colours.palette.m3onSurfaceVariant;\n                }\n                font.pointSize: Math.round(Appearance.font.size.large * 1.2)\n            }\n        }\n\n        ColumnLayout {\n            Layout.fillWidth: true\n            spacing: 0\n\n            StyledText {\n                id: title\n\n                Layout.fillWidth: true\n                text: root.modelData.title\n                color: {\n                    if (root.modelData.type === Toast.Success)\n                        return Colours.palette.m3onSuccessContainer;\n                    if (root.modelData.type === Toast.Warning)\n                        return Colours.palette.m3onSecondary;\n                    if (root.modelData.type === Toast.Error)\n                        return Colours.palette.m3onErrorContainer;\n                    return Colours.palette.m3onSurface;\n                }\n                font.pointSize: Appearance.font.size.normal\n                elide: Text.ElideRight\n            }\n\n            StyledText {\n                Layout.fillWidth: true\n                textFormat: Text.StyledText\n                text: root.modelData.message\n                color: {\n                    if (root.modelData.type === Toast.Success)\n                        return Colours.palette.m3onSuccessContainer;\n                    if (root.modelData.type === Toast.Warning)\n                        return Colours.palette.m3onSecondary;\n                    if (root.modelData.type === Toast.Error)\n                        return Colours.palette.m3onErrorContainer;\n                    return Colours.palette.m3onSurface;\n                }\n                opacity: 0.8\n                elide: Text.ElideRight\n            }\n        }\n    }\n\n    Behavior on border.color {\n        CAnim {}\n    }\n}\n"
  },
  {
    "path": "modules/utilities/toasts/Toasts.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.config\nimport Caelestia\nimport Quickshell\nimport QtQuick\n\nItem {\n    id: root\n\n    readonly property int spacing: Appearance.spacing.small\n    property bool flag\n\n    implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2\n    implicitHeight: {\n        let h = -spacing;\n        for (let i = 0; i < repeater.count; i++) {\n            const item = repeater.itemAt(i) as ToastWrapper;\n            if (!item.modelData.closed && !item.previewHidden)\n                h += item.implicitHeight + spacing;\n        }\n        return h;\n    }\n\n    Repeater {\n        id: repeater\n\n        model: ScriptModel {\n            values: {\n                const toasts = [];\n                let count = 0;\n                for (const toast of Toaster.toasts) {\n                    toasts.push(toast);\n                    if (!toast.closed) {\n                        count++;\n                        if (count > Config.utilities.maxToasts)\n                            break;\n                    }\n                }\n                return toasts;\n            }\n            onValuesChanged: root.flagChanged()\n        }\n\n        ToastWrapper {}\n    }\n\n    component ToastWrapper: MouseArea {\n        id: toast\n\n        required property int index\n        required property Toast modelData\n\n        readonly property bool previewHidden: {\n            let extraHidden = 0;\n            for (let i = 0; i < index; i++)\n                if (Toaster.toasts[i].closed)\n                    extraHidden++;\n            return index >= Config.utilities.maxToasts + extraHidden;\n        }\n\n        onPreviewHiddenChanged: {\n            if (initAnim.running && previewHidden)\n                initAnim.stop();\n        }\n\n        opacity: modelData.closed || previewHidden ? 0 : 1\n        scale: modelData.closed || previewHidden ? 0.7 : 1\n\n        anchors.bottomMargin: {\n            root.flag; // Force update\n            let y = 0;\n            for (let i = 0; i < index; i++) {\n                const item = repeater.itemAt(i) as ToastWrapper;\n                if (item && !item.modelData.closed && !item.previewHidden)\n                    y += item.implicitHeight + root.spacing;\n            }\n            return y;\n        }\n\n        anchors.left: parent.left\n        anchors.right: parent.right\n        anchors.bottom: parent.bottom\n        implicitHeight: toastInner.implicitHeight\n\n        acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton\n        onClicked: modelData.close()\n\n        Component.onCompleted: modelData.lock(this)\n\n        Anim {\n            id: initAnim\n\n            Component.onCompleted: running = !toast.previewHidden\n\n            target: toast\n            properties: \"opacity,scale\"\n            from: 0\n            to: 1\n            duration: Appearance.anim.durations.expressiveDefaultSpatial\n            easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n        }\n\n        ParallelAnimation {\n            running: toast.modelData.closed\n            onStarted: toast.anchors.bottomMargin = toast.anchors.bottomMargin\n            onFinished: toast.modelData.unlock(toast)\n\n            Anim {\n                target: toast\n                property: \"opacity\"\n                to: 0\n            }\n            Anim {\n                target: toast\n                property: \"scale\"\n                to: 0.7\n            }\n        }\n\n        ToastItem {\n            id: toastInner\n\n            modelData: toast.modelData\n        }\n\n        Behavior on opacity {\n            Anim {}\n        }\n\n        Behavior on scale {\n            Anim {}\n        }\n\n        Behavior on anchors.bottomMargin {\n            Anim {\n                duration: Appearance.anim.durations.expressiveDefaultSpatial\n                easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "modules/windowinfo/Buttons.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Quickshell.Widgets\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property var client\n    property bool moveToWsExpanded\n\n    anchors.fill: parent\n    spacing: Appearance.spacing.small\n\n    RowLayout {\n        Layout.topMargin: Appearance.padding.large\n        Layout.leftMargin: Appearance.padding.large\n        Layout.rightMargin: Appearance.padding.large\n\n        spacing: Appearance.spacing.normal\n\n        StyledText {\n            Layout.fillWidth: true\n            text: qsTr(\"Move to workspace\")\n            elide: Text.ElideRight\n        }\n\n        StyledRect {\n            color: Colours.palette.m3primary\n            radius: Appearance.rounding.small\n\n            implicitWidth: moveToWsIcon.implicitWidth + Appearance.padding.small * 2\n            implicitHeight: moveToWsIcon.implicitHeight + Appearance.padding.small\n\n            StateLayer {\n                function onClicked(): void {\n                    root.moveToWsExpanded = !root.moveToWsExpanded;\n                }\n\n                color: Colours.palette.m3onPrimary\n            }\n\n            MaterialIcon {\n                id: moveToWsIcon\n\n                anchors.centerIn: parent\n\n                animate: true\n                text: root.moveToWsExpanded ? \"expand_more\" : \"keyboard_arrow_right\"\n                color: Colours.palette.m3onPrimary\n                font.pointSize: Appearance.font.size.large\n            }\n        }\n    }\n\n    WrapperItem {\n        Layout.fillWidth: true\n        Layout.leftMargin: Appearance.padding.large * 2\n        Layout.rightMargin: Appearance.padding.large * 2\n\n        Layout.preferredHeight: root.moveToWsExpanded ? implicitHeight : 0\n        clip: true\n\n        topMargin: Appearance.spacing.normal\n        bottomMargin: Appearance.spacing.normal\n\n        GridLayout {\n            id: wsGrid\n\n            rowSpacing: Appearance.spacing.smaller\n            columnSpacing: Appearance.spacing.normal\n            columns: 5\n\n            Repeater {\n                model: 10\n\n                Button {\n                    required property int index\n                    readonly property int wsId: Math.floor((Hypr.activeWsId - 1) / 10) * 10 + index + 1\n                    readonly property bool isCurrent: root.client?.workspace.id === wsId\n\n                    function onClicked(): void {\n                        Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`);\n                    }\n\n                    color: isCurrent ? Colours.tPalette.m3surfaceContainerHighest : Colours.palette.m3tertiaryContainer\n                    onColor: isCurrent ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer\n                    text: wsId\n                    disabled: isCurrent\n                }\n            }\n        }\n\n        Behavior on Layout.preferredHeight {\n            Anim {}\n        }\n    }\n\n    RowLayout {\n        Layout.fillWidth: true\n        Layout.leftMargin: Appearance.padding.large\n        Layout.rightMargin: Appearance.padding.large\n        Layout.bottomMargin: Appearance.padding.large\n\n        spacing: root.client?.lastIpcObject.floating ? Appearance.spacing.normal : Appearance.spacing.small\n\n        Button {\n            function onClicked(): void {\n                Hypr.dispatch(`togglefloating address:0x${root.client?.address}`);\n            }\n\n            color: Colours.palette.m3secondaryContainer\n            onColor: Colours.palette.m3onSecondaryContainer\n            text: root.client?.lastIpcObject.floating ? qsTr(\"Tile\") : qsTr(\"Float\")\n        }\n\n        Loader {\n            asynchronous: true\n            active: root.client?.lastIpcObject.floating\n            Layout.fillWidth: active\n            Layout.leftMargin: active ? 0 : -parent.spacing\n            Layout.rightMargin: active ? 0 : -parent.spacing\n\n            sourceComponent: Button {\n                function onClicked(): void {\n                    Hypr.dispatch(`pin address:0x${root.client?.address}`);\n                }\n\n                color: Colours.palette.m3secondaryContainer\n                onColor: Colours.palette.m3onSecondaryContainer\n                text: root.client?.lastIpcObject.pinned ? qsTr(\"Unpin\") : qsTr(\"Pin\")\n            }\n        }\n\n        Button {\n            function onClicked(): void {\n                Hypr.dispatch(`killwindow address:0x${root.client?.address}`);\n            }\n\n            color: Colours.palette.m3errorContainer\n            onColor: Colours.palette.m3onErrorContainer\n            text: qsTr(\"Kill\")\n        }\n    }\n\n    component Button: StyledRect {\n        property color onColor: Colours.palette.m3onSurface\n        property alias disabled: stateLayer.disabled\n        property alias text: label.text\n\n        function onClicked(): void {\n        }\n\n        radius: Appearance.rounding.small\n\n        Layout.fillWidth: true\n        implicitHeight: label.implicitHeight + Appearance.padding.small * 2\n\n        StateLayer {\n            id: stateLayer\n\n            function onClicked(): void {\n                parent.onClicked();\n            }\n\n            color: parent.onColor\n        }\n\n        StyledText {\n            id: label\n\n            anchors.centerIn: parent\n\n            animate: true\n            color: parent.onColor\n            font.pointSize: Appearance.font.size.normal\n        }\n    }\n}\n"
  },
  {
    "path": "modules/windowinfo/Details.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport Quickshell.Hyprland\nimport QtQuick\nimport QtQuick.Layouts\n\nColumnLayout {\n    id: root\n\n    required property HyprlandToplevel client\n\n    anchors.fill: parent\n    spacing: Appearance.spacing.small\n\n    Label {\n        Layout.topMargin: Appearance.padding.large * 2\n\n        text: root.client?.title ?? qsTr(\"No active client\")\n        wrapMode: Text.WrapAtWordBoundaryOrAnywhere\n\n        font.pointSize: Appearance.font.size.large\n        font.weight: 500\n    }\n\n    Label {\n        text: root.client?.lastIpcObject.class ?? qsTr(\"No active client\")\n        color: Colours.palette.m3tertiary\n\n        font.pointSize: Appearance.font.size.larger\n    }\n\n    StyledRect {\n        Layout.fillWidth: true\n        Layout.preferredHeight: 1\n        Layout.leftMargin: Appearance.padding.large * 2\n        Layout.rightMargin: Appearance.padding.large * 2\n        Layout.topMargin: Appearance.spacing.normal\n        Layout.bottomMargin: Appearance.spacing.large\n\n        color: Colours.palette.m3secondary\n    }\n\n    Detail {\n        icon: \"location_on\"\n        text: qsTr(\"Address: %1\").arg(`0x${root.client?.address}` ?? \"unknown\")\n        color: Colours.palette.m3primary\n    }\n\n    Detail {\n        icon: \"location_searching\"\n        text: qsTr(\"Position: %1, %2\").arg(root.client?.lastIpcObject.at[0] ?? -1).arg(root.client?.lastIpcObject.at[1] ?? -1)\n    }\n\n    Detail {\n        icon: \"resize\"\n        text: qsTr(\"Size: %1 x %2\").arg(root.client?.lastIpcObject.size[0] ?? -1).arg(root.client?.lastIpcObject.size[1] ?? -1)\n        color: Colours.palette.m3tertiary\n    }\n\n    Detail {\n        icon: \"workspaces\"\n        text: qsTr(\"Workspace: %1 (%2)\").arg(root.client?.workspace.name ?? -1).arg(root.client?.workspace.id ?? -1)\n        color: Colours.palette.m3secondary\n    }\n\n    Detail {\n        icon: \"desktop_windows\"\n        text: {\n            const mon = root.client?.monitor;\n            if (mon)\n                return qsTr(\"Monitor: %1 (%2) at %3, %4\").arg(mon.name).arg(mon.id).arg(mon.x).arg(mon.y);\n            return qsTr(\"Monitor: unknown\");\n        }\n    }\n\n    Detail {\n        icon: \"page_header\"\n        text: qsTr(\"Initial title: %1\").arg(root.client?.lastIpcObject.initialTitle ?? \"unknown\")\n        color: Colours.palette.m3tertiary\n    }\n\n    Detail {\n        icon: \"category\"\n        text: qsTr(\"Initial class: %1\").arg(root.client?.lastIpcObject.initialClass ?? \"unknown\")\n    }\n\n    Detail {\n        icon: \"account_tree\"\n        text: qsTr(\"Process id: %1\").arg(root.client?.lastIpcObject.pid ?? -1)\n        color: Colours.palette.m3primary\n    }\n\n    Detail {\n        icon: \"picture_in_picture_center\"\n        text: qsTr(\"Floating: %1\").arg(root.client?.lastIpcObject.floating ? \"yes\" : \"no\")\n        color: Colours.palette.m3secondary\n    }\n\n    Detail {\n        icon: \"gradient\"\n        text: qsTr(\"Xwayland: %1\").arg(root.client?.lastIpcObject.xwayland ? \"yes\" : \"no\")\n    }\n\n    Detail {\n        icon: \"keep\"\n        text: qsTr(\"Pinned: %1\").arg(root.client?.lastIpcObject.pinned ? \"yes\" : \"no\")\n        color: Colours.palette.m3secondary\n    }\n\n    Detail {\n        icon: \"fullscreen\"\n        text: {\n            const fs = root.client?.lastIpcObject.fullscreen;\n            if (fs)\n                return qsTr(\"Fullscreen state: %1\").arg(fs == 0 ? \"off\" : fs == 1 ? \"maximised\" : \"on\");\n            return qsTr(\"Fullscreen state: unknown\");\n        }\n        color: Colours.palette.m3tertiary\n    }\n\n    Item {\n        Layout.fillHeight: true\n    }\n\n    component Detail: RowLayout {\n        id: detail\n\n        required property string icon\n        required property string text\n        property alias color: icon.color\n\n        Layout.leftMargin: Appearance.padding.large\n        Layout.rightMargin: Appearance.padding.large\n        Layout.fillWidth: true\n\n        spacing: Appearance.spacing.smaller\n\n        MaterialIcon {\n            id: icon\n\n            Layout.alignment: Qt.AlignVCenter\n            text: detail.icon\n        }\n\n        StyledText {\n            Layout.fillWidth: true\n            Layout.alignment: Qt.AlignVCenter\n\n            text: detail.text\n            elide: Text.ElideRight\n            font.pointSize: Appearance.font.size.normal\n        }\n    }\n\n    component Label: StyledText {\n        Layout.leftMargin: Appearance.padding.large\n        Layout.rightMargin: Appearance.padding.large\n        Layout.fillWidth: true\n        elide: Text.ElideRight\n        horizontalAlignment: Text.AlignHCenter\n        animate: true\n    }\n}\n"
  },
  {
    "path": "modules/windowinfo/Preview.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.components\nimport qs.services\nimport qs.config\nimport Quickshell\nimport Quickshell.Wayland\nimport Quickshell.Hyprland\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property ShellScreen screen\n    required property HyprlandToplevel client\n\n    Layout.preferredWidth: preview.implicitWidth + Appearance.padding.large * 2\n    Layout.fillHeight: true\n\n    StyledClippingRect {\n        id: preview\n\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.top: parent.top\n        anchors.bottom: label.top\n        anchors.topMargin: Appearance.padding.large\n        anchors.bottomMargin: Appearance.spacing.normal\n\n        implicitWidth: view.implicitWidth\n\n        color: Colours.tPalette.m3surfaceContainer\n        radius: Appearance.rounding.small\n\n        Loader {\n            asynchronous: true\n            anchors.centerIn: parent\n            active: !root.client\n\n            sourceComponent: ColumnLayout {\n                spacing: 0\n\n                MaterialIcon {\n                    Layout.alignment: Qt.AlignHCenter\n                    text: \"web_asset_off\"\n                    color: Colours.palette.m3outline\n                    font.pointSize: Appearance.font.size.extraLarge * 3\n                }\n\n                StyledText {\n                    Layout.alignment: Qt.AlignHCenter\n                    text: qsTr(\"No active client\")\n                    color: Colours.palette.m3outline\n                    font.pointSize: Appearance.font.size.extraLarge\n                    font.weight: 500\n                }\n\n                StyledText {\n                    Layout.alignment: Qt.AlignHCenter\n                    text: qsTr(\"Try switching to a window\")\n                    color: Colours.palette.m3outline\n                    font.pointSize: Appearance.font.size.large\n                }\n            }\n        }\n\n        ScreencopyView {\n            id: view\n\n            anchors.centerIn: parent\n\n            captureSource: root.client?.wayland ?? null\n            live: true\n\n            constraintSize.width: root.client ? parent.height * Math.min(root.screen.width / root.screen.height, root.client?.lastIpcObject.size[0] / root.client?.lastIpcObject.size[1]) : parent.height\n            constraintSize.height: parent.height\n        }\n    }\n\n    StyledText {\n        id: label\n\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.bottom: parent.bottom\n        anchors.bottomMargin: Appearance.padding.large\n\n        animate: true\n        text: {\n            const client = root.client;\n            if (!client)\n                return qsTr(\"No active client\");\n\n            const mon = client.monitor;\n            return qsTr(\"%1 on monitor %2 at %3, %4\").arg(client.title).arg(mon.name).arg(client.lastIpcObject.at[0]).arg(client.lastIpcObject.at[1]);\n        }\n    }\n}\n"
  },
  {
    "path": "modules/windowinfo/WindowInfo.qml",
    "content": "import qs.components\nimport qs.services\nimport qs.config\nimport Quickshell\nimport Quickshell.Hyprland\nimport QtQuick\nimport QtQuick.Layouts\n\nItem {\n    id: root\n\n    required property ShellScreen screen\n    required property HyprlandToplevel client\n\n    implicitWidth: child.implicitWidth\n    implicitHeight: screen.height * Config.winfo.sizes.heightMult\n\n    RowLayout {\n        id: child\n\n        anchors.fill: parent\n        anchors.margins: Appearance.padding.large\n\n        spacing: Appearance.spacing.normal\n\n        Preview {\n            screen: root.screen\n            client: root.client\n        }\n\n        ColumnLayout {\n            spacing: Appearance.spacing.normal\n\n            Layout.preferredWidth: Config.winfo.sizes.detailsWidth\n            Layout.fillHeight: true\n\n            StyledRect {\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n\n                color: Colours.tPalette.m3surfaceContainer\n                radius: Appearance.rounding.normal\n\n                Details {\n                    client: root.client\n                }\n            }\n\n            StyledRect {\n                Layout.fillWidth: true\n                Layout.preferredHeight: buttons.implicitHeight\n\n                color: Colours.tPalette.m3surfaceContainer\n                radius: Appearance.rounding.normal\n\n                Buttons {\n                    id: buttons\n\n                    client: root.client\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "nix/default.nix",
    "content": "{\n  rev,\n  lib,\n  stdenv,\n  makeWrapper,\n  makeFontsConf,\n  fish,\n  ddcutil,\n  brightnessctl,\n  app2unit,\n  networkmanager,\n  lm_sensors,\n  swappy,\n  wl-clipboard,\n  libqalculate,\n  bash,\n  hyprland,\n  material-symbols,\n  rubik,\n  nerd-fonts,\n  qt6,\n  quickshell,\n  aubio,\n  libcava,\n  fftw,\n  pipewire,\n  xkeyboard-config,\n  cmake,\n  ninja,\n  pkg-config,\n  caelestia-cli,\n  debug ? false,\n  withCli ? false,\n  extraRuntimeDeps ? [],\n}: let\n  version = \"1.0.0\";\n\n  runtimeDeps =\n    [\n      fish\n      ddcutil\n      brightnessctl\n      app2unit\n      networkmanager\n      lm_sensors\n      swappy\n      wl-clipboard\n      libqalculate\n      bash\n      hyprland\n    ]\n    ++ extraRuntimeDeps\n    ++ lib.optional withCli caelestia-cli;\n\n  fontconfig = makeFontsConf {\n    fontDirectories = [material-symbols rubik nerd-fonts.caskaydia-cove];\n  };\n\n  cmakeBuildType =\n    if debug\n    then \"Debug\"\n    else \"RelWithDebInfo\";\n\n  cmakeVersionFlags = [\n    (lib.cmakeFeature \"VERSION\" version)\n    (lib.cmakeFeature \"GIT_REVISION\" rev)\n    (lib.cmakeFeature \"DISTRIBUTOR\" \"nix-flake\")\n  ];\n\n  extras = stdenv.mkDerivation {\n    inherit cmakeBuildType;\n    name = \"caelestia-extras${lib.optionalString debug \"-debug\"}\";\n    src = lib.fileset.toSource {\n      root = ./..;\n      fileset = lib.fileset.union ./../CMakeLists.txt ./../extras;\n    };\n\n    nativeBuildInputs = [cmake ninja];\n\n    cmakeFlags =\n      [\n        (lib.cmakeFeature \"ENABLE_MODULES\" \"extras\")\n        (lib.cmakeFeature \"INSTALL_LIBDIR\" \"${placeholder \"out\"}/lib\")\n      ]\n      ++ cmakeVersionFlags;\n  };\n\n  plugin = stdenv.mkDerivation {\n    inherit cmakeBuildType;\n    name = \"caelestia-qml-plugin${lib.optionalString debug \"-debug\"}\";\n    src = lib.fileset.toSource {\n      root = ./..;\n      fileset = lib.fileset.union ./../CMakeLists.txt ./../plugin;\n    };\n\n    nativeBuildInputs = [cmake ninja pkg-config];\n    buildInputs = [qt6.qtbase qt6.qtdeclarative libqalculate pipewire aubio libcava fftw];\n\n    dontWrapQtApps = true;\n    cmakeFlags =\n      [\n        (lib.cmakeFeature \"ENABLE_MODULES\" \"plugin\")\n        (lib.cmakeFeature \"INSTALL_QMLDIR\" qt6.qtbase.qtQmlPrefix)\n      ]\n      ++ cmakeVersionFlags;\n  };\nin\n  stdenv.mkDerivation {\n    inherit version cmakeBuildType;\n    pname = \"caelestia-shell${lib.optionalString debug \"-debug\"}\";\n    src = ./..;\n\n    nativeBuildInputs = [cmake ninja makeWrapper qt6.wrapQtAppsHook];\n    buildInputs = [quickshell extras plugin xkeyboard-config qt6.qtbase];\n    propagatedBuildInputs = runtimeDeps;\n\n    cmakeFlags =\n      [\n        (lib.cmakeFeature \"ENABLE_MODULES\" \"shell\")\n        (lib.cmakeFeature \"INSTALL_QSCONFDIR\" \"${placeholder \"out\"}/share/caelestia-shell\")\n      ]\n      ++ cmakeVersionFlags;\n\n    dontStrip = debug;\n\n    prePatch = ''\n      substituteInPlace assets/pam.d/fprint \\\n        --replace-fail pam_fprintd.so /run/current-system/sw/lib/security/pam_fprintd.so\n      substituteInPlace shell.qml \\\n        --replace-fail 'ShellRoot {' 'ShellRoot {  settings.watchFiles: false'\n    '';\n\n    postInstall = ''\n      makeWrapper ${quickshell}/bin/qs $out/bin/caelestia-shell \\\n      \t--prefix PATH : \"${lib.makeBinPath runtimeDeps}\" \\\n      \t--set FONTCONFIG_FILE \"${fontconfig}\" \\\n      \t--set CAELESTIA_LIB_DIR ${extras}/lib \\\n        --set CAELESTIA_XKB_RULES_PATH ${xkeyboard-config}/share/xkeyboard-config-2/rules/base.lst \\\n      \t--add-flags \"-p $out/share/caelestia-shell\"\n\n      mkdir -p $out/lib\n      ln -s ${extras}/lib/* $out/lib/\n\n      # Ensure wrap_term_launch.sh is executable\n      chmod 755 $out/share/caelestia-shell/assets/wrap_term_launch.sh\n    '';\n\n    passthru = {\n      inherit plugin extras;\n    };\n\n    meta = {\n      description = \"A very segsy desktop shell\";\n      homepage = \"https://github.com/caelestia-dots/shell\";\n      license = lib.licenses.gpl3Only;\n      mainProgram = \"caelestia-shell\";\n    };\n  }\n"
  },
  {
    "path": "nix/hm-module.nix",
    "content": "self: {\n  config,\n  pkgs,\n  lib,\n  ...\n}: let\n  inherit (pkgs.stdenv.hostPlatform) system;\n\n  cli-default = self.inputs.caelestia-cli.packages.${system}.default;\n  shell-default = self.packages.${system}.with-cli;\n\n  cfg = config.programs.caelestia;\nin {\n  imports = [\n    (lib.mkRenamedOptionModule [\"programs\" \"caelestia\" \"environment\"] [\"programs\" \"caelestia\" \"systemd\" \"environment\"])\n  ];\n  options = with lib; {\n    programs.caelestia = {\n      enable = mkEnableOption \"Enable Caelestia shell\";\n      package = mkOption {\n        type = types.package;\n        default = shell-default;\n        description = \"The package of Caelestia shell\";\n      };\n      systemd = {\n        enable = mkOption {\n          type = types.bool;\n          default = true;\n          description = \"Enable the systemd service for Caelestia shell\";\n        };\n        target = mkOption {\n          type = types.str;\n          description = ''\n            The systemd target that will automatically start the Caelestia shell.\n          '';\n          default = config.wayland.systemd.target;\n        };\n        environment = mkOption {\n          type = types.listOf types.str;\n          description = \"Extra Environment variables to pass to the Caelestia shell systemd service.\";\n          default = [];\n          example = [\n            \"QT_QPA_PLATFORMTHEME=gtk3\"\n          ];\n        };\n      };\n      settings = mkOption {\n        type = types.attrsOf types.anything;\n        default = {};\n        description = \"Caelestia shell settings\";\n      };\n      extraConfig = mkOption {\n        type = types.str;\n        default = \"\";\n        description = \"Caelestia shell extra configs written to shell.json\";\n      };\n      cli = {\n        enable = mkEnableOption \"Enable Caelestia CLI\";\n        package = mkOption {\n          type = types.package;\n          default = cli-default;\n          description = \"The package of Caelestia CLI\"; # Doesn't override the shell's CLI, only change from home.packages\n        };\n        settings = mkOption {\n          type = types.attrsOf types.anything;\n          default = {};\n          description = \"Caelestia CLI settings\";\n        };\n        extraConfig = mkOption {\n          type = types.str;\n          default = \"\";\n          description = \"Caelestia CLI extra configs written to cli.json\";\n        };\n      };\n    };\n  };\n\n  config = let\n    cli = cfg.cli.package;\n    shell = cfg.package;\n  in\n    lib.mkIf cfg.enable {\n      systemd.user.services.caelestia = lib.mkIf cfg.systemd.enable {\n        Unit = {\n          Description = \"Caelestia Shell Service\";\n          After = [cfg.systemd.target];\n          PartOf = [cfg.systemd.target];\n          X-Restart-Triggers = lib.mkIf (cfg.settings != {}) [\n            \"${config.xdg.configFile.\"caelestia/shell.json\".source}\"\n          ];\n        };\n\n        Service = {\n          Type = \"exec\";\n          ExecStart = \"${shell}/bin/caelestia-shell\";\n          Restart = \"on-failure\";\n          RestartSec = \"5s\";\n          TimeoutStopSec = \"5s\";\n          Environment =\n            [\n              \"QT_QPA_PLATFORM=wayland\"\n            ]\n            ++ cfg.systemd.environment;\n\n          Slice = \"session.slice\";\n        };\n\n        Install = {\n          WantedBy = [cfg.systemd.target];\n        };\n      };\n\n      xdg.configFile = let\n        mkConfig = c:\n          lib.pipe (\n            if c.extraConfig != \"\"\n            then c.extraConfig\n            else \"{}\"\n          ) [\n            builtins.fromJSON\n            (lib.recursiveUpdate c.settings)\n            builtins.toJSON\n          ];\n        shouldGenerate = c: c.extraConfig != \"\" || c.settings != {};\n      in {\n        \"caelestia/shell.json\" = lib.mkIf (shouldGenerate cfg) {\n          text = mkConfig cfg;\n        };\n        \"caelestia/cli.json\" = lib.mkIf (shouldGenerate cfg.cli) {\n          text = mkConfig cfg.cli;\n        };\n      };\n\n      home.packages = [shell] ++ lib.optional cfg.cli.enable cli;\n    };\n}\n"
  },
  {
    "path": "plugin/CMakeLists.txt",
    "content": "add_subdirectory(src/Caelestia)\n"
  },
  {
    "path": "plugin/src/Caelestia/CMakeLists.txt",
    "content": "find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus)\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)\npkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)\npkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)\npkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)\nif(NOT Cava_FOUND)\n    pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)\nendif()\n\nset(QT_QML_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/qml\")\nqt_standard_project_setup(REQUIRES 6.9)\n\nfunction(qml_module arg_TARGET)\n    cmake_parse_arguments(PARSE_ARGV 1 arg \"\" \"URI\" \"SOURCES;LIBRARIES\")\n\n    qt_add_qml_module(${arg_TARGET}\n        URI ${arg_URI}\n        VERSION ${VERSION}\n        SOURCES ${arg_SOURCES}\n    )\n\n    qt_query_qml_module(${arg_TARGET}\n        URI module_uri\n        VERSION module_version\n        PLUGIN_TARGET module_plugin_target\n        TARGET_PATH module_target_path\n        QMLDIR module_qmldir\n        TYPEINFO module_typeinfo\n    )\n\n    message(STATUS \"Created QML module ${module_uri}, version ${module_version}\")\n\n    set(module_dir \"${INSTALL_QMLDIR}/${module_target_path}\")\n    install(TARGETS ${arg_TARGET} LIBRARY DESTINATION \"${module_dir}\" RUNTIME DESTINATION \"${module_dir}\")\n    install(TARGETS \"${module_plugin_target}\" LIBRARY DESTINATION \"${module_dir}\" RUNTIME DESTINATION \"${module_dir}\")\n    install(FILES \"${module_qmldir}\" DESTINATION \"${module_dir}\")\n    install(FILES \"${module_typeinfo}\" DESTINATION \"${module_dir}\")\n\n    target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES})\nendfunction()\n\nqml_module(caelestia\n    URI Caelestia\n    SOURCES\n        cutils.hpp cutils.cpp\n        qalculator.hpp qalculator.cpp\n        appdb.hpp appdb.cpp\n        requests.hpp requests.cpp\n        toaster.hpp toaster.cpp\n        imageanalyser.hpp imageanalyser.cpp\n    LIBRARIES\n        Qt::Gui\n        Qt::Quick\n        Qt::Concurrent\n        Qt::Sql\n        PkgConfig::Qalculate\n)\n\nadd_subdirectory(Internal)\nadd_subdirectory(Models)\nadd_subdirectory(Services)\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/CMakeLists.txt",
    "content": "qml_module(caelestia-internal\n    URI Caelestia.Internal\n    SOURCES\n        arcgauge.hpp arcgauge.cpp\n        cachingimagemanager.hpp cachingimagemanager.cpp\n        circularbuffer.hpp circularbuffer.cpp\n        circularindicatormanager.hpp circularindicatormanager.cpp\n        hyprdevices.hpp hyprdevices.cpp\n        hyprextras.hpp hyprextras.cpp\n        logindmanager.hpp logindmanager.cpp\n        sparklineitem.hpp sparklineitem.cpp\n    LIBRARIES\n        Qt::Gui\n        Qt::Quick\n        Qt::Concurrent\n        Qt::Network\n        Qt::DBus\n)\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/arcgauge.cpp",
    "content": "#include \"arcgauge.hpp\"\n\n#include <QtMath>\n#include <qpainter.h>\n#include <qpen.h>\n\nnamespace caelestia::internal {\n\nArcGauge::ArcGauge(QQuickItem* parent)\n    : QQuickPaintedItem(parent) {\n    setAntialiasing(true);\n}\n\nvoid ArcGauge::paint(QPainter* painter) {\n    const qreal w = width();\n    const qreal h = height();\n    const qreal side = qMin(w, h);\n    const qreal radius = (side - m_lineWidth - 2.0) / 2.0;\n    const qreal cx = w / 2.0;\n    const qreal cy = h / 2.0;\n\n    const QRectF arcRect(cx - radius, cy - radius, radius * 2.0, radius * 2.0);\n\n    // Convert from Canvas convention (CW radians from 3 o'clock) to QPainter (CCW 1/16th degrees)\n    const int startAngle16 = qRound(-(m_startAngle * 180.0 / M_PI) * 16.0);\n    const int sweepAngle16 = qRound(-(m_sweepAngle * 180.0 / M_PI) * 16.0);\n\n    painter->setRenderHint(QPainter::Antialiasing, true);\n\n    // Draw track arc\n    QPen trackPen(m_trackColor, m_lineWidth);\n    trackPen.setCapStyle(Qt::RoundCap);\n    painter->setPen(trackPen);\n    painter->setBrush(Qt::NoBrush);\n    painter->drawArc(arcRect, startAngle16, sweepAngle16);\n\n    // Draw value arc\n    if (m_percentage > 0.0) {\n        const int valueSweep16 = qRound(static_cast<qreal>(sweepAngle16) * m_percentage);\n        QPen valuePen(m_accentColor, m_lineWidth);\n        valuePen.setCapStyle(Qt::RoundCap);\n        painter->setPen(valuePen);\n        painter->drawArc(arcRect, startAngle16, valueSweep16);\n    }\n}\n\nqreal ArcGauge::percentage() const {\n    return m_percentage;\n}\n\nvoid ArcGauge::setPercentage(qreal percentage) {\n    if (qFuzzyCompare(m_percentage, percentage))\n        return;\n    m_percentage = percentage;\n    emit percentageChanged();\n    update();\n}\n\nQColor ArcGauge::accentColor() const {\n    return m_accentColor;\n}\n\nvoid ArcGauge::setAccentColor(const QColor& color) {\n    if (m_accentColor == color)\n        return;\n    m_accentColor = color;\n    emit accentColorChanged();\n    update();\n}\n\nQColor ArcGauge::trackColor() const {\n    return m_trackColor;\n}\n\nvoid ArcGauge::setTrackColor(const QColor& color) {\n    if (m_trackColor == color)\n        return;\n    m_trackColor = color;\n    emit trackColorChanged();\n    update();\n}\n\nqreal ArcGauge::startAngle() const {\n    return m_startAngle;\n}\n\nvoid ArcGauge::setStartAngle(qreal angle) {\n    if (qFuzzyCompare(m_startAngle, angle))\n        return;\n    m_startAngle = angle;\n    emit startAngleChanged();\n    update();\n}\n\nqreal ArcGauge::sweepAngle() const {\n    return m_sweepAngle;\n}\n\nvoid ArcGauge::setSweepAngle(qreal angle) {\n    if (qFuzzyCompare(m_sweepAngle, angle))\n        return;\n    m_sweepAngle = angle;\n    emit sweepAngleChanged();\n    update();\n}\n\nqreal ArcGauge::lineWidth() const {\n    return m_lineWidth;\n}\n\nvoid ArcGauge::setLineWidth(qreal width) {\n    if (qFuzzyCompare(m_lineWidth, width))\n        return;\n    m_lineWidth = width;\n    emit lineWidthChanged();\n    update();\n}\n\n} // namespace caelestia::internal\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/arcgauge.hpp",
    "content": "#pragma once\n\n#include <qcolor.h>\n#include <qobject.h>\n#include <qqmlintegration.h>\n#include <qquickpainteditem.h>\n\nnamespace caelestia::internal {\n\nclass ArcGauge : public QQuickPaintedItem {\n    Q_OBJECT\n    QML_ELEMENT\n\n    Q_PROPERTY(qreal percentage READ percentage WRITE setPercentage NOTIFY percentageChanged)\n    Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged)\n    Q_PROPERTY(QColor trackColor READ trackColor WRITE setTrackColor NOTIFY trackColorChanged)\n    Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged)\n    Q_PROPERTY(qreal sweepAngle READ sweepAngle WRITE setSweepAngle NOTIFY sweepAngleChanged)\n    Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged)\n\npublic:\n    explicit ArcGauge(QQuickItem* parent = nullptr);\n\n    void paint(QPainter* painter) override;\n\n    [[nodiscard]] qreal percentage() const;\n    void setPercentage(qreal percentage);\n\n    [[nodiscard]] QColor accentColor() const;\n    void setAccentColor(const QColor& color);\n\n    [[nodiscard]] QColor trackColor() const;\n    void setTrackColor(const QColor& color);\n\n    [[nodiscard]] qreal startAngle() const;\n    void setStartAngle(qreal angle);\n\n    [[nodiscard]] qreal sweepAngle() const;\n    void setSweepAngle(qreal angle);\n\n    [[nodiscard]] qreal lineWidth() const;\n    void setLineWidth(qreal width);\n\nsignals:\n    void percentageChanged();\n    void accentColorChanged();\n    void trackColorChanged();\n    void startAngleChanged();\n    void sweepAngleChanged();\n    void lineWidthChanged();\n\nprivate:\n    qreal m_percentage = 0.0;\n    QColor m_accentColor;\n    QColor m_trackColor;\n    qreal m_startAngle = 0.75 * M_PI;\n    qreal m_sweepAngle = 1.5 * M_PI;\n    qreal m_lineWidth = 10.0;\n};\n\n} // namespace caelestia::internal\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/cachingimagemanager.cpp",
    "content": "#include \"cachingimagemanager.hpp\"\n\n#include <QtQuick/qquickwindow.h>\n#include <qcryptographichash.h>\n#include <qdir.h>\n#include <qfileinfo.h>\n#include <qfuturewatcher.h>\n#include <qimagereader.h>\n#include <qpainter.h>\n#include <qtconcurrentrun.h>\n\nnamespace caelestia::internal {\n\nqreal CachingImageManager::effectiveScale() const {\n    if (m_item && m_item->window()) {\n        return m_item->window()->devicePixelRatio();\n    }\n\n    return 1.0;\n}\n\nQSize CachingImageManager::effectiveSize() const {\n    if (!m_item) {\n        return QSize();\n    }\n\n    const qreal scale = effectiveScale();\n    const QSize size = QSizeF(m_item->width() * scale, m_item->height() * scale).toSize();\n    m_item->setProperty(\"sourceSize\", size);\n    return size;\n}\n\nQQuickItem* CachingImageManager::item() const {\n    return m_item;\n}\n\nvoid CachingImageManager::setItem(QQuickItem* item) {\n    if (m_item == item) {\n        return;\n    }\n\n    if (m_widthConn) {\n        disconnect(m_widthConn);\n    }\n    if (m_heightConn) {\n        disconnect(m_heightConn);\n    }\n\n    m_item = item;\n    emit itemChanged();\n\n    if (item) {\n        m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() {\n            updateSource();\n        });\n        m_heightConn = connect(item, &QQuickItem::heightChanged, this, [this]() {\n            updateSource();\n        });\n        updateSource();\n    }\n}\n\nQUrl CachingImageManager::cacheDir() const {\n    return m_cacheDir;\n}\n\nvoid CachingImageManager::setCacheDir(const QUrl& cacheDir) {\n    if (m_cacheDir == cacheDir) {\n        return;\n    }\n\n    m_cacheDir = cacheDir;\n    if (!m_cacheDir.path().endsWith(\"/\")) {\n        m_cacheDir.setPath(m_cacheDir.path() + \"/\");\n    }\n    emit cacheDirChanged();\n}\n\nQString CachingImageManager::path() const {\n    return m_path;\n}\n\nvoid CachingImageManager::setPath(const QString& path) {\n    if (m_path == path) {\n        return;\n    }\n\n    m_path = path;\n    emit pathChanged();\n\n    if (!path.isEmpty()) {\n        updateSource(path);\n    }\n}\n\nvoid CachingImageManager::updateSource() {\n    updateSource(m_path);\n}\n\nvoid CachingImageManager::updateSource(const QString& path) {\n    if (path.isEmpty() || path == m_shaPath) {\n        // Path is empty or already calculating sha for path\n        return;\n    }\n\n    m_shaPath = path;\n\n    const auto future = QtConcurrent::run(&CachingImageManager::sha256sum, path);\n\n    const auto watcher = new QFutureWatcher<QString>(this);\n\n    connect(watcher, &QFutureWatcher<QString>::finished, this, [watcher, path, this]() {\n        if (m_path != path) {\n            // Object is destroyed or path has changed, ignore\n            watcher->deleteLater();\n            return;\n        }\n\n        const QSize size = effectiveSize();\n\n        if (!m_item || !size.width() || !size.height()) {\n            watcher->deleteLater();\n            return;\n        }\n\n        const QString fillMode = m_item->property(\"fillMode\").toString();\n        // clang-format off\n        const QString filename = QString(\"%1@%2x%3-%4.png\")\n            .arg(watcher->result()).arg(size.width()).arg(size.height())\n            .arg(fillMode == \"PreserveAspectCrop\" ? \"crop\" : fillMode == \"PreserveAspectFit\" ? \"fit\" : \"stretch\");\n        // clang-format on\n\n        const QUrl cache = m_cacheDir.resolved(QUrl(filename));\n        if (m_cachePath == cache) {\n            watcher->deleteLater();\n            return;\n        }\n\n        m_cachePath = cache;\n        emit cachePathChanged();\n\n        if (!cache.isLocalFile()) {\n            qWarning() << \"CachingImageManager::updateSource: cachePath\" << cache << \"is not a local file\";\n            watcher->deleteLater();\n            return;\n        }\n\n        const QImageReader reader(cache.toLocalFile());\n        if (reader.canRead()) {\n            m_item->setProperty(\"source\", cache);\n        } else {\n            m_item->setProperty(\"source\", QUrl::fromLocalFile(path));\n            createCache(path, cache.toLocalFile(), fillMode, size);\n        }\n\n        // Clear current running sha if same\n        if (m_shaPath == path) {\n            m_shaPath = QString();\n        }\n\n        watcher->deleteLater();\n    });\n\n    watcher->setFuture(future);\n}\n\nQUrl CachingImageManager::cachePath() const {\n    return m_cachePath;\n}\n\nvoid CachingImageManager::createCache(\n    const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const {\n    QThreadPool::globalInstance()->start([path, cache, fillMode, size] {\n        QImage image(path);\n\n        if (image.isNull()) {\n            qWarning() << \"CachingImageManager::createCache: failed to read\" << path;\n            return;\n        }\n\n        image.convertTo(QImage::Format_ARGB32);\n\n        if (fillMode == \"PreserveAspectCrop\") {\n            image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);\n        } else if (fillMode == \"PreserveAspectFit\") {\n            image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);\n        } else {\n            image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);\n        }\n\n        if (fillMode == \"PreserveAspectCrop\" || fillMode == \"PreserveAspectFit\") {\n            QImage canvas(size, QImage::Format_ARGB32);\n            canvas.fill(Qt::transparent);\n\n            QPainter painter(&canvas);\n            painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image);\n            painter.end();\n\n            image = canvas;\n        }\n\n        const QString parent = QFileInfo(cache).absolutePath();\n        if (!QDir().mkpath(parent) || !image.save(cache)) {\n            qWarning() << \"CachingImageManager::createCache: failed to save to\" << cache;\n        }\n    });\n}\n\nQString CachingImageManager::sha256sum(const QString& path) {\n    QFile file(path);\n    if (!file.open(QIODevice::ReadOnly)) {\n        qWarning() << \"CachingImageManager::sha256sum: failed to open\" << path;\n        return \"\";\n    }\n\n    QCryptographicHash hash(QCryptographicHash::Sha256);\n    hash.addData(&file);\n    file.close();\n\n    return hash.result().toHex();\n}\n\n} // namespace caelestia::internal\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/cachingimagemanager.hpp",
    "content": "#pragma once\n\n#include <QtQuick/qquickitem.h>\n#include <qobject.h>\n#include <qqmlintegration.h>\n\nnamespace caelestia::internal {\n\nclass CachingImageManager : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n\n    Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED)\n    Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED)\n\n    Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)\n    Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged)\n\npublic:\n    explicit CachingImageManager(QObject* parent = nullptr)\n        : QObject(parent)\n        , m_item(nullptr) {}\n\n    [[nodiscard]] QQuickItem* item() const;\n    void setItem(QQuickItem* item);\n\n    [[nodiscard]] QUrl cacheDir() const;\n    void setCacheDir(const QUrl& cacheDir);\n\n    [[nodiscard]] QString path() const;\n    void setPath(const QString& path);\n\n    [[nodiscard]] QUrl cachePath() const;\n\n    Q_INVOKABLE void updateSource();\n    Q_INVOKABLE void updateSource(const QString& path);\n\nsignals:\n    void itemChanged();\n    void cacheDirChanged();\n\n    void pathChanged();\n    void cachePathChanged();\n    void usingCacheChanged();\n\nprivate:\n    QString m_shaPath;\n\n    QQuickItem* m_item;\n    QUrl m_cacheDir;\n\n    QString m_path;\n    QUrl m_cachePath;\n\n    QMetaObject::Connection m_widthConn;\n    QMetaObject::Connection m_heightConn;\n\n    [[nodiscard]] qreal effectiveScale() const;\n    [[nodiscard]] QSize effectiveSize() const;\n\n    void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const;\n    [[nodiscard]] static QString sha256sum(const QString& path);\n};\n\n} // namespace caelestia::internal\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/circularbuffer.cpp",
    "content": "#include \"circularbuffer.hpp\"\n\n#include <algorithm>\n\nnamespace caelestia::internal {\n\nCircularBuffer::CircularBuffer(QObject* parent)\n    : QObject(parent) {}\n\nint CircularBuffer::capacity() const {\n    return m_capacity;\n}\n\nvoid CircularBuffer::setCapacity(int capacity) {\n    if (capacity < 0)\n        capacity = 0;\n    if (m_capacity == capacity)\n        return;\n\n    const auto old = values();\n\n    m_capacity = capacity;\n    m_data.resize(capacity);\n    m_data.fill(0.0);\n    m_head = 0;\n    m_count = 0;\n\n    // Re-push old values, keeping the most recent ones\n    const auto start = old.size() > capacity ? old.size() - capacity : 0;\n    for (auto i = start; i < old.size(); ++i) {\n        m_data[m_head] = old[i];\n        m_head = (m_head + 1) % m_capacity;\n        m_count++;\n    }\n\n    emit capacityChanged();\n    emit countChanged();\n    emit valuesChanged();\n}\n\nint CircularBuffer::count() const {\n    return m_count;\n}\n\nQList<qreal> CircularBuffer::values() const {\n    QList<qreal> result;\n    result.reserve(m_count);\n    for (int i = 0; i < m_count; ++i)\n        result.append(at(i));\n    return result;\n}\n\nqreal CircularBuffer::maximum() const {\n    if (m_count == 0)\n        return 0.0;\n\n    qreal maxVal = at(0);\n    for (int i = 1; i < m_count; ++i)\n        maxVal = std::max(maxVal, at(i));\n    return maxVal;\n}\n\nvoid CircularBuffer::push(qreal value) {\n    if (m_capacity <= 0)\n        return;\n\n    m_data[m_head] = value;\n    m_head = (m_head + 1) % m_capacity;\n    if (m_count < m_capacity) {\n        m_count++;\n        emit countChanged();\n    }\n    emit valuesChanged();\n}\n\nvoid CircularBuffer::clear() {\n    if (m_count == 0)\n        return;\n\n    m_head = 0;\n    m_count = 0;\n    emit countChanged();\n    emit valuesChanged();\n}\n\nqreal CircularBuffer::at(int index) const {\n    if (index < 0 || index >= m_count)\n        return 0.0;\n\n    const int actualIndex = (m_head - m_count + index + m_capacity) % m_capacity;\n    return m_data[actualIndex];\n}\n\n} // namespace caelestia::internal\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/circularbuffer.hpp",
    "content": "#pragma once\n\n#include <qobject.h>\n#include <qqmlintegration.h>\n#include <qvector.h>\n\nnamespace caelestia::internal {\n\nclass CircularBuffer : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n\n    Q_PROPERTY(int capacity READ capacity WRITE setCapacity NOTIFY capacityChanged)\n    Q_PROPERTY(int count READ count NOTIFY countChanged)\n    Q_PROPERTY(QList<qreal> values READ values NOTIFY valuesChanged)\n    Q_PROPERTY(qreal maximum READ maximum NOTIFY valuesChanged)\n\npublic:\n    explicit CircularBuffer(QObject* parent = nullptr);\n\n    [[nodiscard]] int capacity() const;\n    void setCapacity(int capacity);\n\n    [[nodiscard]] int count() const;\n    [[nodiscard]] QList<qreal> values() const;\n    [[nodiscard]] qreal maximum() const;\n\n    Q_INVOKABLE void push(qreal value);\n    Q_INVOKABLE void clear();\n    Q_INVOKABLE [[nodiscard]] qreal at(int index) const;\n\nsignals:\n    void capacityChanged();\n    void countChanged();\n    void valuesChanged();\n\nprivate:\n    QVector<qreal> m_data;\n    int m_head = 0;\n    int m_count = 0;\n    int m_capacity = 0;\n};\n\n} // namespace caelestia::internal\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/circularindicatormanager.cpp",
    "content": "#include \"circularindicatormanager.hpp\"\n#include <qeasingcurve.h>\n#include <qpoint.h>\n\nnamespace {\n\nnamespace advance {\n\nconstexpr qint32 TOTAL_CYCLES = 4;\nconstexpr qint32 TOTAL_DURATION_IN_MS = 5400;\nconstexpr qint32 DURATION_TO_EXPAND_IN_MS = 667;\nconstexpr qint32 DURATION_TO_COLLAPSE_IN_MS = 667;\nconstexpr qint32 DURATION_TO_COMPLETE_END_IN_MS = 333;\nconstexpr qint32 TAIL_DEGREES_OFFSET = -20;\nconstexpr qint32 EXTRA_DEGREES_PER_CYCLE = 250;\nconstexpr qint32 CONSTANT_ROTATION_DEGREES = 1520;\n\nconstexpr std::array<qint32, TOTAL_CYCLES> DELAY_TO_EXPAND_IN_MS = { 0, 1350, 2700, 4050 };\nconstexpr std::array<qint32, TOTAL_CYCLES> DELAY_TO_COLLAPSE_IN_MS = { 667, 2017, 3367, 4717 };\n\n} // namespace advance\n\nnamespace retreat {\n\nconstexpr qint32 TOTAL_DURATION_IN_MS = 6000;\nconstexpr qint32 DURATION_SPIN_IN_MS = 500;\nconstexpr qint32 DURATION_GROW_ACTIVE_IN_MS = 3000;\nconstexpr qint32 DURATION_SHRINK_ACTIVE_IN_MS = 3000;\nconstexpr std::array DELAY_SPINS_IN_MS = { 0, 1500, 3000, 4500 };\nconstexpr qint32 DELAY_GROW_ACTIVE_IN_MS = 0;\nconstexpr qint32 DELAY_SHRINK_ACTIVE_IN_MS = 3000;\nconstexpr qint32 DURATION_TO_COMPLETE_END_IN_MS = 500;\n\n// Constants for animation values.\n\n// The total degrees that a constant rotation goes by.\nconstexpr qint32 CONSTANT_ROTATION_DEGREES = 1080;\n// Despite of the constant rotation, there are also 5 extra rotations the entire animation. The\n// total degrees that each extra rotation goes by.\nconstexpr qint32 SPIN_ROTATION_DEGREES = 90;\nconstexpr std::array<qreal, 2> END_FRACTION_RANGE = { 0.10, 0.87 };\n\n} // namespace retreat\n\ninline qreal getFractionInRange(qreal playtime, qreal start, qreal duration) {\n    const auto fraction = (playtime - start) / duration;\n    return std::clamp(fraction, 0.0, 1.0);\n}\n\n} // namespace\n\nnamespace caelestia::internal {\n\nCircularIndicatorManager::CircularIndicatorManager(QObject* parent)\n    : QObject(parent)\n    , m_type(IndeterminateAnimationType::Advance)\n    , m_curve(QEasingCurve(QEasingCurve::BezierSpline))\n    , m_progress(0)\n    , m_startFraction(0)\n    , m_endFraction(0)\n    , m_rotation(0)\n    , m_completeEndProgress(0) {\n    // Fast out slow in\n    m_curve.addCubicBezierSegment({ 0.4, 0.0 }, { 0.2, 1.0 }, { 1.0, 1.0 });\n}\n\nqreal CircularIndicatorManager::startFraction() const {\n    return m_startFraction;\n}\n\nqreal CircularIndicatorManager::endFraction() const {\n    return m_endFraction;\n}\n\nqreal CircularIndicatorManager::rotation() const {\n    return m_rotation;\n}\n\nqreal CircularIndicatorManager::progress() const {\n    return m_progress;\n}\n\nvoid CircularIndicatorManager::setProgress(qreal progress) {\n    update(progress);\n}\n\nqreal CircularIndicatorManager::duration() const {\n    if (m_type == IndeterminateAnimationType::Advance) {\n        return advance::TOTAL_DURATION_IN_MS;\n    } else {\n        return retreat::TOTAL_DURATION_IN_MS;\n    }\n}\n\nqreal CircularIndicatorManager::completeEndDuration() const {\n    if (m_type == IndeterminateAnimationType::Advance) {\n        return advance::DURATION_TO_COMPLETE_END_IN_MS;\n    } else {\n        return retreat::DURATION_TO_COMPLETE_END_IN_MS;\n    }\n}\n\nCircularIndicatorManager::IndeterminateAnimationType CircularIndicatorManager::indeterminateAnimationType() const {\n    return m_type;\n}\n\nvoid CircularIndicatorManager::setIndeterminateAnimationType(IndeterminateAnimationType t) {\n    if (m_type != t) {\n        m_type = t;\n        emit indeterminateAnimationTypeChanged();\n    }\n}\n\nqreal CircularIndicatorManager::completeEndProgress() const {\n    return m_completeEndProgress;\n}\n\nvoid CircularIndicatorManager::setCompleteEndProgress(qreal progress) {\n    if (qFuzzyCompare(m_completeEndProgress + 1.0, progress + 1.0)) {\n        return;\n    }\n\n    m_completeEndProgress = progress;\n    emit completeEndProgressChanged();\n\n    update(m_progress);\n}\n\nvoid CircularIndicatorManager::update(qreal progress) {\n    if (qFuzzyCompare(m_progress + 1.0, progress + 1.0)) {\n        return;\n    }\n\n    if (m_type == IndeterminateAnimationType::Advance) {\n        updateAdvance(progress);\n    } else {\n        updateRetreat(progress);\n    }\n\n    m_progress = progress;\n    emit progressChanged();\n}\n\nvoid CircularIndicatorManager::updateRetreat(qreal progress) {\n    using namespace retreat;\n    const auto playtime = progress * TOTAL_DURATION_IN_MS;\n\n    // Constant rotation.\n    const qreal constantRotation = CONSTANT_ROTATION_DEGREES * progress;\n    // Extra rotation for the faster spinning.\n    qreal spinRotation = 0;\n    for (const int spinDelay : DELAY_SPINS_IN_MS) {\n        spinRotation += m_curve.valueForProgress(getFractionInRange(playtime, spinDelay, DURATION_SPIN_IN_MS)) *\n                        SPIN_ROTATION_DEGREES;\n    }\n    const auto oldRotation = m_rotation;\n    m_rotation = constantRotation + spinRotation;\n    if (!qFuzzyCompare(m_rotation + 1.0, oldRotation + 1.0))\n        emit rotationChanged();\n\n    // Grow active indicator.\n    qreal fraction =\n        m_curve.valueForProgress(getFractionInRange(playtime, DELAY_GROW_ACTIVE_IN_MS, DURATION_GROW_ACTIVE_IN_MS));\n    fraction -=\n        m_curve.valueForProgress(getFractionInRange(playtime, DELAY_SHRINK_ACTIVE_IN_MS, DURATION_SHRINK_ACTIVE_IN_MS));\n\n    if (!qFuzzyIsNull(m_startFraction)) {\n        m_startFraction = 0.0;\n        emit startFractionChanged();\n    }\n    const auto oldEndFrac = m_endFraction;\n    m_endFraction = std::lerp(END_FRACTION_RANGE[0], END_FRACTION_RANGE[1], fraction);\n\n    // Completing animation.\n    if (m_completeEndProgress > 0) {\n        m_endFraction *= 1 - m_completeEndProgress;\n    }\n\n    if (!qFuzzyCompare(m_endFraction + 1.0, oldEndFrac + 1.0)) {\n        emit endFractionChanged();\n    }\n}\n\nvoid CircularIndicatorManager::updateAdvance(qreal progress) {\n    using namespace advance;\n    const auto playtime = progress * TOTAL_DURATION_IN_MS;\n    const auto oldStart = m_startFraction;\n    const auto oldEnd = m_endFraction;\n\n    // Adds constant rotation to segment positions.\n    m_startFraction = CONSTANT_ROTATION_DEGREES * progress + TAIL_DEGREES_OFFSET;\n    m_endFraction = CONSTANT_ROTATION_DEGREES * progress;\n\n    // Adds cycle specific rotation to segment positions.\n    for (size_t cycleIndex = 0; cycleIndex < TOTAL_CYCLES; ++cycleIndex) {\n        // While expanding.\n        qreal fraction = getFractionInRange(playtime, DELAY_TO_EXPAND_IN_MS[cycleIndex], DURATION_TO_EXPAND_IN_MS);\n        m_endFraction += m_curve.valueForProgress(fraction) * EXTRA_DEGREES_PER_CYCLE;\n\n        // While collapsing.\n        fraction = getFractionInRange(playtime, DELAY_TO_COLLAPSE_IN_MS[cycleIndex], DURATION_TO_COLLAPSE_IN_MS);\n        m_startFraction += m_curve.valueForProgress(fraction) * EXTRA_DEGREES_PER_CYCLE;\n    }\n\n    // Closes the gap between head and tail for complete end.\n    m_startFraction += (m_endFraction - m_startFraction) * m_completeEndProgress;\n\n    m_startFraction /= 360.0;\n    m_endFraction /= 360.0;\n\n    if (!qFuzzyCompare(m_startFraction + 1.0, oldStart + 1.0))\n        emit startFractionChanged();\n    if (!qFuzzyCompare(m_endFraction + 1.0, oldEnd + 1.0))\n        emit endFractionChanged();\n}\n\n} // namespace caelestia::internal\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/circularindicatormanager.hpp",
    "content": "#pragma once\n\n#include <qeasingcurve.h>\n#include <qobject.h>\n#include <qqmlintegration.h>\n\nnamespace caelestia::internal {\n\nclass CircularIndicatorManager : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n\n    Q_PROPERTY(qreal startFraction READ startFraction NOTIFY startFractionChanged)\n    Q_PROPERTY(qreal endFraction READ endFraction NOTIFY endFractionChanged)\n    Q_PROPERTY(qreal rotation READ rotation NOTIFY rotationChanged)\n    Q_PROPERTY(qreal progress READ progress WRITE setProgress NOTIFY progressChanged)\n    Q_PROPERTY(qreal completeEndProgress READ completeEndProgress WRITE setCompleteEndProgress NOTIFY\n            completeEndProgressChanged)\n    Q_PROPERTY(qreal duration READ duration NOTIFY indeterminateAnimationTypeChanged)\n    Q_PROPERTY(qreal completeEndDuration READ completeEndDuration NOTIFY indeterminateAnimationTypeChanged)\n    Q_PROPERTY(IndeterminateAnimationType indeterminateAnimationType READ indeterminateAnimationType WRITE\n            setIndeterminateAnimationType NOTIFY indeterminateAnimationTypeChanged)\n\npublic:\n    explicit CircularIndicatorManager(QObject* parent = nullptr);\n\n    enum IndeterminateAnimationType {\n        Advance = 0,\n        Retreat\n    };\n    Q_ENUM(IndeterminateAnimationType)\n\n    [[nodiscard]] qreal startFraction() const;\n    [[nodiscard]] qreal endFraction() const;\n    [[nodiscard]] qreal rotation() const;\n\n    [[nodiscard]] qreal progress() const;\n    void setProgress(qreal progress);\n\n    [[nodiscard]] qreal completeEndProgress() const;\n    void setCompleteEndProgress(qreal progress);\n\n    [[nodiscard]] qreal duration() const;\n    [[nodiscard]] qreal completeEndDuration() const;\n\n    [[nodiscard]] IndeterminateAnimationType indeterminateAnimationType() const;\n    void setIndeterminateAnimationType(IndeterminateAnimationType t);\n\nsignals:\n    void startFractionChanged();\n    void endFractionChanged();\n    void rotationChanged();\n    void progressChanged();\n    void completeEndProgressChanged();\n    void indeterminateAnimationTypeChanged();\n\nprivate:\n    IndeterminateAnimationType m_type;\n    QEasingCurve m_curve;\n\n    qreal m_progress;\n    qreal m_startFraction;\n    qreal m_endFraction;\n    qreal m_rotation;\n    qreal m_completeEndProgress;\n\n    void update(qreal progress);\n    void updateAdvance(qreal progress);\n    void updateRetreat(qreal progress);\n};\n\n} // namespace caelestia::internal\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/hyprdevices.cpp",
    "content": "#include \"hyprdevices.hpp\"\n\n#include <qjsonarray.h>\n\nnamespace caelestia::internal::hypr {\n\nHyprKeyboard::HyprKeyboard(QJsonObject ipcObject, QObject* parent)\n    : QObject(parent)\n    , m_lastIpcObject(ipcObject) {}\n\nQVariantHash HyprKeyboard::lastIpcObject() const {\n    return m_lastIpcObject.toVariantHash();\n}\n\nQString HyprKeyboard::address() const {\n    return m_lastIpcObject.value(\"address\").toString();\n}\n\nQString HyprKeyboard::name() const {\n    return m_lastIpcObject.value(\"name\").toString();\n}\n\nQString HyprKeyboard::layout() const {\n    return m_lastIpcObject.value(\"layout\").toString();\n}\n\nQString HyprKeyboard::activeKeymap() const {\n    return m_lastIpcObject.value(\"active_keymap\").toString();\n}\n\nbool HyprKeyboard::capsLock() const {\n    return m_lastIpcObject.value(\"capsLock\").toBool();\n}\n\nbool HyprKeyboard::numLock() const {\n    return m_lastIpcObject.value(\"numLock\").toBool();\n}\n\nbool HyprKeyboard::main() const {\n    return m_lastIpcObject.value(\"main\").toBool();\n}\n\nbool HyprKeyboard::updateLastIpcObject(QJsonObject object) {\n    if (m_lastIpcObject == object) {\n        return false;\n    }\n\n    const auto last = m_lastIpcObject;\n\n    m_lastIpcObject = object;\n    emit lastIpcObjectChanged();\n\n    bool dirty = false;\n    if (last.value(\"address\") != object.value(\"address\")) {\n        dirty = true;\n        emit addressChanged();\n    }\n    if (last.value(\"name\") != object.value(\"name\")) {\n        dirty = true;\n        emit nameChanged();\n    }\n    if (last.value(\"layout\") != object.value(\"layout\")) {\n        dirty = true;\n        emit layoutChanged();\n    }\n    if (last.value(\"active_keymap\") != object.value(\"active_keymap\")) {\n        dirty = true;\n        emit activeKeymapChanged();\n    }\n    if (last.value(\"capsLock\") != object.value(\"capsLock\")) {\n        dirty = true;\n        emit capsLockChanged();\n    }\n    if (last.value(\"numLock\") != object.value(\"numLock\")) {\n        dirty = true;\n        emit numLockChanged();\n    }\n    if (last.value(\"main\") != object.value(\"main\")) {\n        dirty = true;\n        emit mainChanged();\n    }\n    return dirty;\n}\n\nHyprDevices::HyprDevices(QObject* parent)\n    : QObject(parent) {}\n\nQQmlListProperty<HyprKeyboard> HyprDevices::keyboards() {\n    return QQmlListProperty<HyprKeyboard>(this, &m_keyboards);\n}\n\nbool HyprDevices::updateLastIpcObject(QJsonObject object) {\n    const auto val = object.value(\"keyboards\").toArray();\n    bool dirty = false;\n\n    for (auto it = m_keyboards.begin(); it != m_keyboards.end();) {\n        auto* const keyboard = *it;\n        const auto inNewValues = std::any_of(val.begin(), val.end(), [keyboard](const QJsonValue& o) {\n            return o.toObject().value(\"address\").toString() == keyboard->address();\n        });\n\n        if (!inNewValues) {\n            dirty = true;\n            it = m_keyboards.erase(it);\n            keyboard->deleteLater();\n        } else {\n            ++it;\n        }\n    }\n\n    for (const auto& o : val) {\n        const auto obj = o.toObject();\n        const auto addr = obj.value(\"address\").toString();\n\n        auto it = std::find_if(m_keyboards.begin(), m_keyboards.end(), [addr](const HyprKeyboard* kb) {\n            return kb->address() == addr;\n        });\n\n        if (it != m_keyboards.end()) {\n            dirty |= (*it)->updateLastIpcObject(obj);\n        } else {\n            dirty = true;\n            m_keyboards << new HyprKeyboard(obj, this);\n        }\n    }\n\n    if (dirty) {\n        emit keyboardsChanged();\n    }\n\n    return dirty;\n}\n\n} // namespace caelestia::internal::hypr\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/hyprdevices.hpp",
    "content": "#pragma once\n\n#include <qjsonobject.h>\n#include <qobject.h>\n#include <qqmlintegration.h>\n#include <qqmllist.h>\n\nnamespace caelestia::internal::hypr {\n\nclass HyprKeyboard : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n    QML_UNCREATABLE(\"HyprKeyboard instances can only be retrieved from a HyprDevices\")\n\n    Q_PROPERTY(QVariantHash lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged)\n    Q_PROPERTY(QString address READ address NOTIFY addressChanged)\n    Q_PROPERTY(QString name READ name NOTIFY nameChanged)\n    Q_PROPERTY(QString layout READ layout NOTIFY layoutChanged)\n    Q_PROPERTY(QString activeKeymap READ activeKeymap NOTIFY activeKeymapChanged)\n    Q_PROPERTY(bool capsLock READ capsLock NOTIFY capsLockChanged)\n    Q_PROPERTY(bool numLock READ numLock NOTIFY numLockChanged)\n    Q_PROPERTY(bool main READ main NOTIFY mainChanged)\n\npublic:\n    explicit HyprKeyboard(QJsonObject ipcObject, QObject* parent = nullptr);\n\n    [[nodiscard]] QVariantHash lastIpcObject() const;\n    [[nodiscard]] QString address() const;\n    [[nodiscard]] QString name() const;\n    [[nodiscard]] QString layout() const;\n    [[nodiscard]] QString activeKeymap() const;\n    [[nodiscard]] bool capsLock() const;\n    [[nodiscard]] bool numLock() const;\n    [[nodiscard]] bool main() const;\n\n    bool updateLastIpcObject(QJsonObject object);\n\nsignals:\n    void lastIpcObjectChanged();\n    void addressChanged();\n    void nameChanged();\n    void layoutChanged();\n    void activeKeymapChanged();\n    void capsLockChanged();\n    void numLockChanged();\n    void mainChanged();\n\nprivate:\n    QJsonObject m_lastIpcObject;\n};\n\nclass HyprDevices : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n    QML_UNCREATABLE(\"HyprDevices instances can only be retrieved from a HyprExtras\")\n\n    Q_PROPERTY(\n        QQmlListProperty<caelestia::internal::hypr::HyprKeyboard> keyboards READ keyboards NOTIFY keyboardsChanged)\n\npublic:\n    explicit HyprDevices(QObject* parent = nullptr);\n\n    [[nodiscard]] QQmlListProperty<HyprKeyboard> keyboards();\n\n    bool updateLastIpcObject(QJsonObject object);\n\nsignals:\n    void keyboardsChanged();\n\nprivate:\n    QList<HyprKeyboard*> m_keyboards;\n};\n\n} // namespace caelestia::internal::hypr\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/hyprextras.cpp",
    "content": "#include \"hyprextras.hpp\"\n\n#include <qdir.h>\n#include <qjsonarray.h>\n#include <qlocalsocket.h>\n#include <qvariant.h>\n\nnamespace caelestia::internal::hypr {\n\nHyprExtras::HyprExtras(QObject* parent)\n    : QObject(parent)\n    , m_requestSocket(\"\")\n    , m_eventSocket(\"\")\n    , m_socket(nullptr)\n    , m_socketValid(false)\n    , m_devices(new HyprDevices(this)) {\n    const auto his = qEnvironmentVariable(\"HYPRLAND_INSTANCE_SIGNATURE\");\n    if (his.isEmpty()) {\n        qWarning()\n            << \"HyprExtras::HyprExtras: $HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket.\";\n        return;\n    }\n\n    auto hyprDir = QString(\"%1/hypr/%2\").arg(qEnvironmentVariable(\"XDG_RUNTIME_DIR\"), his);\n    if (!QDir(hyprDir).exists()) {\n        hyprDir = \"/tmp/hypr/\" + his;\n\n        if (!QDir(hyprDir).exists()) {\n            qWarning() << \"HyprExtras::HyprExtras: Hyprland socket directory does not exist. Unable to connect to \"\n                          \"Hyprland socket.\";\n            return;\n        }\n    }\n\n    m_requestSocket = hyprDir + \"/.socket.sock\";\n    m_eventSocket = hyprDir + \"/.socket2.sock\";\n\n    refreshOptions();\n    refreshDevices();\n\n    m_socket = new QLocalSocket(this);\n\n    QObject::connect(m_socket, &QLocalSocket::errorOccurred, this, &HyprExtras::socketError);\n    QObject::connect(m_socket, &QLocalSocket::stateChanged, this, &HyprExtras::socketStateChanged);\n    QObject::connect(m_socket, &QLocalSocket::readyRead, this, &HyprExtras::readEvent);\n\n    m_socket->connectToServer(m_eventSocket, QLocalSocket::ReadOnly);\n}\n\nQVariantHash HyprExtras::options() const {\n    return m_options;\n}\n\nHyprDevices* HyprExtras::devices() const {\n    return m_devices;\n}\n\nvoid HyprExtras::message(const QString& message) {\n    if (message.isEmpty()) {\n        return;\n    }\n\n    makeRequest(message, [](bool success, const QByteArray& res) {\n        if (!success) {\n            qWarning() << \"HyprExtras::message: request error:\" << QString::fromUtf8(res);\n        }\n    });\n}\n\nvoid HyprExtras::batchMessage(const QStringList& messages) {\n    if (messages.isEmpty()) {\n        return;\n    }\n\n    makeRequest(\"[[BATCH]]\" + messages.join(\";\"), [](bool success, const QByteArray& res) {\n        if (!success) {\n            qWarning() << \"HyprExtras::batchMessage: request error:\" << QString::fromUtf8(res);\n        }\n    });\n}\n\nvoid HyprExtras::applyOptions(const QVariantHash& options) {\n    if (options.isEmpty()) {\n        return;\n    }\n\n    QString request;\n    request.reserve(12 + options.size() * 40);\n    request += QLatin1String(\"[[BATCH]]\");\n    for (auto it = options.constBegin(); it != options.constEnd(); ++it) {\n        request += QLatin1String(\"keyword \") + it.key() + QLatin1Char(' ') + it.value().toString() + QLatin1Char(';');\n    }\n\n    makeRequest(request, [this](bool success, const QByteArray& res) {\n        if (success) {\n            refreshOptions();\n        } else {\n            qWarning() << \"HyprExtras::applyOptions: request error\" << QString::fromUtf8(res);\n        }\n    });\n}\n\nvoid HyprExtras::refreshOptions() {\n    if (!m_optionsRefresh.isNull()) {\n        m_optionsRefresh->close();\n    }\n\n    m_optionsRefresh = makeRequestJson(\"descriptions\", [this](bool success, const QJsonDocument& response) {\n        m_optionsRefresh.reset();\n        if (!success) {\n            return;\n        }\n\n        const auto options = response.array();\n        bool dirty = false;\n\n        for (const auto& o : std::as_const(options)) {\n            const auto obj = o.toObject();\n            const auto key = obj.value(\"value\").toString();\n            const auto value = obj.value(\"data\").toObject().value(\"current\").toVariant();\n            if (m_options.value(key) != value) {\n                dirty = true;\n                m_options.insert(key, value);\n            }\n        }\n\n        if (dirty) {\n            emit optionsChanged();\n        }\n    });\n}\n\nvoid HyprExtras::refreshDevices() {\n    if (!m_devicesRefresh.isNull()) {\n        m_devicesRefresh->close();\n    }\n\n    m_devicesRefresh = makeRequestJson(\"devices\", [this](bool success, const QJsonDocument& response) {\n        m_devicesRefresh.reset();\n        if (success) {\n            m_devices->updateLastIpcObject(response.object());\n        }\n    });\n}\n\nvoid HyprExtras::socketError(QLocalSocket::LocalSocketError error) const {\n    if (!m_socketValid) {\n        qWarning() << \"HyprExtras::socketError: unable to connect to Hyprland event socket:\" << error;\n    } else {\n        qWarning() << \"HyprExtras::socketError: Hyprland event socket error:\" << error;\n    }\n}\n\nvoid HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) {\n    if (state == QLocalSocket::UnconnectedState && m_socketValid) {\n        qWarning() << \"HyprExtras::socketStateChanged: Hyprland event socket disconnected.\";\n    }\n\n    m_socketValid = state == QLocalSocket::ConnectedState;\n}\n\nvoid HyprExtras::readEvent() {\n    while (true) {\n        auto rawEvent = m_socket->readLine();\n        if (rawEvent.isEmpty()) {\n            break;\n        }\n        rawEvent.truncate(rawEvent.length() - 1); // Remove trailing \\n\n        const auto event = QByteArrayView(rawEvent.data(), rawEvent.indexOf(\">>\"));\n        handleEvent(QString::fromUtf8(event));\n    }\n}\n\nvoid HyprExtras::handleEvent(const QString& event) {\n    if (event == \"configreloaded\") {\n        refreshOptions();\n    } else if (event == \"activelayout\") {\n        refreshDevices();\n    }\n}\n\nHyprExtras::SocketPtr HyprExtras::makeRequestJson(\n    const QString& request, const std::function<void(bool, QJsonDocument)>& callback) {\n    return makeRequest(\"j/\" + request, [callback](bool success, const QByteArray& response) {\n        callback(success, QJsonDocument::fromJson(response));\n    });\n}\n\nHyprExtras::SocketPtr HyprExtras::makeRequest(\n    const QString& request, const std::function<void(bool, QByteArray)>& callback) {\n    if (m_requestSocket.isEmpty()) {\n        return SocketPtr();\n    }\n\n    auto socket = SocketPtr::create(this);\n\n    QObject::connect(socket.data(), &QLocalSocket::connected, this, [=, this]() {\n        QObject::connect(socket.data(), &QLocalSocket::readyRead, this, [socket, callback]() {\n            const auto response = socket->readAll();\n            callback(true, std::move(response));\n            socket->close();\n        });\n\n        socket->write(request.toUtf8());\n        socket->flush();\n    });\n\n    QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) {\n        qWarning() << \"HyprExtras::makeRequest: error making request:\" << err << \"| request:\" << request;\n        callback(false, {});\n        socket->close();\n    });\n\n    socket->connectToServer(m_requestSocket);\n\n    return socket;\n}\n\n} // namespace caelestia::internal::hypr\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/hyprextras.hpp",
    "content": "#pragma once\n\n#include \"hyprdevices.hpp\"\n#include <qlocalsocket.h>\n#include <qobject.h>\n#include <qqmlintegration.h>\n\nnamespace caelestia::internal::hypr {\n\nclass HyprExtras : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n\n    Q_PROPERTY(QVariantHash options READ options NOTIFY optionsChanged)\n    Q_PROPERTY(caelestia::internal::hypr::HyprDevices* devices READ devices CONSTANT)\n\npublic:\n    explicit HyprExtras(QObject* parent = nullptr);\n\n    [[nodiscard]] QVariantHash options() const;\n    [[nodiscard]] HyprDevices* devices() const;\n\n    Q_INVOKABLE void message(const QString& message);\n    Q_INVOKABLE void batchMessage(const QStringList& messages);\n    Q_INVOKABLE void applyOptions(const QVariantHash& options);\n\n    Q_INVOKABLE void refreshOptions();\n    Q_INVOKABLE void refreshDevices();\n\nsignals:\n    void optionsChanged();\n\nprivate:\n    using SocketPtr = QSharedPointer<QLocalSocket>;\n\n    QString m_requestSocket;\n    QString m_eventSocket;\n    QLocalSocket* m_socket;\n    bool m_socketValid;\n\n    QVariantHash m_options;\n    HyprDevices* const m_devices;\n\n    SocketPtr m_optionsRefresh;\n    SocketPtr m_devicesRefresh;\n\n    void socketError(QLocalSocket::LocalSocketError error) const;\n    void socketStateChanged(QLocalSocket::LocalSocketState state);\n    void readEvent();\n    void handleEvent(const QString& event);\n\n    SocketPtr makeRequestJson(const QString& request, const std::function<void(bool, QJsonDocument)>& callback);\n    SocketPtr makeRequest(const QString& request, const std::function<void(bool, QByteArray)>& callback);\n};\n\n} // namespace caelestia::internal::hypr\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/logindmanager.cpp",
    "content": "#include \"logindmanager.hpp\"\n\n#include <QtDBus/qdbusconnection.h>\n#include <QtDBus/qdbuserror.h>\n#include <QtDBus/qdbusinterface.h>\n#include <QtDBus/qdbusreply.h>\n\nnamespace caelestia::internal {\n\nLogindManager::LogindManager(QObject* parent)\n    : QObject(parent) {\n    auto bus = QDBusConnection::systemBus();\n    if (!bus.isConnected()) {\n        qWarning() << \"LogindManager::LogindManager: failed to connect to system bus:\" << bus.lastError().message();\n        return;\n    }\n\n    bool ok = bus.connect(\"org.freedesktop.login1\", \"/org/freedesktop/login1\", \"org.freedesktop.login1.Manager\",\n        \"PrepareForSleep\", this, SLOT(handlePrepareForSleep(bool)));\n\n    if (!ok) {\n        qWarning() << \"LogindManager::LogindManager: failed to connect to PrepareForSleep signal:\"\n                   << bus.lastError().message();\n    }\n\n    QDBusInterface login1(\"org.freedesktop.login1\", \"/org/freedesktop/login1\", \"org.freedesktop.login1.Manager\", bus);\n    const QDBusReply<QDBusObjectPath> reply = login1.call(\"GetSession\", \"auto\");\n    if (!reply.isValid()) {\n        qWarning() << \"LogindManager::LogindManager: failed to get session path\";\n        return;\n    }\n    const auto sessionPath = reply.value().path();\n\n    ok = bus.connect(\"org.freedesktop.login1\", sessionPath, \"org.freedesktop.login1.Session\", \"Lock\", this,\n        SLOT(handleLockRequested()));\n\n    if (!ok) {\n        qWarning() << \"LogindManager::LogindManager: failed to connect to Lock signal:\" << bus.lastError().message();\n    }\n\n    ok = bus.connect(\"org.freedesktop.login1\", sessionPath, \"org.freedesktop.login1.Session\", \"Unlock\", this,\n        SLOT(handleUnlockRequested()));\n\n    if (!ok) {\n        qWarning() << \"LogindManager::LogindManager: failed to connect to Unlock signal:\" << bus.lastError().message();\n    }\n}\n\nvoid LogindManager::handlePrepareForSleep(bool sleep) {\n    if (sleep) {\n        emit aboutToSleep();\n    } else {\n        emit resumed();\n    }\n}\n\nvoid LogindManager::handleLockRequested() {\n    emit lockRequested();\n}\n\nvoid LogindManager::handleUnlockRequested() {\n    emit unlockRequested();\n}\n\n} // namespace caelestia::internal\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/logindmanager.hpp",
    "content": "#pragma once\n\n#include <qobject.h>\n#include <qqmlintegration.h>\n\nnamespace caelestia::internal {\n\nclass LogindManager : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n\npublic:\n    explicit LogindManager(QObject* parent = nullptr);\n\nsignals:\n    void aboutToSleep();\n    void resumed();\n    void lockRequested();\n    void unlockRequested();\n\nprivate slots:\n    void handlePrepareForSleep(bool sleep);\n    void handleLockRequested();\n    void handleUnlockRequested();\n};\n\n} // namespace caelestia::internal\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/sparklineitem.cpp",
    "content": "#include \"sparklineitem.hpp\"\n\n#include <qpainter.h>\n#include <qpainterpath.h>\n#include <qpen.h>\n\nnamespace caelestia::internal {\n\nSparklineItem::SparklineItem(QQuickItem* parent)\n    : QQuickPaintedItem(parent) {\n    setAntialiasing(true);\n}\n\nvoid SparklineItem::paint(QPainter* painter) {\n    const bool has1 = m_line1 && m_line1->count() >= 2;\n    const bool has2 = m_line2 && m_line2->count() >= 2;\n    if (!has1 && !has2)\n        return;\n\n    painter->setRenderHint(QPainter::Antialiasing, true);\n\n    // Draw line1 first (behind), then line2 (in front)\n    if (has1)\n        drawLine(painter, m_line1, m_line1Color, m_line1FillAlpha);\n    if (has2)\n        drawLine(painter, m_line2, m_line2Color, m_line2FillAlpha);\n}\n\nvoid SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha) {\n    if (m_historyLength < 2)\n        return;\n\n    const qreal w = width();\n    const qreal h = height();\n    const int len = buffer->count();\n    const qreal stepX = w / static_cast<qreal>(m_historyLength - 1);\n    const qreal startX = w - (len - 1) * stepX - stepX * m_slideProgress + stepX;\n\n    // Build line path\n    QPainterPath linePath;\n    linePath.moveTo(startX, h - (buffer->at(0) / m_maxValue) * h);\n    for (int i = 1; i < len; ++i) {\n        const qreal x = startX + i * stepX;\n        const qreal y = h - (buffer->at(i) / m_maxValue) * h;\n        linePath.lineTo(x, y);\n    }\n\n    // Stroke the line\n    QPen pen(color, m_lineWidth);\n    pen.setCapStyle(Qt::RoundCap);\n    pen.setJoinStyle(Qt::RoundJoin);\n    painter->setPen(pen);\n    painter->setBrush(Qt::NoBrush);\n    painter->drawPath(linePath);\n\n    // Fill under the line\n    QPainterPath fillPath = linePath;\n    fillPath.lineTo(startX + (len - 1) * stepX, h);\n    fillPath.lineTo(startX, h);\n    fillPath.closeSubpath();\n\n    QColor fillColor = color;\n    fillColor.setAlphaF(static_cast<float>(fillAlpha));\n    painter->setPen(Qt::NoPen);\n    painter->setBrush(fillColor);\n    painter->drawPath(fillPath);\n}\n\nvoid SparklineItem::connectBuffer(CircularBuffer* buffer) {\n    if (!buffer)\n        return;\n\n    connect(buffer, &CircularBuffer::valuesChanged, this, [this]() {\n        update();\n    });\n    connect(buffer, &QObject::destroyed, this, [this, buffer]() {\n        if (m_line1 == buffer) {\n            m_line1 = nullptr;\n            emit line1Changed();\n        }\n        if (m_line2 == buffer) {\n            m_line2 = nullptr;\n            emit line2Changed();\n        }\n        update();\n    });\n}\n\nCircularBuffer* SparklineItem::line1() const {\n    return m_line1;\n}\n\nvoid SparklineItem::setLine1(CircularBuffer* buffer) {\n    if (m_line1 == buffer)\n        return;\n    if (m_line1)\n        disconnect(m_line1, nullptr, this, nullptr);\n    m_line1 = buffer;\n    connectBuffer(buffer);\n    emit line1Changed();\n    update();\n}\n\nCircularBuffer* SparklineItem::line2() const {\n    return m_line2;\n}\n\nvoid SparklineItem::setLine2(CircularBuffer* buffer) {\n    if (m_line2 == buffer)\n        return;\n    if (m_line2)\n        disconnect(m_line2, nullptr, this, nullptr);\n    m_line2 = buffer;\n    connectBuffer(buffer);\n    emit line2Changed();\n    update();\n}\n\nQColor SparklineItem::line1Color() const {\n    return m_line1Color;\n}\n\nvoid SparklineItem::setLine1Color(const QColor& color) {\n    if (m_line1Color == color)\n        return;\n    m_line1Color = color;\n    emit line1ColorChanged();\n    update();\n}\n\nQColor SparklineItem::line2Color() const {\n    return m_line2Color;\n}\n\nvoid SparklineItem::setLine2Color(const QColor& color) {\n    if (m_line2Color == color)\n        return;\n    m_line2Color = color;\n    emit line2ColorChanged();\n    update();\n}\n\nqreal SparklineItem::line1FillAlpha() const {\n    return m_line1FillAlpha;\n}\n\nvoid SparklineItem::setLine1FillAlpha(qreal alpha) {\n    if (qFuzzyCompare(m_line1FillAlpha, alpha))\n        return;\n    m_line1FillAlpha = alpha;\n    emit line1FillAlphaChanged();\n    update();\n}\n\nqreal SparklineItem::line2FillAlpha() const {\n    return m_line2FillAlpha;\n}\n\nvoid SparklineItem::setLine2FillAlpha(qreal alpha) {\n    if (qFuzzyCompare(m_line2FillAlpha, alpha))\n        return;\n    m_line2FillAlpha = alpha;\n    emit line2FillAlphaChanged();\n    update();\n}\n\nqreal SparklineItem::maxValue() const {\n    return m_maxValue;\n}\n\nvoid SparklineItem::setMaxValue(qreal value) {\n    if (qFuzzyCompare(m_maxValue, value))\n        return;\n    m_maxValue = value;\n    emit maxValueChanged();\n    update();\n}\n\nqreal SparklineItem::slideProgress() const {\n    return m_slideProgress;\n}\n\nvoid SparklineItem::setSlideProgress(qreal progress) {\n    if (qFuzzyCompare(m_slideProgress, progress))\n        return;\n    m_slideProgress = progress;\n    emit slideProgressChanged();\n    update();\n}\n\nint SparklineItem::historyLength() const {\n    return m_historyLength;\n}\n\nvoid SparklineItem::setHistoryLength(int length) {\n    if (m_historyLength == length)\n        return;\n    m_historyLength = length;\n    emit historyLengthChanged();\n    update();\n}\n\nqreal SparklineItem::lineWidth() const {\n    return m_lineWidth;\n}\n\nvoid SparklineItem::setLineWidth(qreal width) {\n    if (qFuzzyCompare(m_lineWidth, width))\n        return;\n    m_lineWidth = width;\n    emit lineWidthChanged();\n    update();\n}\n\n} // namespace caelestia::internal\n"
  },
  {
    "path": "plugin/src/Caelestia/Internal/sparklineitem.hpp",
    "content": "#pragma once\n\n#include <qcolor.h>\n#include <qobject.h>\n#include <qqmlintegration.h>\n#include <qquickpainteditem.h>\n\n#include \"circularbuffer.hpp\"\n\nnamespace caelestia::internal {\n\nclass SparklineItem : public QQuickPaintedItem {\n    Q_OBJECT\n    QML_ELEMENT\n\n    Q_PROPERTY(CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY line1Changed)\n    Q_PROPERTY(CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY line2Changed)\n    Q_PROPERTY(QColor line1Color READ line1Color WRITE setLine1Color NOTIFY line1ColorChanged)\n    Q_PROPERTY(QColor line2Color READ line2Color WRITE setLine2Color NOTIFY line2ColorChanged)\n    Q_PROPERTY(qreal line1FillAlpha READ line1FillAlpha WRITE setLine1FillAlpha NOTIFY line1FillAlphaChanged)\n    Q_PROPERTY(qreal line2FillAlpha READ line2FillAlpha WRITE setLine2FillAlpha NOTIFY line2FillAlphaChanged)\n    Q_PROPERTY(qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged)\n    Q_PROPERTY(qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY slideProgressChanged)\n    Q_PROPERTY(int historyLength READ historyLength WRITE setHistoryLength NOTIFY historyLengthChanged)\n    Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged)\n\npublic:\n    explicit SparklineItem(QQuickItem* parent = nullptr);\n\n    void paint(QPainter* painter) override;\n\n    [[nodiscard]] CircularBuffer* line1() const;\n    void setLine1(CircularBuffer* buffer);\n\n    [[nodiscard]] CircularBuffer* line2() const;\n    void setLine2(CircularBuffer* buffer);\n\n    [[nodiscard]] QColor line1Color() const;\n    void setLine1Color(const QColor& color);\n\n    [[nodiscard]] QColor line2Color() const;\n    void setLine2Color(const QColor& color);\n\n    [[nodiscard]] qreal line1FillAlpha() const;\n    void setLine1FillAlpha(qreal alpha);\n\n    [[nodiscard]] qreal line2FillAlpha() const;\n    void setLine2FillAlpha(qreal alpha);\n\n    [[nodiscard]] qreal maxValue() const;\n    void setMaxValue(qreal value);\n\n    [[nodiscard]] qreal slideProgress() const;\n    void setSlideProgress(qreal progress);\n\n    [[nodiscard]] int historyLength() const;\n    void setHistoryLength(int length);\n\n    [[nodiscard]] qreal lineWidth() const;\n    void setLineWidth(qreal width);\n\nsignals:\n    void line1Changed();\n    void line2Changed();\n    void line1ColorChanged();\n    void line2ColorChanged();\n    void line1FillAlphaChanged();\n    void line2FillAlphaChanged();\n    void maxValueChanged();\n    void slideProgressChanged();\n    void historyLengthChanged();\n    void lineWidthChanged();\n\nprivate:\n    void drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha);\n    void connectBuffer(CircularBuffer* buffer);\n\n    CircularBuffer* m_line1 = nullptr;\n    CircularBuffer* m_line2 = nullptr;\n    QColor m_line1Color;\n    QColor m_line2Color;\n    qreal m_line1FillAlpha = 0.15;\n    qreal m_line2FillAlpha = 0.2;\n    qreal m_maxValue = 1024.0;\n    qreal m_slideProgress = 0.0;\n    int m_historyLength = 30;\n    qreal m_lineWidth = 2.0;\n};\n\n} // namespace caelestia::internal\n"
  },
  {
    "path": "plugin/src/Caelestia/Models/CMakeLists.txt",
    "content": "qml_module(caelestia-models\n    URI Caelestia.Models\n    SOURCES\n        filesystemmodel.hpp filesystemmodel.cpp\n    LIBRARIES\n        Qt::Gui\n        Qt::Concurrent\n)\n"
  },
  {
    "path": "plugin/src/Caelestia/Models/filesystemmodel.cpp",
    "content": "#include \"filesystemmodel.hpp\"\n\n#include <qdiriterator.h>\n#include <qfuturewatcher.h>\n#include <qtconcurrentrun.h>\n\nnamespace caelestia::models {\n\nFileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent)\n    : QObject(parent)\n    , m_fileInfo(path)\n    , m_path(path)\n    , m_relativePath(relativePath)\n    , m_isImageInitialised(false)\n    , m_mimeTypeInitialised(false) {}\n\nQString FileSystemEntry::path() const {\n    return m_path;\n};\n\nQString FileSystemEntry::relativePath() const {\n    return m_relativePath;\n};\n\nQString FileSystemEntry::name() const {\n    return m_fileInfo.fileName();\n};\n\nQString FileSystemEntry::baseName() const {\n    return m_fileInfo.baseName();\n};\n\nQString FileSystemEntry::parentDir() const {\n    return m_fileInfo.absolutePath();\n};\n\nQString FileSystemEntry::suffix() const {\n    return m_fileInfo.completeSuffix();\n};\n\nqint64 FileSystemEntry::size() const {\n    return m_fileInfo.size();\n};\n\nbool FileSystemEntry::isDir() const {\n    return m_fileInfo.isDir();\n};\n\nbool FileSystemEntry::isImage() const {\n    if (!m_isImageInitialised) {\n        QImageReader reader(m_path);\n        m_isImage = reader.canRead();\n        m_isImageInitialised = true;\n    }\n    return m_isImage;\n}\n\nQString FileSystemEntry::mimeType() const {\n    if (!m_mimeTypeInitialised) {\n        static const QMimeDatabase s_db;\n        m_mimeType = s_db.mimeTypeForFile(m_path).name();\n        m_mimeTypeInitialised = true;\n    }\n    return m_mimeType;\n}\n\nvoid FileSystemEntry::updateRelativePath(const QDir& dir) {\n    const auto relPath = dir.relativeFilePath(m_path);\n    if (m_relativePath != relPath) {\n        m_relativePath = relPath;\n        emit relativePathChanged();\n    }\n}\n\nFileSystemModel::FileSystemModel(QObject* parent)\n    : QAbstractListModel(parent)\n    , m_recursive(false)\n    , m_watchChanges(true)\n    , m_showHidden(false)\n    , m_filter(NoFilter) {\n    connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive);\n    connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir);\n}\n\nint FileSystemModel::rowCount(const QModelIndex& parent) const {\n    if (parent != QModelIndex()) {\n        return 0;\n    }\n    return static_cast<int>(m_entries.size());\n}\n\nQVariant FileSystemModel::data(const QModelIndex& index, int role) const {\n    if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) {\n        return QVariant();\n    }\n    return QVariant::fromValue(m_entries.at(index.row()));\n}\n\nQHash<int, QByteArray> FileSystemModel::roleNames() const {\n    return { { Qt::UserRole, \"modelData\" } };\n}\n\nQString FileSystemModel::path() const {\n    return m_path;\n}\n\nvoid FileSystemModel::setPath(const QString& path) {\n    if (m_path == path) {\n        return;\n    }\n\n    m_path = path;\n    emit pathChanged();\n\n    m_dir.setPath(m_path);\n\n    for (const auto& entry : std::as_const(m_entries)) {\n        entry->updateRelativePath(m_dir);\n    }\n\n    update();\n}\n\nbool FileSystemModel::recursive() const {\n    return m_recursive;\n}\n\nvoid FileSystemModel::setRecursive(bool recursive) {\n    if (m_recursive == recursive) {\n        return;\n    }\n\n    m_recursive = recursive;\n    emit recursiveChanged();\n\n    update();\n}\n\nbool FileSystemModel::watchChanges() const {\n    return m_watchChanges;\n}\n\nvoid FileSystemModel::setWatchChanges(bool watchChanges) {\n    if (m_watchChanges == watchChanges) {\n        return;\n    }\n\n    m_watchChanges = watchChanges;\n    emit watchChangesChanged();\n\n    update();\n}\n\nbool FileSystemModel::showHidden() const {\n    return m_showHidden;\n}\n\nvoid FileSystemModel::setShowHidden(bool showHidden) {\n    if (m_showHidden == showHidden) {\n        return;\n    }\n\n    m_showHidden = showHidden;\n    emit showHiddenChanged();\n\n    update();\n}\n\nbool FileSystemModel::sortReverse() const {\n    return m_sortReverse;\n}\n\nvoid FileSystemModel::setSortReverse(bool sortReverse) {\n    if (m_sortReverse == sortReverse) {\n        return;\n    }\n\n    m_sortReverse = sortReverse;\n    emit sortReverseChanged();\n\n    update();\n}\n\nFileSystemModel::Filter FileSystemModel::filter() const {\n    return m_filter;\n}\n\nvoid FileSystemModel::setFilter(Filter filter) {\n    if (m_filter == filter) {\n        return;\n    }\n\n    m_filter = filter;\n    emit filterChanged();\n\n    update();\n}\n\nQStringList FileSystemModel::nameFilters() const {\n    return m_nameFilters;\n}\n\nvoid FileSystemModel::setNameFilters(const QStringList& nameFilters) {\n    if (m_nameFilters == nameFilters) {\n        return;\n    }\n\n    m_nameFilters = nameFilters;\n    emit nameFiltersChanged();\n\n    update();\n}\n\nQQmlListProperty<FileSystemEntry> FileSystemModel::entries() {\n    return QQmlListProperty<FileSystemEntry>(this, &m_entries);\n}\n\nvoid FileSystemModel::watchDirIfRecursive(const QString& path) {\n    if (m_recursive && m_watchChanges) {\n        const auto currentDir = m_dir;\n        const bool showHidden = m_showHidden;\n        const auto future = QtConcurrent::run([showHidden, path]() {\n            QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot;\n            if (showHidden) {\n                filters |= QDir::Hidden;\n            }\n\n            QDirIterator iter(path, filters, QDirIterator::Subdirectories);\n            QStringList dirs;\n            while (iter.hasNext()) {\n                dirs << iter.next();\n            }\n            return dirs;\n        });\n        const auto watcher = new QFutureWatcher<QStringList>(this);\n        connect(watcher, &QFutureWatcher<QStringList>::finished, this, [currentDir, showHidden, watcher, this]() {\n            const auto paths = watcher->result();\n            if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) {\n                // Ignore if dir or showHidden has changed\n                m_watcher.addPaths(paths);\n            }\n            watcher->deleteLater();\n        });\n        watcher->setFuture(future);\n    }\n}\n\nvoid FileSystemModel::update() {\n    updateWatcher();\n    updateEntries();\n}\n\nvoid FileSystemModel::updateWatcher() {\n    if (!m_watcher.directories().isEmpty()) {\n        m_watcher.removePaths(m_watcher.directories());\n    }\n\n    if (!m_watchChanges || m_path.isEmpty()) {\n        return;\n    }\n\n    m_watcher.addPath(m_path);\n    watchDirIfRecursive(m_path);\n}\n\nvoid FileSystemModel::updateEntries() {\n    if (m_path.isEmpty()) {\n        if (!m_entries.isEmpty()) {\n            beginResetModel();\n            qDeleteAll(m_entries);\n            m_entries.clear();\n            endResetModel();\n            emit entriesChanged();\n        }\n\n        return;\n    }\n\n    for (auto& future : m_futures) {\n        future.cancel();\n    }\n    m_futures.clear();\n\n    updateEntriesForDir(m_path);\n}\n\nvoid FileSystemModel::updateEntriesForDir(const QString& dir) {\n    const auto recursive = m_recursive;\n    const auto showHidden = m_showHidden;\n    const auto filter = m_filter;\n    const auto nameFilters = m_nameFilters;\n\n    QSet<QString> oldPaths;\n    for (const auto& entry : std::as_const(m_entries)) {\n        oldPaths << entry->path();\n    }\n\n    const auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {\n        const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;\n\n        std::optional<QDirIterator> iter;\n\n        if (filter == Images) {\n            QStringList extraNameFilters = nameFilters;\n            const auto formats = QImageReader::supportedImageFormats();\n            for (const auto& format : formats) {\n                extraNameFilters << \"*.\" + format;\n            }\n\n            QDir::Filters filters = QDir::Files;\n            if (showHidden) {\n                filters |= QDir::Hidden;\n            }\n\n            iter.emplace(dir, extraNameFilters, filters, flags);\n        } else {\n            QDir::Filters filters;\n\n            if (filter == Files) {\n                filters = QDir::Files;\n            } else if (filter == Dirs) {\n                filters = QDir::Dirs | QDir::NoDotAndDotDot;\n            } else {\n                filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot;\n            }\n\n            if (showHidden) {\n                filters |= QDir::Hidden;\n            }\n\n            if (nameFilters.isEmpty()) {\n                iter.emplace(dir, filters, flags);\n            } else {\n                iter.emplace(dir, nameFilters, filters, flags);\n            }\n        }\n\n        QSet<QString> newPaths;\n        while (iter->hasNext()) {\n            if (promise.isCanceled()) {\n                return;\n            }\n\n            QString path = iter->next();\n\n            if (filter == Images) {\n                QImageReader reader(path);\n                if (!reader.canRead()) {\n                    continue;\n                }\n            }\n\n            newPaths.insert(path);\n        }\n\n        if (promise.isCanceled() || newPaths == oldPaths) {\n            return;\n        }\n\n        promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths));\n    });\n\n    if (m_futures.contains(dir)) {\n        m_futures[dir].cancel();\n    }\n    m_futures.insert(dir, future);\n\n    const auto watcher = new QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>(this);\n\n    connect(watcher, &QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>::finished, this, [dir, watcher, this]() {\n        m_futures.remove(dir);\n\n        if (!watcher->future().isResultReadyAt(0)) {\n            watcher->deleteLater();\n            return;\n        }\n\n        const auto result = watcher->result();\n        applyChanges(result.first, result.second);\n\n        watcher->deleteLater();\n    });\n\n    watcher->setFuture(future);\n}\n\nvoid FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) {\n    QList<int> removedIndices;\n    for (int i = 0; i < m_entries.size(); ++i) {\n        if (removedPaths.contains(m_entries[i]->path())) {\n            removedIndices << i;\n        }\n    }\n    std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());\n\n    // Batch remove old entries\n    int start = -1;\n    int end = -1;\n    for (int idx : std::as_const(removedIndices)) {\n        if (start == -1) {\n            start = idx;\n            end = idx;\n        } else if (idx == end - 1) {\n            end = idx;\n        } else {\n            beginRemoveRows(QModelIndex(), end, start);\n            for (int i = start; i >= end; --i) {\n                m_entries.takeAt(i)->deleteLater();\n            }\n            endRemoveRows();\n\n            start = idx;\n            end = idx;\n        }\n    }\n    if (start != -1) {\n        beginRemoveRows(QModelIndex(), end, start);\n        for (int i = start; i >= end; --i) {\n            m_entries.takeAt(i)->deleteLater();\n        }\n        endRemoveRows();\n    }\n\n    // Create new entries\n    QList<FileSystemEntry*> newEntries;\n    for (const auto& path : addedPaths) {\n        newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this);\n    }\n    std::sort(newEntries.begin(), newEntries.end(), [this](const FileSystemEntry* a, const FileSystemEntry* b) {\n        return compareEntries(a, b);\n    });\n\n    // Batch insert new entries\n    int insertStart = -1;\n    QList<FileSystemEntry*> batchItems;\n    for (const auto& entry : std::as_const(newEntries)) {\n        const auto it = std::lower_bound(\n            m_entries.begin(), m_entries.end(), entry, [this](const FileSystemEntry* a, const FileSystemEntry* b) {\n                return compareEntries(a, b);\n            });\n        const auto row = static_cast<int>(it - m_entries.begin());\n\n        if (insertStart == -1) {\n            insertStart = row;\n            batchItems << entry;\n        } else if (row == insertStart + batchItems.size()) {\n            batchItems << entry;\n        } else {\n            beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);\n            for (int i = 0; i < batchItems.size(); ++i) {\n                m_entries.insert(insertStart + i, batchItems[i]);\n            }\n            endInsertRows();\n\n            insertStart = row;\n            batchItems.clear();\n            batchItems << entry;\n        }\n    }\n    if (!batchItems.isEmpty()) {\n        beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);\n        for (int i = 0; i < batchItems.size(); ++i) {\n            m_entries.insert(insertStart + i, batchItems[i]);\n        }\n        endInsertRows();\n    }\n\n    emit entriesChanged();\n}\n\nbool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const {\n    if (a->isDir() != b->isDir()) {\n        return m_sortReverse ^ a->isDir();\n    }\n    const auto cmp = a->relativePath().localeAwareCompare(b->relativePath());\n    return m_sortReverse ? cmp > 0 : cmp < 0;\n}\n\n} // namespace caelestia::models\n"
  },
  {
    "path": "plugin/src/Caelestia/Models/filesystemmodel.hpp",
    "content": "#pragma once\n\n#include <qabstractitemmodel.h>\n#include <qdir.h>\n#include <qfilesystemwatcher.h>\n#include <qfuture.h>\n#include <qimagereader.h>\n#include <qmimedatabase.h>\n#include <qobject.h>\n#include <qqmlintegration.h>\n#include <qqmllist.h>\n\nnamespace caelestia::models {\n\nclass FileSystemEntry : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n    QML_UNCREATABLE(\"FileSystemEntry instances can only be retrieved from a FileSystemModel\")\n\n    Q_PROPERTY(QString path READ path CONSTANT)\n    Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)\n    Q_PROPERTY(QString name READ name CONSTANT)\n    Q_PROPERTY(QString baseName READ baseName CONSTANT)\n    Q_PROPERTY(QString parentDir READ parentDir CONSTANT)\n    Q_PROPERTY(QString suffix READ suffix CONSTANT)\n    Q_PROPERTY(qint64 size READ size CONSTANT)\n    Q_PROPERTY(bool isDir READ isDir CONSTANT)\n    Q_PROPERTY(bool isImage READ isImage CONSTANT)\n    Q_PROPERTY(QString mimeType READ mimeType CONSTANT)\n\npublic:\n    explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr);\n\n    [[nodiscard]] QString path() const;\n    [[nodiscard]] QString relativePath() const;\n    [[nodiscard]] QString name() const;\n    [[nodiscard]] QString baseName() const;\n    [[nodiscard]] QString parentDir() const;\n    [[nodiscard]] QString suffix() const;\n    [[nodiscard]] qint64 size() const;\n    [[nodiscard]] bool isDir() const;\n    [[nodiscard]] bool isImage() const;\n    [[nodiscard]] QString mimeType() const;\n\n    void updateRelativePath(const QDir& dir);\n\nsignals:\n    void relativePathChanged();\n\nprivate:\n    const QFileInfo m_fileInfo;\n\n    const QString m_path;\n    QString m_relativePath;\n\n    mutable bool m_isImage;\n    mutable bool m_isImageInitialised;\n\n    mutable QString m_mimeType;\n    mutable bool m_mimeTypeInitialised;\n};\n\nclass FileSystemModel : public QAbstractListModel {\n    Q_OBJECT\n    QML_ELEMENT\n\n    Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)\n    Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged)\n    Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged)\n    Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged)\n    Q_PROPERTY(bool sortReverse READ sortReverse WRITE setSortReverse NOTIFY sortReverseChanged)\n    Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged)\n    Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged)\n\n    Q_PROPERTY(QQmlListProperty<caelestia::models::FileSystemEntry> entries READ entries NOTIFY entriesChanged)\n\npublic:\n    enum Filter {\n        NoFilter,\n        Images,\n        Files,\n        Dirs\n    };\n    Q_ENUM(Filter)\n\n    explicit FileSystemModel(QObject* parent = nullptr);\n\n    int rowCount(const QModelIndex& parent = QModelIndex()) const override;\n    QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;\n    QHash<int, QByteArray> roleNames() const override;\n\n    [[nodiscard]] QString path() const;\n    void setPath(const QString& path);\n\n    [[nodiscard]] bool recursive() const;\n    void setRecursive(bool recursive);\n\n    [[nodiscard]] bool watchChanges() const;\n    void setWatchChanges(bool watchChanges);\n\n    [[nodiscard]] bool showHidden() const;\n    void setShowHidden(bool showHidden);\n\n    [[nodiscard]] bool sortReverse() const;\n    void setSortReverse(bool sortReverse);\n\n    [[nodiscard]] Filter filter() const;\n    void setFilter(Filter filter);\n\n    [[nodiscard]] QStringList nameFilters() const;\n    void setNameFilters(const QStringList& nameFilters);\n\n    [[nodiscard]] QQmlListProperty<FileSystemEntry> entries();\n\nsignals:\n    void pathChanged();\n    void recursiveChanged();\n    void watchChangesChanged();\n    void showHiddenChanged();\n    void sortReverseChanged();\n    void filterChanged();\n    void nameFiltersChanged();\n    void entriesChanged();\n\nprivate:\n    QDir m_dir;\n    QFileSystemWatcher m_watcher;\n    QList<FileSystemEntry*> m_entries;\n    QHash<QString, QFuture<QPair<QSet<QString>, QSet<QString>>>> m_futures;\n\n    QString m_path;\n    bool m_recursive;\n    bool m_watchChanges;\n    bool m_showHidden;\n    bool m_sortReverse;\n    Filter m_filter;\n    QStringList m_nameFilters;\n\n    void watchDirIfRecursive(const QString& path);\n    void update();\n    void updateWatcher();\n    void updateEntries();\n    void updateEntriesForDir(const QString& dir);\n    void applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);\n    [[nodiscard]] bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const;\n};\n\n} // namespace caelestia::models\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/CMakeLists.txt",
    "content": "qml_module(caelestia-services\n    URI Caelestia.Services\n    SOURCES\n        service.hpp service.cpp\n        serviceref.hpp serviceref.cpp\n        beattracker.hpp beattracker.cpp\n        audiocollector.hpp audiocollector.cpp\n        audioprovider.hpp audioprovider.cpp\n        cavaprovider.hpp cavaprovider.cpp\n    LIBRARIES\n        PkgConfig::Pipewire\n        PkgConfig::Aubio\n        PkgConfig::Cava\n)\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/audiocollector.cpp",
    "content": "#include \"audiocollector.hpp\"\n\n#include \"service.hpp\"\n#include <algorithm>\n#include <pipewire/pipewire.h>\n#include <qdebug.h>\n#include <qmutex.h>\n#include <spa/param/audio/format-utils.h>\n#include <spa/param/latency-utils.h>\n#include <stop_token>\n#include <vector>\n\nnamespace caelestia::services {\n\nPipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector)\n    : m_loop(nullptr)\n    , m_stream(nullptr)\n    , m_timer(nullptr)\n    , m_idle(true)\n    , m_token(token)\n    , m_collector(collector) {\n    pw_init(nullptr, nullptr);\n\n    m_loop = pw_main_loop_new(nullptr);\n    if (!m_loop) {\n        qWarning() << \"PipeWireWorker::init: failed to create PipeWire main loop\";\n        pw_deinit();\n        return;\n    }\n\n    timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC };\n    m_timer = pw_loop_add_timer(pw_main_loop_get_loop(m_loop), handleTimeout, this);\n    pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false);\n\n    auto props = pw_properties_new(\n        PW_KEY_MEDIA_TYPE, \"Audio\", PW_KEY_MEDIA_CATEGORY, \"Capture\", PW_KEY_MEDIA_ROLE, \"Music\", nullptr);\n    pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, \"true\");\n    pw_properties_setf(\n        props, PW_KEY_NODE_LATENCY, \"%u/%u\", nextPowerOf2(512 * ac::SAMPLE_RATE / 48000), ac::SAMPLE_RATE);\n    pw_properties_set(props, PW_KEY_NODE_PASSIVE, \"true\");\n    pw_properties_set(props, PW_KEY_NODE_VIRTUAL, \"true\");\n    pw_properties_set(props, PW_KEY_STREAM_DONT_REMIX, \"false\");\n    pw_properties_set(props, \"channelmix.upmix\", \"true\");\n\n    std::vector<uint8_t> buffer(ac::CHUNK_SIZE);\n    spa_pod_builder b;\n    spa_pod_builder_init(&b, buffer.data(), static_cast<quint32>(buffer.size()));\n\n    spa_audio_info_raw info{};\n    info.format = SPA_AUDIO_FORMAT_S16;\n    info.rate = ac::SAMPLE_RATE;\n    info.channels = 1;\n\n    const spa_pod* params[1];\n    params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info);\n\n    pw_stream_events events{};\n    events.state_changed = [](void* data, pw_stream_state, pw_stream_state state, const char*) {\n        auto* self = static_cast<PipeWireWorker*>(data);\n        self->streamStateChanged(state);\n    };\n    events.process = [](void* data) {\n        auto* self = static_cast<PipeWireWorker*>(data);\n        self->processStream();\n    };\n\n    m_stream = pw_stream_new_simple(pw_main_loop_get_loop(m_loop), \"caelestia-shell\", props, &events, this);\n\n    const int success = pw_stream_connect(m_stream, PW_DIRECTION_INPUT, PW_ID_ANY,\n        static_cast<pw_stream_flags>(\n            PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS),\n        params, 1);\n    if (success < 0) {\n        qWarning() << \"PipeWireWorker::init: failed to connect stream\";\n        pw_stream_destroy(m_stream);\n        pw_main_loop_destroy(m_loop);\n        pw_deinit();\n        return;\n    }\n\n    pw_main_loop_run(m_loop);\n\n    pw_stream_destroy(m_stream);\n    pw_main_loop_destroy(m_loop);\n    pw_deinit();\n}\n\nvoid PipeWireWorker::handleTimeout(void* data, uint64_t expirations) {\n    auto* self = static_cast<PipeWireWorker*>(data);\n\n    if (self->m_token.stop_requested()) {\n        pw_main_loop_quit(self->m_loop);\n        return;\n    }\n\n    if (!self->m_idle) {\n        if (expirations < 10) {\n            self->m_collector->clearBuffer();\n        } else {\n            self->m_idle = true;\n            timespec timeout = { 0, 500 * SPA_NSEC_PER_MSEC };\n            pw_loop_update_timer(pw_main_loop_get_loop(self->m_loop), self->m_timer, &timeout, &timeout, false);\n        }\n    }\n}\n\nvoid PipeWireWorker::streamStateChanged(pw_stream_state state) {\n    m_idle = false;\n    switch (state) {\n    case PW_STREAM_STATE_PAUSED: {\n        timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC };\n        pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false);\n        break;\n    }\n    case PW_STREAM_STATE_STREAMING:\n        pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, nullptr, nullptr, false);\n        break;\n    case PW_STREAM_STATE_ERROR:\n        pw_main_loop_quit(m_loop);\n        break;\n    default:\n        break;\n    }\n}\n\nvoid PipeWireWorker::processStream() {\n    if (m_token.stop_requested()) {\n        pw_main_loop_quit(m_loop);\n        return;\n    }\n\n    pw_buffer* buffer = pw_stream_dequeue_buffer(m_stream);\n    if (buffer == nullptr) {\n        return;\n    }\n\n    const spa_buffer* buf = buffer->buffer;\n    const qint16* samples = reinterpret_cast<const qint16*>(buf->datas[0].data);\n    if (samples == nullptr) {\n        return;\n    }\n\n    const quint32 count = buf->datas[0].chunk->size / 2;\n    m_collector->loadChunk(samples, count);\n\n    pw_stream_queue_buffer(m_stream, buffer);\n}\n\nunsigned int PipeWireWorker::nextPowerOf2(unsigned int n) {\n    if (n == 0) {\n        return 1;\n    }\n\n    n--;\n    n |= n >> 1;\n    n |= n >> 2;\n    n |= n >> 4;\n    n |= n >> 8;\n    n |= n >> 16;\n    n++;\n\n    return n;\n}\n\nAudioCollector& AudioCollector::instance() {\n    static AudioCollector instance;\n    return instance;\n}\n\nvoid AudioCollector::clearBuffer() {\n    auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed);\n    std::fill(writeBuffer->begin(), writeBuffer->end(), 0.0f);\n\n    auto* oldRead = m_readBuffer.exchange(writeBuffer, std::memory_order_acq_rel);\n    m_writeBuffer.store(oldRead, std::memory_order_release);\n}\n\nvoid AudioCollector::loadChunk(const qint16* samples, quint32 count) {\n    if (count > ac::CHUNK_SIZE) {\n        count = ac::CHUNK_SIZE;\n    }\n\n    auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed);\n    std::transform(samples, samples + count, writeBuffer->begin(), [](qint16 sample) {\n        return sample / 32768.0f;\n    });\n\n    auto* oldRead = m_readBuffer.exchange(writeBuffer, std::memory_order_acq_rel);\n    m_writeBuffer.store(oldRead, std::memory_order_release);\n}\n\nquint32 AudioCollector::readChunk(float* out, quint32 count) {\n    if (count == 0 || count > ac::CHUNK_SIZE) {\n        count = ac::CHUNK_SIZE;\n    }\n\n    auto* readBuffer = m_readBuffer.load(std::memory_order_acquire);\n    std::memcpy(out, readBuffer->data(), count * sizeof(float));\n\n    return count;\n}\n\nquint32 AudioCollector::readChunk(double* out, quint32 count) {\n    if (count == 0 || count > ac::CHUNK_SIZE) {\n        count = ac::CHUNK_SIZE;\n    }\n\n    auto* readBuffer = m_readBuffer.load(std::memory_order_acquire);\n    std::transform(readBuffer->begin(), readBuffer->begin() + count, out, [](float sample) {\n        return static_cast<double>(sample);\n    });\n\n    return count;\n}\n\nAudioCollector::AudioCollector(QObject* parent)\n    : Service(parent)\n    , m_buffer1(ac::CHUNK_SIZE)\n    , m_buffer2(ac::CHUNK_SIZE)\n    , m_readBuffer(&m_buffer1)\n    , m_writeBuffer(&m_buffer2) {}\n\nAudioCollector::~AudioCollector() {\n    stop();\n}\n\nvoid AudioCollector::start() {\n    if (m_thread.joinable()) {\n        return;\n    }\n\n    clearBuffer();\n\n    m_thread = std::jthread([this](std::stop_token token) {\n        PipeWireWorker worker(token, this);\n    });\n}\n\nvoid AudioCollector::stop() {\n    if (m_thread.joinable()) {\n        m_thread.request_stop();\n        m_thread.join();\n    }\n}\n\n} // namespace caelestia::services\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/audiocollector.hpp",
    "content": "#pragma once\n\n#include \"service.hpp\"\n#include <atomic>\n#include <pipewire/pipewire.h>\n#include <qmutex.h>\n#include <qqmlintegration.h>\n#include <spa/param/audio/format-utils.h>\n#include <stop_token>\n#include <thread>\n#include <vector>\n\nnamespace caelestia::services {\n\nnamespace ac {\n\nconstexpr quint32 SAMPLE_RATE = 44100;\nconstexpr quint32 CHUNK_SIZE = 512;\n\n} // namespace ac\n\nclass AudioCollector;\n\nclass PipeWireWorker {\npublic:\n    explicit PipeWireWorker(std::stop_token token, AudioCollector* collector);\n\n    void run();\n\nprivate:\n    pw_main_loop* m_loop;\n    pw_stream* m_stream;\n    spa_source* m_timer;\n    bool m_idle;\n\n    std::stop_token m_token;\n    AudioCollector* m_collector;\n\n    static void handleTimeout(void* data, uint64_t expirations);\n    void streamStateChanged(pw_stream_state state);\n    void processStream();\n\n    [[nodiscard]] unsigned int nextPowerOf2(unsigned int n);\n};\n\nclass AudioCollector : public Service {\n    Q_OBJECT\n\npublic:\n    AudioCollector(const AudioCollector&) = delete;\n    AudioCollector& operator=(const AudioCollector&) = delete;\n\n    static AudioCollector& instance();\n\n    void clearBuffer();\n    void loadChunk(const qint16* samples, quint32 count);\n    quint32 readChunk(float* out, quint32 count = 0);\n    quint32 readChunk(double* out, quint32 count = 0);\n\nprivate:\n    explicit AudioCollector(QObject* parent = nullptr);\n    ~AudioCollector();\n\n    std::jthread m_thread;\n    std::vector<float> m_buffer1;\n    std::vector<float> m_buffer2;\n    std::atomic<std::vector<float>*> m_readBuffer;\n    std::atomic<std::vector<float>*> m_writeBuffer;\n    quint32 m_sampleCount;\n\n    void reload();\n    void start() override;\n    void stop() override;\n};\n\n} // namespace caelestia::services\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/audioprovider.cpp",
    "content": "#include \"audioprovider.hpp\"\n\n#include \"audiocollector.hpp\"\n#include \"service.hpp\"\n#include <qdebug.h>\n#include <qthread.h>\n\nnamespace caelestia::services {\n\nAudioProcessor::AudioProcessor(QObject* parent)\n    : QObject(parent) {}\n\nAudioProcessor::~AudioProcessor() {\n    stop();\n}\n\nvoid AudioProcessor::init() {\n    m_timer = new QTimer(this);\n    m_timer->setInterval(static_cast<int>(ac::CHUNK_SIZE * 1000.0 / ac::SAMPLE_RATE));\n    connect(m_timer, &QTimer::timeout, this, &AudioProcessor::process);\n}\n\nvoid AudioProcessor::start() {\n    QMetaObject::invokeMethod(&AudioCollector::instance(), &AudioCollector::ref, Qt::QueuedConnection, this);\n    if (m_timer) {\n        m_timer->start();\n    }\n}\n\nvoid AudioProcessor::stop() {\n    if (m_timer) {\n        m_timer->stop();\n    }\n    QMetaObject::invokeMethod(&AudioCollector::instance(), &AudioCollector::unref, Qt::QueuedConnection, this);\n}\n\nAudioProvider::AudioProvider(QObject* parent)\n    : Service(parent)\n    , m_processor(nullptr)\n    , m_thread(nullptr) {}\n\nAudioProvider::~AudioProvider() {\n    if (m_thread) {\n        m_thread->quit();\n        m_thread->wait();\n    }\n}\n\nvoid AudioProvider::init() {\n    if (!m_processor) {\n        qWarning() << \"AudioProvider::init: attempted to init with no processor set\";\n        return;\n    }\n\n    m_thread = new QThread(this);\n    m_processor->moveToThread(m_thread);\n\n    connect(m_thread, &QThread::started, m_processor, &AudioProcessor::init);\n    connect(m_thread, &QThread::finished, m_processor, &AudioProcessor::deleteLater);\n    connect(m_thread, &QThread::finished, m_thread, &QThread::deleteLater);\n\n    m_thread->start();\n}\n\nvoid AudioProvider::start() {\n    if (m_processor) {\n        AudioCollector::instance(); // Create instance on main thread\n        QMetaObject::invokeMethod(m_processor, &AudioProcessor::start);\n    }\n}\n\nvoid AudioProvider::stop() {\n    if (m_processor) {\n        QMetaObject::invokeMethod(m_processor, &AudioProcessor::stop);\n    }\n}\n\n} // namespace caelestia::services\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/audioprovider.hpp",
    "content": "#pragma once\n\n#include \"service.hpp\"\n#include <qqmlintegration.h>\n#include <qtimer.h>\n\nnamespace caelestia::services {\n\nclass AudioProcessor : public QObject {\n    Q_OBJECT\n\npublic:\n    explicit AudioProcessor(QObject* parent = nullptr);\n    ~AudioProcessor();\n\n    void init();\n\npublic slots:\n    void start();\n    void stop();\n\nprotected:\n    virtual void process() = 0;\n\nprivate:\n    QTimer* m_timer = nullptr;\n};\n\nclass AudioProvider : public Service {\n    Q_OBJECT\n\npublic:\n    explicit AudioProvider(QObject* parent = nullptr);\n    ~AudioProvider();\n\nprotected:\n    AudioProcessor* m_processor;\n\n    void init();\n\nprivate:\n    QThread* m_thread;\n\n    void start() override;\n    void stop() override;\n};\n\n} // namespace caelestia::services\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/beattracker.cpp",
    "content": "#include \"beattracker.hpp\"\n\n#include \"audiocollector.hpp\"\n#include \"audioprovider.hpp\"\n#include <aubio/aubio.h>\n\nnamespace caelestia::services {\n\nBeatProcessor::BeatProcessor(QObject* parent)\n    : AudioProcessor(parent)\n    , m_tempo(new_aubio_tempo(\"default\", 1024, ac::CHUNK_SIZE, ac::SAMPLE_RATE))\n    , m_in(new_fvec(ac::CHUNK_SIZE))\n    , m_out(new_fvec(2)) {};\n\nBeatProcessor::~BeatProcessor() {\n    if (m_tempo) {\n        del_aubio_tempo(m_tempo);\n    }\n    if (m_in) {\n        del_fvec(m_in);\n    }\n    if (m_out) {\n        del_fvec(m_out);\n    }\n}\n\nvoid BeatProcessor::process() {\n    if (!m_tempo || !m_in) {\n        return;\n    }\n\n    AudioCollector::instance().readChunk(m_in->data);\n\n    aubio_tempo_do(m_tempo, m_in, m_out);\n    if (!qFuzzyIsNull(m_out->data[0])) {\n        emit beat(aubio_tempo_get_bpm(m_tempo));\n    }\n}\n\nBeatTracker::BeatTracker(QObject* parent)\n    : AudioProvider(parent)\n    , m_bpm(120) {\n    m_processor = new BeatProcessor();\n    init();\n\n    connect(static_cast<BeatProcessor*>(m_processor), &BeatProcessor::beat, this, &BeatTracker::updateBpm);\n}\n\nsmpl_t BeatTracker::bpm() const {\n    return m_bpm;\n}\n\nvoid BeatTracker::updateBpm(smpl_t bpm) {\n    if (!qFuzzyCompare(bpm + 1.0f, m_bpm + 1.0f)) {\n        m_bpm = bpm;\n        emit bpmChanged();\n    }\n}\n\n} // namespace caelestia::services\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/beattracker.hpp",
    "content": "#pragma once\n\n#include \"audioprovider.hpp\"\n#include <aubio/aubio.h>\n#include <qqmlintegration.h>\n\nnamespace caelestia::services {\n\nclass BeatProcessor : public AudioProcessor {\n    Q_OBJECT\n\npublic:\n    explicit BeatProcessor(QObject* parent = nullptr);\n    ~BeatProcessor();\n\nsignals:\n    void beat(smpl_t bpm);\n\nprotected:\n    void process() override;\n\nprivate:\n    aubio_tempo_t* m_tempo;\n    fvec_t* m_in;\n    fvec_t* m_out;\n};\n\nclass BeatTracker : public AudioProvider {\n    Q_OBJECT\n    QML_ELEMENT\n\n    Q_PROPERTY(smpl_t bpm READ bpm NOTIFY bpmChanged)\n\npublic:\n    explicit BeatTracker(QObject* parent = nullptr);\n\n    [[nodiscard]] smpl_t bpm() const;\n\nsignals:\n    void bpmChanged();\n    void beat(smpl_t bpm);\n\nprivate:\n    smpl_t m_bpm;\n\n    void updateBpm(smpl_t bpm);\n};\n\n} // namespace caelestia::services\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/cavaprovider.cpp",
    "content": "#include \"cavaprovider.hpp\"\n\n#include \"audiocollector.hpp\"\n#include \"audioprovider.hpp\"\n#include <cava/cavacore.h>\n#include <cstddef>\n#include <qdebug.h>\n\nnamespace caelestia::services {\n\nCavaProcessor::CavaProcessor(QObject* parent)\n    : AudioProcessor(parent)\n    , m_plan(nullptr)\n    , m_in(new double[ac::CHUNK_SIZE])\n    , m_out(nullptr)\n    , m_bars(0) {};\n\nCavaProcessor::~CavaProcessor() {\n    cleanup();\n    delete[] m_in;\n}\n\nvoid CavaProcessor::process() {\n    if (!m_plan || m_bars == 0 || !m_out) {\n        return;\n    }\n\n    const int count = static_cast<int>(AudioCollector::instance().readChunk(m_in));\n\n    // Process in data via cava\n    cava_execute(m_in, count, m_out, m_plan);\n\n    // Apply monstercat filter\n    QVector<double> values(m_bars);\n\n    // Left to right pass\n    const double inv = 1.0 / 1.5;\n    double carry = 0.0;\n    for (int i = 0; i < m_bars; ++i) {\n        carry = std::max(m_out[i], carry * inv);\n        values[i] = carry;\n    }\n\n    // Right to left pass and combine\n    carry = 0.0;\n    for (int i = m_bars - 1; i >= 0; --i) {\n        carry = std::max(m_out[i], carry * inv);\n        values[i] = std::max(values[i], carry);\n    }\n\n    // Update values\n    if (values != m_values) {\n        m_values = std::move(values);\n        emit valuesChanged(m_values);\n    }\n}\n\nvoid CavaProcessor::setBars(int bars) {\n    if (bars < 0) {\n        qWarning() << \"CavaProcessor::setBars: bars must be greater than 0. Setting to 0.\";\n        bars = 0;\n    }\n\n    if (m_bars != bars) {\n        m_bars = bars;\n        reload();\n    }\n}\n\nvoid CavaProcessor::reload() {\n    cleanup();\n    initCava();\n}\n\nvoid CavaProcessor::cleanup() {\n    if (m_plan) {\n        cava_destroy(m_plan);\n        m_plan = nullptr;\n    }\n\n    if (m_out) {\n        delete[] m_out;\n        m_out = nullptr;\n    }\n}\n\nvoid CavaProcessor::initCava() {\n    if (m_plan || m_bars == 0) {\n        return;\n    }\n\n    m_plan = cava_init(m_bars, ac::SAMPLE_RATE, 1, 1, 0.85, 50, 10000);\n    m_out = new double[static_cast<size_t>(m_bars)];\n}\n\nCavaProvider::CavaProvider(QObject* parent)\n    : AudioProvider(parent)\n    , m_bars(0)\n    , m_values(m_bars, 0.0) {\n    m_processor = new CavaProcessor();\n    init();\n\n    connect(static_cast<CavaProcessor*>(m_processor), &CavaProcessor::valuesChanged, this, &CavaProvider::updateValues);\n}\n\nint CavaProvider::bars() const {\n    return m_bars;\n}\n\nvoid CavaProvider::setBars(int bars) {\n    if (bars < 0) {\n        qWarning() << \"CavaProvider::setBars: bars must be greater than 0. Setting to 0.\";\n        bars = 0;\n    }\n\n    if (m_bars == bars) {\n        return;\n    }\n\n    m_values.resize(bars, 0.0);\n    m_bars = bars;\n    emit barsChanged();\n    emit valuesChanged();\n\n    QMetaObject::invokeMethod(\n        static_cast<CavaProcessor*>(m_processor), &CavaProcessor::setBars, Qt::QueuedConnection, bars);\n}\n\nQVector<double> CavaProvider::values() const {\n    return m_values;\n}\n\nvoid CavaProvider::updateValues(QVector<double> values) {\n    if (values != m_values) {\n        m_values = values;\n        emit valuesChanged();\n    }\n}\n\n} // namespace caelestia::services\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/cavaprovider.hpp",
    "content": "#pragma once\n\n#include \"audioprovider.hpp\"\n#include <cava/cavacore.h>\n#include <qqmlintegration.h>\n\nnamespace caelestia::services {\n\nclass CavaProcessor : public AudioProcessor {\n    Q_OBJECT\n\npublic:\n    explicit CavaProcessor(QObject* parent = nullptr);\n    ~CavaProcessor();\n\n    void setBars(int bars);\n\nsignals:\n    void valuesChanged(QVector<double> values);\n\nprotected:\n    void process() override;\n\nprivate:\n    struct cava_plan* m_plan;\n    double* m_in;\n    double* m_out;\n\n    int m_bars;\n    QVector<double> m_values;\n\n    void reload();\n    void initCava();\n    void cleanup();\n};\n\nclass CavaProvider : public AudioProvider {\n    Q_OBJECT\n    QML_ELEMENT\n\n    Q_PROPERTY(int bars READ bars WRITE setBars NOTIFY barsChanged)\n\n    Q_PROPERTY(QVector<double> values READ values NOTIFY valuesChanged)\n\npublic:\n    explicit CavaProvider(QObject* parent = nullptr);\n\n    [[nodiscard]] int bars() const;\n    void setBars(int bars);\n\n    [[nodiscard]] QVector<double> values() const;\n\nsignals:\n    void barsChanged();\n    void valuesChanged();\n\nprivate:\n    int m_bars;\n    QVector<double> m_values;\n\n    void updateValues(QVector<double> values);\n};\n\n} // namespace caelestia::services\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/service.cpp",
    "content": "#include \"service.hpp\"\n\n#include <qdebug.h>\n#include <qpointer.h>\n\nnamespace caelestia::services {\n\nService::Service(QObject* parent)\n    : QObject(parent) {}\n\nvoid Service::ref(QObject* sender) {\n    if (m_refs.isEmpty()) {\n        start();\n    }\n\n    QObject::connect(sender, &QObject::destroyed, this, &Service::unref);\n    m_refs << sender;\n}\n\nvoid Service::unref(QObject* sender) {\n    if (m_refs.remove(sender) && m_refs.isEmpty()) {\n        stop();\n    }\n}\n\n} // namespace caelestia::services\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/service.hpp",
    "content": "#pragma once\n\n#include <qobject.h>\n#include <qset.h>\n\nnamespace caelestia::services {\n\nclass Service : public QObject {\n    Q_OBJECT\n\npublic:\n    explicit Service(QObject* parent = nullptr);\n\n    void ref(QObject* sender);\n    void unref(QObject* sender);\n\nprivate:\n    QSet<QObject*> m_refs;\n\n    virtual void start() = 0;\n    virtual void stop() = 0;\n};\n\n} // namespace caelestia::services\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/serviceref.cpp",
    "content": "#include \"serviceref.hpp\"\n\n#include \"service.hpp\"\n\nnamespace caelestia::services {\n\nServiceRef::ServiceRef(Service* service, QObject* parent)\n    : QObject(parent)\n    , m_service(service) {\n    if (m_service) {\n        m_service->ref(this);\n    }\n}\n\nService* ServiceRef::service() const {\n    return m_service;\n}\n\nvoid ServiceRef::setService(Service* service) {\n    if (m_service == service) {\n        return;\n    }\n\n    if (m_service) {\n        m_service->unref(this);\n    }\n\n    m_service = service;\n    emit serviceChanged();\n\n    if (m_service) {\n        m_service->ref(this);\n    }\n}\n\n} // namespace caelestia::services\n"
  },
  {
    "path": "plugin/src/Caelestia/Services/serviceref.hpp",
    "content": "#pragma once\n\n#include \"service.hpp\"\n#include <qpointer.h>\n#include <qqmlintegration.h>\n\nnamespace caelestia::services {\n\nclass ServiceRef : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n\n    Q_PROPERTY(caelestia::services::Service* service READ service WRITE setService NOTIFY serviceChanged)\n\npublic:\n    explicit ServiceRef(Service* service = nullptr, QObject* parent = nullptr);\n\n    [[nodiscard]] Service* service() const;\n    void setService(Service* service);\n\nsignals:\n    void serviceChanged();\n\nprivate:\n    QPointer<Service> m_service;\n};\n\n} // namespace caelestia::services\n"
  },
  {
    "path": "plugin/src/Caelestia/appdb.cpp",
    "content": "#include \"appdb.hpp\"\n\n#include <qsqldatabase.h>\n#include <qsqlquery.h>\n#include <quuid.h>\n\nnamespace caelestia {\n\nAppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent)\n    : QObject(parent)\n    , m_entry(entry)\n    , m_frequency(frequency) {\n    const auto mo = m_entry->metaObject();\n    const auto tmo = metaObject();\n\n    for (const auto& prop :\n        { \"name\", \"comment\", \"execString\", \"startupClass\", \"genericName\", \"categories\", \"keywords\" }) {\n        const auto metaProp = mo->property(mo->indexOfProperty(prop));\n        const auto thisMetaProp = tmo->property(tmo->indexOfProperty(prop));\n        QObject::connect(m_entry, metaProp.notifySignal(), this, thisMetaProp.notifySignal());\n    }\n\n    QObject::connect(m_entry, &QObject::destroyed, this, [this]() {\n        m_entry = nullptr;\n        deleteLater();\n    });\n}\n\nQObject* AppEntry::entry() const {\n    return m_entry;\n}\n\nquint32 AppEntry::frequency() const {\n    return m_frequency;\n}\n\nvoid AppEntry::setFrequency(unsigned int frequency) {\n    if (m_frequency != frequency) {\n        m_frequency = frequency;\n        emit frequencyChanged();\n    }\n}\n\nvoid AppEntry::incrementFrequency() {\n    m_frequency++;\n    emit frequencyChanged();\n}\n\nQString AppEntry::id() const {\n    if (!m_entry) {\n        return \"\";\n    }\n    return m_entry->property(\"id\").toString();\n}\n\nQString AppEntry::name() const {\n    if (!m_entry) {\n        return \"\";\n    }\n    return m_entry->property(\"name\").toString();\n}\n\nQString AppEntry::comment() const {\n    if (!m_entry) {\n        return \"\";\n    }\n    return m_entry->property(\"comment\").toString();\n}\n\nQString AppEntry::execString() const {\n    if (!m_entry) {\n        return \"\";\n    }\n    return m_entry->property(\"execString\").toString();\n}\n\nQString AppEntry::startupClass() const {\n    if (!m_entry) {\n        return \"\";\n    }\n    return m_entry->property(\"startupClass\").toString();\n}\n\nQString AppEntry::genericName() const {\n    if (!m_entry) {\n        return \"\";\n    }\n    return m_entry->property(\"genericName\").toString();\n}\n\nQString AppEntry::categories() const {\n    if (!m_entry) {\n        return \"\";\n    }\n    return m_entry->property(\"categories\").toStringList().join(\" \");\n}\n\nQString AppEntry::keywords() const {\n    if (!m_entry) {\n        return \"\";\n    }\n    return m_entry->property(\"keywords\").toStringList().join(\" \");\n}\n\nAppDb::AppDb(QObject* parent)\n    : QObject(parent)\n    , m_timer(new QTimer(this))\n    , m_uuid(QUuid::createUuid().toString()) {\n    m_timer->setSingleShot(true);\n    m_timer->setInterval(300);\n    QObject::connect(m_timer, &QTimer::timeout, this, &AppDb::updateApps);\n\n    auto db = QSqlDatabase::addDatabase(\"QSQLITE\", m_uuid);\n    db.setDatabaseName(\":memory:\");\n    db.open();\n\n    QSqlQuery query(db);\n    query.exec(\"CREATE TABLE IF NOT EXISTS frequencies (id TEXT PRIMARY KEY, frequency INTEGER)\");\n}\n\nQString AppDb::uuid() const {\n    return m_uuid;\n}\n\nQString AppDb::path() const {\n    return m_path;\n}\n\nvoid AppDb::setPath(const QString& path) {\n    auto newPath = path.isEmpty() ? \":memory:\" : path;\n\n    if (m_path == newPath) {\n        return;\n    }\n\n    m_path = newPath;\n    emit pathChanged();\n\n    auto db = QSqlDatabase::database(m_uuid, false);\n    db.close();\n    db.setDatabaseName(newPath);\n    db.open();\n\n    QSqlQuery query(db);\n    query.exec(\"CREATE TABLE IF NOT EXISTS frequencies (id TEXT PRIMARY KEY, frequency INTEGER)\");\n\n    updateAppFrequencies();\n}\n\nQObjectList AppDb::entries() const {\n    return m_entries;\n}\n\nvoid AppDb::setEntries(const QObjectList& entries) {\n    if (m_entries == entries) {\n        return;\n    }\n\n    m_entries = entries;\n    emit entriesChanged();\n\n    m_timer->start();\n}\n\nQStringList AppDb::favouriteApps() const {\n    return m_favouriteApps;\n}\n\nvoid AppDb::setFavouriteApps(const QStringList& favApps) {\n    if (m_favouriteApps == favApps) {\n        return;\n    }\n\n    m_favouriteApps = favApps;\n    emit favouriteAppsChanged();\n    m_favouriteAppsRegex.clear();\n    m_favouriteAppsRegex.reserve(m_favouriteApps.size());\n    for (const QString& item : std::as_const(m_favouriteApps)) {\n        const QRegularExpression re(regexifyString(item));\n        if (re.isValid()) {\n            m_favouriteAppsRegex << re;\n        } else {\n            qWarning() << \"AppDb::setFavouriteApps: Regular expression is not valid: \" << re.pattern();\n        }\n    }\n\n    emit appsChanged();\n}\n\nQString AppDb::regexifyString(const QString& original) const {\n    if (original.startsWith('^') && original.endsWith('$'))\n        return original;\n\n    const QString escaped = QRegularExpression::escape(original);\n    return QStringLiteral(\"^%1$\").arg(escaped);\n}\n\nQQmlListProperty<AppEntry> AppDb::apps() {\n    return QQmlListProperty<AppEntry>(this, &getSortedApps());\n}\n\nvoid AppDb::incrementFrequency(const QString& id) {\n    auto db = QSqlDatabase::database(m_uuid);\n    QSqlQuery query(db);\n\n    query.prepare(\"INSERT INTO frequencies (id, frequency) \"\n                  \"VALUES (:id, 1) \"\n                  \"ON CONFLICT (id) DO UPDATE SET frequency = frequency + 1\");\n    query.bindValue(\":id\", id);\n    query.exec();\n\n    auto* app = m_apps.value(id);\n    if (app) {\n        const auto before = getSortedApps();\n        app->incrementFrequency();\n        getSortedApps();\n        if (before != m_sortedApps) {\n            emit appsChanged();\n        }\n    } else {\n        qWarning() << \"AppDb::incrementFrequency: could not find app with id\" << id;\n    }\n}\n\nQList<AppEntry*>& AppDb::getSortedApps() const {\n    m_sortedApps = m_apps.values();\n\n    // Pre-compute favourite status to avoid repeated regex matching during sort\n    QSet<QString> favSet;\n    favSet.reserve(m_sortedApps.size());\n    for (const auto* app : std::as_const(m_sortedApps)) {\n        if (isFavourite(app))\n            favSet.insert(app->id());\n    }\n\n    std::sort(m_sortedApps.begin(), m_sortedApps.end(), [&favSet](AppEntry* a, AppEntry* b) {\n        const bool aIsFav = favSet.contains(a->id());\n        const bool bIsFav = favSet.contains(b->id());\n        if (aIsFav != bIsFav)\n            return aIsFav;\n        if (a->frequency() != b->frequency())\n            return a->frequency() > b->frequency();\n        return a->name().localeAwareCompare(b->name()) < 0;\n    });\n    return m_sortedApps;\n}\n\nbool AppDb::isFavourite(const AppEntry* app) const {\n    for (const QRegularExpression& re : m_favouriteAppsRegex) {\n        if (re.match(app->id()).hasMatch()) {\n            return true;\n        }\n    }\n    return false;\n}\n\nquint32 AppDb::getFrequency(const QString& id) const {\n    auto db = QSqlDatabase::database(m_uuid);\n    QSqlQuery query(db);\n\n    query.prepare(\"SELECT frequency FROM frequencies WHERE id = :id\");\n    query.bindValue(\":id\", id);\n\n    if (query.exec() && query.next()) {\n        return query.value(0).toUInt();\n    }\n\n    return 0;\n}\n\nvoid AppDb::updateAppFrequencies() {\n    const auto before = getSortedApps();\n\n    for (auto* app : std::as_const(m_apps)) {\n        app->setFrequency(getFrequency(app->id()));\n    }\n\n    getSortedApps();\n    if (before != m_sortedApps) {\n        emit appsChanged();\n    }\n}\n\nvoid AppDb::updateApps() {\n    bool dirty = false;\n\n    for (const auto& entry : std::as_const(m_entries)) {\n        const auto id = entry->property(\"id\").toString();\n        if (!m_apps.contains(id)) {\n            dirty = true;\n            auto* const newEntry = new AppEntry(entry, getFrequency(id), this);\n            QObject::connect(newEntry, &QObject::destroyed, this, [id, this]() {\n                if (m_apps.remove(id)) {\n                    emit appsChanged();\n                }\n            });\n            m_apps.insert(id, newEntry);\n        }\n    }\n\n    QSet<QString> newIds;\n    for (const auto& entry : std::as_const(m_entries)) {\n        newIds.insert(entry->property(\"id\").toString());\n    }\n\n    for (auto it = m_apps.keyBegin(); it != m_apps.keyEnd(); ++it) {\n        const auto& id = *it;\n        if (!newIds.contains(id)) {\n            dirty = true;\n            m_apps.take(id)->deleteLater();\n        }\n    }\n\n    if (dirty) {\n        emit appsChanged();\n    }\n}\n\n} // namespace caelestia\n"
  },
  {
    "path": "plugin/src/Caelestia/appdb.hpp",
    "content": "#pragma once\n\n#include <qhash.h>\n#include <qobject.h>\n#include <qqmlintegration.h>\n#include <qqmllist.h>\n#include <qregularexpression.h>\n#include <qtimer.h>\n\nnamespace caelestia {\n\nclass AppEntry : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n    QML_UNCREATABLE(\"AppEntry instances can only be retrieved from an AppDb\")\n\n    // The actual DesktopEntry, but we don't have access to the type so it's a QObject\n    Q_PROPERTY(QObject* entry READ entry CONSTANT)\n\n    Q_PROPERTY(quint32 frequency READ frequency NOTIFY frequencyChanged)\n    Q_PROPERTY(QString id READ id CONSTANT)\n    Q_PROPERTY(QString name READ name NOTIFY nameChanged)\n    Q_PROPERTY(QString comment READ comment NOTIFY commentChanged)\n    Q_PROPERTY(QString execString READ execString NOTIFY execStringChanged)\n    Q_PROPERTY(QString startupClass READ startupClass NOTIFY startupClassChanged)\n    Q_PROPERTY(QString genericName READ genericName NOTIFY genericNameChanged)\n    Q_PROPERTY(QString categories READ categories NOTIFY categoriesChanged)\n    Q_PROPERTY(QString keywords READ keywords NOTIFY keywordsChanged)\n\npublic:\n    explicit AppEntry(QObject* entry, quint32 frequency, QObject* parent = nullptr);\n\n    [[nodiscard]] QObject* entry() const;\n\n    [[nodiscard]] quint32 frequency() const;\n    void setFrequency(quint32 frequency);\n    void incrementFrequency();\n\n    [[nodiscard]] QString id() const;\n    [[nodiscard]] QString name() const;\n    [[nodiscard]] QString comment() const;\n    [[nodiscard]] QString execString() const;\n    [[nodiscard]] QString startupClass() const;\n    [[nodiscard]] QString genericName() const;\n    [[nodiscard]] QString categories() const;\n    [[nodiscard]] QString keywords() const;\n\nsignals:\n    void frequencyChanged();\n    void nameChanged();\n    void commentChanged();\n    void execStringChanged();\n    void startupClassChanged();\n    void genericNameChanged();\n    void categoriesChanged();\n    void keywordsChanged();\n\nprivate:\n    QObject* m_entry;\n    quint32 m_frequency;\n};\n\nclass AppDb : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n\n    Q_PROPERTY(QString uuid READ uuid CONSTANT)\n    Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)\n    Q_PROPERTY(QObjectList entries READ entries WRITE setEntries NOTIFY entriesChanged REQUIRED)\n    Q_PROPERTY(QStringList favouriteApps READ favouriteApps WRITE setFavouriteApps NOTIFY favouriteAppsChanged REQUIRED)\n    Q_PROPERTY(QQmlListProperty<caelestia::AppEntry> apps READ apps NOTIFY appsChanged)\n\npublic:\n    explicit AppDb(QObject* parent = nullptr);\n\n    [[nodiscard]] QString uuid() const;\n\n    [[nodiscard]] QString path() const;\n    void setPath(const QString& path);\n\n    [[nodiscard]] QObjectList entries() const;\n    void setEntries(const QObjectList& entries);\n\n    [[nodiscard]] QStringList favouriteApps() const;\n    void setFavouriteApps(const QStringList& favApps);\n\n    [[nodiscard]] QQmlListProperty<AppEntry> apps();\n\n    Q_INVOKABLE void incrementFrequency(const QString& id);\n\nsignals:\n    void pathChanged();\n    void entriesChanged();\n    void favouriteAppsChanged();\n    void appsChanged();\n\nprivate:\n    QTimer* m_timer;\n\n    const QString m_uuid;\n    QString m_path;\n    QObjectList m_entries;\n    QStringList m_favouriteApps;                    // unedited string list from qml\n    QList<QRegularExpression> m_favouriteAppsRegex; // pre-regexified m_favouriteApps list\n    QHash<QString, AppEntry*> m_apps;\n    mutable QList<AppEntry*> m_sortedApps;\n\n    QString regexifyString(const QString& original) const;\n    QList<AppEntry*>& getSortedApps() const;\n    bool isFavourite(const AppEntry* app) const;\n    quint32 getFrequency(const QString& id) const;\n    void updateAppFrequencies();\n    void updateApps();\n};\n\n} // namespace caelestia\n"
  },
  {
    "path": "plugin/src/Caelestia/cutils.cpp",
    "content": "#include \"cutils.hpp\"\n\n#include <QtConcurrent/qtconcurrentrun.h>\n#include <QtQuick/qquickitemgrabresult.h>\n#include <QtQuick/qquickwindow.h>\n#include <qdir.h>\n#include <qfileinfo.h>\n#include <qfuturewatcher.h>\n#include <qqmlengine.h>\n\nnamespace caelestia {\n\nvoid CUtils::saveItem(QQuickItem* target, const QUrl& path) {\n    this->saveItem(target, path, QRect(), QJSValue(), QJSValue());\n}\n\nvoid CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect) {\n    this->saveItem(target, path, rect, QJSValue(), QJSValue());\n}\n\nvoid CUtils::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved) {\n    this->saveItem(target, path, QRect(), onSaved, QJSValue());\n}\n\nvoid CUtils::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed) {\n    this->saveItem(target, path, QRect(), onSaved, onFailed);\n}\n\nvoid CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved) {\n    this->saveItem(target, path, rect, onSaved, QJSValue());\n}\n\nvoid CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed) {\n    if (!target) {\n        qWarning() << \"CUtils::saveItem: a target is required\";\n        return;\n    }\n\n    if (!path.isLocalFile()) {\n        qWarning() << \"CUtils::saveItem:\" << path << \"is not a local file\";\n        return;\n    }\n\n    if (!target->window()) {\n        qWarning() << \"CUtils::saveItem: unable to save target\" << target << \"without a window\";\n        return;\n    }\n\n    auto scaledRect = rect;\n    const qreal scale = target->window()->devicePixelRatio();\n    if (rect.isValid() && !qFuzzyCompare(scale + 1.0, 2.0)) {\n        scaledRect =\n            QRectF(rect.left() * scale, rect.top() * scale, rect.width() * scale, rect.height() * scale).toRect();\n    }\n\n    const QSharedPointer<const QQuickItemGrabResult> grabResult = target->grabToImage();\n\n    QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this,\n        [grabResult, scaledRect, path, onSaved, onFailed, this]() {\n            const auto future = QtConcurrent::run([=]() {\n                QImage image = grabResult->image();\n\n                if (scaledRect.isValid()) {\n                    image = image.copy(scaledRect);\n                }\n\n                const QString file = path.toLocalFile();\n                const QString parent = QFileInfo(file).absolutePath();\n                return QDir().mkpath(parent) && image.save(file);\n            });\n\n            auto* watcher = new QFutureWatcher<bool>(this);\n            auto* engine = qmlEngine(this);\n\n            QObject::connect(watcher, &QFutureWatcher<bool>::finished, this, [=]() {\n                if (watcher->result()) {\n                    if (onSaved.isCallable()) {\n                        onSaved.call(\n                            { QJSValue(path.toLocalFile()), engine->toScriptValue(QVariant::fromValue(path)) });\n                    }\n                } else {\n                    qWarning() << \"CUtils::saveItem: failed to save\" << path;\n                    if (onFailed.isCallable()) {\n                        onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) });\n                    }\n                }\n                watcher->deleteLater();\n            });\n            watcher->setFuture(future);\n        });\n}\n\nbool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const {\n    if (!source.isLocalFile()) {\n        qWarning() << \"CUtils::copyFile: source\" << source << \"is not a local file\";\n        return false;\n    }\n    if (!target.isLocalFile()) {\n        qWarning() << \"CUtils::copyFile: target\" << target << \"is not a local file\";\n        return false;\n    }\n\n    if (overwrite && QFile::exists(target.toLocalFile())) {\n        if (!QFile::remove(target.toLocalFile())) {\n            qWarning() << \"CUtils::copyFile: overwrite was specified but failed to remove\" << target.toLocalFile();\n            return false;\n        }\n    }\n\n    return QFile::copy(source.toLocalFile(), target.toLocalFile());\n}\n\nbool CUtils::deleteFile(const QUrl& path) const {\n    if (!path.isLocalFile()) {\n        qWarning() << \"CUtils::deleteFile: path\" << path << \"is not a local file\";\n        return false;\n    }\n\n    return QFile::remove(path.toLocalFile());\n}\n\nQString CUtils::toLocalFile(const QUrl& url) const {\n    if (!url.isLocalFile()) {\n        qWarning() << \"CUtils::toLocalFile: given url is not a local file\" << url;\n        return QString();\n    }\n\n    return url.toLocalFile();\n}\n\n} // namespace caelestia\n"
  },
  {
    "path": "plugin/src/Caelestia/cutils.hpp",
    "content": "#pragma once\n\n#include <QtQuick/qquickitem.h>\n#include <qobject.h>\n#include <qqmlintegration.h>\n\nnamespace caelestia {\n\nclass CUtils : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n    QML_SINGLETON\n\npublic:\n    // clang-format off\n    Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path);\n    Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect);\n    Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved);\n    Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed);\n    Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved);\n    Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed);\n    // clang-format on\n\n    Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const;\n    Q_INVOKABLE bool deleteFile(const QUrl& path) const;\n    Q_INVOKABLE QString toLocalFile(const QUrl& url) const;\n};\n\n} // namespace caelestia\n"
  },
  {
    "path": "plugin/src/Caelestia/imageanalyser.cpp",
    "content": "#include \"imageanalyser.hpp\"\n\n#include <QtConcurrent/qtconcurrentrun.h>\n#include <QtQuick/qquickitemgrabresult.h>\n#include <qfuturewatcher.h>\n#include <qimage.h>\n#include <qquickwindow.h>\n\nnamespace caelestia {\n\nImageAnalyser::ImageAnalyser(QObject* parent)\n    : QObject(parent)\n    , m_futureWatcher(new QFutureWatcher<AnalyseResult>(this))\n    , m_source(\"\")\n    , m_sourceItem(nullptr)\n    , m_rescaleSize(128)\n    , m_dominantColour(0, 0, 0)\n    , m_luminance(0) {\n    QObject::connect(m_futureWatcher, &QFutureWatcher<AnalyseResult>::finished, this, [this]() {\n        if (!m_futureWatcher->future().isResultReadyAt(0)) {\n            return;\n        }\n\n        const auto result = m_futureWatcher->result();\n        if (m_dominantColour != result.first) {\n            m_dominantColour = result.first;\n            emit dominantColourChanged();\n        }\n        if (!qFuzzyCompare(m_luminance + 1.0, result.second + 1.0)) {\n            m_luminance = result.second;\n            emit luminanceChanged();\n        }\n    });\n}\n\nQString ImageAnalyser::source() const {\n    return m_source;\n}\n\nvoid ImageAnalyser::setSource(const QString& source) {\n    if (m_source == source) {\n        return;\n    }\n\n    m_source = source;\n    emit sourceChanged();\n\n    if (m_sourceItem) {\n        m_sourceItem = nullptr;\n        emit sourceItemChanged();\n    }\n\n    requestUpdate();\n}\n\nQQuickItem* ImageAnalyser::sourceItem() const {\n    return m_sourceItem;\n}\n\nvoid ImageAnalyser::setSourceItem(QQuickItem* sourceItem) {\n    if (m_sourceItem == sourceItem) {\n        return;\n    }\n\n    m_sourceItem = sourceItem;\n    emit sourceItemChanged();\n\n    if (!m_source.isEmpty()) {\n        m_source = \"\";\n        emit sourceChanged();\n    }\n\n    requestUpdate();\n}\n\nint ImageAnalyser::rescaleSize() const {\n    return m_rescaleSize;\n}\n\nvoid ImageAnalyser::setRescaleSize(int rescaleSize) {\n    if (m_rescaleSize == rescaleSize) {\n        return;\n    }\n\n    m_rescaleSize = rescaleSize;\n    emit rescaleSizeChanged();\n\n    requestUpdate();\n}\n\nQColor ImageAnalyser::dominantColour() const {\n    return m_dominantColour;\n}\n\nqreal ImageAnalyser::luminance() const {\n    return m_luminance;\n}\n\nvoid ImageAnalyser::requestUpdate() {\n    if (m_source.isEmpty() && !m_sourceItem) {\n        return;\n    }\n\n    if (!m_sourceItem || (m_sourceItem->window() && m_sourceItem->window()->isVisible() && m_sourceItem->width() > 0 &&\n                             m_sourceItem->height() > 0)) {\n        update();\n    } else if (m_sourceItem) {\n        if (!m_sourceItem->window()) {\n            QObject::connect(m_sourceItem, &QQuickItem::windowChanged, this, &ImageAnalyser::requestUpdate,\n                Qt::SingleShotConnection);\n        } else if (!m_sourceItem->window()->isVisible()) {\n            QObject::connect(m_sourceItem->window(), &QQuickWindow::visibleChanged, this, &ImageAnalyser::requestUpdate,\n                Qt::SingleShotConnection);\n        }\n        if (m_sourceItem->width() <= 0) {\n            QObject::connect(\n                m_sourceItem, &QQuickItem::widthChanged, this, &ImageAnalyser::requestUpdate, Qt::SingleShotConnection);\n        }\n        if (m_sourceItem->height() <= 0) {\n            QObject::connect(m_sourceItem, &QQuickItem::heightChanged, this, &ImageAnalyser::requestUpdate,\n                Qt::SingleShotConnection);\n        }\n    }\n}\n\nvoid ImageAnalyser::update() {\n    if (m_source.isEmpty() && !m_sourceItem) {\n        return;\n    }\n\n    if (m_futureWatcher->isRunning()) {\n        m_futureWatcher->cancel();\n    }\n\n    if (m_sourceItem) {\n        const QSharedPointer<const QQuickItemGrabResult> grabResult = m_sourceItem->grabToImage();\n        QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, this]() {\n            m_futureWatcher->setFuture(QtConcurrent::run(&ImageAnalyser::analyse, grabResult->image(), m_rescaleSize));\n        });\n    } else {\n        m_futureWatcher->setFuture(QtConcurrent::run([=, this](QPromise<AnalyseResult>& promise) {\n            const QImage image(m_source);\n            analyse(promise, image, m_rescaleSize);\n        }));\n    }\n}\n\nvoid ImageAnalyser::analyse(QPromise<AnalyseResult>& promise, const QImage& image, int rescaleSize) {\n    if (image.isNull()) {\n        qWarning() << \"ImageAnalyser::analyse: image is null\";\n        return;\n    }\n\n    QImage img = image;\n\n    if (rescaleSize > 0 && (img.width() > rescaleSize || img.height() > rescaleSize)) {\n        img = img.scaled(rescaleSize, rescaleSize, Qt::KeepAspectRatio, Qt::FastTransformation);\n    }\n\n    if (promise.isCanceled()) {\n        return;\n    }\n\n    if (img.format() != QImage::Format_ARGB32) {\n        img = img.convertToFormat(QImage::Format_ARGB32);\n    }\n\n    if (promise.isCanceled()) {\n        return;\n    }\n\n    const uchar* data = img.bits();\n    const int width = img.width();\n    const int height = img.height();\n    const qsizetype bytesPerLine = img.bytesPerLine();\n\n    std::unordered_map<quint32, int> colours;\n    qreal totalLuminance = 0.0;\n    int count = 0;\n\n    for (int y = 0; y < height; ++y) {\n        const uchar* line = data + y * bytesPerLine;\n        for (int x = 0; x < width; ++x) {\n            if (promise.isCanceled()) {\n                return;\n            }\n\n            const uchar* pixel = line + x * 4;\n\n            if (pixel[3] == 0) {\n                continue;\n            }\n\n            const quint32 mr = static_cast<quint32>(pixel[2] & 0xF8);\n            const quint32 mg = static_cast<quint32>(pixel[1] & 0xF8);\n            const quint32 mb = static_cast<quint32>(pixel[0] & 0xF8);\n            ++colours[(mr << 16) | (mg << 8) | mb];\n\n            const qreal r = pixel[2] / 255.0;\n            const qreal g = pixel[1] / 255.0;\n            const qreal b = pixel[0] / 255.0;\n            totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b);\n            ++count;\n        }\n    }\n\n    quint32 dominantColour = 0;\n    int maxCount = 0;\n    for (const auto& [colour, colourCount] : colours) {\n        if (promise.isCanceled()) {\n            return;\n        }\n\n        if (colourCount > maxCount) {\n            dominantColour = colour;\n            maxCount = colourCount;\n        }\n    }\n\n    promise.addResult(qMakePair(QColor((0xFFu << 24) | dominantColour), count == 0 ? 0.0 : totalLuminance / count));\n}\n\n} // namespace caelestia\n"
  },
  {
    "path": "plugin/src/Caelestia/imageanalyser.hpp",
    "content": "#pragma once\n\n#include <QtQuick/qquickitem.h>\n#include <qfuture.h>\n#include <qfuturewatcher.h>\n#include <qobject.h>\n#include <qqmlintegration.h>\n\nnamespace caelestia {\n\nclass ImageAnalyser : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n\n    Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged)\n    Q_PROPERTY(QQuickItem* sourceItem READ sourceItem WRITE setSourceItem NOTIFY sourceItemChanged)\n    Q_PROPERTY(int rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged)\n    Q_PROPERTY(QColor dominantColour READ dominantColour NOTIFY dominantColourChanged)\n    Q_PROPERTY(qreal luminance READ luminance NOTIFY luminanceChanged)\n\npublic:\n    explicit ImageAnalyser(QObject* parent = nullptr);\n\n    [[nodiscard]] QString source() const;\n    void setSource(const QString& source);\n\n    [[nodiscard]] QQuickItem* sourceItem() const;\n    void setSourceItem(QQuickItem* sourceItem);\n\n    [[nodiscard]] int rescaleSize() const;\n    void setRescaleSize(int rescaleSize);\n\n    [[nodiscard]] QColor dominantColour() const;\n    [[nodiscard]] qreal luminance() const;\n\n    Q_INVOKABLE void requestUpdate();\n\nsignals:\n    void sourceChanged();\n    void sourceItemChanged();\n    void rescaleSizeChanged();\n    void dominantColourChanged();\n    void luminanceChanged();\n\nprivate:\n    using AnalyseResult = QPair<QColor, qreal>;\n\n    QFutureWatcher<AnalyseResult>* const m_futureWatcher;\n\n    QString m_source;\n    QQuickItem* m_sourceItem;\n    int m_rescaleSize;\n\n    QColor m_dominantColour;\n    qreal m_luminance;\n\n    void update();\n    static void analyse(QPromise<AnalyseResult>& promise, const QImage& image, int rescaleSize);\n};\n\n} // namespace caelestia\n"
  },
  {
    "path": "plugin/src/Caelestia/qalculator.cpp",
    "content": "#include \"qalculator.hpp\"\n\n#include <libqalculate/qalculate.h>\n#include <qfuturewatcher.h>\n#include <qtconcurrentrun.h>\n\nnamespace caelestia {\n\nQMutex Qalculator::s_calculatorMutex;\n\nQalculator::Qalculator(QObject* parent)\n    : QObject(parent) {\n    if (!CALCULATOR) {\n        new Calculator();\n        CALCULATOR->loadExchangeRates();\n        CALCULATOR->loadGlobalDefinitions();\n        CALCULATOR->loadLocalDefinitions();\n    }\n}\n\nQString Qalculator::eval(const QString& expr, bool printExpr) const {\n    if (expr.isEmpty()) {\n        return QString();\n    }\n\n    QMutexLocker locker(&s_calculatorMutex);\n\n    EvaluationOptions eo;\n    PrintOptions po;\n\n    std::string parsed;\n    std::string result = CALCULATOR->calculateAndPrint(\n        CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed);\n\n    std::string error;\n    while (CALCULATOR->message()) {\n        if (!CALCULATOR->message()->message().empty()) {\n            if (CALCULATOR->message()->type() == MESSAGE_ERROR) {\n                error += \"error: \";\n            } else if (CALCULATOR->message()->type() == MESSAGE_WARNING) {\n                error += \"warning: \";\n            }\n            error += CALCULATOR->message()->message();\n        }\n        CALCULATOR->nextMessage();\n    }\n    if (!error.empty()) {\n        return QString::fromStdString(error);\n    }\n\n    if (printExpr) {\n        return QString(\"%1 = %2\").arg(parsed).arg(result);\n    }\n\n    return QString::fromStdString(result);\n}\n\nvoid Qalculator::evalAsync(const QString& expr) {\n    const quint64 gen = ++m_generation;\n\n    if (expr.isEmpty()) {\n        if (!m_result.isEmpty()) {\n            m_result.clear();\n            emit resultChanged();\n        }\n        if (!m_rawResult.isEmpty()) {\n            m_rawResult.clear();\n            emit rawResultChanged();\n        }\n        if (m_busy) {\n            m_busy = false;\n            emit busyChanged();\n        }\n        return;\n    }\n\n    if (!m_busy) {\n        m_busy = true;\n        emit busyChanged();\n    }\n\n    const auto future = QtConcurrent::run([expr]() -> QPair<QString, QString> {\n        QMutexLocker locker(&s_calculatorMutex);\n\n        EvaluationOptions eo;\n        PrintOptions po;\n\n        std::string parsed;\n        std::string result = CALCULATOR->calculateAndPrint(\n            CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed);\n\n        std::string error;\n        while (CALCULATOR->message()) {\n            if (!CALCULATOR->message()->message().empty()) {\n                if (CALCULATOR->message()->type() == MESSAGE_ERROR) {\n                    error += \"error: \";\n                } else if (CALCULATOR->message()->type() == MESSAGE_WARNING) {\n                    error += \"warning: \";\n                }\n                error += CALCULATOR->message()->message();\n            }\n            CALCULATOR->nextMessage();\n        }\n\n        if (!error.empty()) {\n            const QString errorStr = QString::fromStdString(error);\n            return { errorStr, errorStr };\n        }\n\n        const QString rawStr = QString::fromStdString(result);\n        return { QString(\"%1 = %2\").arg(parsed).arg(result), rawStr };\n    });\n\n    auto* watcher = new QFutureWatcher<QPair<QString, QString>>(this);\n\n    connect(watcher, &QFutureWatcher<QPair<QString, QString>>::finished, this, [this, watcher, gen]() {\n        watcher->deleteLater();\n\n        if (gen != m_generation) {\n            return;\n        }\n\n        const auto [formatted, raw] = watcher->result();\n\n        if (m_result != formatted) {\n            m_result = formatted;\n            emit resultChanged();\n        }\n        if (m_rawResult != raw) {\n            m_rawResult = raw;\n            emit rawResultChanged();\n        }\n        if (m_busy) {\n            m_busy = false;\n            emit busyChanged();\n        }\n    });\n\n    watcher->setFuture(future);\n}\n\nQString Qalculator::result() const {\n    return m_result;\n}\n\nQString Qalculator::rawResult() const {\n    return m_rawResult;\n}\n\nbool Qalculator::busy() const {\n    return m_busy;\n}\n\n} // namespace caelestia\n"
  },
  {
    "path": "plugin/src/Caelestia/qalculator.hpp",
    "content": "#pragma once\n\n#include <qmutex.h>\n#include <qobject.h>\n#include <qqmlintegration.h>\n\nnamespace caelestia {\n\nclass Qalculator : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n    QML_SINGLETON\n\n    Q_PROPERTY(QString result READ result NOTIFY resultChanged)\n    Q_PROPERTY(QString rawResult READ rawResult NOTIFY rawResultChanged)\n    Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)\n\npublic:\n    explicit Qalculator(QObject* parent = nullptr);\n\n    Q_INVOKABLE QString eval(const QString& expr, bool printExpr = true) const;\n    Q_INVOKABLE void evalAsync(const QString& expr);\n\n    [[nodiscard]] QString result() const;\n    [[nodiscard]] QString rawResult() const;\n    [[nodiscard]] bool busy() const;\n\nsignals:\n    void resultChanged();\n    void rawResultChanged();\n    void busyChanged();\n\nprivate:\n    static QMutex s_calculatorMutex;\n\n    QString m_result;\n    QString m_rawResult;\n    bool m_busy = false;\n    quint64 m_generation = 0;\n};\n\n} // namespace caelestia\n"
  },
  {
    "path": "plugin/src/Caelestia/requests.cpp",
    "content": "#include \"requests.hpp\"\n\n#include <qjsvalueiterator.h>\n#include <qnetworkaccessmanager.h>\n#include <qnetworkcookiejar.h>\n#include <qnetworkreply.h>\n#include <qnetworkrequest.h>\n\nnamespace caelestia {\n\nRequests::Requests(QObject* parent)\n    : QObject(parent)\n    , m_manager(new QNetworkAccessManager(this)) {}\n\nvoid Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError, QJSValue headers) const {\n    if (!onSuccess.isCallable()) {\n        qWarning() << \"Requests::get: onSuccess is not callable\";\n        return;\n    }\n\n    QNetworkRequest request(url);\n    request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);\n    request.setAttribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Manual);\n    request.setRawHeader(\"Cache-Control\", \"no-cache, no-store\");\n    request.setRawHeader(\"Pragma\", \"no-cache\");\n    request.setRawHeader(\"Connection\", \"close\");\n\n    if (headers.isObject()) {\n        QJSValueIterator it(headers);\n        while (it.hasNext()) {\n            it.next();\n            request.setRawHeader(it.name().toUtf8(), it.value().toString().toUtf8());\n        }\n    }\n\n    auto reply = m_manager->get(request);\n\n    QObject::connect(reply, &QNetworkReply::finished, [reply, onSuccess, onError]() {\n        if (reply->error() == QNetworkReply::NoError) {\n            onSuccess.call({ QString(reply->readAll()) });\n        } else if (onError.isCallable()) {\n            onError.call({ reply->errorString() });\n        } else {\n            qWarning() << \"Requests::get: request failed with error\" << reply->errorString();\n        }\n\n        reply->deleteLater();\n    });\n}\n\nvoid Requests::resetCookies() const {\n    m_manager->setCookieJar(new QNetworkCookieJar(m_manager));\n}\n\n} // namespace caelestia\n"
  },
  {
    "path": "plugin/src/Caelestia/requests.hpp",
    "content": "#pragma once\n\n#include <qnetworkaccessmanager.h>\n#include <qobject.h>\n#include <qqmlengine.h>\n\nnamespace caelestia {\n\nclass Requests : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n    QML_SINGLETON\n\npublic:\n    explicit Requests(QObject* parent = nullptr);\n\n    Q_INVOKABLE void get(\n        const QUrl& url, QJSValue callback, QJSValue onError = QJSValue(), QJSValue headers = QJSValue()) const;\n    Q_INVOKABLE void resetCookies() const;\n\nprivate:\n    QNetworkAccessManager* m_manager;\n};\n\n} // namespace caelestia\n"
  },
  {
    "path": "plugin/src/Caelestia/toaster.cpp",
    "content": "#include \"toaster.hpp\"\n\n#include <qdebug.h>\n#include <qlogging.h>\n#include <qtimer.h>\n\nnamespace caelestia {\n\nToast::Toast(const QString& title, const QString& message, const QString& icon, Type type, int timeout, QObject* parent)\n    : QObject(parent)\n    , m_closed(false)\n    , m_title(title)\n    , m_message(message)\n    , m_icon(icon)\n    , m_type(type)\n    , m_timeout(timeout) {\n    QTimer::singleShot(timeout, this, &Toast::close);\n\n    if (m_icon.isEmpty()) {\n        switch (m_type) {\n        case Type::Success:\n            m_icon = \"check_circle_unread\";\n            break;\n        case Type::Warning:\n            m_icon = \"warning\";\n            break;\n        case Type::Error:\n            m_icon = \"error\";\n            break;\n        default:\n            m_icon = \"info\";\n            break;\n        }\n    }\n\n    if (timeout <= 0) {\n        switch (m_type) {\n        case Type::Warning:\n            m_timeout = 7000;\n            break;\n        case Type::Error:\n            m_timeout = 10000;\n            break;\n        default:\n            m_timeout = 5000;\n            break;\n        }\n    }\n}\n\nbool Toast::closed() const {\n    return m_closed;\n}\n\nQString Toast::title() const {\n    return m_title;\n}\n\nQString Toast::message() const {\n    return m_message;\n}\n\nQString Toast::icon() const {\n    return m_icon;\n}\n\nint Toast::timeout() const {\n    return m_timeout;\n}\n\nToast::Type Toast::type() const {\n    return m_type;\n}\n\nvoid Toast::close() {\n    if (!m_closed) {\n        m_closed = true;\n        emit closedChanged();\n    }\n\n    if (m_locks.isEmpty()) {\n        emit finishedClose();\n    }\n}\n\nvoid Toast::lock(QObject* sender) {\n    m_locks << sender;\n    QObject::connect(sender, &QObject::destroyed, this, &Toast::unlock);\n}\n\nvoid Toast::unlock(QObject* sender) {\n    if (m_locks.remove(sender) && m_closed) {\n        close();\n    }\n}\n\nToaster::Toaster(QObject* parent)\n    : QObject(parent) {}\n\nQQmlListProperty<Toast> Toaster::toasts() {\n    return QQmlListProperty<Toast>(this, &m_toasts);\n}\n\nvoid Toaster::toast(const QString& title, const QString& message, const QString& icon, Toast::Type type, int timeout) {\n    auto* toast = new Toast(title, message, icon, type, timeout, this);\n    QObject::connect(toast, &Toast::finishedClose, this, [toast, this]() {\n        if (m_toasts.removeOne(toast)) {\n            emit toastsChanged();\n            toast->deleteLater();\n        }\n    });\n    m_toasts.push_front(toast);\n    emit toastsChanged();\n}\n\n} // namespace caelestia\n"
  },
  {
    "path": "plugin/src/Caelestia/toaster.hpp",
    "content": "#pragma once\n\n#include <qobject.h>\n#include <qqmlintegration.h>\n#include <qqmllist.h>\n#include <qset.h>\n\nnamespace caelestia {\n\nclass Toast : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n    QML_UNCREATABLE(\"Toast instances can only be retrieved from a Toaster\")\n\n    Q_PROPERTY(bool closed READ closed NOTIFY closedChanged)\n    Q_PROPERTY(QString title READ title CONSTANT)\n    Q_PROPERTY(QString message READ message CONSTANT)\n    Q_PROPERTY(QString icon READ icon CONSTANT)\n    Q_PROPERTY(int timeout READ timeout CONSTANT)\n    Q_PROPERTY(Type type READ type CONSTANT)\n\npublic:\n    enum class Type {\n        Info = 0,\n        Success,\n        Warning,\n        Error\n    };\n    Q_ENUM(Type)\n\n    explicit Toast(const QString& title, const QString& message, const QString& icon, Type type, int timeout,\n        QObject* parent = nullptr);\n\n    [[nodiscard]] bool closed() const;\n    [[nodiscard]] QString title() const;\n    [[nodiscard]] QString message() const;\n    [[nodiscard]] QString icon() const;\n    [[nodiscard]] int timeout() const;\n    [[nodiscard]] Type type() const;\n\n    Q_INVOKABLE void close();\n    Q_INVOKABLE void lock(QObject* sender);\n    Q_INVOKABLE void unlock(QObject* sender);\n\nsignals:\n    void closedChanged();\n    void finishedClose();\n\nprivate:\n    QSet<QObject*> m_locks;\n\n    bool m_closed;\n    QString m_title;\n    QString m_message;\n    QString m_icon;\n    Type m_type;\n    int m_timeout;\n};\n\nclass Toaster : public QObject {\n    Q_OBJECT\n    QML_ELEMENT\n    QML_SINGLETON\n\n    Q_PROPERTY(QQmlListProperty<caelestia::Toast> toasts READ toasts NOTIFY toastsChanged)\n\npublic:\n    explicit Toaster(QObject* parent = nullptr);\n\n    [[nodiscard]] QQmlListProperty<Toast> toasts();\n\n    Q_INVOKABLE void toast(const QString& title, const QString& message, const QString& icon = QString(),\n        caelestia::Toast::Type type = Toast::Type::Info, int timeout = 5000);\n\nsignals:\n    void toastsChanged();\n\nprivate:\n    QList<Toast*> m_toasts;\n};\n\n} // namespace caelestia\n"
  },
  {
    "path": "scripts/qml-lint-conventions.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Checks QML files for Qt coding convention violations.\n\nhttps://doc.qt.io/qt-6/qml-codingconventions.html\n\nRequired ordering within each QML object (with blank line between sections):\n  1. id\n  2. property declarations\n  3. signal declarations\n  4. JavaScript functions\n  5. object properties (bindings)\n  6. child objects\n  7. component definitions\n\"\"\"\n\nimport re\nimport sys\nfrom enum import IntEnum\nfrom pathlib import Path\n\nRED = \"\\033[0;31m\"\nYELLOW = \"\\033[0;33m\"\nCYAN = \"\\033[0;36m\"\nGREEN = \"\\033[0;32m\"\nMAGENTA = \"\\033[0;35m\"\nBOLD = \"\\033[1m\"\nRESET = \"\\033[0m\"\n\nREPO_ROOT = Path(__file__).resolve().parent.parent\n\n\nclass Section(IntEnum):\n    ID = 0\n    PROPERTY = 1\n    SIGNAL = 2\n    FUNCTION = 3\n    BINDING = 4\n    CHILD = 5\n    COMPONENT_DEF = 6\n\n\nSECTION_NAMES = {\n    Section.ID: \"id\",\n    Section.PROPERTY: \"property declarations\",\n    Section.SIGNAL: \"signal declarations\",\n    Section.FUNCTION: \"functions\",\n    Section.BINDING: \"bindings\",\n    Section.CHILD: \"child objects\",\n    Section.COMPONENT_DEF: \"component definitions\",\n}\n\nRULE_COLOURS = {\n    \"section-order\": YELLOW,\n    \"missing-section-separator\": CYAN,\n    \"blank-after-open-brace\": MAGENTA,\n    \"blank-before-close-brace\": MAGENTA,\n}\n\n# Regexes\nPROPERTY_DECL_RE = re.compile(r\"^(?:required\\s+|readonly\\s+|default\\s+)*property\\s\")\nSIGNAL_RE = re.compile(r\"^signal\\s\")\nFUNCTION_RE = re.compile(r\"^function\\s\")\nID_RE = re.compile(r\"^id\\s*:\\s*[a-zA-Z_]\\w*\\s*$\")\nENUM_RE = re.compile(r\"^enum\\s\")\nCOMPONENT_DEF_RE = re.compile(r\"^component\\s+\\w+\\s*:\")\nCOMMENT_LINE_RE = re.compile(r\"^//\")\nBLOCK_COMMENT_START = re.compile(r\"/\\*\")\nBLOCK_COMMENT_END = re.compile(r\"\\*/\")\nBINDING_RE = re.compile(r\"^[a-z][a-zA-Z0-9_.]*\\s*:\")\nSIGNAL_HANDLER_RE = re.compile(r\"^on[A-Z][a-zA-Z]*\\s*:\")\n# Child object: starts with uppercase or is a known child-like pattern\nCHILD_OBJECT_RE = re.compile(r\"^[A-Z][a-zA-Z0-9_.]*\\s*\\{\")\n# Inline component: Component { ... }\nINLINE_COMPONENT_RE = re.compile(r\"^Component\\s*\\{\")\n# Behavior on <property> {, NumberAnimation on <property> {, etc.\nBEHAVIOR_ON_RE = re.compile(r\"^[A-Z]\\w+\\s+on\\s+\\w[\\w.]*\\s*\\{\")\n# Attached signal handler: Component.onCompleted:, Drag.onDragStarted:, etc.\nATTACHED_HANDLER_RE = re.compile(r\"^[A-Z]\\w+\\.on[A-Z]\\w*\\s*:\")\n\n\nclass Violation:\n    def __init__(self, file: str, line: int, rule: str, msg: str):\n        self.file = file\n        self.line = line\n        self.rule = rule\n        self.msg = msg\n\n    def __str__(self):\n        c = RULE_COLOURS.get(self.rule, \"\")\n        return f\"{c}[{self.rule}]{RESET} {self.file}:{self.line}: {self.msg}\"\n\n\nclass ScopeTracker:\n    \"\"\"Tracks the current section and last-seen state for one indent level.\"\"\"\n\n    def __init__(self):\n        self.last_section: Section | None = None\n        self.last_section_line: int = 0\n        self.had_blank_before_current: bool = True  # no separator needed at start\n\n\ndef get_indent(line: str) -> str:\n    return line[: len(line) - len(line.lstrip())]\n\n\ndef classify_line(stripped: str) -> Section | None:\n    \"\"\"Classify a stripped QML line into a section category.\"\"\"\n    if ID_RE.match(stripped):\n        return Section.ID\n    if PROPERTY_DECL_RE.match(stripped):\n        return Section.PROPERTY\n    if SIGNAL_RE.match(stripped):\n        return Section.SIGNAL\n    if FUNCTION_RE.match(stripped):\n        return Section.FUNCTION\n    if ENUM_RE.match(stripped):\n        return Section.PROPERTY  # enums go with declarations\n    if COMPONENT_DEF_RE.match(stripped):\n        return Section.COMPONENT_DEF\n    if BEHAVIOR_ON_RE.match(stripped):\n        return Section.CHILD\n    if CHILD_OBJECT_RE.match(stripped):\n        return Section.CHILD\n    if INLINE_COMPONENT_RE.match(stripped):\n        return Section.CHILD\n    if BINDING_RE.match(stripped) or SIGNAL_HANDLER_RE.match(stripped):\n        return Section.BINDING\n    if ATTACHED_HANDLER_RE.match(stripped):\n        return Section.BINDING\n    return None\n\n\ndef check_file(filepath: Path) -> list[Violation]:\n    violations = []\n    rel = str(filepath.relative_to(REPO_ROOT))\n\n    try:\n        lines = filepath.read_text().splitlines()\n    except (OSError, UnicodeDecodeError):\n        return violations\n\n    scopes: dict[str, ScopeTracker] = {}  # indent -> tracker\n    in_block_comment = False\n    func_skip_depth = 0  # brace depth for skipping function bodies only\n    prev_blank: dict[str, bool] = {}  # indent -> was previous relevant line a blank?\n\n    for i, line in enumerate(lines):\n        lineno = i + 1\n        stripped = line.strip()\n        indent = get_indent(line)\n\n        # Handle block comments\n        if in_block_comment:\n            if BLOCK_COMMENT_END.search(stripped):\n                in_block_comment = False\n            continue\n        if BLOCK_COMMENT_START.search(stripped) and not BLOCK_COMMENT_END.search(stripped):\n            in_block_comment = True\n            continue\n\n        # Track blank lines per indent\n        if not stripped:\n            # Check: blank line right after opening brace of a QML object\n            if i > 0 and func_skip_depth == 0 and not in_block_comment and lines[i - 1].strip().endswith(\"{\"):\n                violations.append(\n                    Violation(\n                        rel,\n                        lineno,\n                        \"blank-after-open-brace\",\n                        \"no blank line expected after opening brace\",\n                    )\n                )\n            for key in prev_blank:\n                prev_blank[key] = True\n            continue\n\n        # Skip line comments\n        if COMMENT_LINE_RE.match(stripped):\n            continue\n\n        # Skip inside function bodies (JS code, not QML structure)\n        if func_skip_depth > 0:\n            func_skip_depth += stripped.count(\"{\") - stripped.count(\"}\")\n            if func_skip_depth <= 0:\n                func_skip_depth = 0\n            continue\n\n        # Closing brace: pop all scopes deeper than this indent\n        # (the scope at this indent belongs to the parent object and must persist)\n        if stripped == \"}\":\n            # Check: blank line right before closing brace\n            if i > 0 and not lines[i - 1].strip():\n                violations.append(\n                    Violation(\n                        rel,\n                        lineno,\n                        \"blank-before-close-brace\",\n                        \"no blank line expected before closing brace\",\n                    )\n                )\n            to_remove = [k for k in scopes if len(k) > len(indent)]\n            for k in to_remove:\n                del scopes[k]\n                prev_blank.pop(k, None)\n            continue\n\n        section = classify_line(stripped)\n        if section is None:\n            continue\n\n        # Get or create scope tracker for this indent\n        if indent not in scopes:\n            scopes[indent] = ScopeTracker()\n            prev_blank[indent] = True  # treat start of object as having separator\n\n        tracker = scopes[indent]\n        had_blank = prev_blank.get(indent, True)\n\n        # --- Check 1: Section ordering ---\n        if tracker.last_section is not None and section < tracker.last_section:\n            violations.append(\n                Violation(\n                    rel,\n                    lineno,\n                    \"section-order\",\n                    f\"{SECTION_NAMES[section]} should appear before \"\n                    f\"{SECTION_NAMES[tracker.last_section]} \"\n                    f\"(seen at line {tracker.last_section_line})\",\n                )\n            )\n\n        # --- Check 2: Missing blank line between different sections ---\n        if tracker.last_section is not None and section != tracker.last_section and not had_blank:\n            violations.append(\n                Violation(\n                    rel,\n                    lineno,\n                    \"missing-section-separator\",\n                    f\"blank line expected between {SECTION_NAMES[tracker.last_section]} and {SECTION_NAMES[section]}\",\n                )\n            )\n\n        # Update tracker\n        if tracker.last_section is None or section >= tracker.last_section:\n            tracker.last_section = section\n            tracker.last_section_line = lineno\n\n        prev_blank[indent] = False\n\n        # Skip function bodies (they contain JS, not QML structure)\n        brace_count = stripped.count(\"{\") - stripped.count(\"}\")\n        if brace_count > 0 and section == Section.FUNCTION:\n            func_skip_depth = brace_count\n\n        # Skip JS blocks in bindings (signal handlers, attached handlers,\n        # and expression blocks like `color: { ... }`)\n        if brace_count > 0 and section == Section.BINDING:\n            colon_idx = stripped.index(\":\")\n            after_colon = stripped[colon_idx + 1 :].strip()\n            # If content after : doesn't start with an uppercase type name,\n            # it's a JS block (not an inline QML object like `contentItem: Rect {`)\n            if not re.match(r\"^[A-Z]\", after_colon):\n                func_skip_depth = brace_count\n\n        # Child object/component opening resets deeper scopes\n        if brace_count > 0 and section in (Section.CHILD, Section.COMPONENT_DEF):\n            to_remove = [k for k in scopes if len(k) > len(indent)]\n            for k in to_remove:\n                del scopes[k]\n                prev_blank.pop(k, None)\n\n    return violations\n\n\ndef main():\n    qml_files = sorted(p for p in REPO_ROOT.rglob(\"*.qml\") if \"build\" not in p.parts)\n\n    print(f\"{BOLD}Checking {len(qml_files)} QML files for convention violations...{RESET}\\n\")\n\n    all_violations: list[Violation] = []\n    for f in qml_files:\n        all_violations.extend(check_file(f))\n\n    for v in all_violations:\n        print(v)\n\n    print()\n    if all_violations:\n        by_rule: dict[str, int] = {}\n        for v in all_violations:\n            by_rule[v.rule] = by_rule.get(v.rule, 0) + 1\n        for rule, count in sorted(by_rule.items()):\n            print(f\"  {RULE_COLOURS.get(rule, '')}{rule}{RESET}: {count}\")\n        print(f\"\\n{BOLD}Found {len(all_violations)} violation(s).{RESET}\")\n        return 1\n    else:\n        print(f\"{BOLD}No violations found.{RESET}\")\n        return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "services/Audio.qml",
    "content": "pragma Singleton\n\nimport qs.config\nimport Caelestia.Services\nimport Caelestia\nimport Quickshell\nimport Quickshell.Services.Pipewire\nimport QtQuick\n\nSingleton {\n    id: root\n\n    property string previousSinkName: \"\"\n    property string previousSourceName: \"\"\n\n    property list<PwNode> sinks: []\n    property list<PwNode> sources: []\n    property list<PwNode> streams: []\n\n    readonly property PwNode sink: Pipewire.defaultAudioSink\n    readonly property PwNode source: Pipewire.defaultAudioSource\n\n    readonly property bool muted: !!sink?.audio?.muted\n    readonly property real volume: sink?.audio?.volume ?? 0\n\n    readonly property bool sourceMuted: !!source?.audio?.muted\n    readonly property real sourceVolume: source?.audio?.volume ?? 0\n\n    readonly property alias cava: cava\n    readonly property alias beatTracker: beatTracker\n\n    function setVolume(newVolume: real): void {\n        if (sink?.ready && sink?.audio) {\n            sink.audio.muted = false;\n            sink.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume));\n        }\n    }\n\n    function incrementVolume(amount: real): void {\n        setVolume(volume + (amount || Config.services.audioIncrement));\n    }\n\n    function decrementVolume(amount: real): void {\n        setVolume(volume - (amount || Config.services.audioIncrement));\n    }\n\n    function setSourceVolume(newVolume: real): void {\n        if (source?.ready && source?.audio) {\n            source.audio.muted = false;\n            source.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume));\n        }\n    }\n\n    function incrementSourceVolume(amount: real): void {\n        setSourceVolume(sourceVolume + (amount || Config.services.audioIncrement));\n    }\n\n    function decrementSourceVolume(amount: real): void {\n        setSourceVolume(sourceVolume - (amount || Config.services.audioIncrement));\n    }\n\n    function setAudioSink(newSink: PwNode): void {\n        Pipewire.preferredDefaultAudioSink = newSink;\n    }\n\n    function setAudioSource(newSource: PwNode): void {\n        Pipewire.preferredDefaultAudioSource = newSource;\n    }\n\n    function setStreamVolume(stream: PwNode, newVolume: real): void {\n        if (stream?.ready && stream?.audio) {\n            stream.audio.muted = false;\n            stream.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume));\n        }\n    }\n\n    function setStreamMuted(stream: PwNode, muted: bool): void {\n        if (stream?.ready && stream?.audio) {\n            stream.audio.muted = muted;\n        }\n    }\n\n    function getStreamVolume(stream: PwNode): real {\n        return stream?.audio?.volume ?? 0;\n    }\n\n    function getStreamMuted(stream: PwNode): bool {\n        return !!stream?.audio?.muted;\n    }\n\n    function getStreamName(stream: PwNode): string {\n        if (!stream)\n            return qsTr(\"Unknown\");\n        // Try application name first, then description, then name\n        return stream.properties[\"application.name\"] || stream.description || stream.name || qsTr(\"Unknown Application\");\n    }\n\n    onSinkChanged: {\n        if (!sink?.ready)\n            return;\n\n        const newSinkName = sink.description || sink.name || qsTr(\"Unknown Device\");\n\n        if (previousSinkName && previousSinkName !== newSinkName && Config.utilities.toasts.audioOutputChanged)\n            Toaster.toast(qsTr(\"Audio output changed\"), qsTr(\"Now using: %1\").arg(newSinkName), \"volume_up\");\n\n        previousSinkName = newSinkName;\n    }\n\n    onSourceChanged: {\n        if (!source?.ready)\n            return;\n\n        const newSourceName = source.description || source.name || qsTr(\"Unknown Device\");\n\n        if (previousSourceName && previousSourceName !== newSourceName && Config.utilities.toasts.audioInputChanged)\n            Toaster.toast(qsTr(\"Audio input changed\"), qsTr(\"Now using: %1\").arg(newSourceName), \"mic\");\n\n        previousSourceName = newSourceName;\n    }\n\n    Component.onCompleted: {\n        previousSinkName = sink?.description || sink?.name || qsTr(\"Unknown Device\");\n        previousSourceName = source?.description || source?.name || qsTr(\"Unknown Device\");\n    }\n\n    Connections {\n        function onValuesChanged(): void {\n            const newSinks = [];\n            const newSources = [];\n            const newStreams = [];\n\n            for (const node of Pipewire.nodes.values) {\n                if (!node.isStream) {\n                    if (node.isSink)\n                        newSinks.push(node);\n                    else if (node.audio)\n                        newSources.push(node);\n                } else if (node.audio) {\n                    newStreams.push(node);\n                }\n            }\n\n            root.sinks = newSinks;\n            root.sources = newSources;\n            root.streams = newStreams;\n        }\n\n        target: Pipewire.nodes\n    }\n\n    PwObjectTracker {\n        objects: [...root.sinks, ...root.sources, ...root.streams]\n    }\n\n    CavaProvider {\n        id: cava\n\n        bars: Config.services.visualiserBars\n    }\n\n    BeatTracker {\n        id: beatTracker\n    }\n}\n"
  },
  {
    "path": "services/Brightness.qml",
    "content": "pragma Singleton\npragma ComponentBehavior: Bound\n\nimport qs.config\nimport qs.components.misc\nimport Quickshell\nimport Quickshell.Io\nimport QtQuick\n\nSingleton {\n    id: root\n\n    property list<var> ddcMonitors: []\n    readonly property var ddcMonitorMap: {\n        const map = {};\n        for (const m of ddcMonitors)\n            map[m.connector] = m;\n        return map;\n    }\n    readonly property list<Monitor> monitors: variants.instances // qmllint disable incompatible-type\n    property bool appleDisplayPresent: false\n\n    function getMonitorForScreen(screen: ShellScreen): var {\n        return monitors.find(m => m.modelData === screen); // qmllint disable missing-property\n    }\n\n    function getMonitor(query: string): var {\n        if (query === \"active\") {\n            return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); // qmllint disable missing-property\n        }\n\n        if (query.startsWith(\"model:\")) {\n            const model = query.slice(6);\n            return monitors.find(m => m.modelData.model === model); // qmllint disable missing-property\n        }\n\n        if (query.startsWith(\"serial:\")) {\n            const serial = query.slice(7);\n            return monitors.find(m => m.modelData.serialNumber === serial); // qmllint disable missing-property\n        }\n\n        if (query.startsWith(\"id:\")) {\n            const id = parseInt(query.slice(3), 10);\n            return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); // qmllint disable missing-property\n        }\n\n        return monitors.find(m => m.modelData.name === query); // qmllint disable missing-property\n    }\n\n    function increaseBrightness(): void {\n        const monitor = getMonitor(\"active\");\n        if (monitor)\n            monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement);\n    }\n\n    function decreaseBrightness(): void {\n        const monitor = getMonitor(\"active\");\n        if (monitor)\n            monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement);\n    }\n\n    onMonitorsChanged: {\n        ddcMonitors = [];\n        ddcProc.running = true;\n    }\n\n    Variants {\n        id: variants\n\n        model: Quickshell.screens // Don't respect excluded screens cause ipc\n\n        Monitor {}\n    }\n\n    Process {\n        running: true\n        command: [\"sh\", \"-c\", \"asdbctl get\"] // To avoid warnings if asdbctl is not installed\n        stdout: StdioCollector {\n            onStreamFinished: root.appleDisplayPresent = text.trim().length > 0\n        }\n    }\n\n    Process {\n        id: ddcProc\n\n        command: [\"ddcutil\", \"detect\", \"--brief\"]\n        stdout: StdioCollector {\n            onStreamFinished: root.ddcMonitors = text.trim().split(\"\\n\\n\").filter(d => d.startsWith(\"Display \")).map(d => ({\n                        busNum: d.match(/I2C bus:[ ]*\\/dev\\/i2c-([0-9]+)/)[1],\n                        connector: d.match(/DRM connector:\\s+(.*)/)[1].replace(/^card\\d+-/, \"\") // strip \"card1-\"\n                    }))\n        }\n    }\n\n    CustomShortcut {\n        name: \"brightnessUp\"\n        description: \"Increase brightness\"\n        onPressed: root.increaseBrightness()\n    }\n\n    CustomShortcut {\n        name: \"brightnessDown\"\n        description: \"Decrease brightness\"\n        onPressed: root.decreaseBrightness()\n    }\n\n    IpcHandler {\n        function get(): real {\n            return getFor(\"active\");\n        }\n\n        // Allows searching by active/model/serial/id/name\n        function getFor(query: string): real {\n            return root.getMonitor(query)?.brightness ?? -1;\n        }\n\n        function set(value: string): string {\n            return setFor(\"active\", value);\n        }\n\n        // Handles brightness value like brightnessctl: 0.1, +0.1, 0.1-, 10%, +10%, 10%-\n        function setFor(query: string, value: string): string {\n            const monitor = root.getMonitor(query);\n            if (!monitor)\n                return \"Invalid monitor: \" + query;\n\n            let targetBrightness;\n            if (value.endsWith(\"%-\")) {\n                const percent = parseFloat(value.slice(0, -2));\n                targetBrightness = monitor.brightness - (percent / 100);\n            } else if (value.startsWith(\"+\") && value.endsWith(\"%\")) {\n                const percent = parseFloat(value.slice(1, -1));\n                targetBrightness = monitor.brightness + (percent / 100);\n            } else if (value.endsWith(\"%\")) {\n                const percent = parseFloat(value.slice(0, -1));\n                targetBrightness = percent / 100;\n            } else if (value.startsWith(\"+\")) {\n                const increment = parseFloat(value.slice(1));\n                targetBrightness = monitor.brightness + increment;\n            } else if (value.endsWith(\"-\")) {\n                const decrement = parseFloat(value.slice(0, -1));\n                targetBrightness = monitor.brightness - decrement;\n            } else if (value.includes(\"%\") || value.includes(\"-\") || value.includes(\"+\")) {\n                return `Invalid brightness format: ${value}\\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`;\n            } else {\n                targetBrightness = parseFloat(value);\n            }\n\n            if (isNaN(targetBrightness))\n                return `Failed to parse value: ${value}\\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`;\n\n            monitor.setBrightness(targetBrightness);\n\n            return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`;\n        }\n\n        target: \"brightness\"\n    }\n\n    component Monitor: QtObject {\n        id: monitor\n\n        required property ShellScreen modelData\n        readonly property var ddcInfo: root.ddcMonitorMap[modelData.name] ?? null\n        readonly property bool isDdc: ddcInfo !== null\n        readonly property string busNum: ddcInfo?.busNum ?? \"\"\n        readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith(\"StudioDisplay\")\n        property real brightness\n        property real queuedBrightness: NaN\n\n        readonly property Process initProc: Process {\n            stdout: StdioCollector {\n                onStreamFinished: {\n                    if (monitor.isAppleDisplay) {\n                        const val = parseInt(text.trim());\n                        monitor.brightness = val / 101;\n                    } else {\n                        const [, , , cur, max] = text.split(\" \");\n                        monitor.brightness = parseInt(cur) / parseInt(max);\n                    }\n                }\n            }\n        }\n\n        readonly property Timer timer: Timer {\n            interval: 500\n            onTriggered: {\n                if (!isNaN(monitor.queuedBrightness)) {\n                    monitor.setBrightness(monitor.queuedBrightness);\n                    monitor.queuedBrightness = NaN;\n                }\n            }\n        }\n\n        function setBrightness(value: real): void {\n            value = Math.max(0, Math.min(1, value));\n            const rounded = Math.round(value * 100);\n            if (Math.round(brightness * 100) === rounded)\n                return;\n\n            if (isDdc && timer.running) {\n                queuedBrightness = value;\n                return;\n            }\n\n            brightness = value;\n\n            if (isAppleDisplay)\n                Quickshell.execDetached([\"asdbctl\", \"set\", rounded]);\n            else if (isDdc)\n                Quickshell.execDetached([\"ddcutil\", \"-b\", busNum, \"setvcp\", \"10\", rounded]);\n            else\n                Quickshell.execDetached([\"brightnessctl\", \"s\", `${rounded}%`]);\n\n            if (isDdc)\n                timer.restart();\n        }\n\n        function initBrightness(): void {\n            if (isAppleDisplay)\n                initProc.command = [\"asdbctl\", \"get\"];\n            else if (isDdc)\n                initProc.command = [\"ddcutil\", \"-b\", busNum, \"getvcp\", \"10\", \"--brief\"];\n            else\n                initProc.command = [\"sh\", \"-c\", \"echo a b c $(brightnessctl g) $(brightnessctl m)\"];\n\n            initProc.running = true;\n        }\n\n        onBusNumChanged: initBrightness()\n        Component.onCompleted: initBrightness()\n    }\n}\n"
  },
  {
    "path": "services/Colours.qml",
    "content": "pragma Singleton\npragma ComponentBehavior: Bound\n\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Caelestia\nimport Quickshell\nimport Quickshell.Io\nimport QtQuick\n\nSingleton {\n    id: root\n\n    property bool showPreview\n    property string scheme\n    property string flavour\n    readonly property bool light: showPreview ? previewLight : currentLight\n    property bool currentLight\n    property bool previewLight\n    readonly property M3Palette palette: showPreview ? preview : current\n    readonly property M3TPalette tPalette: M3TPalette {}\n    readonly property M3Palette current: M3Palette {}\n    readonly property M3Palette preview: M3Palette {}\n    readonly property Transparency transparency: Transparency {}\n    readonly property alias wallLuminance: analyser.luminance\n\n    function getLuminance(c: color): real {\n        if (c.r == 0 && c.g == 0 && c.b == 0)\n            return 0;\n        return Math.sqrt(0.299 * (c.r ** 2) + 0.587 * (c.g ** 2) + 0.114 * (c.b ** 2));\n    }\n\n    function alterColour(c: color, a: real, layer: int): color {\n        const luminance = getLuminance(c);\n\n        const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (1 - transparency.base) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5));\n        const scale = (luminance + offset) / luminance;\n        const r = Math.max(0, Math.min(1, c.r * scale));\n        const g = Math.max(0, Math.min(1, c.g * scale));\n        const b = Math.max(0, Math.min(1, c.b * scale));\n\n        return Qt.rgba(r, g, b, a);\n    }\n\n    function layer(c: color, layer: var): color {\n        if (!transparency.enabled)\n            return c;\n\n        return layer === 0 ? Qt.alpha(c, transparency.base) : alterColour(c, transparency.layers, layer ?? 1);\n    }\n\n    function on(c: color): color {\n        if (c.hslLightness < 0.5)\n            return Qt.hsla(c.hslHue, c.hslSaturation, 0.9, 1);\n        return Qt.hsla(c.hslHue, c.hslSaturation, 0.1, 1);\n    }\n\n    function load(data: string, isPreview: bool): void {\n        const colours = isPreview ? preview : current;\n        const scheme = JSON.parse(data);\n\n        if (!isPreview) {\n            root.scheme = scheme.name;\n            flavour = scheme.flavour;\n            currentLight = scheme.mode === \"light\";\n        } else {\n            previewLight = scheme.mode === \"light\";\n        }\n\n        for (const [name, colour] of Object.entries(scheme.colours)) {\n            const propName = name.startsWith(\"term\") ? name : `m3${name}`;\n            if (colours.hasOwnProperty(propName))\n                colours[propName] = `#${colour}`;\n        }\n    }\n\n    function setMode(mode: string): void {\n        Quickshell.execDetached([\"caelestia\", \"scheme\", \"set\", \"--notify\", \"-m\", mode]);\n    }\n\n    FileView {\n        path: `${Paths.state}/scheme.json`\n        watchChanges: true\n        onFileChanged: reload()\n        onLoaded: root.load(text(), false)\n    }\n\n    ImageAnalyser {\n        id: analyser\n\n        source: Wallpapers.current\n    }\n\n    component Transparency: QtObject {\n        readonly property bool enabled: Appearance.transparency.enabled\n        readonly property real base: Appearance.transparency.base - (root.light ? 0.1 : 0)\n        readonly property real layers: Appearance.transparency.layers\n    }\n\n    component M3TPalette: QtObject {\n        readonly property color m3primary_paletteKeyColor: root.layer(root.palette.m3primary_paletteKeyColor)\n        readonly property color m3secondary_paletteKeyColor: root.layer(root.palette.m3secondary_paletteKeyColor)\n        readonly property color m3tertiary_paletteKeyColor: root.layer(root.palette.m3tertiary_paletteKeyColor)\n        readonly property color m3neutral_paletteKeyColor: root.layer(root.palette.m3neutral_paletteKeyColor)\n        readonly property color m3neutral_variant_paletteKeyColor: root.layer(root.palette.m3neutral_variant_paletteKeyColor)\n        readonly property color m3background: root.layer(root.palette.m3background, 0)\n        readonly property color m3onBackground: root.layer(root.palette.m3onBackground)\n        readonly property color m3surface: root.layer(root.palette.m3surface, 0)\n        readonly property color m3surfaceDim: root.layer(root.palette.m3surfaceDim, 0)\n        readonly property color m3surfaceBright: root.layer(root.palette.m3surfaceBright, 0)\n        readonly property color m3surfaceContainerLowest: root.layer(root.palette.m3surfaceContainerLowest)\n        readonly property color m3surfaceContainerLow: root.layer(root.palette.m3surfaceContainerLow)\n        readonly property color m3surfaceContainer: root.layer(root.palette.m3surfaceContainer)\n        readonly property color m3surfaceContainerHigh: root.layer(root.palette.m3surfaceContainerHigh)\n        readonly property color m3surfaceContainerHighest: root.layer(root.palette.m3surfaceContainerHighest)\n        readonly property color m3onSurface: root.layer(root.palette.m3onSurface)\n        readonly property color m3surfaceVariant: root.layer(root.palette.m3surfaceVariant, 0)\n        readonly property color m3onSurfaceVariant: root.layer(root.palette.m3onSurfaceVariant)\n        readonly property color m3inverseSurface: root.layer(root.palette.m3inverseSurface, 0)\n        readonly property color m3inverseOnSurface: root.layer(root.palette.m3inverseOnSurface)\n        readonly property color m3outline: root.layer(root.palette.m3outline)\n        readonly property color m3outlineVariant: root.layer(root.palette.m3outlineVariant)\n        readonly property color m3shadow: root.layer(root.palette.m3shadow)\n        readonly property color m3scrim: root.layer(root.palette.m3scrim)\n        readonly property color m3surfaceTint: root.layer(root.palette.m3surfaceTint)\n        readonly property color m3primary: root.layer(root.palette.m3primary)\n        readonly property color m3onPrimary: root.layer(root.palette.m3onPrimary)\n        readonly property color m3primaryContainer: root.layer(root.palette.m3primaryContainer)\n        readonly property color m3onPrimaryContainer: root.layer(root.palette.m3onPrimaryContainer)\n        readonly property color m3inversePrimary: root.layer(root.palette.m3inversePrimary)\n        readonly property color m3secondary: root.layer(root.palette.m3secondary)\n        readonly property color m3onSecondary: root.layer(root.palette.m3onSecondary)\n        readonly property color m3secondaryContainer: root.layer(root.palette.m3secondaryContainer)\n        readonly property color m3onSecondaryContainer: root.layer(root.palette.m3onSecondaryContainer)\n        readonly property color m3tertiary: root.layer(root.palette.m3tertiary)\n        readonly property color m3onTertiary: root.layer(root.palette.m3onTertiary)\n        readonly property color m3tertiaryContainer: root.layer(root.palette.m3tertiaryContainer)\n        readonly property color m3onTertiaryContainer: root.layer(root.palette.m3onTertiaryContainer)\n        readonly property color m3error: root.layer(root.palette.m3error)\n        readonly property color m3onError: root.layer(root.palette.m3onError)\n        readonly property color m3errorContainer: root.layer(root.palette.m3errorContainer)\n        readonly property color m3onErrorContainer: root.layer(root.palette.m3onErrorContainer)\n        readonly property color m3success: root.layer(root.palette.m3success)\n        readonly property color m3onSuccess: root.layer(root.palette.m3onSuccess)\n        readonly property color m3successContainer: root.layer(root.palette.m3successContainer)\n        readonly property color m3onSuccessContainer: root.layer(root.palette.m3onSuccessContainer)\n        readonly property color m3primaryFixed: root.layer(root.palette.m3primaryFixed)\n        readonly property color m3primaryFixedDim: root.layer(root.palette.m3primaryFixedDim)\n        readonly property color m3onPrimaryFixed: root.layer(root.palette.m3onPrimaryFixed)\n        readonly property color m3onPrimaryFixedVariant: root.layer(root.palette.m3onPrimaryFixedVariant)\n        readonly property color m3secondaryFixed: root.layer(root.palette.m3secondaryFixed)\n        readonly property color m3secondaryFixedDim: root.layer(root.palette.m3secondaryFixedDim)\n        readonly property color m3onSecondaryFixed: root.layer(root.palette.m3onSecondaryFixed)\n        readonly property color m3onSecondaryFixedVariant: root.layer(root.palette.m3onSecondaryFixedVariant)\n        readonly property color m3tertiaryFixed: root.layer(root.palette.m3tertiaryFixed)\n        readonly property color m3tertiaryFixedDim: root.layer(root.palette.m3tertiaryFixedDim)\n        readonly property color m3onTertiaryFixed: root.layer(root.palette.m3onTertiaryFixed)\n        readonly property color m3onTertiaryFixedVariant: root.layer(root.palette.m3onTertiaryFixedVariant)\n    }\n\n    component M3Palette: QtObject {\n        property color m3primary_paletteKeyColor: \"#a8627b\"\n        property color m3secondary_paletteKeyColor: \"#8e6f78\"\n        property color m3tertiary_paletteKeyColor: \"#986e4c\"\n        property color m3neutral_paletteKeyColor: \"#807477\"\n        property color m3neutral_variant_paletteKeyColor: \"#837377\"\n        property color m3background: \"#191114\"\n        property color m3onBackground: \"#efdfe2\"\n        property color m3surface: \"#191114\"\n        property color m3surfaceDim: \"#191114\"\n        property color m3surfaceBright: \"#403739\"\n        property color m3surfaceContainerLowest: \"#130c0e\"\n        property color m3surfaceContainerLow: \"#22191c\"\n        property color m3surfaceContainer: \"#261d20\"\n        property color m3surfaceContainerHigh: \"#31282a\"\n        property color m3surfaceContainerHighest: \"#3c3235\"\n        property color m3onSurface: \"#efdfe2\"\n        property color m3surfaceVariant: \"#514347\"\n        property color m3onSurfaceVariant: \"#d5c2c6\"\n        property color m3inverseSurface: \"#efdfe2\"\n        property color m3inverseOnSurface: \"#372e30\"\n        property color m3outline: \"#9e8c91\"\n        property color m3outlineVariant: \"#514347\"\n        property color m3shadow: \"#000000\"\n        property color m3scrim: \"#000000\"\n        property color m3surfaceTint: \"#ffb0ca\"\n        property color m3primary: \"#ffb0ca\"\n        property color m3onPrimary: \"#541d34\"\n        property color m3primaryContainer: \"#6f334a\"\n        property color m3onPrimaryContainer: \"#ffd9e3\"\n        property color m3inversePrimary: \"#8b4a62\"\n        property color m3secondary: \"#e2bdc7\"\n        property color m3onSecondary: \"#422932\"\n        property color m3secondaryContainer: \"#5a3f48\"\n        property color m3onSecondaryContainer: \"#ffd9e3\"\n        property color m3tertiary: \"#f0bc95\"\n        property color m3onTertiary: \"#48290c\"\n        property color m3tertiaryContainer: \"#b58763\"\n        property color m3onTertiaryContainer: \"#000000\"\n        property color m3error: \"#ffb4ab\"\n        property color m3onError: \"#690005\"\n        property color m3errorContainer: \"#93000a\"\n        property color m3onErrorContainer: \"#ffdad6\"\n        property color m3success: \"#B5CCBA\"\n        property color m3onSuccess: \"#213528\"\n        property color m3successContainer: \"#374B3E\"\n        property color m3onSuccessContainer: \"#D1E9D6\"\n        property color m3primaryFixed: \"#ffd9e3\"\n        property color m3primaryFixedDim: \"#ffb0ca\"\n        property color m3onPrimaryFixed: \"#39071f\"\n        property color m3onPrimaryFixedVariant: \"#6f334a\"\n        property color m3secondaryFixed: \"#ffd9e3\"\n        property color m3secondaryFixedDim: \"#e2bdc7\"\n        property color m3onSecondaryFixed: \"#2b151d\"\n        property color m3onSecondaryFixedVariant: \"#5a3f48\"\n        property color m3tertiaryFixed: \"#ffdcc3\"\n        property color m3tertiaryFixedDim: \"#f0bc95\"\n        property color m3onTertiaryFixed: \"#2f1500\"\n        property color m3onTertiaryFixedVariant: \"#623f21\"\n        property color term0: \"#353434\"\n        property color term1: \"#ff4c8a\"\n        property color term2: \"#ffbbb7\"\n        property color term3: \"#ffdedf\"\n        property color term4: \"#b3a2d5\"\n        property color term5: \"#e98fb0\"\n        property color term6: \"#ffba93\"\n        property color term7: \"#eed1d2\"\n        property color term8: \"#b39e9e\"\n        property color term9: \"#ff80a3\"\n        property color term10: \"#ffd3d0\"\n        property color term11: \"#fff1f0\"\n        property color term12: \"#dcbc93\"\n        property color term13: \"#f9a8c2\"\n        property color term14: \"#ffd1c0\"\n        property color term15: \"#ffffff\"\n    }\n}\n"
  },
  {
    "path": "services/GameMode.qml",
    "content": "pragma Singleton\n\nimport qs.services\nimport qs.config\nimport Caelestia\nimport Quickshell\nimport Quickshell.Io\nimport QtQuick\n\nSingleton {\n    id: root\n\n    property alias enabled: props.enabled\n\n    function setDynamicConfs(): void {\n        Hypr.extras.applyOptions({\n            \"animations:enabled\": 0,\n            \"decoration:shadow:enabled\": 0,\n            \"decoration:blur:enabled\": 0,\n            \"general:gaps_in\": 0,\n            \"general:gaps_out\": 0,\n            \"general:border_size\": 1,\n            \"decoration:rounding\": 0,\n            \"general:allow_tearing\": 1\n        });\n    }\n\n    onEnabledChanged: {\n        if (enabled) {\n            setDynamicConfs();\n            if (Config.utilities.toasts.gameModeChanged)\n                Toaster.toast(qsTr(\"Game mode enabled\"), qsTr(\"Disabled Hyprland animations, blur, gaps and shadows\"), \"gamepad\");\n        } else {\n            Hypr.extras.message(\"reload\");\n            if (Config.utilities.toasts.gameModeChanged)\n                Toaster.toast(qsTr(\"Game mode disabled\"), qsTr(\"Hyprland settings restored\"), \"gamepad\");\n        }\n    }\n\n    PersistentProperties {\n        id: props\n\n        property bool enabled: Hypr.options[\"animations:enabled\"] === 0 // qmllint disable missing-property\n\n        reloadableId: \"gameMode\"\n    }\n\n    Connections {\n        function onConfigReloaded(): void {\n            if (props.enabled)\n                root.setDynamicConfs();\n        }\n\n        target: Hypr\n    }\n\n    IpcHandler {\n        function isEnabled(): bool {\n            return props.enabled;\n        }\n\n        function toggle(): void {\n            props.enabled = !props.enabled;\n        }\n\n        function enable(): void {\n            props.enabled = true;\n        }\n\n        function disable(): void {\n            props.enabled = false;\n        }\n\n        target: \"gameMode\"\n    }\n}\n"
  },
  {
    "path": "services/Hypr.qml",
    "content": "pragma Singleton\n\nimport qs.components.misc\nimport qs.config\nimport Caelestia\nimport Caelestia.Internal\nimport Quickshell\nimport Quickshell.Hyprland\nimport Quickshell.Io\nimport QtQuick\n\nSingleton {\n    id: root\n\n    readonly property var toplevels: Hyprland.toplevels\n    readonly property var workspaces: Hyprland.workspaces\n    readonly property var monitors: Hyprland.monitors\n\n    readonly property HyprlandToplevel activeToplevel: {\n        const t = Hyprland.activeToplevel;\n        return t?.workspace?.name.startsWith(\"special:\") || Hyprland.focusedWorkspace?.toplevels.values.length > 0 ? t : null;\n    }\n    readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace\n    readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor\n    readonly property int activeWsId: focusedWorkspace?.id ?? 1\n\n    readonly property HyprKeyboard keyboard: extras.devices.keyboards.find(kb => kb.main) ?? null\n    readonly property bool capsLock: keyboard?.capsLock ?? false\n    readonly property bool numLock: keyboard?.numLock ?? false\n    readonly property string defaultKbLayout: keyboard?.layout.split(\",\")[0] ?? \"??\"\n    readonly property string kbLayoutFull: keyboard?.activeKeymap ?? \"Unknown\"\n    readonly property string kbLayout: kbMap.get(kbLayoutFull) ?? \"??\"\n    readonly property var kbMap: new Map()\n\n    readonly property alias extras: extras\n    readonly property alias options: extras.options\n    readonly property alias devices: extras.devices\n\n    property bool hadKeyboard\n    property string lastSpecialWorkspace: \"\"\n\n    signal configReloaded\n\n    function dispatch(request: string): void {\n        Hyprland.dispatch(request);\n    }\n\n    function cycleSpecialWorkspace(direction: string): void {\n        const openSpecials = workspaces.values.filter(w => w.name.startsWith(\"special:\") && w.lastIpcObject.windows > 0);\n\n        if (openSpecials.length === 0)\n            return;\n\n        const activeSpecial = focusedMonitor.lastIpcObject.specialWorkspace.name ?? \"\";\n\n        if (!activeSpecial) {\n            if (lastSpecialWorkspace) {\n                const workspace = workspaces.values.find(w => w.name === lastSpecialWorkspace);\n                if (workspace && workspace.lastIpcObject.windows > 0) {\n                    dispatch(`workspace ${lastSpecialWorkspace}`);\n                    return;\n                }\n            }\n            dispatch(`workspace ${openSpecials[0].name}`);\n            return;\n        }\n\n        const currentIndex = openSpecials.findIndex(w => w.name === activeSpecial);\n        let nextIndex = 0;\n\n        if (currentIndex !== -1) {\n            if (direction === \"next\")\n                nextIndex = (currentIndex + 1) % openSpecials.length;\n            else\n                nextIndex = (currentIndex - 1 + openSpecials.length) % openSpecials.length;\n        }\n\n        dispatch(`workspace ${openSpecials[nextIndex].name}`);\n    }\n\n    function monitorNames(): list<string> {\n        return monitors.values.map(e => e.name);\n    }\n\n    function monitorFor(screen: ShellScreen): HyprlandMonitor {\n        return Hyprland.monitorFor(screen);\n    }\n\n    function reloadDynamicConfs(): void {\n        extras.batchMessage([\"keyword bindlni ,Caps_Lock,global,caelestia:refreshDevices\", \"keyword bindlni ,Num_Lock,global,caelestia:refreshDevices\"]);\n    }\n\n    Component.onCompleted: reloadDynamicConfs()\n\n    onCapsLockChanged: {\n        if (!Config.utilities.toasts.capsLockChanged)\n            return;\n\n        if (capsLock)\n            Toaster.toast(qsTr(\"Caps lock enabled\"), qsTr(\"Caps lock is currently enabled\"), \"keyboard_capslock_badge\");\n        else\n            Toaster.toast(qsTr(\"Caps lock disabled\"), qsTr(\"Caps lock is currently disabled\"), \"keyboard_capslock\");\n    }\n\n    onNumLockChanged: {\n        if (!Config.utilities.toasts.numLockChanged)\n            return;\n\n        if (numLock)\n            Toaster.toast(qsTr(\"Num lock enabled\"), qsTr(\"Num lock is currently enabled\"), \"looks_one\");\n        else\n            Toaster.toast(qsTr(\"Num lock disabled\"), qsTr(\"Num lock is currently disabled\"), \"timer_1\");\n    }\n\n    onKbLayoutFullChanged: {\n        if (hadKeyboard && Config.utilities.toasts.kbLayoutChanged)\n            Toaster.toast(qsTr(\"Keyboard layout changed\"), qsTr(\"Layout changed to: %1\").arg(kbLayoutFull), \"keyboard\");\n\n        hadKeyboard = !!keyboard;\n    }\n\n    Connections {\n        function onRawEvent(event: HyprlandEvent): void {\n            const n = event.name;\n            if (n.endsWith(\"v2\"))\n                return;\n\n            if (n === \"configreloaded\") {\n                root.configReloaded();\n                root.reloadDynamicConfs();\n            } else if ([\"workspace\", \"moveworkspace\", \"activespecial\", \"focusedmon\"].includes(n)) {\n                Hyprland.refreshWorkspaces();\n                Hyprland.refreshMonitors();\n            } else if ([\"openwindow\", \"closewindow\", \"movewindow\"].includes(n)) {\n                Hyprland.refreshToplevels();\n                Hyprland.refreshWorkspaces();\n            } else if (n.includes(\"mon\")) {\n                Hyprland.refreshMonitors();\n            } else if (n.includes(\"workspace\")) {\n                Hyprland.refreshWorkspaces();\n            } else if (n.includes(\"window\") || n.includes(\"group\") || [\"pin\", \"fullscreen\", \"changefloatingmode\", \"minimize\"].includes(n)) {\n                Hyprland.refreshToplevels();\n            }\n        }\n\n        target: Hyprland\n    }\n\n    Connections {\n        function onLastIpcObjectChanged(): void {\n            const specialName = root.focusedMonitor.lastIpcObject.specialWorkspace.name;\n\n            if (specialName && specialName.startsWith(\"special:\")) {\n                root.lastSpecialWorkspace = specialName;\n            }\n        }\n\n        target: root.focusedMonitor\n    }\n\n    FileView {\n        id: kbLayoutFile\n\n        path: Quickshell.env(\"CAELESTIA_XKB_RULES_PATH\") || \"/usr/share/X11/xkb/rules/base.lst\"\n        onLoaded: {\n            const layoutMatch = text().match(/! layout\\n([\\s\\S]*?)\\n\\n/);\n            if (layoutMatch) {\n                const lines = layoutMatch[1].split(\"\\n\");\n                for (const line of lines) {\n                    if (!line.trim() || line.trim().startsWith(\"!\"))\n                        continue;\n\n                    const match = line.match(/^\\s*([a-z]{2,})\\s+([a-zA-Z() ]+)$/);\n                    if (match)\n                        root.kbMap.set(match[2], match[1]);\n                }\n            }\n\n            const variantMatch = text().match(/! variant\\n([\\s\\S]*?)\\n\\n/);\n            if (variantMatch) {\n                const lines = variantMatch[1].split(\"\\n\");\n                for (const line of lines) {\n                    if (!line.trim() || line.trim().startsWith(\"!\"))\n                        continue;\n\n                    const match = line.match(/^\\s*([a-zA-Z0-9_-]+)\\s+([a-z]{2,}): (.+)$/);\n                    if (match)\n                        root.kbMap.set(match[3], match[2]);\n                }\n            }\n        }\n    }\n\n    IpcHandler {\n        function refreshDevices(): void {\n            extras.refreshDevices();\n        }\n\n        function cycleSpecialWorkspace(direction: string): void {\n            root.cycleSpecialWorkspace(direction);\n        }\n\n        function listSpecialWorkspaces(): string {\n            return root.workspaces.values.filter(w => w.name.startsWith(\"special:\") && w.lastIpcObject.windows > 0).map(w => w.name).join(\"\\n\");\n        }\n\n        target: \"hypr\"\n    }\n\n    CustomShortcut {\n        name: \"refreshDevices\"\n        description: \"Reload devices\"\n        onPressed: extras.refreshDevices()\n        onReleased: extras.refreshDevices()\n    }\n\n    HyprExtras {\n        id: extras\n    }\n}\n"
  },
  {
    "path": "services/IdleInhibitor.qml",
    "content": "pragma Singleton\n\nimport Quickshell\nimport Quickshell.Io\nimport Quickshell.Wayland\n\nSingleton {\n    id: root\n\n    property alias enabled: props.enabled\n    readonly property alias enabledSince: props.enabledSince\n\n    onEnabledChanged: {\n        if (enabled)\n            props.enabledSince = new Date();\n    }\n\n    PersistentProperties {\n        id: props\n\n        property bool enabled\n        property date enabledSince\n\n        reloadableId: \"idleInhibitor\"\n    }\n\n    IdleInhibitor {\n        enabled: props.enabled\n        window: PanelWindow {\n            implicitWidth: 0\n            implicitHeight: 0\n            color: \"transparent\"\n            mask: Region {}\n        }\n    }\n\n    IpcHandler {\n        function isEnabled(): bool {\n            return props.enabled;\n        }\n\n        function toggle(): void {\n            props.enabled = !props.enabled;\n        }\n\n        function enable(): void {\n            props.enabled = true;\n        }\n\n        function disable(): void {\n            props.enabled = false;\n        }\n\n        target: \"idleInhibitor\"\n    }\n}\n"
  },
  {
    "path": "services/LyricsService.qml",
    "content": "pragma Singleton\n\nimport qs.config\nimport qs.utils\nimport Caelestia\nimport QtQuick\nimport Quickshell\nimport Quickshell.Io\nimport \"../utils/scripts/lrcparser.js\" as Lrc\n\nSingleton {\n    id: root\n\n    property var player: Players.active\n    property int currentIndex: -1\n    property bool loading: false\n    property bool isManualSeeking: false\n    property bool lyricsVisible: Config.services.showLyrics\n    property string backend: \"Local\"\n    property real currentSongId: 0\n\n    property real offset\n\n    readonly property string lyricsDir: Paths.absolutePath(Config.paths.lyricsDir)\n    readonly property string lyricsMapFile: Paths.absolutePath(Config.paths.lyricsDir) + \"/lyrics_map.json\"\n\n    property int currentRequestId: 0\n\n    // The data source for the UI\n    readonly property alias model: lyricsModel\n    readonly property alias candidatesModel: fetchedCandidatesModel\n\n    property var lyricsMap: ({})\n\n    // shared headers for all NetEase requests\n    readonly property var _netEaseHeaders: ({\n            \"User-Agent\": \"Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0\",\n            \"Referer\": \"https://music.163.com/\"\n        })\n\n    ListModel {\n        id: lyricsModel\n    }\n    ListModel {\n        id: fetchedCandidatesModel\n    }\n\n    Timer {\n        id: seekTimer\n\n        interval: 500\n        onTriggered: root.isManualSeeking = false\n    }\n\n    // If no local lyrics were loaded within the interval, fall back to NetEase\n    Timer {\n        id: fallbackTimer\n\n        interval: 200\n        onTriggered: {\n            if (lyricsModel.count === 0) {\n                root.backend = \"NetEase\";\n                fallbackToOnline();\n            }\n        }\n    }\n\n    Timer {\n        id: loadDebounce\n\n        interval: 50\n        onTriggered: root._doLoadLyrics()\n    }\n\n    FileView {\n        id: lyricsMapFileView\n\n        path: root.lyricsMapFile\n        printErrors: false\n        onLoaded: {\n            try {\n                root.lyricsMap = JSON.parse(text());\n            } catch (e) {\n                root.lyricsMap = {};\n            }\n        }\n    }\n\n    FileView {\n        id: lrcFile\n\n        printErrors: false\n        onLoaded: {\n            fallbackTimer.stop();\n            let parsed = Lrc.parseLrc(text());\n            if (parsed.length > 0) {\n                root.backend = \"Local\";\n                updateModel(parsed);\n                loading = false;\n            } else {\n                root.backend = \"NetEase\";\n                fallbackToOnline();\n            }\n        }\n    }\n\n    Connections {\n        function onActiveChanged() {\n            root.player = Players.active;\n            loadLyrics();\n        }\n\n        target: Players\n    }\n\n    Connections {\n        function onMetadataChanged() {\n            loadLyrics();\n        }\n\n        target: root.player\n        ignoreUnknownSignals: true\n    }\n\n    Process {\n        id: saveLyricsMap\n\n        command: [\"sh\", \"-c\", `mkdir -p \"${root.lyricsDir}\" && echo '${JSON.stringify(root.lyricsMap)}' > \"${root.lyricsMapFile}\"`]\n    }\n\n    function getMetadata() {\n        if (!player || !player.metadata)\n            return null;\n        let artist = player.metadata[\"xesam:artist\"];\n        const title = player.metadata[\"xesam:title\"];\n        if (Array.isArray(artist))\n            artist = artist.join(\", \");\n        return {\n            artist: artist || \"Unknown\",\n            title: title || \"Unknown\"\n        };\n    }\n\n    function _metaKey(meta) {\n        return `${meta.artist} - ${meta.title}`;\n    }\n\n    function savePrefs() {\n        let meta = getMetadata();\n        if (!meta)\n            return;\n        let key = _metaKey(meta);\n        let existing = root.lyricsMap[key] ?? {};\n        root.lyricsMap[key] = {\n            offset: root.offset,\n            backend: root.backend,\n            neteaseId: existing.neteaseId ?? null\n        };\n        // reassign to notify QML bindings of the map change\n        root.lyricsMap = root.lyricsMap;\n        saveLyricsMap.command = [\"sh\", \"-c\", `mkdir -p \"${root.lyricsDir}\" && echo '${JSON.stringify(root.lyricsMap).replace(/'/g, \"'\\\\''\")}' > \"${root.lyricsMapFile}\"`];\n        saveLyricsMap.running = true;\n    }\n\n    function toggleVisibility() {\n        Config.services.showLyrics = !Config.services.showLyrics;\n        Config.save();\n    }\n\n    function loadLyrics() {\n        loadDebounce.restart();\n    }\n\n    function _doLoadLyrics() {\n        const meta = getMetadata();\n        if (!meta)\n            return;\n\n        loading = true;\n        lyricsModel.clear();\n        currentIndex = -1;\n        root.currentSongId = 0;\n        root.backend = \"Local\";\n\n        root.currentRequestId++;\n        let requestId = root.currentRequestId;\n\n        let key = _metaKey(meta);\n        let saved = root.lyricsMap[key];\n        root.offset = saved?.offset ?? 0.0;\n\n        if (saved?.neteaseId && saved?.backend === \"NetEase\") {\n            root.backend = \"NetEase\";\n            root.currentSongId = saved.neteaseId;\n            fetchNetEaseLyrics(saved.neteaseId, requestId);\n            fetchNetEaseCandidates(meta.title, meta.artist, requestId);\n            return;\n        }\n\n        if (saved?.backend === \"NetEase\") {\n            fallbackTimer.restart();\n            return;\n        }\n\n        let cleanDir = lyricsDir.replace(/\\/$/, \"\");\n        let fullPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`;\n\n        lrcFile.path = \"\";\n        lrcFile.path = fullPath;\n        fetchNetEaseCandidates(meta.title, meta.artist, requestId); //to populate the list regardless\n\n        // if the file is missing, FileView will not fire onLoaded, so we arm the fallback timer here as a safety net. It is cancelled in onLoaded if the file loads successfully.\n        if (saved?.backend !== \"Local\")\n            fallbackTimer.restart();\n    }\n\n    function updateModel(parsedArray) {\n        root.currentIndex = -1;\n        lyricsModel.clear();\n        for (let line of parsedArray) {\n            lyricsModel.append({\n                time: line.time,\n                lyricLine: line.text\n            });\n        }\n    }\n\n    function fallbackToOnline() {\n        let meta = getMetadata();\n        if (!meta)\n            return;\n        fetchNetEase(meta.title, meta.artist, root.currentRequestId);\n    }\n\n    // NetEase\n\n    // searches NetEase and populates the candidates model. returns the result array via the onResults callback\n    function _searchNetEase(title, artist, reqId, onResults) {\n        Requests.resetCookies();\n        const query = encodeURIComponent(`${title} ${artist}`);\n        const url = `https://music.163.com/api/search/get?s=${query}&type=1&limit=5`;\n\n        Requests.get(url, text => {\n            if (reqId !== root.currentRequestId)\n                return;\n            const res = JSON.parse(text);\n            const songs = res.result?.songs || [];\n\n            fetchedCandidatesModel.clear();\n            for (let s of songs) {\n                fetchedCandidatesModel.append({\n                    id: s.id,\n                    title: s.name || \"Unknown Title\",\n                    artist: s.artists?.map(a => a.name).join(\", \") || \"Unknown Artist\"\n                });\n            }\n\n            onResults(songs);\n        }, err => {}, root._netEaseHeaders);\n    }\n\n    // populates the candidates model only. used when a saved NetEase ID already exists and we just want to refresh the picker list.\n    function fetchNetEaseCandidates(title, artist, reqId) {\n        _searchNetEase(title, artist, reqId, _songs => {});\n    }\n\n    // searches NetEase, populates candidates, then auto-selects the best match and fetches its lyrics.\n    function fetchNetEase(title, artist, reqId) {\n        _searchNetEase(title, artist, reqId, songs => {\n            const bestMatch = songs.find(s => {\n                const inputArtist = String(artist || \"\").toLowerCase();\n                const sArtist = String(s.artists?.[0]?.name || \"\").toLowerCase();\n                return inputArtist.includes(sArtist) || sArtist.includes(inputArtist);\n            });\n\n            if (!bestMatch) {\n                return; // No reliable lyrics found\n            }\n\n            let key = `${artist} - ${title}`;\n            root.lyricsMap[key] = {\n                offset: root.lyricsMap[key]?.offset ?? 0.0,\n                backend: \"NetEase\",\n                neteaseId: bestMatch.id\n            };\n            root.currentSongId = bestMatch.id;\n            savePrefs();\n            fetchNetEaseLyrics(bestMatch.id, reqId);\n        });\n    }\n\n    function fetchNetEaseLyrics(id, reqId) {\n        const url = `https://music.163.com/api/song/lyric?id=${id}&lv=1&kv=1&tv=-1`;\n        Requests.get(url, text => {\n            if (reqId !== root.currentRequestId)\n                return;\n            const res = JSON.parse(text);\n            if (res.lrc?.lyric) {\n                updateModel(Lrc.parseLrc(res.lrc.lyric));\n                loading = false;\n            }\n        });\n    }\n\n    function selectCandidate(songId) {\n        let meta = getMetadata();\n        if (!meta)\n            return;\n        root.backend = \"NetEase\";\n        root.currentSongId = songId;\n        let key = _metaKey(meta);\n        root.lyricsMap[key] = {\n            offset: root.lyricsMap[key]?.offset ?? 0.0,\n            neteaseId: songId\n        };\n        savePrefs();\n        fetchNetEaseLyrics(songId, currentRequestId);\n    }\n\n    function updatePosition() {\n        if (isManualSeeking || loading || !player || lyricsModel.count === 0)\n            return;\n\n        let pos = player.position - root.offset;\n        let newIdx = -1;\n        for (let i = lyricsModel.count - 1; i >= 0; i--) {\n            if (pos >= lyricsModel.get(i).time - 0.1) { // 100ms fudge factor\n                newIdx = i;\n                break;\n            }\n        }\n\n        if (newIdx !== currentIndex) {\n            root.currentIndex = newIdx;\n        }\n    }\n\n    function jumpTo(index, time) {\n        root.isManualSeeking = true;\n        root.currentIndex = index;\n\n        if (player) {\n            player.position = time + root.offset + 0.01; // compensate for rounding\n        }\n\n        seekTimer.restart();\n    }\n}\n"
  },
  {
    "path": "services/Network.qml",
    "content": "pragma Singleton\n\nimport Quickshell\nimport Quickshell.Io\nimport QtQuick\nimport qs.services\n\nSingleton {\n    id: root\n\n    readonly property list<AccessPoint> networks: []\n    readonly property AccessPoint active: networks.find(n => n.active) ?? null\n    property bool wifiEnabled: true\n    readonly property bool scanning: Nmcli.scanning\n    property list<var> ethernetDevices: []\n    readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null\n    property int ethernetDeviceCount: 0\n    property bool ethernetProcessRunning: false\n    property var ethernetDeviceDetails: null\n    property var wirelessDeviceDetails: null\n    property var pendingConnection: null\n    property list<string> savedConnections: []\n    property list<string> savedConnectionSsids: []\n\n    signal connectionFailed(string ssid)\n\n    function enableWifi(enabled: bool): void {\n        Nmcli.enableWifi(enabled, result => {\n            if (result.success) {\n                root.getWifiStatus();\n                Nmcli.getNetworks(() => {\n                    syncNetworksFromNmcli();\n                });\n            }\n        });\n    }\n\n    function toggleWifi(): void {\n        Nmcli.toggleWifi(result => {\n            if (result.success) {\n                root.getWifiStatus();\n                Nmcli.getNetworks(() => {\n                    syncNetworksFromNmcli();\n                });\n            }\n        });\n    }\n\n    function rescanWifi(): void {\n        Nmcli.rescanWifi();\n    }\n\n    function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void {\n        // Set up pending connection tracking if callback provided\n        if (callback) {\n            const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0;\n            root.pendingConnection = {\n                ssid: ssid,\n                bssid: hasBssid ? bssid : \"\",\n                callback: callback\n            };\n        }\n\n        Nmcli.connectToNetwork(ssid, password, bssid, result => {\n            if (result && result.success) {\n                // Connection successful\n                if (callback)\n                    callback(result);\n                root.pendingConnection = null;\n            } else if (result && result.needsPassword) {\n                // Password needed - callback will handle showing dialog\n                if (callback)\n                    callback(result);\n            } else {\n                // Connection failed\n                if (result && result.error) {\n                    root.connectionFailed(ssid);\n                }\n                if (callback)\n                    callback(result);\n                root.pendingConnection = null;\n            }\n        });\n    }\n\n    function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void {\n        // Set up pending connection tracking\n        const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0;\n        root.pendingConnection = {\n            ssid: ssid,\n            bssid: hasBssid ? bssid : \"\",\n            callback: callback\n        };\n\n        Nmcli.connectToNetworkWithPasswordCheck(ssid, isSecure, result => {\n            if (result && result.success) {\n                // Connection successful\n                if (callback)\n                    callback(result);\n                root.pendingConnection = null;\n            } else if (result && result.needsPassword) {\n                // Password needed - callback will handle showing dialog\n                if (callback)\n                    callback(result);\n            } else {\n                // Connection failed\n                if (result && result.error) {\n                    root.connectionFailed(ssid);\n                }\n                if (callback)\n                    callback(result);\n                root.pendingConnection = null;\n            }\n        }, bssid);\n    }\n\n    function disconnectFromNetwork(): void {\n        // Try to disconnect - use connection name if available, otherwise use device\n        Nmcli.disconnectFromNetwork();\n        // Refresh network list after disconnection\n        Qt.callLater(() => {\n            Nmcli.getNetworks(() => {\n                syncNetworksFromNmcli();\n            });\n        }, 500);\n    }\n\n    function forgetNetwork(ssid: string): void {\n        // Delete the connection profile for this network\n        // This will remove the saved password and connection settings\n        Nmcli.forgetNetwork(ssid, result => {\n            if (result.success) {\n                // Refresh network list after deletion\n                Qt.callLater(() => {\n                    Nmcli.getNetworks(() => {\n                        syncNetworksFromNmcli();\n                    });\n                }, 500);\n            }\n        });\n    }\n\n    function syncNetworksFromNmcli(): void {\n        const rNetworks = root.networks;\n        const nNetworks = Nmcli.networks;\n\n        // Build a map of existing networks by key\n        const existingMap = new Map();\n        for (const rn of rNetworks) {\n            const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`;\n            existingMap.set(key, rn);\n        }\n\n        // Build a map of new networks by key\n        const newMap = new Map();\n        for (const nn of nNetworks) {\n            const key = `${nn.frequency}:${nn.ssid}:${nn.bssid}`;\n            newMap.set(key, nn);\n        }\n\n        // Remove networks that no longer exist\n        for (const [key, network] of existingMap) {\n            if (!newMap.has(key)) {\n                const index = rNetworks.indexOf(network);\n                if (index >= 0) {\n                    rNetworks.splice(index, 1);\n                    network.destroy();\n                }\n            }\n        }\n\n        // Add or update networks from Nmcli\n        for (const [key, nNetwork] of newMap) {\n            const existing = existingMap.get(key);\n            if (existing) {\n                // Update existing network's lastIpcObject\n                existing.lastIpcObject = nNetwork.lastIpcObject;\n            } else {\n                // Create new AccessPoint from Nmcli's data\n                rNetworks.push(apComp.createObject(root, {\n                    lastIpcObject: nNetwork.lastIpcObject\n                }));\n            }\n        }\n    }\n\n    function hasSavedProfile(ssid: string): bool {\n        // Use Nmcli's hasSavedProfile which has the same logic\n        return Nmcli.hasSavedProfile(ssid);\n    }\n\n    function getWifiStatus(): void {\n        Nmcli.getWifiStatus(enabled => {\n            root.wifiEnabled = enabled;\n        });\n    }\n\n    function getEthernetDevices(): void {\n        root.ethernetProcessRunning = true;\n        Nmcli.getEthernetInterfaces(interfaces => {\n            root.ethernetDevices = Nmcli.ethernetDevices;\n            root.ethernetDeviceCount = Nmcli.ethernetDevices.length;\n            root.ethernetProcessRunning = false;\n        });\n    }\n\n    function connectEthernet(connectionName: string, interfaceName: string): void {\n        Nmcli.connectEthernet(connectionName, interfaceName, result => {\n            if (result.success) {\n                getEthernetDevices();\n                // Refresh device details after connection\n                Qt.callLater(() => {\n                    const activeDevice = root.ethernetDevices.find(function (d) {\n                        return d.connected;\n                    });\n                    if (activeDevice && activeDevice.interface) {\n                        updateEthernetDeviceDetails(activeDevice.interface);\n                    }\n                }, 1000);\n            }\n        });\n    }\n\n    function disconnectEthernet(connectionName: string): void {\n        Nmcli.disconnectEthernet(connectionName, result => {\n            if (result.success) {\n                getEthernetDevices();\n                // Clear device details after disconnection\n                Qt.callLater(() => {\n                    root.ethernetDeviceDetails = null;\n                });\n            }\n        });\n    }\n\n    function updateEthernetDeviceDetails(interfaceName: string): void {\n        Nmcli.getEthernetDeviceDetails(interfaceName, details => {\n            root.ethernetDeviceDetails = details;\n        });\n    }\n\n    function updateWirelessDeviceDetails(): void {\n        // Find the wireless interface by looking for wifi devices\n        // Pass empty string to let Nmcli find the active interface automatically\n        Nmcli.getWirelessDeviceDetails(\"\", details => {\n            root.wirelessDeviceDetails = details;\n        });\n    }\n\n    function cidrToSubnetMask(cidr: string): string {\n        // Convert CIDR notation (e.g., \"24\") to subnet mask (e.g., \"255.255.255.0\")\n        const cidrNum = parseInt(cidr);\n        if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) {\n            return \"\";\n        }\n\n        const mask = (0xffffffff << (32 - cidrNum)) >>> 0;\n        const octets = [(mask >>> 24) & 0xff, (mask >>> 16) & 0xff, (mask >>> 8) & 0xff, mask & 0xff];\n\n        return octets.join(\".\");\n    }\n\n    Component.onCompleted: {\n        // Trigger ethernet device detection after initialization\n        Qt.callLater(() => {\n            getEthernetDevices();\n        });\n        // Load saved connections on startup\n        Nmcli.loadSavedConnections(() => {\n            root.savedConnections = Nmcli.savedConnections;\n            root.savedConnectionSsids = Nmcli.savedConnectionSsids;\n        });\n        // Get initial WiFi status\n        Nmcli.getWifiStatus(enabled => {\n            root.wifiEnabled = enabled;\n        });\n        // Sync networks from Nmcli on startup\n        Qt.callLater(() => {\n            syncNetworksFromNmcli();\n        }, 100);\n    }\n\n    // Sync saved connections from Nmcli when they're updated\n    Connections {\n        function onSavedConnectionsChanged() {\n            root.savedConnections = Nmcli.savedConnections;\n        }\n\n        function onSavedConnectionSsidsChanged() {\n            root.savedConnectionSsids = Nmcli.savedConnectionSsids;\n        }\n\n        target: Nmcli\n    }\n\n    Timer {\n        id: monitorDebounce\n\n        interval: 200\n        onTriggered: {\n            Nmcli.getNetworks(() => {\n                syncNetworksFromNmcli();\n            });\n            getEthernetDevices();\n        }\n    }\n\n    Process {\n        running: true\n        command: [\"nmcli\", \"m\"]\n        stdout: SplitParser {\n            onRead: monitorDebounce.start()\n        }\n    }\n\n    component AccessPoint: QtObject {\n        required property var lastIpcObject\n        readonly property string ssid: lastIpcObject.ssid\n        readonly property string bssid: lastIpcObject.bssid\n        readonly property int strength: lastIpcObject.strength\n        readonly property int frequency: lastIpcObject.frequency\n        readonly property bool active: lastIpcObject.active\n        readonly property string security: lastIpcObject.security\n        readonly property bool isSecure: security.length > 0\n    }\n\n    Component {\n        id: apComp\n\n        AccessPoint {}\n    }\n}\n"
  },
  {
    "path": "services/NetworkUsage.qml",
    "content": "pragma Singleton\n\nimport qs.config\n\nimport Quickshell\nimport Quickshell.Io\n\nimport Caelestia.Internal\n\nimport QtQuick\n\nSingleton {\n    id: root\n\n    property int refCount: 0\n\n    // Current speeds in bytes per second\n    readonly property real downloadSpeed: _downloadSpeed\n    readonly property real uploadSpeed: _uploadSpeed\n\n    // Total bytes transferred since tracking started\n    readonly property real downloadTotal: _downloadTotal\n    readonly property real uploadTotal: _uploadTotal\n\n    // History buffers for sparkline\n    readonly property CircularBuffer downloadBuffer: _downloadBuffer\n    readonly property CircularBuffer uploadBuffer: _uploadBuffer\n    readonly property int historyLength: 30\n\n    // Private properties\n    property real _downloadSpeed: 0\n    property real _uploadSpeed: 0\n    property real _downloadTotal: 0\n    property real _uploadTotal: 0\n\n    // Previous readings for calculating speed\n    property real _prevRxBytes: 0\n    property real _prevTxBytes: 0\n    property real _prevTimestamp: 0\n\n    // Initial readings for calculating totals\n    property real _initialRxBytes: 0\n    property real _initialTxBytes: 0\n    property bool _initialized: false\n\n    function formatBytes(bytes: real): var {\n        // Handle negative or invalid values\n        if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {\n            return {\n                value: 0,\n                unit: \"B/s\"\n            };\n        }\n\n        if (bytes < 1024) {\n            return {\n                value: bytes,\n                unit: \"B/s\"\n            };\n        } else if (bytes < 1024 * 1024) {\n            return {\n                value: bytes / 1024,\n                unit: \"KB/s\"\n            };\n        } else if (bytes < 1024 * 1024 * 1024) {\n            return {\n                value: bytes / (1024 * 1024),\n                unit: \"MB/s\"\n            };\n        } else {\n            return {\n                value: bytes / (1024 * 1024 * 1024),\n                unit: \"GB/s\"\n            };\n        }\n    }\n\n    function formatBytesTotal(bytes: real): var {\n        // Handle negative or invalid values\n        if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {\n            return {\n                value: 0,\n                unit: \"B\"\n            };\n        }\n\n        if (bytes < 1024) {\n            return {\n                value: bytes,\n                unit: \"B\"\n            };\n        } else if (bytes < 1024 * 1024) {\n            return {\n                value: bytes / 1024,\n                unit: \"KB\"\n            };\n        } else if (bytes < 1024 * 1024 * 1024) {\n            return {\n                value: bytes / (1024 * 1024),\n                unit: \"MB\"\n            };\n        } else {\n            return {\n                value: bytes / (1024 * 1024 * 1024),\n                unit: \"GB\"\n            };\n        }\n    }\n\n    function parseNetDev(content: string): var {\n        const lines = content.split(\"\\n\");\n        let totalRx = 0;\n        let totalTx = 0;\n\n        for (let i = 2; i < lines.length; i++) {\n            const line = lines[i].trim();\n            if (!line)\n                continue;\n\n            const parts = line.split(/\\s+/);\n            if (parts.length < 10)\n                continue;\n\n            const iface = parts[0].replace(\":\", \"\");\n            // Skip loopback interface\n            if (iface === \"lo\")\n                continue;\n\n            const rxBytes = parseFloat(parts[1]) || 0;\n            const txBytes = parseFloat(parts[9]) || 0;\n\n            totalRx += rxBytes;\n            totalTx += txBytes;\n        }\n\n        return {\n            rx: totalRx,\n            tx: totalTx\n        };\n    }\n\n    CircularBuffer {\n        id: _downloadBuffer\n\n        capacity: root.historyLength + 1\n    }\n\n    CircularBuffer {\n        id: _uploadBuffer\n\n        capacity: root.historyLength + 1\n    }\n\n    FileView {\n        id: netDevFile\n\n        path: \"/proc/net/dev\"\n    }\n\n    Timer {\n        interval: Config.dashboard.resourceUpdateInterval\n        running: root.refCount > 0\n        repeat: true\n        triggeredOnStart: true\n\n        onTriggered: {\n            netDevFile.reload();\n            const content = netDevFile.text();\n            if (!content)\n                return;\n\n            const data = root.parseNetDev(content);\n            const now = Date.now();\n\n            if (!root._initialized) {\n                root._initialRxBytes = data.rx;\n                root._initialTxBytes = data.tx;\n                root._prevRxBytes = data.rx;\n                root._prevTxBytes = data.tx;\n                root._prevTimestamp = now;\n                root._initialized = true;\n                return;\n            }\n\n            const timeDelta = (now - root._prevTimestamp) / 1000; // seconds\n            if (timeDelta > 0) {\n                // Calculate byte deltas\n                let rxDelta = data.rx - root._prevRxBytes;\n                let txDelta = data.tx - root._prevTxBytes;\n\n                // Handle counter overflow (when counters wrap around from max to 0)\n                // This happens when counters exceed 32-bit or 64-bit limits\n                if (rxDelta < 0) {\n                    // Counter wrapped around - assume 64-bit counter\n                    rxDelta += Math.pow(2, 64);\n                }\n                if (txDelta < 0) {\n                    txDelta += Math.pow(2, 64);\n                }\n\n                // Calculate speeds\n                root._downloadSpeed = rxDelta / timeDelta;\n                root._uploadSpeed = txDelta / timeDelta;\n\n                if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed))\n                    _downloadBuffer.push(root._downloadSpeed);\n\n                if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed))\n                    _uploadBuffer.push(root._uploadSpeed);\n            }\n\n            // Calculate totals with overflow handling\n            let downTotal = data.rx - root._initialRxBytes;\n            let upTotal = data.tx - root._initialTxBytes;\n\n            // Handle counter overflow for totals\n            if (downTotal < 0) {\n                downTotal += Math.pow(2, 64);\n            }\n            if (upTotal < 0) {\n                upTotal += Math.pow(2, 64);\n            }\n\n            root._downloadTotal = downTotal;\n            root._uploadTotal = upTotal;\n\n            root._prevRxBytes = data.rx;\n            root._prevTxBytes = data.tx;\n            root._prevTimestamp = now;\n        }\n    }\n}\n"
  },
  {
    "path": "services/Nmcli.qml",
    "content": "pragma Singleton\npragma ComponentBehavior: Bound\n\nimport Quickshell\nimport Quickshell.Io\nimport QtQuick\n\nSingleton {\n    id: root\n\n    property var deviceStatus: null\n    property var wirelessInterfaces: []\n    property var ethernetInterfaces: []\n    property bool isConnected: false\n    property string activeInterface: \"\"\n    property string activeConnection: \"\"\n    property bool wifiEnabled: true\n    readonly property bool scanning: rescanProc.running\n    readonly property list<AccessPoint> networks: []\n    readonly property AccessPoint active: networks.find(n => n.active) ?? null\n    property list<string> savedConnections: []\n    property list<string> savedConnectionSsids: []\n\n    property var wifiConnectionQueue: []\n    property int currentSsidQueryIndex: 0\n    property var pendingConnection: null\n    property var wirelessDeviceDetails: null\n    property var ethernetDeviceDetails: null\n    property list<var> ethernetDevices: []\n    readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null\n    property list<var> activeProcesses: []\n\n    readonly property alias connectionCheckTimer: connectionCheckTimer\n    readonly property alias immediateCheckTimer: immediateCheckTimer\n\n    // Constants\n    readonly property string deviceTypeWifi: \"wifi\"\n    readonly property string deviceTypeEthernet: \"ethernet\"\n    readonly property string connectionTypeWireless: \"802-11-wireless\"\n    readonly property string nmcliCommandDevice: \"device\"\n    readonly property string nmcliCommandConnection: \"connection\"\n    readonly property string nmcliCommandWifi: \"wifi\"\n    readonly property string nmcliCommandRadio: \"radio\"\n    readonly property string deviceStatusFields: \"DEVICE,TYPE,STATE,CONNECTION\"\n    readonly property string connectionListFields: \"NAME,TYPE\"\n    readonly property string wirelessSsidField: \"802-11-wireless.ssid\"\n    readonly property string networkListFields: \"SSID,SIGNAL,SECURITY\"\n    readonly property string networkDetailFields: \"ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY\"\n    readonly property string securityKeyMgmt: \"802-11-wireless-security.key-mgmt\"\n    readonly property string securityPsk: \"802-11-wireless-security.psk\"\n    readonly property string keyMgmtWpaPsk: \"wpa-psk\"\n    readonly property string connectionParamType: \"type\"\n    readonly property string connectionParamConName: \"con-name\"\n    readonly property string connectionParamIfname: \"ifname\"\n    readonly property string connectionParamSsid: \"ssid\"\n    readonly property string connectionParamPassword: \"password\"\n    readonly property string connectionParamBssid: \"802-11-wireless.bssid\"\n\n    signal connectionFailed(string ssid)\n\n    function detectPasswordRequired(error: string): bool {\n        if (!error || error.length === 0) {\n            return false;\n        }\n\n        return (error.includes(\"Secrets were required\") || error.includes(\"Secrets were required, but not provided\") || error.includes(\"No secrets provided\") || error.includes(\"802-11-wireless-security.psk\") || error.includes(\"password for\") || (error.includes(\"password\") && !error.includes(\"Connection activated\") && !error.includes(\"successfully\")) || (error.includes(\"Secrets\") && !error.includes(\"Connection activated\") && !error.includes(\"successfully\")) || (error.includes(\"802.11\") && !error.includes(\"Connection activated\") && !error.includes(\"successfully\"))) && !error.includes(\"Connection activated\") && !error.includes(\"successfully\");\n    }\n\n    function parseNetworkOutput(output: string): list<var> {\n        if (!output || output.length === 0) {\n            return [];\n        }\n\n        const PLACEHOLDER = \"STRINGWHICHHOPEFULLYWONTBEUSED\";\n        const rep = new RegExp(\"\\\\\\\\:\", \"g\");\n        const rep2 = new RegExp(PLACEHOLDER, \"g\");\n\n        const allNetworks = output.trim().split(\"\\n\").filter(line => line && line.length > 0).map(n => {\n            const net = n.replace(rep, PLACEHOLDER).split(\":\");\n            return {\n                active: net[0] === \"yes\",\n                strength: parseInt(net[1] || \"0\", 10) || 0,\n                frequency: parseInt(net[2] || \"0\", 10) || 0,\n                ssid: (net[3]?.replace(rep2, \":\") ?? \"\").trim(),\n                bssid: (net[4]?.replace(rep2, \":\") ?? \"\").trim(),\n                security: (net[5] ?? \"\").trim()\n            };\n        }).filter(n => n.ssid && n.ssid.length > 0);\n\n        return allNetworks;\n    }\n\n    function deduplicateNetworks(networks: list<var>): list<var> {\n        if (!networks || networks.length === 0) {\n            return [];\n        }\n\n        const networkMap = new Map();\n        for (const network of networks) {\n            const existing = networkMap.get(network.ssid);\n            if (!existing) {\n                networkMap.set(network.ssid, network);\n            } else {\n                if (network.active && !existing.active) {\n                    networkMap.set(network.ssid, network);\n                } else if (!network.active && !existing.active) {\n                    if (network.strength > existing.strength) {\n                        networkMap.set(network.ssid, network);\n                    }\n                }\n            }\n        }\n\n        return Array.from(networkMap.values());\n    }\n\n    function isConnectionCommand(command: list<string>): bool {\n        if (!command || command.length === 0) {\n            return false;\n        }\n\n        return command.includes(root.nmcliCommandWifi) || command.includes(root.nmcliCommandConnection);\n    }\n\n    function parseDeviceStatusOutput(output: string, filterType: string): list<var> {\n        if (!output || output.length === 0) {\n            return [];\n        }\n\n        const interfaces = [];\n        const lines = output.trim().split(\"\\n\");\n\n        for (const line of lines) {\n            const parts = line.split(\":\");\n            if (parts.length >= 2) {\n                const deviceType = parts[1];\n                let shouldInclude = false;\n\n                if (filterType === root.deviceTypeWifi && deviceType === root.deviceTypeWifi) {\n                    shouldInclude = true;\n                } else if (filterType === root.deviceTypeEthernet && deviceType === root.deviceTypeEthernet) {\n                    shouldInclude = true;\n                } else if (filterType === \"both\" && (deviceType === root.deviceTypeWifi || deviceType === root.deviceTypeEthernet)) {\n                    shouldInclude = true;\n                }\n\n                if (shouldInclude) {\n                    interfaces.push({\n                        device: parts[0] || \"\",\n                        type: parts[1] || \"\",\n                        state: parts[2] || \"\",\n                        connection: parts[3] || \"\"\n                    });\n                }\n            }\n        }\n\n        return interfaces;\n    }\n\n    function isConnectedState(state: string): bool {\n        if (!state || state.length === 0) {\n            return false;\n        }\n\n        return state === \"100 (connected)\" || state === \"connected\" || state.startsWith(\"connected\");\n    }\n\n    function executeCommand(args: list<string>, callback: var): void {\n        const proc = commandProc.createObject(root);\n        proc.command = [\"nmcli\", ...args];\n        proc.callback = callback;\n\n        activeProcesses.push(proc);\n\n        proc.processFinished.connect(() => {\n            const index = activeProcesses.indexOf(proc);\n            if (index >= 0) {\n                activeProcesses.splice(index, 1);\n            }\n        });\n\n        Qt.callLater(() => {\n            proc.exec(proc.command);\n        });\n    }\n\n    function getDeviceStatus(callback: var): void {\n        executeCommand([\"-t\", \"-f\", root.deviceStatusFields, root.nmcliCommandDevice, \"status\"], result => {\n            if (callback)\n                callback(result.output);\n        });\n    }\n\n    function getWirelessInterfaces(callback: var): void {\n        executeCommand([\"-t\", \"-f\", root.deviceStatusFields, root.nmcliCommandDevice, \"status\"], result => {\n            const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeWifi);\n            root.wirelessInterfaces = interfaces;\n            if (callback)\n                callback(interfaces);\n        });\n    }\n\n    function getEthernetInterfaces(callback: var): void {\n        executeCommand([\"-t\", \"-f\", root.deviceStatusFields, root.nmcliCommandDevice, \"status\"], result => {\n            const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeEthernet);\n            const devices = [];\n\n            for (const iface of interfaces) {\n                const connected = isConnectedState(iface.state);\n\n                devices.push({\n                    interface: iface.device,\n                    type: iface.type,\n                    state: iface.state,\n                    connection: iface.connection,\n                    connected: connected,\n                    ipAddress: \"\",\n                    gateway: \"\",\n                    dns: [],\n                    subnet: \"\",\n                    macAddress: \"\",\n                    speed: \"\"\n                });\n            }\n\n            root.ethernetInterfaces = interfaces;\n            root.ethernetDevices = devices;\n            if (callback)\n                callback(interfaces);\n        });\n    }\n\n    function connectEthernet(connectionName: string, interfaceName: string, callback: var): void {\n        if (connectionName && connectionName.length > 0) {\n            executeCommand([root.nmcliCommandConnection, \"up\", connectionName], result => {\n                if (result.success) {\n                    Qt.callLater(() => {\n                        getEthernetInterfaces(() => {});\n                        if (interfaceName && interfaceName.length > 0) {\n                            Qt.callLater(() => {\n                                getEthernetDeviceDetails(interfaceName, () => {});\n                            }, 1000);\n                        }\n                    }, 500);\n                }\n                if (callback)\n                    callback(result);\n            });\n        } else if (interfaceName && interfaceName.length > 0) {\n            executeCommand([root.nmcliCommandDevice, \"connect\", interfaceName], result => {\n                if (result.success) {\n                    Qt.callLater(() => {\n                        getEthernetInterfaces(() => {});\n                        Qt.callLater(() => {\n                            getEthernetDeviceDetails(interfaceName, () => {});\n                        }, 1000);\n                    }, 500);\n                }\n                if (callback)\n                    callback(result);\n            });\n        } else {\n            if (callback)\n                callback({\n                    success: false,\n                    output: \"\",\n                    error: \"No connection name or interface specified\",\n                    exitCode: -1\n                });\n        }\n    }\n\n    function disconnectEthernet(connectionName: string, callback: var): void {\n        if (!connectionName || connectionName.length === 0) {\n            if (callback)\n                callback({\n                    success: false,\n                    output: \"\",\n                    error: \"No connection name specified\",\n                    exitCode: -1\n                });\n            return;\n        }\n\n        executeCommand([root.nmcliCommandConnection, \"down\", connectionName], result => {\n            if (result.success) {\n                root.ethernetDeviceDetails = null;\n                Qt.callLater(() => {\n                    getEthernetInterfaces(() => {});\n                }, 500);\n            }\n            if (callback)\n                callback(result);\n        });\n    }\n\n    function getAllInterfaces(callback: var): void {\n        executeCommand([\"-t\", \"-f\", root.deviceStatusFields, root.nmcliCommandDevice, \"status\"], result => {\n            const interfaces = parseDeviceStatusOutput(result.output, \"both\");\n            if (callback)\n                callback(interfaces);\n        });\n    }\n\n    function isInterfaceConnected(interfaceName: string, callback: var): void {\n        executeCommand([root.nmcliCommandDevice, \"status\"], result => {\n            const lines = result.output.trim().split(\"\\n\");\n            for (const line of lines) {\n                const parts = line.split(/\\s+/);\n                if (parts.length >= 3 && parts[0] === interfaceName) {\n                    const connected = isConnectedState(parts[2]);\n                    if (callback)\n                        callback(connected);\n                    return;\n                }\n            }\n            if (callback)\n                callback(false);\n        });\n    }\n\n    function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void {\n        if (isSecure) {\n            const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0;\n            connectWireless(ssid, \"\", bssid, result => {\n                if (result.success) {\n                    if (callback)\n                        callback({\n                            success: true,\n                            usedSavedPassword: true,\n                            output: result.output,\n                            error: \"\",\n                            exitCode: 0\n                        });\n                } else if (result.needsPassword) {\n                    if (callback)\n                        callback({\n                            success: false,\n                            needsPassword: true,\n                            output: result.output,\n                            error: result.error,\n                            exitCode: result.exitCode\n                        });\n                } else {\n                    if (callback)\n                        callback(result);\n                }\n            });\n        } else {\n            connectWireless(ssid, \"\", bssid, callback);\n        }\n    }\n\n    function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void {\n        connectWireless(ssid, password, bssid, callback);\n    }\n\n    function connectWireless(ssid: string, password: string, bssid: string, callback: var, retryCount: int): void {\n        const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0;\n        const retries = retryCount !== undefined ? retryCount : 0;\n        const maxRetries = 2;\n\n        if (callback) {\n            root.pendingConnection = {\n                ssid: ssid,\n                bssid: hasBssid ? bssid : \"\",\n                callback: callback,\n                retryCount: retries\n            };\n            connectionCheckTimer.start();\n            immediateCheckTimer.checkCount = 0;\n            immediateCheckTimer.start();\n        }\n\n        if (password && password.length > 0 && hasBssid) {\n            const bssidUpper = bssid.toUpperCase();\n            createConnectionWithPassword(ssid, bssidUpper, password, callback);\n            return;\n        }\n\n        let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, \"connect\", ssid];\n        if (password && password.length > 0) {\n            cmd.push(root.connectionParamPassword, password);\n        }\n        executeCommand(cmd, result => {\n            if (result.needsPassword && callback) {\n                if (callback)\n                    callback(result);\n                return;\n            }\n\n            if (!result.success && root.pendingConnection && retries < maxRetries) {\n                console.warn(\"[NMCLI] Connection failed, retrying... (attempt \" + (retries + 1) + \"/\" + maxRetries + \")\");\n                Qt.callLater(() => {\n                    connectWireless(ssid, password, bssid, callback, retries + 1);\n                }, 1000);\n            } else if (!result.success && root.pendingConnection) {} else if (result.success && callback) {} else if (!result.success && !root.pendingConnection) {\n                if (callback)\n                    callback(result);\n            }\n        });\n    }\n\n    function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void {\n        checkAndDeleteConnection(ssid, () => {\n            const cmd = [root.nmcliCommandConnection, \"add\", root.connectionParamType, root.deviceTypeWifi, root.connectionParamConName, ssid, root.connectionParamIfname, \"*\", root.connectionParamSsid, ssid, root.connectionParamBssid, bssidUpper, root.securityKeyMgmt, root.keyMgmtWpaPsk, root.securityPsk, password];\n\n            executeCommand(cmd, result => {\n                if (result.success) {\n                    loadSavedConnections(() => {});\n                    activateConnection(ssid, callback);\n                } else {\n                    const hasDuplicateWarning = result.error && (result.error.includes(\"another connection with the name\") || result.error.includes(\"Reference the connection by its uuid\"));\n\n                    if (hasDuplicateWarning || (result.exitCode > 0 && result.exitCode < 10)) {\n                        loadSavedConnections(() => {});\n                        activateConnection(ssid, callback);\n                    } else {\n                        console.warn(\"[NMCLI] Connection profile creation failed, trying fallback...\");\n                        let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, \"connect\", ssid, root.connectionParamPassword, password];\n                        executeCommand(fallbackCmd, fallbackResult => {\n                            if (callback)\n                                callback(fallbackResult);\n                        });\n                    }\n                }\n            });\n        });\n    }\n\n    function checkAndDeleteConnection(ssid: string, callback: var): void {\n        executeCommand([root.nmcliCommandConnection, \"show\", ssid], result => {\n            if (result.success) {\n                executeCommand([root.nmcliCommandConnection, \"delete\", ssid], deleteResult => {\n                    Qt.callLater(() => {\n                        if (callback)\n                            callback();\n                    }, 300);\n                });\n            } else {\n                if (callback)\n                    callback();\n            }\n        });\n    }\n\n    function activateConnection(connectionName: string, callback: var): void {\n        executeCommand([root.nmcliCommandConnection, \"up\", connectionName], result => {\n            if (callback)\n                callback(result);\n        });\n    }\n\n    function loadSavedConnections(callback: var): void {\n        executeCommand([\"-t\", \"-f\", root.connectionListFields, root.nmcliCommandConnection, \"show\"], result => {\n            if (!result.success) {\n                root.savedConnections = [];\n                root.savedConnectionSsids = [];\n                if (callback)\n                    callback([]);\n                return;\n            }\n\n            parseConnectionList(result.output, callback);\n        });\n    }\n\n    function parseConnectionList(output: string, callback: var): void {\n        const lines = output.trim().split(\"\\n\").filter(line => line.length > 0);\n        const wifiConnections = [];\n        const connections = [];\n\n        for (const line of lines) {\n            const parts = line.split(\":\");\n            if (parts.length >= 2) {\n                const name = parts[0];\n                const type = parts[1];\n                connections.push(name);\n\n                if (type === root.connectionTypeWireless) {\n                    wifiConnections.push(name);\n                }\n            }\n        }\n\n        root.savedConnections = connections;\n\n        if (wifiConnections.length > 0) {\n            root.wifiConnectionQueue = wifiConnections;\n            root.currentSsidQueryIndex = 0;\n            root.savedConnectionSsids = [];\n            queryNextSsid(callback);\n        } else {\n            root.savedConnectionSsids = [];\n            root.wifiConnectionQueue = [];\n            if (callback)\n                callback(root.savedConnectionSsids);\n        }\n    }\n\n    function queryNextSsid(callback: var): void {\n        if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) {\n            const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex];\n            root.currentSsidQueryIndex++;\n\n            executeCommand([\"-t\", \"-f\", root.wirelessSsidField, root.nmcliCommandConnection, \"show\", connectionName], result => {\n                if (result.success) {\n                    processSsidOutput(result.output);\n                }\n                queryNextSsid(callback);\n            });\n        } else {\n            root.wifiConnectionQueue = [];\n            root.currentSsidQueryIndex = 0;\n            if (callback)\n                callback(root.savedConnectionSsids);\n        }\n    }\n\n    function processSsidOutput(output: string): void {\n        const lines = output.trim().split(\"\\n\");\n        for (const line of lines) {\n            if (line.startsWith(\"802-11-wireless.ssid:\")) {\n                const ssid = line.substring(\"802-11-wireless.ssid:\".length).trim();\n                if (ssid && ssid.length > 0) {\n                    const ssidLower = ssid.toLowerCase();\n                    const exists = root.savedConnectionSsids.some(s => s && s.toLowerCase() === ssidLower);\n                    if (!exists) {\n                        const newList = root.savedConnectionSsids.slice();\n                        newList.push(ssid);\n                        root.savedConnectionSsids = newList;\n                    }\n                }\n            }\n        }\n    }\n\n    function hasSavedProfile(ssid: string): bool {\n        if (!ssid || ssid.length === 0) {\n            return false;\n        }\n        const ssidLower = ssid.toLowerCase().trim();\n\n        if (root.active && root.active.ssid) {\n            const activeSsidLower = root.active.ssid.toLowerCase().trim();\n            if (activeSsidLower === ssidLower) {\n                return true;\n            }\n        }\n\n        const hasSsid = root.savedConnectionSsids.some(savedSsid => savedSsid && savedSsid.toLowerCase().trim() === ssidLower);\n\n        if (hasSsid) {\n            return true;\n        }\n\n        const hasConnectionName = root.savedConnections.some(connName => connName && connName.toLowerCase().trim() === ssidLower);\n\n        return hasConnectionName;\n    }\n\n    function forgetNetwork(ssid: string, callback: var): void {\n        if (!ssid || ssid.length === 0) {\n            if (callback)\n                callback({\n                    success: false,\n                    output: \"\",\n                    error: \"No SSID specified\",\n                    exitCode: -1\n                });\n            return;\n        }\n\n        const connectionName = root.savedConnections.find(conn => conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim()) || ssid;\n\n        executeCommand([root.nmcliCommandConnection, \"delete\", connectionName], result => {\n            if (result.success) {\n                Qt.callLater(() => {\n                    loadSavedConnections(() => {});\n                }, 500);\n            }\n            if (callback)\n                callback(result);\n        });\n    }\n\n    function disconnect(interfaceName: string, callback: var): void {\n        if (interfaceName && interfaceName.length > 0) {\n            executeCommand([root.nmcliCommandDevice, \"disconnect\", interfaceName], result => {\n                if (callback)\n                    callback(result.success ? result.output : \"\");\n            });\n        } else {\n            executeCommand([root.nmcliCommandDevice, \"disconnect\", root.deviceTypeWifi], result => {\n                if (callback)\n                    callback(result.success ? result.output : \"\");\n            });\n        }\n    }\n\n    function disconnectFromNetwork(): void {\n        if (active && active.ssid) {\n            executeCommand([root.nmcliCommandConnection, \"down\", active.ssid], result => {\n                if (result.success) {\n                    getNetworks(() => {});\n                }\n            });\n        } else {\n            executeCommand([root.nmcliCommandDevice, \"disconnect\", root.deviceTypeWifi], result => {\n                if (result.success) {\n                    getNetworks(() => {});\n                }\n            });\n        }\n    }\n\n    function getDeviceDetails(interfaceName: string, callback: var): void {\n        executeCommand([root.nmcliCommandDevice, \"show\", interfaceName], result => {\n            if (callback)\n                callback(result.output);\n        });\n    }\n\n    function refreshStatus(callback: var): void {\n        getDeviceStatus(output => {\n            const lines = output.trim().split(\"\\n\");\n            let connected = false;\n            let activeIf = \"\";\n            let activeConn = \"\";\n\n            for (const line of lines) {\n                const parts = line.split(\":\");\n                if (parts.length >= 4) {\n                    const state = parts[2] || \"\";\n                    if (isConnectedState(state)) {\n                        connected = true;\n                        activeIf = parts[0] || \"\";\n                        activeConn = parts[3] || \"\";\n                        break;\n                    }\n                }\n            }\n\n            root.isConnected = connected;\n            root.activeInterface = activeIf;\n            root.activeConnection = activeConn;\n\n            if (callback)\n                callback({\n                    connected,\n                    interface: activeIf,\n                    connection: activeConn\n                });\n        });\n    }\n\n    function bringInterfaceUp(interfaceName: string, callback: var): void {\n        if (interfaceName && interfaceName.length > 0) {\n            executeCommand([root.nmcliCommandDevice, \"connect\", interfaceName], result => {\n                if (callback) {\n                    callback(result);\n                }\n            });\n        } else {\n            if (callback)\n                callback({\n                    success: false,\n                    output: \"\",\n                    error: \"No interface specified\",\n                    exitCode: -1\n                });\n        }\n    }\n\n    function bringInterfaceDown(interfaceName: string, callback: var): void {\n        if (interfaceName && interfaceName.length > 0) {\n            executeCommand([root.nmcliCommandDevice, \"disconnect\", interfaceName], result => {\n                if (callback) {\n                    callback(result);\n                }\n            });\n        } else {\n            if (callback)\n                callback({\n                    success: false,\n                    output: \"\",\n                    error: \"No interface specified\",\n                    exitCode: -1\n                });\n        }\n    }\n\n    function scanWirelessNetworks(interfaceName: string, callback: var): void {\n        let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, \"rescan\"];\n        if (interfaceName && interfaceName.length > 0) {\n            cmd.push(root.connectionParamIfname, interfaceName);\n        }\n        executeCommand(cmd, result => {\n            if (callback) {\n                callback(result);\n            }\n        });\n    }\n\n    function rescanWifi(): void {\n        rescanProc.running = true;\n    }\n\n    function enableWifi(enabled: bool, callback: var): void {\n        const cmd = enabled ? \"on\" : \"off\";\n        executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, cmd], result => {\n            if (result.success) {\n                getWifiStatus(status => {\n                    root.wifiEnabled = status;\n                    if (callback)\n                        callback(result);\n                });\n            } else {\n                if (callback)\n                    callback(result);\n            }\n        });\n    }\n\n    function toggleWifi(callback: var): void {\n        const newState = !root.wifiEnabled;\n        enableWifi(newState, callback);\n    }\n\n    function getWifiStatus(callback: var): void {\n        executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi], result => {\n            if (result.success) {\n                const enabled = result.output.trim() === \"enabled\";\n                root.wifiEnabled = enabled;\n                if (callback)\n                    callback(enabled);\n            } else {\n                if (callback)\n                    callback(root.wifiEnabled);\n            }\n        });\n    }\n\n    function getNetworks(callback: var): void {\n        executeCommand([\"-g\", root.networkDetailFields, \"d\", \"w\"], result => {\n            if (!result.success) {\n                if (callback)\n                    callback([]);\n                return;\n            }\n\n            const allNetworks = parseNetworkOutput(result.output);\n            const networks = deduplicateNetworks(allNetworks);\n            const rNetworks = root.networks;\n\n            const newMap = new Map();\n            for (const n of networks)\n                newMap.set(`${n.frequency}:${n.ssid}:${n.bssid}`, n);\n\n            for (let i = rNetworks.length - 1; i >= 0; i--) {\n                const rn = rNetworks[i];\n                const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`;\n                if (!newMap.has(key)) {\n                    rNetworks.splice(i, 1);\n                    rn.destroy();\n                }\n            }\n\n            const existingMap = new Map();\n            for (const rn of rNetworks)\n                existingMap.set(`${rn.frequency}:${rn.ssid}:${rn.bssid}`, rn);\n\n            for (const [key, network] of newMap) {\n                const match = existingMap.get(key);\n                if (match) {\n                    match.lastIpcObject = network;\n                } else {\n                    rNetworks.push(apComp.createObject(root, {\n                        lastIpcObject: network\n                    }));\n                }\n            }\n\n            if (callback)\n                callback(root.networks);\n            checkPendingConnection();\n        });\n    }\n\n    function getWirelessSSIDs(interfaceName: string, callback: var): void {\n        let cmd = [\"-t\", \"-f\", root.networkListFields, root.nmcliCommandDevice, root.nmcliCommandWifi, \"list\"];\n        if (interfaceName && interfaceName.length > 0) {\n            cmd.push(root.connectionParamIfname, interfaceName);\n        }\n        executeCommand(cmd, result => {\n            if (!result.success) {\n                if (callback)\n                    callback([]);\n                return;\n            }\n\n            const ssids = [];\n            const lines = result.output.trim().split(\"\\n\");\n            const seenSSIDs = new Set();\n\n            for (const line of lines) {\n                if (!line || line.length === 0)\n                    continue;\n\n                const parts = line.split(\":\");\n                if (parts.length >= 1) {\n                    const ssid = parts[0].trim();\n                    if (ssid && ssid.length > 0 && !seenSSIDs.has(ssid)) {\n                        seenSSIDs.add(ssid);\n                        const signalStr = parts.length >= 2 ? parts[1].trim() : \"\";\n                        const signal = signalStr ? parseInt(signalStr, 10) : 0;\n                        const security = parts.length >= 3 ? parts[2].trim() : \"\";\n                        ssids.push({\n                            ssid: ssid,\n                            signal: signalStr,\n                            signalValue: isNaN(signal) ? 0 : signal,\n                            security: security\n                        });\n                    }\n                }\n            }\n\n            ssids.sort((a, b) => {\n                return b.signalValue - a.signalValue;\n            });\n\n            if (callback)\n                callback(ssids);\n        });\n    }\n\n    function handlePasswordRequired(proc: var, error: string, output: string, exitCode: int): bool {\n        if (!proc || !error || error.length === 0) {\n            return false;\n        }\n\n        if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) {\n            return false;\n        }\n\n        const needsPassword = detectPasswordRequired(error);\n\n        if (needsPassword && !proc.callbackCalled && root.pendingConnection) {\n            connectionCheckTimer.stop();\n            immediateCheckTimer.stop();\n            immediateCheckTimer.checkCount = 0;\n            const pending = root.pendingConnection;\n            root.pendingConnection = null;\n            proc.callbackCalled = true;\n            const result = {\n                success: false,\n                output: output || \"\",\n                error: error,\n                exitCode: exitCode,\n                needsPassword: true\n            };\n            if (pending.callback) {\n                pending.callback(result);\n            }\n            if (proc.callback && proc.callback !== pending.callback) {\n                proc.callback(result);\n            }\n            return true;\n        }\n\n        return false;\n    }\n\n    component CommandProcess: Process {\n        id: proc\n\n        property var callback: null\n        property list<string> command: []\n        property bool callbackCalled: false\n        property int exitCode: 0\n\n        signal processFinished\n\n        environment: ({\n                LANG: \"C.UTF-8\",\n                LC_ALL: \"C.UTF-8\"\n            })\n\n        stdout: StdioCollector {\n            id: stdoutCollector\n        }\n\n        stderr: StdioCollector {\n            id: stderrCollector\n\n            onStreamFinished: {\n                const error = text.trim();\n                if (error && error.length > 0) {\n                    const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : \"\";\n                    root.handlePasswordRequired(proc, error, output, -1);\n                }\n            }\n        }\n\n        onExited: code => {\n            exitCode = code;\n\n            Qt.callLater(() => {\n                if (callbackCalled) {\n                    processFinished();\n                    return;\n                }\n\n                if (proc.callback) {\n                    const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : \"\";\n                    const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : \"\";\n                    const success = exitCode === 0;\n                    const cmdIsConnection = isConnectionCommand(proc.command);\n\n                    if (root.handlePasswordRequired(proc, error, output, exitCode)) {\n                        processFinished();\n                        return;\n                    }\n\n                    const needsPassword = cmdIsConnection && root.detectPasswordRequired(error);\n\n                    if (!success && cmdIsConnection && root.pendingConnection) {\n                        const failedSsid = root.pendingConnection.ssid;\n                        root.connectionFailed(failedSsid);\n                    }\n\n                    callbackCalled = true;\n                    callback({\n                        success: success,\n                        output: output,\n                        error: error,\n                        exitCode: proc.exitCode,\n                        needsPassword: needsPassword || false\n                    });\n                    processFinished();\n                } else {\n                    processFinished();\n                }\n            });\n        }\n    }\n\n    Component {\n        id: commandProc\n\n        CommandProcess {}\n    }\n\n    component AccessPoint: QtObject {\n        required property var lastIpcObject\n        readonly property string ssid: lastIpcObject.ssid\n        readonly property string bssid: lastIpcObject.bssid\n        readonly property int strength: lastIpcObject.strength\n        readonly property int frequency: lastIpcObject.frequency\n        readonly property bool active: lastIpcObject.active\n        readonly property string security: lastIpcObject.security\n        readonly property bool isSecure: security.length > 0\n    }\n\n    Component {\n        id: apComp\n\n        AccessPoint {}\n    }\n\n    Timer {\n        id: connectionCheckTimer\n\n        interval: 4000\n        onTriggered: {\n            if (root.pendingConnection) {\n                const connected = root.active && root.active.ssid === root.pendingConnection.ssid;\n\n                if (!connected && root.pendingConnection.callback) {\n                    let foundPasswordError = false;\n                    for (let i = 0; i < root.activeProcesses.length; i++) {\n                        const proc = root.activeProcesses[i];\n                        if (proc && proc.stderr && proc.stderr.text) {\n                            const error = proc.stderr.text.trim();\n                            if (error && error.length > 0) {\n                                if (root.isConnectionCommand(proc.command)) {\n                                    const needsPassword = root.detectPasswordRequired(error);\n\n                                    if (needsPassword && !proc.callbackCalled && root.pendingConnection) {\n                                        const pending = root.pendingConnection;\n                                        root.pendingConnection = null;\n                                        immediateCheckTimer.stop();\n                                        immediateCheckTimer.checkCount = 0;\n                                        proc.callbackCalled = true;\n                                        const result = {\n                                            success: false,\n                                            output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : \"\",\n                                            error: error,\n                                            exitCode: -1,\n                                            needsPassword: true\n                                        };\n                                        if (pending.callback) {\n                                            pending.callback(result);\n                                        }\n                                        if (proc.callback && proc.callback !== pending.callback) {\n                                            proc.callback(result);\n                                        }\n                                        foundPasswordError = true;\n                                        break;\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    if (!foundPasswordError) {\n                        const pending = root.pendingConnection;\n                        const failedSsid = pending.ssid;\n                        root.pendingConnection = null;\n                        immediateCheckTimer.stop();\n                        immediateCheckTimer.checkCount = 0;\n                        root.connectionFailed(failedSsid);\n                        pending.callback({\n                            success: false,\n                            output: \"\",\n                            error: \"Connection timeout\",\n                            exitCode: -1,\n                            needsPassword: false\n                        });\n                    }\n                } else if (connected) {\n                    root.pendingConnection = null;\n                    immediateCheckTimer.stop();\n                    immediateCheckTimer.checkCount = 0;\n                }\n            }\n        }\n    }\n\n    Timer {\n        id: immediateCheckTimer\n\n        property int checkCount: 0\n\n        interval: 500\n        repeat: true\n        triggeredOnStart: false\n\n        onTriggered: {\n            if (root.pendingConnection) {\n                checkCount++;\n                const connected = root.active && root.active.ssid === root.pendingConnection.ssid;\n\n                if (connected) {\n                    connectionCheckTimer.stop();\n                    immediateCheckTimer.stop();\n                    immediateCheckTimer.checkCount = 0;\n                    if (root.pendingConnection.callback) {\n                        root.pendingConnection.callback({\n                            success: true,\n                            output: \"Connected\",\n                            error: \"\",\n                            exitCode: 0\n                        });\n                    }\n                    root.pendingConnection = null;\n                } else {\n                    for (let i = 0; i < root.activeProcesses.length; i++) {\n                        const proc = root.activeProcesses[i];\n                        if (proc && proc.stderr && proc.stderr.text) {\n                            const error = proc.stderr.text.trim();\n                            if (error && error.length > 0) {\n                                if (root.isConnectionCommand(proc.command)) {\n                                    const needsPassword = root.detectPasswordRequired(error);\n\n                                    if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) {\n                                        connectionCheckTimer.stop();\n                                        immediateCheckTimer.stop();\n                                        immediateCheckTimer.checkCount = 0;\n                                        const pending = root.pendingConnection;\n                                        root.pendingConnection = null;\n                                        proc.callbackCalled = true;\n                                        const result = {\n                                            success: false,\n                                            output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : \"\",\n                                            error: error,\n                                            exitCode: -1,\n                                            needsPassword: true\n                                        };\n                                        if (pending.callback) {\n                                            pending.callback(result);\n                                        }\n                                        if (proc.callback && proc.callback !== pending.callback) {\n                                            proc.callback(result);\n                                        }\n                                        return;\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    if (checkCount >= 6) {\n                        immediateCheckTimer.stop();\n                        immediateCheckTimer.checkCount = 0;\n                    }\n                }\n            } else {\n                immediateCheckTimer.stop();\n                immediateCheckTimer.checkCount = 0;\n            }\n        }\n    }\n\n    function checkPendingConnection(): void {\n        if (root.pendingConnection) {\n            Qt.callLater(() => {\n                const connected = root.active && root.active.ssid === root.pendingConnection.ssid;\n                if (connected) {\n                    connectionCheckTimer.stop();\n                    immediateCheckTimer.stop();\n                    immediateCheckTimer.checkCount = 0;\n                    if (root.pendingConnection.callback) {\n                        root.pendingConnection.callback({\n                            success: true,\n                            output: \"Connected\",\n                            error: \"\",\n                            exitCode: 0\n                        });\n                    }\n                    root.pendingConnection = null;\n                } else {\n                    if (!immediateCheckTimer.running) {\n                        immediateCheckTimer.start();\n                    }\n                }\n            });\n        }\n    }\n\n    function cidrToSubnetMask(cidr: string): string {\n        const cidrNum = parseInt(cidr, 10);\n        if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) {\n            return \"\";\n        }\n\n        const mask = (0xffffffff << (32 - cidrNum)) >>> 0;\n        const octet1 = (mask >>> 24) & 0xff;\n        const octet2 = (mask >>> 16) & 0xff;\n        const octet3 = (mask >>> 8) & 0xff;\n        const octet4 = mask & 0xff;\n\n        return `${octet1}.${octet2}.${octet3}.${octet4}`;\n    }\n\n    function getWirelessDeviceDetails(interfaceName: string, callback: var): void {\n        if (!interfaceName || interfaceName.length === 0) {\n            const activeInterface = root.wirelessInterfaces.find(iface => {\n                return isConnectedState(iface.state);\n            });\n            if (activeInterface && activeInterface.device) {\n                interfaceName = activeInterface.device;\n            } else {\n                if (callback)\n                    callback(null);\n                return;\n            }\n        }\n\n        executeCommand([\"device\", \"show\", interfaceName], result => {\n            if (!result.success || !result.output) {\n                root.wirelessDeviceDetails = null;\n                if (callback)\n                    callback(null);\n                return;\n            }\n\n            const details = parseDeviceDetails(result.output, false);\n            root.wirelessDeviceDetails = details;\n            if (callback)\n                callback(details);\n        });\n    }\n\n    function getEthernetDeviceDetails(interfaceName: string, callback: var): void {\n        if (!interfaceName || interfaceName.length === 0) {\n            const activeInterface = root.ethernetInterfaces.find(iface => {\n                return isConnectedState(iface.state);\n            });\n            if (activeInterface && activeInterface.device) {\n                interfaceName = activeInterface.device;\n            } else {\n                if (callback)\n                    callback(null);\n                return;\n            }\n        }\n\n        executeCommand([\"device\", \"show\", interfaceName], result => {\n            if (!result.success || !result.output) {\n                root.ethernetDeviceDetails = null;\n                if (callback)\n                    callback(null);\n                return;\n            }\n\n            const details = parseDeviceDetails(result.output, true);\n            root.ethernetDeviceDetails = details;\n            if (callback)\n                callback(details);\n        });\n    }\n\n    function parseDeviceDetails(output: string, isEthernet: bool): var {\n        const details = {\n            ipAddress: \"\",\n            gateway: \"\",\n            dns: [],\n            subnet: \"\",\n            macAddress: \"\",\n            speed: \"\"\n        };\n\n        if (!output || output.length === 0) {\n            return details;\n        }\n\n        const lines = output.trim().split(\"\\n\");\n\n        for (let i = 0; i < lines.length; i++) {\n            const line = lines[i];\n            const parts = line.split(\":\");\n            if (parts.length >= 2) {\n                const key = parts[0].trim();\n                const value = parts.slice(1).join(\":\").trim();\n\n                if (key.startsWith(\"IP4.ADDRESS\")) {\n                    const ipParts = value.split(\"/\");\n                    details.ipAddress = ipParts[0] || \"\";\n                    if (ipParts[1]) {\n                        details.subnet = cidrToSubnetMask(ipParts[1]);\n                    } else {\n                        details.subnet = \"\";\n                    }\n                } else if (key === \"IP4.GATEWAY\") {\n                    if (value !== \"--\") {\n                        details.gateway = value;\n                    }\n                } else if (key.startsWith(\"IP4.DNS\")) {\n                    if (value !== \"--\" && value.length > 0) {\n                        details.dns.push(value);\n                    }\n                } else if (isEthernet && key === \"WIRED-PROPERTIES.MAC\") {\n                    details.macAddress = value;\n                } else if (isEthernet && key === \"WIRED-PROPERTIES.SPEED\") {\n                    details.speed = value;\n                } else if (!isEthernet && key === \"GENERAL.HWADDR\") {\n                    details.macAddress = value;\n                }\n            }\n        }\n\n        return details;\n    }\n\n    Process {\n        id: rescanProc\n\n        command: [\"nmcli\", \"dev\", root.nmcliCommandWifi, \"list\", \"--rescan\", \"yes\"]\n        onExited: root.getNetworks()\n    }\n\n    Process {\n        id: monitorProc\n\n        running: true\n        command: [\"nmcli\", \"monitor\"]\n        environment: ({\n                LANG: \"C.UTF-8\",\n                LC_ALL: \"C.UTF-8\"\n            })\n        stdout: SplitParser {\n            onRead: root.refreshOnConnectionChange()\n        }\n        onExited: monitorRestartTimer.start()\n    }\n\n    Timer {\n        id: monitorRestartTimer\n\n        interval: 2000\n        onTriggered: {\n            monitorProc.running = true;\n        }\n    }\n\n    function refreshOnConnectionChange(): void {\n        getNetworks(networks => {\n            const newActive = root.active;\n\n            if (newActive && newActive.active) {\n                Qt.callLater(() => {\n                    if (root.wirelessInterfaces.length > 0) {\n                        const activeWireless = root.wirelessInterfaces.find(iface => {\n                            return isConnectedState(iface.state);\n                        });\n                        if (activeWireless && activeWireless.device) {\n                            getWirelessDeviceDetails(activeWireless.device, () => {});\n                        }\n                    }\n\n                    if (root.ethernetInterfaces.length > 0) {\n                        const activeEthernet = root.ethernetInterfaces.find(iface => {\n                            return isConnectedState(iface.state);\n                        });\n                        if (activeEthernet && activeEthernet.device) {\n                            getEthernetDeviceDetails(activeEthernet.device, () => {});\n                        }\n                    }\n                }, 500);\n            } else {\n                root.wirelessDeviceDetails = null;\n                root.ethernetDeviceDetails = null;\n            }\n\n            getWirelessInterfaces(() => {});\n            getEthernetInterfaces(() => {\n                if (root.activeEthernet && root.activeEthernet.connected) {\n                    Qt.callLater(() => {\n                        getEthernetDeviceDetails(root.activeEthernet.interface, () => {});\n                    }, 500);\n                }\n            });\n        });\n    }\n\n    Component.onCompleted: {\n        getWifiStatus(() => {});\n        getNetworks(() => {});\n        loadSavedConnections(() => {});\n        getEthernetInterfaces(() => {});\n\n        Qt.callLater(() => {\n            if (root.wirelessInterfaces.length > 0) {\n                const activeWireless = root.wirelessInterfaces.find(iface => {\n                    return isConnectedState(iface.state);\n                });\n                if (activeWireless && activeWireless.device) {\n                    getWirelessDeviceDetails(activeWireless.device, () => {});\n                }\n            }\n\n            if (root.ethernetInterfaces.length > 0) {\n                const activeEthernet = root.ethernetInterfaces.find(iface => {\n                    return isConnectedState(iface.state);\n                });\n                if (activeEthernet && activeEthernet.device) {\n                    getEthernetDeviceDetails(activeEthernet.device, () => {});\n                }\n            }\n        }, 2000);\n    }\n}\n"
  },
  {
    "path": "services/NotifData.qml",
    "content": "pragma ComponentBehavior: Bound\n\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Caelestia\nimport Quickshell\nimport Quickshell.Services.Notifications\nimport QtQuick\n\nQtObject {\n    id: notif\n\n    property bool popup\n    property bool closed\n    property var locks: new Set()\n\n    property date time: new Date()\n    property string timeStr: qsTr(\"now\")\n\n    readonly property Timer timeStrTimer: Timer {\n        running: !notif.closed\n        repeat: true\n        interval: 5000\n        onTriggered: notif.updateTimeStr()\n    }\n\n    property Notification notification\n    property string id\n    property string summary\n    property string body\n    property string appIcon\n    property string appName\n    property string image\n    property var hints // Hints are not persisted across restarts\n    property real expireTimeout: Config.notifs.defaultExpireTimeout\n    property int urgency: NotificationUrgency.Normal\n    property bool resident\n    property bool hasActionIcons\n    property list<var> actions\n\n    readonly property Timer timer: Timer {\n        running: true\n        interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout\n        onTriggered: {\n            if (Config.notifs.expire)\n                notif.popup = false;\n        }\n    }\n\n    readonly property LazyLoader dummyImageLoader: LazyLoader {\n        active: false\n\n        PanelWindow {\n            implicitWidth: Config.notifs.sizes.image\n            implicitHeight: Config.notifs.sizes.image\n            color: \"transparent\"\n            mask: Region {}\n\n            Image {\n                function tryCache(): void {\n                    if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image)\n                        return;\n\n                    const cacheKey = notif.appName + notif.summary + notif.id;\n                    let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch;\n                    for (let i = 0; i < cacheKey.length; i++) {\n                        ch = cacheKey.charCodeAt(i);\n                        h1 = Math.imul(h1 ^ ch, 2654435761);\n                        h2 = Math.imul(h2 ^ ch, 1597334677);\n                    }\n                    h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);\n                    h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n                    h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);\n                    h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n                    const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0);\n\n                    const cache = Paths.notifimagecache + \"/${hash}.png\";\n                    CUtils.saveItem(this, Qt.resolvedUrl(cache), () => {\n                        notif.image = cache;\n                        notif.dummyImageLoader.active = false;\n                    });\n                }\n\n                anchors.fill: parent\n                source: Qt.resolvedUrl(notif.image)\n                fillMode: Image.PreserveAspectCrop\n                cache: false\n                asynchronous: true\n                opacity: 0\n\n                onStatusChanged: tryCache()\n                onWidthChanged: tryCache()\n                onHeightChanged: tryCache()\n            }\n        }\n    }\n\n    readonly property Connections conn: Connections {\n        function onClosed(): void {\n            notif.close();\n        }\n\n        function onSummaryChanged(): void {\n            notif.summary = notif.notification.summary;\n        }\n\n        function onBodyChanged(): void {\n            notif.body = notif.notification.body;\n        }\n\n        function onAppIconChanged(): void {\n            notif.appIcon = notif.notification.appIcon;\n        }\n\n        function onAppNameChanged(): void {\n            notif.appName = notif.notification.appName;\n        }\n\n        function onImageChanged(): void {\n            notif.image = notif.notification.image;\n            if (notif.notification?.image)\n                notif.dummyImageLoader.active = true;\n        }\n\n        function onExpireTimeoutChanged(): void {\n            notif.expireTimeout = notif.notification.expireTimeout;\n        }\n\n        function onUrgencyChanged(): void {\n            notif.urgency = notif.notification.urgency;\n        }\n\n        function onResidentChanged(): void {\n            notif.resident = notif.notification.resident;\n        }\n\n        function onHasActionIconsChanged(): void {\n            notif.hasActionIcons = notif.notification.hasActionIcons;\n        }\n\n        function onActionsChanged(): void {\n            // qmllint disable unresolved-type\n            notif.actions = notif.notification.actions.map(a => ({\n                        // qmllint enable unresolved-type\n                        identifier: a.identifier,\n                        text: a.text,\n                        invoke: () => a.invoke()\n                    }));\n        }\n\n        function onHintsChanged(): void {\n            notif.hints = notif.notification.hints;\n        }\n\n        target: notif.notification\n    }\n\n    function updateTimeStr(): void {\n        const diff = Date.now() - time.getTime();\n        const m = Math.floor(diff / 60000);\n\n        if (m < 1) {\n            timeStr = qsTr(\"now\");\n            timeStrTimer.interval = 5000;\n        } else {\n            const h = Math.floor(m / 60);\n            const d = Math.floor(h / 24);\n\n            if (d > 0) {\n                timeStr = `${d}d`;\n                timeStrTimer.interval = 3600000;\n            } else if (h > 0) {\n                timeStr = `${h}h`;\n                timeStrTimer.interval = 300000;\n            } else {\n                timeStr = `${m}m`;\n                timeStrTimer.interval = m < 10 ? 30000 : 60000;\n            }\n        }\n    }\n\n    function lock(item: Item): void {\n        locks.add(item);\n    }\n\n    function unlock(item: Item): void {\n        locks.delete(item);\n        if (closed)\n            close();\n    }\n\n    function close(): void {\n        closed = true;\n        if (locks.size === 0 && Notifs.list.includes(this)) {\n            Notifs.list = Notifs.list.filter(n => n !== this);\n            notification?.dismiss();\n            destroy();\n        }\n    }\n\n    Component.onCompleted: {\n        if (!notification)\n            return;\n\n        id = notification.id;\n        summary = notification.summary;\n        body = notification.body;\n        appIcon = notification.appIcon;\n        appName = notification.appName;\n        image = notification.image;\n        if (notification?.image)\n            dummyImageLoader.active = true;\n        expireTimeout = notification.expireTimeout;\n        hints = notification.hints;\n        urgency = notification.urgency;\n        resident = notification.resident;\n        hasActionIcons = notification.hasActionIcons;\n        actions = notification.actions.map(a => ({\n                    identifier: a.identifier,\n                    text: a.text,\n                    invoke: () => a.invoke()\n                }));\n    }\n}\n"
  },
  {
    "path": "services/Notifs.qml",
    "content": "pragma Singleton\npragma ComponentBehavior: Bound\n\nimport qs.components.misc\nimport qs.config\nimport qs.services\nimport qs.utils\nimport Caelestia\nimport Quickshell\nimport Quickshell.Io\nimport Quickshell.Services.Notifications\nimport QtQuick\n\nSingleton {\n    id: root\n\n    property list<NotifData> list: []\n    readonly property list<NotifData> notClosed: list.filter(n => !n.closed)\n    readonly property list<NotifData> popups: list.filter(n => n.popup)\n    property alias dnd: props.dnd\n\n    property bool loaded\n\n    onDndChanged: {\n        if (!Config.utilities.toasts.dndChanged)\n            return;\n\n        if (dnd)\n            Toaster.toast(qsTr(\"Do not disturb enabled\"), qsTr(\"Popup notifications are now disabled\"), \"do_not_disturb_on\");\n        else\n            Toaster.toast(qsTr(\"Do not disturb disabled\"), qsTr(\"Popup notifications are now enabled\"), \"do_not_disturb_off\");\n    }\n\n    onListChanged: {\n        if (loaded)\n            saveTimer.restart();\n    }\n\n    Timer {\n        id: saveTimer\n\n        interval: 1000\n        onTriggered: storage.setText(JSON.stringify(root.notClosed.map(n => ({\n                    time: n.time,\n                    id: n.id,\n                    summary: n.summary,\n                    body: n.body,\n                    appIcon: n.appIcon,\n                    appName: n.appName,\n                    image: n.image,\n                    expireTimeout: n.expireTimeout,\n                    urgency: n.urgency,\n                    resident: n.resident,\n                    hasActionIcons: n.hasActionIcons,\n                    actions: n.actions\n                }))))\n    }\n\n    PersistentProperties {\n        id: props\n\n        property bool dnd\n\n        reloadableId: \"notifs\"\n    }\n\n    NotificationServer {\n        id: server\n\n        keepOnReload: false\n        actionsSupported: true\n        bodyHyperlinksSupported: true\n        bodyImagesSupported: true\n        bodyMarkupSupported: true\n        imageSupported: true\n        persistenceSupported: true\n\n        onNotification: notif => {\n            notif.tracked = true;\n\n            const comp = notifComp.createObject(root, {\n                popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar),\n                notification: notif\n            });\n            root.list = [comp, ...root.list];\n        }\n    }\n\n    FileView {\n        id: storage\n\n        path: `${Paths.state}/notifs.json`\n        onLoaded: {\n            const data = JSON.parse(text());\n            for (const notif of data)\n                root.list.push(notifComp.createObject(root, notif));\n            root.list.sort((a, b) => b.time - a.time);\n            root.loaded = true;\n        }\n        onLoadFailed: err => {\n            if (err === FileViewError.FileNotFound) {\n                root.loaded = true;\n                setText(\"[]\");\n            }\n        }\n    }\n\n    CustomShortcut {\n        name: \"clearNotifs\"\n        description: \"Clear all notifications\"\n        onPressed: {\n            for (const notif of root.list.slice())\n                notif.close();\n        }\n    }\n\n    IpcHandler {\n        function clear(): void {\n            for (const notif of root.list.slice())\n                notif.close();\n        }\n\n        function isDndEnabled(): bool {\n            return props.dnd;\n        }\n\n        function toggleDnd(): void {\n            props.dnd = !props.dnd;\n        }\n\n        function enableDnd(): void {\n            props.dnd = true;\n        }\n\n        function disableDnd(): void {\n            props.dnd = false;\n        }\n\n        target: \"notifs\"\n    }\n\n    Component {\n        id: notifComp\n\n        NotifData {}\n    }\n}\n"
  },
  {
    "path": "services/Players.qml",
    "content": "pragma Singleton\n\nimport qs.components.misc\nimport qs.config\nimport Quickshell\nimport Quickshell.Io\nimport Quickshell.Services.Mpris\nimport QtQml\nimport Caelestia\n\nSingleton {\n    id: root\n\n    readonly property list<MprisPlayer> list: Mpris.players.values\n    readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === Config.services.defaultPlayer) ?? list[0] ?? null\n    property alias manualActive: props.manualActive\n\n    function getIdentity(player: MprisPlayer): string {\n        const alias = Config.services.playerAliases.find(a => a.from === player.identity);\n        return alias?.to ?? player.identity;\n    }\n\n    Connections {\n        function onPostTrackChanged() {\n            if (!Config.utilities.toasts.nowPlaying) {\n                return;\n            }\n            if (root.active.trackArtist != \"\" && root.active.trackTitle != \"\") {\n                Toaster.toast(qsTr(\"Now Playing\"), qsTr(\"%1 - %2\").arg(root.active.trackArtist).arg(root.active.trackTitle), \"music_note\");\n            }\n        }\n\n        target: root.active\n    }\n\n    PersistentProperties {\n        id: props\n\n        property MprisPlayer manualActive\n\n        reloadableId: \"players\"\n    }\n\n    CustomShortcut {\n        name: \"mediaToggle\"\n        description: \"Toggle media playback\"\n        onPressed: {\n            const active = root.active;\n            if (active && active.canTogglePlaying)\n                active.togglePlaying();\n        }\n    }\n\n    CustomShortcut {\n        name: \"mediaPrev\"\n        description: \"Previous track\"\n        onPressed: {\n            const active = root.active;\n            if (active && active.canGoPrevious)\n                active.previous();\n        }\n    }\n\n    CustomShortcut {\n        name: \"mediaNext\"\n        description: \"Next track\"\n        onPressed: {\n            const active = root.active;\n            if (active && active.canGoNext)\n                active.next();\n        }\n    }\n\n    CustomShortcut {\n        name: \"mediaStop\"\n        description: \"Stop media playback\"\n        onPressed: root.active?.stop()\n    }\n\n    IpcHandler {\n        function getActive(prop: string): string {\n            const active = root.active;\n            return active ? active[prop] ?? \"Invalid property\" : \"No active player\";\n        }\n\n        function list(): string {\n            return root.list.map(p => root.getIdentity(p)).join(\"\\n\");\n        }\n\n        function play(): void {\n            const active = root.active;\n            if (active?.canPlay)\n                active.play();\n        }\n\n        function pause(): void {\n            const active = root.active;\n            if (active?.canPause)\n                active.pause();\n        }\n\n        function playPause(): void {\n            const active = root.active;\n            if (active?.canTogglePlaying)\n                active.togglePlaying();\n        }\n\n        function previous(): void {\n            const active = root.active;\n            if (active?.canGoPrevious)\n                active.previous();\n        }\n\n        function next(): void {\n            const active = root.active;\n            if (active?.canGoNext)\n                active.next();\n        }\n\n        function stop(): void {\n            root.active?.stop();\n        }\n\n        target: \"mpris\"\n    }\n}\n"
  },
  {
    "path": "services/Recorder.qml",
    "content": "pragma Singleton\n\nimport Quickshell\nimport Quickshell.Io\nimport QtQuick\n\nSingleton {\n    id: root\n\n    readonly property alias running: props.running\n    readonly property alias paused: props.paused\n    readonly property alias elapsed: props.elapsed\n    property bool needsStart\n    property list<string> startArgs\n    property bool needsStop\n    property bool needsPause\n\n    function start(extraArgs = []): void {\n        needsStart = true;\n        startArgs = extraArgs;\n        checkProc.running = true;\n    }\n\n    function stop(): void {\n        needsStop = true;\n        checkProc.running = true;\n    }\n\n    function togglePause(): void {\n        needsPause = true;\n        checkProc.running = true;\n    }\n\n    PersistentProperties {\n        id: props\n\n        property bool running: false\n        property bool paused: false\n        property real elapsed: 0 // Might get too large for int\n\n        reloadableId: \"recorder\"\n    }\n\n    Process {\n        id: checkProc\n\n        running: true\n        command: [\"pidof\", \"gpu-screen-recorder\"]\n        onExited: code => {\n            props.running = code === 0;\n\n            if (code === 0) {\n                if (root.needsStop) {\n                    Quickshell.execDetached([\"caelestia\", \"record\"]);\n                    props.running = false;\n                    props.paused = false;\n                } else if (root.needsPause) {\n                    Quickshell.execDetached([\"caelestia\", \"record\", \"-p\"]);\n                    props.paused = !props.paused;\n                }\n            } else if (root.needsStart) {\n                Quickshell.execDetached([\"caelestia\", \"record\", ...root.startArgs]);\n                props.running = true;\n                props.paused = false;\n                props.elapsed = 0;\n            }\n\n            root.needsStart = false;\n            root.needsStop = false;\n            root.needsPause = false;\n        }\n    }\n\n    Connections {\n        // enabled: props.running && !props.paused\n        function onSecondsChanged(): void {\n            props.elapsed++;\n        }\n\n        target: Time\n    }\n}\n"
  },
  {
    "path": "services/Screens.qml",
    "content": "pragma Singleton\n\nimport qs.config\nimport qs.utils\nimport Quickshell\n\nSingleton {\n    id: root\n\n    readonly property list<ShellScreen> screens: {\n        const excluded = Config.general.excludedScreens;\n        if (excluded.length === 0)\n            return Quickshell.screens;\n        return Quickshell.screens.filter(s => !Strings.testRegexList(excluded, s.name));\n    }\n\n    function isExcluded(screen: ShellScreen): bool {\n        return Strings.testRegexList(Config.general.excludedScreens, screen.name);\n    }\n}\n"
  },
  {
    "path": "services/SystemUsage.qml",
    "content": "pragma Singleton\n\nimport qs.config\nimport Quickshell\nimport Quickshell.Io\nimport QtQuick\n\nSingleton {\n    id: root\n\n    // CPU properties\n    property string cpuName: \"\"\n    property real cpuPerc\n    property real cpuTemp\n\n    // GPU properties\n    readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType\n    property string autoGpuType: \"NONE\"\n    property string gpuName: \"\"\n    property real gpuPerc\n    property real gpuTemp\n\n    // Memory properties\n    property real memUsed\n    property real memTotal\n    readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0\n\n    // Storage properties (aggregated)\n    readonly property real storagePerc: {\n        let totalUsed = 0;\n        let totalSize = 0;\n        for (const disk of disks) {\n            totalUsed += disk.used;\n            totalSize += disk.total;\n        }\n        return totalSize > 0 ? totalUsed / totalSize : 0;\n    }\n\n    // Individual disks: Array of { mount, used, total, free, perc }\n    property var disks: []\n\n    property real lastCpuIdle\n    property real lastCpuTotal\n\n    property int refCount\n\n    function cleanCpuName(name: string): string {\n        return name.replace(/\\(R\\)|\\(TM\\)|CPU|\\d+(?:th|nd|rd|st) Gen |Core |Processor/gi, \"\").replace(/\\s+/g, \" \").trim();\n    }\n\n    function cleanGpuName(name: string): string {\n        return name.replace(/\\(R\\)|\\(TM\\)|Graphics/gi, \"\").replace(/\\s+/g, \" \").trim();\n    }\n\n    function formatKib(kib: real): var {\n        const mib = 1024;\n        const gib = 1024 ** 2;\n        const tib = 1024 ** 3;\n\n        if (kib >= tib)\n            return {\n                value: kib / tib,\n                unit: \"TiB\"\n            };\n        if (kib >= gib)\n            return {\n                value: kib / gib,\n                unit: \"GiB\"\n            };\n        if (kib >= mib)\n            return {\n                value: kib / mib,\n                unit: \"MiB\"\n            };\n        return {\n            value: kib,\n            unit: \"KiB\"\n        };\n    }\n\n    Timer {\n        running: root.refCount > 0\n        interval: Config.dashboard.resourceUpdateInterval\n        repeat: true\n        triggeredOnStart: true\n        onTriggered: {\n            stat.reload();\n            meminfo.reload();\n            storage.running = true;\n            gpuUsage.running = true;\n            sensors.running = true;\n        }\n    }\n\n    // One-time CPU info detection (name)\n    FileView {\n        id: cpuinfoInit\n\n        path: \"/proc/cpuinfo\"\n        onLoaded: {\n            const nameMatch = text().match(/model name\\s*:\\s*(.+)/);\n            if (nameMatch)\n                root.cpuName = root.cleanCpuName(nameMatch[1]);\n        }\n    }\n\n    FileView {\n        id: stat\n\n        path: \"/proc/stat\"\n        onLoaded: {\n            const data = text().match(/^cpu\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)/);\n            if (data) {\n                const stats = data.slice(1).map(n => parseInt(n, 10));\n                const total = stats.reduce((a, b) => a + b, 0);\n                const idle = stats[3] + (stats[4] ?? 0);\n\n                const totalDiff = total - root.lastCpuTotal;\n                const idleDiff = idle - root.lastCpuIdle;\n                root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;\n\n                root.lastCpuTotal = total;\n                root.lastCpuIdle = idle;\n            }\n        }\n    }\n\n    FileView {\n        id: meminfo\n\n        path: \"/proc/meminfo\"\n        onLoaded: {\n            const data = text();\n            root.memTotal = parseInt(data.match(/MemTotal: *(\\d+)/)[1], 10) || 1;\n            root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\\d+)/)[1], 10)) || 0;\n        }\n    }\n\n    Process {\n        id: storage\n\n        // Get physical disks with aggregated usage from their partitions\n        // -J triggers JSON output. -b triggers bytes.\n        command: [\"lsblk\", \"-J\", \"-b\", \"-o\", \"NAME,SIZE,TYPE,FSUSED,FSSIZE,MOUNTPOINT\"]\n\n        stdout: StdioCollector {\n            onStreamFinished: {\n                const data = JSON.parse(text);\n                const diskList = [];\n                const seenDevices = new Set();\n\n                // Helper to recursively sum usage from children (partitions, crypt, lvm)\n                const aggregateUsage = dev => {\n                    let used = 0;\n                    let size = 0;\n                    let isRoot = dev.mountpoint === \"/\" || (dev.mountpoints && dev.mountpoints.includes(\"/\"));\n\n                    if (!seenDevices.has(dev.name)) {\n                        // lsblk returns null for empty/unformatted partitions, which parses to 0 here\n                        used = parseInt(dev.fsused) || 0;\n                        size = parseInt(dev.fssize) || 0;\n                        seenDevices.add(dev.name);\n                    }\n\n                    if (dev.children) {\n                        for (const child of dev.children) {\n                            const stats = aggregateUsage(child);\n                            used += stats.used;\n                            size += stats.size;\n                            if (stats.isRoot)\n                                isRoot = true;\n                        }\n                    }\n                    return {\n                        used,\n                        size,\n                        isRoot\n                    };\n                };\n\n                for (const dev of data.blockdevices) {\n                    // Only process physical disks at the top level\n                    if (dev.type === \"disk\" && !dev.name.startsWith(\"zram\")) {\n                        const stats = aggregateUsage(dev);\n\n                        if (stats.size === 0) {\n                            continue;\n                        }\n\n                        const total = stats.size;\n                        const used = stats.used;\n\n                        diskList.push({\n                            mount: dev.name,\n                            used: used / 1024      // KiB\n                            ,\n                            total: total / 1024    // KiB\n                            ,\n                            free: (total - used) / 1024,\n                            perc: total > 0 ? used / total : 0,\n                            hasRoot: stats.isRoot\n                        });\n                    }\n                }\n\n                // Sort by putting the disk with root first, then sort the rest alphabetically\n                root.disks = diskList.sort((a, b) => {\n                    if (a.hasRoot && !b.hasRoot)\n                        return -1;\n                    if (!a.hasRoot && b.hasRoot)\n                        return 1;\n                    return a.mount.localeCompare(b.mount);\n                });\n            }\n        }\n    }\n\n    // GPU name detection (one-time)\n    Process {\n        id: gpuNameDetect\n\n        running: true\n        command: [\"sh\", \"-c\", \"nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1 || lspci 2>/dev/null | grep -i 'vga\\\\|3d controller\\\\|display' | head -1\"]\n        stdout: StdioCollector {\n            onStreamFinished: {\n                const output = text.trim();\n                if (!output)\n                    return;\n\n                // Check if it's from nvidia-smi (clean GPU name)\n                if (output.toLowerCase().includes(\"nvidia\") || output.toLowerCase().includes(\"geforce\") || output.toLowerCase().includes(\"rtx\") || output.toLowerCase().includes(\"gtx\")) {\n                    root.gpuName = root.cleanGpuName(output);\n                } else if (output.toLowerCase().includes(\"rx\")) {\n                    root.gpuName = root.cleanGpuName(output);\n                } else {\n                    // Parse lspci output: extract name from brackets or after colon\n                    // Handles cases like [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] (rev c0)\n                    const bracketMatch = output.match(/\\[([^\\]]+)\\][^\\[]*$/);\n                    if (bracketMatch) {\n                        root.gpuName = root.cleanGpuName(bracketMatch[1]);\n                    } else {\n                        const colonMatch = output.match(/:\\s*(.+)/);\n                        if (colonMatch)\n                            root.gpuName = root.cleanGpuName(colonMatch[1]);\n                    }\n                }\n            }\n        }\n    }\n\n    Process {\n        id: gpuTypeCheck\n\n        running: !Config.services.gpuType\n        command: [\"sh\", \"-c\", \"if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi\"]\n        stdout: StdioCollector {\n            onStreamFinished: root.autoGpuType = text.trim()\n        }\n    }\n\n    Process {\n        id: gpuUsage\n\n        command: root.gpuType === \"GENERIC\" ? [\"sh\", \"-c\", \"cat /sys/class/drm/card*/device/gpu_busy_percent\"] : root.gpuType === \"NVIDIA\" ? [\"nvidia-smi\", \"--query-gpu=utilization.gpu,temperature.gpu\", \"--format=csv,noheader,nounits\"] : [\"echo\"]\n        stdout: StdioCollector {\n            onStreamFinished: {\n                if (root.gpuType === \"GENERIC\") {\n                    const percs = text.trim().split(\"\\n\");\n                    const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);\n                    root.gpuPerc = sum / percs.length / 100;\n                } else if (root.gpuType === \"NVIDIA\") {\n                    const [usage, temp] = text.trim().split(\",\");\n                    root.gpuPerc = parseInt(usage, 10) / 100;\n                    root.gpuTemp = parseInt(temp, 10);\n                } else {\n                    root.gpuPerc = 0;\n                    root.gpuTemp = 0;\n                }\n            }\n        }\n    }\n\n    Process {\n        id: sensors\n\n        command: [\"sensors\"]\n        environment: ({\n                LANG: \"C.UTF-8\",\n                LC_ALL: \"C.UTF-8\"\n            })\n        stdout: StdioCollector {\n            onStreamFinished: {\n                let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\\s+((\\+|-)[0-9.]+)(°| )C/);\n                if (!cpuTemp)\n                    // If AMD Tdie pattern failed, try fallback on Tctl\n                    cpuTemp = text.match(/Tctl:\\s+((\\+|-)[0-9.]+)(°| )C/);\n\n                if (cpuTemp)\n                    root.cpuTemp = parseFloat(cpuTemp[1]);\n\n                if (root.gpuType !== \"GENERIC\")\n                    return;\n\n                let eligible = false;\n                let sum = 0;\n                let count = 0;\n\n                for (const line of text.trim().split(\"\\n\")) {\n                    if (line === \"Adapter: PCI adapter\")\n                        eligible = true;\n                    else if (line === \"\")\n                        eligible = false;\n                    else if (eligible) {\n                        let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\\s+\\+([0-9]+\\.[0-9]+)(°| )C/);\n                        if (!match)\n                            // Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)\n                            match = line.match(/^(junction|mem)+:\\s+\\+([0-9]+\\.[0-9]+)(°| )C/);\n\n                        if (match) {\n                            sum += parseFloat(match[2]);\n                            count++;\n                        }\n                    }\n                }\n\n                root.gpuTemp = count > 0 ? sum / count : 0;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "services/Time.qml",
    "content": "pragma Singleton\n\nimport qs.config\nimport Quickshell\nimport QtQuick\n\nSingleton {\n    property alias enabled: clock.enabled\n    readonly property date date: clock.date\n    readonly property int hours: clock.hours\n    readonly property int minutes: clock.minutes\n    readonly property int seconds: clock.seconds\n\n    readonly property string timeStr: format(Config.services.useTwelveHourClock ? \"hh:mm:A\" : \"hh:mm\")\n    readonly property list<string> timeComponents: timeStr.split(\":\")\n    readonly property string hourStr: timeComponents[0] ?? \"\"\n    readonly property string minuteStr: timeComponents[1] ?? \"\"\n    readonly property string amPmStr: timeComponents[2] ?? \"\"\n\n    function format(fmt: string): string {\n        return Qt.formatDateTime(clock.date, fmt);\n    }\n\n    SystemClock {\n        id: clock\n\n        precision: SystemClock.Seconds\n    }\n}\n"
  },
  {
    "path": "services/VPN.qml",
    "content": "pragma Singleton\n\nimport Quickshell\nimport Quickshell.Io\nimport QtQuick\nimport qs.config\nimport Caelestia\n\nSingleton {\n    id: root\n\n    property bool connected: false\n\n    readonly property bool connecting: connectProc.running || disconnectProc.running\n    readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === \"object\" ? (p.enabled === true) : false)\n    readonly property var providerInput: {\n        const enabledProvider = Config.utilities.vpn.provider.find(p => typeof p === \"object\" ? (p.enabled === true) : false);\n        return enabledProvider || \"wireguard\";\n    }\n    readonly property bool isCustomProvider: typeof providerInput === \"object\"\n    readonly property string providerName: isCustomProvider ? (providerInput.name || \"custom\") : String(providerInput)\n    readonly property string interfaceName: isCustomProvider ? (providerInput.interface || \"\") : \"\"\n    readonly property var currentConfig: {\n        const name = providerName;\n        const iface = interfaceName;\n        const defaults = getBuiltinDefaults(name, iface);\n\n        if (isCustomProvider) {\n            const custom = providerInput;\n            return {\n                connectCmd: custom.connectCmd || defaults.connectCmd,\n                disconnectCmd: custom.disconnectCmd || defaults.disconnectCmd,\n                interface: custom.interface || defaults.interface,\n                displayName: custom.displayName || defaults.displayName\n            };\n        }\n\n        return defaults;\n    }\n\n    function getBuiltinDefaults(name, iface) {\n        const builtins = {\n            \"wireguard\": {\n                connectCmd: [\"pkexec\", \"wg-quick\", \"up\", iface],\n                disconnectCmd: [\"pkexec\", \"wg-quick\", \"down\", iface],\n                interface: iface,\n                displayName: iface\n            },\n            \"warp\": {\n                connectCmd: [\"warp-cli\", \"connect\"],\n                disconnectCmd: [\"warp-cli\", \"disconnect\"],\n                interface: \"CloudflareWARP\",\n                displayName: \"Warp\"\n            },\n            \"netbird\": {\n                connectCmd: [\"netbird\", \"up\"],\n                disconnectCmd: [\"netbird\", \"down\"],\n                interface: \"wt0\",\n                displayName: \"NetBird\"\n            },\n            \"tailscale\": {\n                connectCmd: [\"tailscale\", \"up\"],\n                disconnectCmd: [\"tailscale\", \"down\"],\n                interface: \"tailscale0\",\n                displayName: \"Tailscale\"\n            }\n        };\n\n        return builtins[name] || {\n            connectCmd: [name, \"up\"],\n            disconnectCmd: [name, \"down\"],\n            interface: iface || name,\n            displayName: name\n        };\n    }\n\n    function connect(): void {\n        if (!connected && !connecting && root.currentConfig && root.currentConfig.connectCmd) {\n            connectProc.exec(root.currentConfig.connectCmd);\n        }\n    }\n\n    function disconnect(): void {\n        if (connected && !connecting && root.currentConfig && root.currentConfig.disconnectCmd) {\n            disconnectProc.exec(root.currentConfig.disconnectCmd);\n        }\n    }\n\n    function toggle(): void {\n        if (connected) {\n            disconnect();\n        } else {\n            connect();\n        }\n    }\n\n    function checkStatus(): void {\n        if (root.enabled) {\n            statusProc.running = true;\n        }\n    }\n\n    onConnectedChanged: {\n        if (!Config.utilities.toasts.vpnChanged)\n            return;\n\n        const displayName = root.currentConfig ? (root.currentConfig.displayName || \"VPN\") : \"VPN\";\n        if (connected) {\n            Toaster.toast(qsTr(\"VPN connected\"), qsTr(\"Connected to %1\").arg(displayName), \"vpn_key\");\n        } else {\n            Toaster.toast(qsTr(\"VPN disconnected\"), qsTr(\"Disconnected from %1\").arg(displayName), \"vpn_key_off\");\n        }\n    }\n\n    Component.onCompleted: root.enabled && statusCheckTimer.start()\n\n    Process {\n        id: nmMonitor\n\n        running: root.enabled\n        command: [\"nmcli\", \"monitor\"]\n        stdout: SplitParser {\n            onRead: statusCheckTimer.restart()\n        }\n    }\n\n    Process {\n        id: statusProc\n\n        command: [\"ip\", \"link\", \"show\"]\n        environment: ({\n                LANG: \"C.UTF-8\",\n                LC_ALL: \"C.UTF-8\"\n            })\n        stdout: StdioCollector {\n            onStreamFinished: {\n                const iface = root.currentConfig ? root.currentConfig.interface : \"\";\n                root.connected = iface && text.includes(iface + \":\");\n            }\n        }\n    }\n\n    Process {\n        id: connectProc\n\n        onExited: statusCheckTimer.start()\n        stderr: StdioCollector {\n            onStreamFinished: {\n                const error = text.trim();\n                if (error && !error.includes(\"[#]\") && !error.includes(\"already exists\")) {\n                    console.warn(\"VPN connection error:\", error);\n                } else if (error.includes(\"already exists\")) {\n                    root.connected = true;\n                }\n            }\n        }\n    }\n\n    Process {\n        id: disconnectProc\n\n        onExited: statusCheckTimer.start()\n        stderr: StdioCollector {\n            onStreamFinished: {\n                const error = text.trim();\n                if (error && !error.includes(\"[#]\")) {\n                    console.warn(\"VPN disconnection error:\", error);\n                }\n            }\n        }\n    }\n\n    Timer {\n        id: statusCheckTimer\n\n        interval: 500\n        onTriggered: root.checkStatus()\n    }\n}\n"
  },
  {
    "path": "services/Visibilities.qml",
    "content": "pragma Singleton\n\nimport qs.components\nimport qs.services\nimport Quickshell\n\nSingleton {\n    property var screens: new Map()\n    property var bars: new Map()\n\n    function load(screen: ShellScreen, visibilities: DrawerVisibilities): void {\n        screens.set(Hypr.monitorFor(screen), visibilities);\n    }\n\n    function getForActive(): DrawerVisibilities {\n        return screens.get(Hypr.focusedMonitor);\n    }\n}\n"
  },
  {
    "path": "services/Wallpapers.qml",
    "content": "pragma Singleton\n\nimport qs.services\nimport qs.config\nimport qs.utils\nimport Caelestia.Models\nimport Quickshell\nimport Quickshell.Io\nimport QtQuick\n\nSearcher {\n    id: root\n\n    readonly property string currentNamePath: `${Paths.state}/wallpaper/path.txt`\n    readonly property list<string> smartArg: Config.services.smartScheme ? [] : [\"--no-smart\"]\n\n    property bool showPreview: false\n    readonly property string current: showPreview ? previewPath : actualCurrent\n    property string previewPath\n    property string actualCurrent\n    property bool previewColourLock\n\n    function setWallpaper(path: string): void {\n        actualCurrent = path;\n        Quickshell.execDetached([\"caelestia\", \"wallpaper\", \"-f\", path, ...smartArg]);\n    }\n\n    function preview(path: string): void {\n        previewPath = path;\n        showPreview = true;\n\n        if (Colours.scheme === \"dynamic\")\n            getPreviewColoursProc.running = true;\n    }\n\n    function stopPreview(): void {\n        showPreview = false;\n        if (!previewColourLock)\n            Colours.showPreview = false;\n    }\n\n    list: wallpapers.entries\n    key: \"relativePath\"\n    useFuzzy: Config.launcher.useFuzzy.wallpapers\n    extraOpts: useFuzzy ? ({}) : ({\n            forward: false\n        })\n\n    IpcHandler {\n        function get(): string {\n            return root.actualCurrent;\n        }\n\n        function set(path: string): void {\n            root.setWallpaper(path);\n        }\n\n        function list(): string {\n            return root.list.map(w => w.path).join(\"\\n\");\n        }\n\n        target: \"wallpaper\"\n    }\n\n    FileView {\n        path: root.currentNamePath\n        watchChanges: true\n        onFileChanged: reload()\n        onLoaded: {\n            root.actualCurrent = text().trim();\n            root.previewColourLock = false;\n        }\n    }\n\n    FileSystemModel {\n        id: wallpapers\n\n        recursive: true\n        path: Paths.wallsdir\n        filter: FileSystemModel.Images\n    }\n\n    Process {\n        id: getPreviewColoursProc\n\n        command: [\"caelestia\", \"wallpaper\", \"-p\", root.previewPath, ...root.smartArg]\n        stdout: StdioCollector {\n            onStreamFinished: {\n                Colours.load(text, true);\n                Colours.showPreview = true;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "services/Weather.qml",
    "content": "pragma Singleton\n\nimport qs.config\nimport qs.utils\nimport Caelestia\nimport Quickshell\nimport QtQuick\n\nSingleton {\n    id: root\n\n    property string city\n    property string loc\n    property var cc\n    property list<var> forecast\n    property list<var> hourlyForecast\n\n    readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : \"cloud_alert\"\n    readonly property string description: cc?.weatherDesc ?? qsTr(\"No weather\")\n    readonly property string temp: Config.services.useFahrenheit ? `${cc?.tempF ?? 0}°F` : `${cc?.tempC ?? 0}°C`\n    readonly property string feelsLike: Config.services.useFahrenheit ? `${cc?.feelsLikeF ?? 0}°F` : `${cc?.feelsLikeC ?? 0}°C`\n    readonly property int humidity: cc?.humidity ?? 0\n    readonly property real windSpeed: cc?.windSpeed ?? 0\n    readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? \"h:mm A\" : \"h:mm\") : \"--:--\"\n    readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? \"h:mm A\" : \"h:mm\") : \"--:--\"\n\n    readonly property var cachedCities: new Map()\n\n    function reload(): void {\n        const configLocation = Config.services.weatherLocation;\n\n        if (configLocation) {\n            if (configLocation.indexOf(\",\") !== -1 && !isNaN(parseFloat(configLocation.split(\",\")[0]))) {\n                loc = configLocation;\n                fetchCityFromCoords(configLocation);\n            } else {\n                fetchCoordsFromCity(configLocation);\n            }\n        } else if (!loc || timer.elapsed() > 900) {\n            Requests.get(\"https://ipinfo.io/json\", text => {\n                const response = JSON.parse(text);\n                if (response.loc) {\n                    loc = response.loc;\n                    city = response.city ?? \"\";\n                    timer.restart();\n                }\n            });\n        }\n    }\n\n    function fetchCityFromCoords(coords: string): void {\n        if (cachedCities.has(coords)) {\n            city = cachedCities.get(coords);\n            return;\n        }\n\n        const [lat, lon] = coords.split(\",\").map(s => s.trim());\n\n        const fallbackToBigDataCloud = () => {\n            const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=en`;\n            Requests.get(fallbackUrl, text => {\n                const geo = JSON.parse(text);\n                const geoCity = geo.city || geo.locality;\n                if (geoCity) {\n                    city = geoCity;\n                    cachedCities.set(coords, geoCity);\n                } else {\n                    city = \"Unknown City\";\n                }\n            });\n        };\n\n        const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`;\n        Requests.get(nominatimUrl, text => {\n            const geo = JSON.parse(text).features?.[0]?.properties.geocoding;\n            if (geo) {\n                const geoCity = geo.type === \"city\" ? geo.name : geo.city;\n                if (geoCity) {\n                    city = geoCity;\n                    cachedCities.set(coords, geoCity);\n                    return;\n                }\n            }\n            fallbackToBigDataCloud();\n        }, fallbackToBigDataCloud);\n    }\n\n    function fetchCoordsFromCity(cityName: string): void {\n        const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=en&format=json`;\n\n        Requests.get(url, text => {\n            const json = JSON.parse(text);\n            if (json.results && json.results.length > 0) {\n                const result = json.results[0];\n                loc = result.latitude + \",\" + result.longitude;\n                city = result.name;\n            } else {\n                loc = \"\";\n                reload();\n            }\n        });\n    }\n\n    function fetchWeatherData(): void {\n        const url = getWeatherUrl();\n        if (url === \"\")\n            return;\n\n        Requests.get(url, text => {\n            const json = JSON.parse(text);\n            if (!json.current || !json.daily)\n                return;\n\n            cc = {\n                weatherCode: json.current.weather_code,\n                weatherDesc: getWeatherCondition(json.current.weather_code),\n                tempC: Math.round(json.current.temperature_2m),\n                tempF: Math.round(toFahrenheit(json.current.temperature_2m)),\n                feelsLikeC: Math.round(json.current.apparent_temperature),\n                feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)),\n                humidity: json.current.relative_humidity_2m,\n                windSpeed: json.current.wind_speed_10m,\n                isDay: json.current.is_day,\n                sunrise: json.daily.sunrise[0],\n                sunset: json.daily.sunset[0]\n            };\n\n            const forecastList = [];\n            for (let i = 0; i < json.daily.time.length; i++)\n                forecastList.push({\n                    date: json.daily.time[i],\n                    maxTempC: Math.round(json.daily.temperature_2m_max[i]),\n                    maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),\n                    minTempC: Math.round(json.daily.temperature_2m_min[i]),\n                    minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),\n                    weatherCode: json.daily.weather_code[i],\n                    icon: Icons.getWeatherIcon(json.daily.weather_code[i])\n                });\n            forecast = forecastList;\n\n            const hourlyList = [];\n            const now = new Date();\n            for (let i = 0; i < json.hourly.time.length; i++) {\n                const time = new Date(json.hourly.time[i]);\n                if (time < now)\n                    continue;\n\n                hourlyList.push({\n                    timestamp: json.hourly.time[i],\n                    hour: time.getHours(),\n                    tempC: Math.round(json.hourly.temperature_2m[i]),\n                    tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])),\n                    weatherCode: json.hourly.weather_code[i],\n                    icon: Icons.getWeatherIcon(json.hourly.weather_code[i])\n                });\n            }\n            hourlyForecast = hourlyList;\n        });\n    }\n\n    function toFahrenheit(celcius: real): real {\n        return celcius * 9 / 5 + 32;\n    }\n\n    function getWeatherUrl(): string {\n        if (!loc || loc.indexOf(\",\") === -1)\n            return \"\";\n\n        const [lat, lon] = loc.split(\",\").map(s => s.trim());\n        const baseUrl = \"https://api.open-meteo.com/v1/forecast\";\n        const params = [\"latitude=\" + lat, \"longitude=\" + lon, \"hourly=weather_code,temperature_2m\", \"daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset\", \"current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m\", \"timezone=auto\", \"forecast_days=7\"];\n\n        return baseUrl + \"?\" + params.join(\"&\");\n    }\n\n    function getWeatherCondition(code: string): string {\n        const conditions = {\n            \"0\": \"Clear\",\n            \"1\": \"Clear\",\n            \"2\": \"Partly cloudy\",\n            \"3\": \"Overcast\",\n            \"45\": \"Fog\",\n            \"48\": \"Fog\",\n            \"51\": \"Drizzle\",\n            \"53\": \"Drizzle\",\n            \"55\": \"Drizzle\",\n            \"56\": \"Freezing drizzle\",\n            \"57\": \"Freezing drizzle\",\n            \"61\": \"Light rain\",\n            \"63\": \"Rain\",\n            \"65\": \"Heavy rain\",\n            \"66\": \"Light rain\",\n            \"67\": \"Heavy rain\",\n            \"71\": \"Light snow\",\n            \"73\": \"Snow\",\n            \"75\": \"Heavy snow\",\n            \"77\": \"Snow\",\n            \"80\": \"Light rain\",\n            \"81\": \"Rain\",\n            \"82\": \"Heavy rain\",\n            \"85\": \"Light snow showers\",\n            \"86\": \"Heavy snow showers\",\n            \"95\": \"Thunderstorm\",\n            \"96\": \"Thunderstorm with hail\",\n            \"99\": \"Thunderstorm with hail\"\n        };\n        return conditions[code] || \"Unknown\";\n    }\n\n    onLocChanged: fetchWeatherData()\n\n    Connections {\n        function onWeatherLocationChanged(): void {\n            root.reload();\n        }\n\n        target: Config.services\n    }\n\n    // Refresh current location hourly\n    Timer {\n        interval: 3600000 // 1 hour\n        running: true\n        repeat: true\n        onTriggered: fetchWeatherData()\n    }\n\n    ElapsedTimer {\n        id: timer\n    }\n}\n"
  },
  {
    "path": "shell.qml",
    "content": "//@ pragma Env QS_NO_RELOAD_POPUP=1\n//@ pragma Env QSG_RENDER_LOOP=threaded\n//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000\n\nimport \"modules\"\nimport \"modules/drawers\"\nimport \"modules/background\"\nimport \"modules/areapicker\"\nimport \"modules/lock\"\nimport Quickshell\n\nShellRoot {\n    Background {}\n    Drawers {}\n    AreaPicker {}\n    Lock {\n        id: lock\n    }\n\n    Shortcuts {}\n    BatteryMonitor {}\n    IdleMonitors {\n        lock: lock\n    }\n}\n"
  },
  {
    "path": "utils/Icons.qml",
    "content": "pragma Singleton\n\nimport qs.config\nimport Quickshell\nimport Quickshell.Services.Notifications\nimport QtQuick\n\nSingleton {\n    id: root\n\n    readonly property var weatherIcons: ({\n            \"0\": \"clear_day\",\n            \"1\": \"clear_day\",\n            \"2\": \"partly_cloudy_day\",\n            \"3\": \"cloud\",\n            \"45\": \"foggy\",\n            \"48\": \"foggy\",\n            \"51\": \"rainy\",\n            \"53\": \"rainy\",\n            \"55\": \"rainy\",\n            \"56\": \"rainy\",\n            \"57\": \"rainy\",\n            \"61\": \"rainy\",\n            \"63\": \"rainy\",\n            \"65\": \"rainy\",\n            \"66\": \"rainy\",\n            \"67\": \"rainy\",\n            \"71\": \"cloudy_snowing\",\n            \"73\": \"cloudy_snowing\",\n            \"75\": \"snowing_heavy\",\n            \"77\": \"cloudy_snowing\",\n            \"80\": \"rainy\",\n            \"81\": \"rainy\",\n            \"82\": \"rainy\",\n            \"85\": \"cloudy_snowing\",\n            \"86\": \"snowing_heavy\",\n            \"95\": \"thunderstorm\",\n            \"96\": \"thunderstorm\",\n            \"99\": \"thunderstorm\"\n        })\n\n    readonly property var categoryIcons: ({\n            WebBrowser: \"web\",\n            Printing: \"print\",\n            Security: \"security\",\n            Network: \"chat\",\n            Archiving: \"archive\",\n            Compression: \"archive\",\n            Development: \"code\",\n            IDE: \"code\",\n            TextEditor: \"edit_note\",\n            Audio: \"music_note\",\n            Music: \"music_note\",\n            Player: \"music_note\",\n            Recorder: \"mic\",\n            Game: \"sports_esports\",\n            FileTools: \"files\",\n            FileManager: \"files\",\n            Filesystem: \"files\",\n            FileTransfer: \"files\",\n            Settings: \"settings\",\n            DesktopSettings: \"settings\",\n            HardwareSettings: \"settings\",\n            TerminalEmulator: \"terminal\",\n            ConsoleOnly: \"terminal\",\n            Utility: \"build\",\n            Monitor: \"monitor_heart\",\n            Midi: \"graphic_eq\",\n            Mixer: \"graphic_eq\",\n            AudioVideoEditing: \"video_settings\",\n            AudioVideo: \"music_video\",\n            Video: \"videocam\",\n            Building: \"construction\",\n            Graphics: \"photo_library\",\n            \"2DGraphics\": \"photo_library\",\n            RasterGraphics: \"photo_library\",\n            TV: \"tv\",\n            System: \"host\",\n            Office: \"content_paste\"\n        })\n\n    // Checks if a name matches an icon config. Icon configs can have the following keys:\n    // - name: The exact name of the icon\n    // - regex: A regex to match against the name (takes priority over name)\n    // - flags: The regex flags (only used if regex is set)\n    // - icon: The icon to use\n    function matchIconConfig(name: string, iconConfig: var): bool {\n        if (!iconConfig.icon)\n            return false;\n\n        if (iconConfig.regex) {\n            const re = new RegExp(iconConfig.regex, iconConfig.flags ?? \"\");\n            if (re.test(name))\n                return true;\n        } else if (iconConfig.name === name) {\n            return true;\n        }\n\n        return false;\n    }\n\n    function getAppIcon(name: string, fallback: string): string {\n        const icon = DesktopEntries.heuristicLookup(name)?.icon;\n        if (fallback !== \"undefined\")\n            return Quickshell.iconPath(icon, fallback);\n        return Quickshell.iconPath(icon);\n    }\n\n    function getAppCategoryIcon(name: string, fallback: string): string {\n        for (const iconConfig of Config.bar.workspaces.windowIcons)\n            if (matchIconConfig(name, iconConfig))\n                return iconConfig.icon;\n\n        const categories = DesktopEntries.heuristicLookup(name)?.categories;\n\n        if (categories)\n            for (const [key, value] of Object.entries(categoryIcons))\n                if (categories.includes(key))\n                    return value;\n        return fallback;\n    }\n\n    function getNetworkIcon(strength: int, isSecure = false): string {\n        if (isSecure) {\n            if (strength >= 80)\n                return \"network_wifi_locked\";\n            if (strength >= 60)\n                return \"network_wifi_3_bar_locked\";\n            if (strength >= 40)\n                return \"network_wifi_2_bar_locked\";\n            if (strength >= 20)\n                return \"network_wifi_1_bar_locked\";\n            return \"signal_wifi_0_bar\";\n        } else {\n            if (strength >= 80)\n                return \"network_wifi\";\n            if (strength >= 60)\n                return \"network_wifi_3_bar\";\n            if (strength >= 40)\n                return \"network_wifi_2_bar\";\n            if (strength >= 20)\n                return \"network_wifi_1_bar\";\n            return \"signal_wifi_0_bar\";\n        }\n    }\n\n    function getBluetoothIcon(icon: string): string {\n        if (icon.includes(\"headset\") || icon.includes(\"headphones\"))\n            return \"headphones\";\n        if (icon.includes(\"audio\"))\n            return \"speaker\";\n        if (icon.includes(\"phone\"))\n            return \"smartphone\";\n        if (icon.includes(\"mouse\"))\n            return \"mouse\";\n        if (icon.includes(\"keyboard\"))\n            return \"keyboard\";\n        return \"bluetooth\";\n    }\n\n    function getWeatherIcon(code: string): string {\n        if (weatherIcons.hasOwnProperty(code))\n            return weatherIcons[code];\n        return \"air\";\n    }\n\n    function getNotifIcon(summary: string, urgency: int): string {\n        summary = summary.toLowerCase();\n        if (summary.includes(\"reboot\"))\n            return \"restart_alt\";\n        if (summary.includes(\"recording\"))\n            return \"screen_record\";\n        if (summary.includes(\"battery\"))\n            return \"power\";\n        if (summary.includes(\"screenshot\"))\n            return \"screenshot_monitor\";\n        if (summary.includes(\"welcome\"))\n            return \"waving_hand\";\n        if (summary.includes(\"time\") || summary.includes(\"a break\"))\n            return \"schedule\";\n        if (summary.includes(\"installed\"))\n            return \"download\";\n        if (summary.includes(\"update\"))\n            return \"update\";\n        if (summary.includes(\"unable to\"))\n            return \"deployed_code_alert\";\n        if (summary.includes(\"profile\"))\n            return \"person\";\n        if (summary.includes(\"file\"))\n            return \"folder_copy\";\n        if (urgency === NotificationUrgency.Critical)\n            return \"release_alert\";\n        return \"chat\";\n    }\n\n    function getVolumeIcon(volume: real, isMuted: bool): string {\n        if (isMuted)\n            return \"no_sound\";\n        if (volume >= 0.5)\n            return \"volume_up\";\n        if (volume > 0)\n            return \"volume_down\";\n        return \"volume_mute\";\n    }\n\n    function getMicVolumeIcon(volume: real, isMuted: bool): string {\n        if (!isMuted && volume > 0)\n            return \"mic\";\n        return \"mic_off\";\n    }\n\n    function getSpecialWsIcon(name: string): string {\n        name = name.toLowerCase().slice(\"special:\".length);\n\n        for (const iconConfig of Config.bar.workspaces.specialWorkspaceIcons)\n            if (matchIconConfig(name, iconConfig))\n                return iconConfig.icon;\n\n        if (name === \"special\")\n            return \"star\";\n        if (name === \"communication\")\n            return \"forum\";\n        if (name === \"music\")\n            return \"music_cast\";\n        if (name === \"todo\")\n            return \"checklist\";\n        if (name === \"sysmon\")\n            return \"monitor_heart\";\n        return name[0].toUpperCase();\n    }\n\n    function getTrayIcon(id: string, icon: string): string {\n        for (const sub of Config.bar.tray.iconSubs)\n            if (sub.id === id)\n                return sub.image ? Qt.resolvedUrl(sub.image) : Quickshell.iconPath(sub.icon);\n\n        if (icon.includes(\"?path=\")) {\n            const [name, path] = icon.split(\"?path=\");\n            icon = Qt.resolvedUrl(`${path}/${name.slice(name.lastIndexOf(\"/\") + 1)}`);\n        }\n        return icon;\n    }\n}\n"
  },
  {
    "path": "utils/Images.qml",
    "content": "pragma Singleton\n\nimport Quickshell\n\nSingleton {\n    readonly property list<string> validImageTypes: [\"jpeg\", \"png\", \"webp\", \"tiff\", \"svg\"]\n    readonly property list<string> validImageExtensions: [\"jpg\", \"jpeg\", \"png\", \"webp\", \"tif\", \"tiff\", \"svg\"]\n\n    function isValidImageByName(name: string): bool {\n        return validImageExtensions.some(t => name.endsWith(`.${t}`));\n    }\n}\n"
  },
  {
    "path": "utils/NetworkConnection.qml",
    "content": "pragma Singleton\n\nimport qs.services\nimport QtQuick\n\n/**\n * NetworkConnection\n *\n * Centralized utility for network connection logic. Provides a single source of truth\n * for connecting to wireless networks, eliminating code duplication across\n * controlcenter components and bar popouts.\n *\n * Usage:\n * ```qml\n * import qs.utils\n *\n * // With Session object (controlcenter)\n * NetworkConnection.handleConnect(network, session);\n *\n * // Without Session object (bar popouts) - provide password dialog callback\n * NetworkConnection.handleConnect(network, null, (network) => {\n *     // Show password dialog\n *     root.passwordNetwork = network;\n *     root.showPasswordDialog = true;\n * });\n * ```\n */\nQtObject {\n    id: root\n\n    /**\n     * Handle network connection with automatic disconnection if needed.\n     * If there's an active network different from the target, disconnects first,\n     * then connects to the target network.\n     *\n     * @param network The network object to connect to (must have ssid property)\n     * @param session Optional Session object (for controlcenter - must have network property with showPasswordDialog and pendingNetwork)\n     * @param onPasswordNeeded Optional callback function(network) called when password is needed (for bar popouts)\n     */\n    function handleConnect(network, session, onPasswordNeeded): void {\n        if (!network) {\n            return;\n        }\n\n        if (Nmcli.active && Nmcli.active.ssid !== network.ssid) {\n            Nmcli.disconnectFromNetwork();\n            Qt.callLater(() => {\n                root.connectToNetwork(network, session, onPasswordNeeded);\n            });\n        } else {\n            root.connectToNetwork(network, session, onPasswordNeeded);\n        }\n    }\n\n    /**\n     * Connect to a wireless network.\n     * Handles both secured and open networks, checks for saved profiles,\n     * and shows password dialog if needed.\n     *\n     * @param network The network object to connect to (must have ssid, isSecure, bssid properties)\n     * @param session Optional Session object (for controlcenter - must have network property with showPasswordDialog and pendingNetwork)\n     * @param onPasswordNeeded Optional callback function(network) called when password is needed (for bar popouts)\n     */\n    function connectToNetwork(network, session, onPasswordNeeded): void {\n        if (!network) {\n            return;\n        }\n\n        if (network.isSecure) {\n            const hasSavedProfile = Nmcli.hasSavedProfile(network.ssid);\n\n            if (hasSavedProfile) {\n                Nmcli.connectToNetwork(network.ssid, \"\", network.bssid, null);\n            } else {\n                // Use password check with callback\n                Nmcli.connectToNetworkWithPasswordCheck(network.ssid, network.isSecure, result => {\n                    if (result.needsPassword) {\n                        // Clear pending connection if exists\n                        if (Nmcli.pendingConnection) {\n                            Nmcli.connectionCheckTimer.stop();\n                            Nmcli.immediateCheckTimer.stop();\n                            Nmcli.immediateCheckTimer.checkCount = 0;\n                            Nmcli.pendingConnection = null;\n                        }\n\n                        // Handle password dialog - use session if available, otherwise use callback\n                        if (session && session.network) {\n                            session.network.showPasswordDialog = true;\n                            session.network.pendingNetwork = network;\n                        } else if (onPasswordNeeded) {\n                            onPasswordNeeded(network);\n                        }\n                    }\n                }, network.bssid);\n            }\n        } else {\n            Nmcli.connectToNetwork(network.ssid, \"\", network.bssid, null);\n        }\n    }\n\n    /**\n     * Connect to a wireless network with a provided password.\n     * Used by password dialogs when the user has already entered a password.\n     *\n     * @param network The network object to connect to (must have ssid, bssid properties)\n     * @param password The password to use for connection\n     * @param onResult Optional callback function(result) called with connection result\n     */\n    function connectWithPassword(network, password, onResult): void {\n        if (!network) {\n            return;\n        }\n\n        Nmcli.connectToNetwork(network.ssid, password || \"\", network.bssid || \"\", onResult || null);\n    }\n}\n"
  },
  {
    "path": "utils/Paths.qml",
    "content": "pragma Singleton\n\nimport qs.config\nimport Caelestia\nimport Quickshell\nimport QtQuick\n\nSingleton {\n    id: root\n\n    readonly property string home: Quickshell.env(\"HOME\")\n    readonly property string pictures: Quickshell.env(\"XDG_PICTURES_DIR\") || `${home}/Pictures`\n    readonly property string videos: Quickshell.env(\"XDG_VIDEOS_DIR\") || `${home}/Videos`\n\n    readonly property string data: `${Quickshell.env(\"XDG_DATA_HOME\") || `${home}/.local/share`}/caelestia`\n    readonly property string state: `${Quickshell.env(\"XDG_STATE_HOME\") || `${home}/.local/state`}/caelestia`\n    readonly property string cache: `${Quickshell.env(\"XDG_CACHE_HOME\") || `${home}/.cache`}/caelestia`\n    readonly property string config: `${Quickshell.env(\"XDG_CONFIG_HOME\") || `${home}/.config`}/caelestia`\n\n    readonly property string imagecache: `${cache}/imagecache`\n    readonly property string notifimagecache: `${imagecache}/notifs`\n    readonly property string wallsdir: Quickshell.env(\"CAELESTIA_WALLPAPERS_DIR\") || absolutePath(Config.paths.wallpaperDir)\n    readonly property string recsdir: Quickshell.env(\"CAELESTIA_RECORDINGS_DIR\") || `${videos}/Recordings`\n    readonly property string libdir: Quickshell.env(\"CAELESTIA_LIB_DIR\") || \"/usr/lib/caelestia\"\n\n    function toLocalFile(path: url): string {\n        path = Qt.resolvedUrl(path);\n        return path.toString() ? CUtils.toLocalFile(path) : \"\";\n    }\n\n    function absolutePath(path: string): string {\n        return toLocalFile(path.replace(/~|(\\$({?)HOME(}?))+/, home));\n    }\n\n    function shortenHome(path: string): string {\n        return path.replace(home, \"~\");\n    }\n}\n"
  },
  {
    "path": "utils/Searcher.qml",
    "content": "import Quickshell\n\nimport \"scripts/fzf.js\" as Fzf\nimport \"scripts/fuzzysort.js\" as Fuzzy\nimport QtQuick\n\nSingleton {\n    required property list<QtObject> list\n    property string key: \"name\"\n    property bool useFuzzy: false\n    property var extraOpts: ({})\n\n    // Extra stuff for fuzzy\n    property list<string> keys: [key]\n    property list<real> weights: [1]\n\n    readonly property var fzf: useFuzzy ? [] : new Fzf.Finder(list, Object.assign({\n        selector\n    }, extraOpts))\n    readonly property list<var> fuzzyPrepped: useFuzzy ? list.map(e => {\n        const obj = {\n            _item: e\n        };\n        for (const k of keys)\n            obj[k] = Fuzzy.prepare(e[k]);\n        return obj;\n    }) : []\n\n    function transformSearch(search: string): string {\n        return search;\n    }\n\n    function selector(item: var): string {\n        // Only for fzf\n        return item[key];\n    }\n\n    function query(search: string): list<var> {\n        search = transformSearch(search);\n        if (!search)\n            return [...list];\n\n        if (useFuzzy)\n            return Fuzzy.go(search, fuzzyPrepped, Object.assign({\n                all: true,\n                keys,\n                scoreFn: r => weights.reduce((a, w, i) => a + r[i].score * w, 0)\n            }, extraOpts)).map(r => r.obj._item);\n\n        return fzf.find(search).sort((a, b) => {\n            if (a.score === b.score)\n                return selector(a.item).trim().length - selector(b.item).trim().length;\n            return b.score - a.score;\n        }).map(r => r.item);\n    }\n}\n"
  },
  {
    "path": "utils/Strings.qml",
    "content": "pragma Singleton\n\nimport Quickshell\n\nSingleton {\n    property var _regexCache: ({})\n\n    function testRegexList(filterList: list<string>, target: string): bool {\n        const regexChecker = /^\\^.*\\$$/;\n        for (const filter of filterList) {\n            if (regexChecker.test(filter)) {\n                let re = _regexCache[filter];\n                if (!re) {\n                    re = new RegExp(filter);\n                    _regexCache[filter] = re;\n                }\n                if (re.test(target))\n                    return true;\n            } else {\n                if (filter === target)\n                    return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "utils/SysInfo.qml",
    "content": "pragma Singleton\n\nimport qs.config\nimport qs.utils\nimport Quickshell\nimport Quickshell.Io\nimport QtQuick\n\nSingleton {\n    id: root\n\n    property string osName\n    property string osPrettyName\n    property string osId\n    property list<string> osIdLike\n    property string osLogo: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/logo.svg`)\n    property bool isDefaultLogo: true\n\n    property string uptime\n    readonly property string user: Quickshell.env(\"USER\")\n    readonly property string wm: Quickshell.env(\"XDG_CURRENT_DESKTOP\") || Quickshell.env(\"XDG_SESSION_DESKTOP\")\n    readonly property string shell: Quickshell.env(\"SHELL\").split(\"/\").pop()\n\n    FileView {\n        id: osRelease\n\n        path: \"/etc/os-release\"\n        onLoaded: {\n            const lines = text().split(\"\\n\");\n\n            const fd = key => lines.find(l => l.startsWith(`${key}=`))?.split(\"=\")[1].replace(/\"/g, \"\") ?? \"\";\n\n            root.osName = fd(\"NAME\");\n            root.osPrettyName = fd(\"PRETTY_NAME\");\n            root.osId = fd(\"ID\");\n            root.osIdLike = fd(\"ID_LIKE\").split(\" \");\n\n            const logo = Quickshell.iconPath(fd(\"LOGO\"), true);\n            if (Config.general.logo === \"caelestia\") {\n                root.osLogo = Qt.resolvedUrl(`${Quickshell.shellDir}/assets/logo.svg`);\n                root.isDefaultLogo = true;\n            } else if (Config.general.logo) {\n                root.osLogo = Quickshell.iconPath(Config.general.logo, true) || \"file://\" + Paths.absolutePath(Config.general.logo);\n                root.isDefaultLogo = false;\n            } else if (logo) {\n                root.osLogo = logo;\n                root.isDefaultLogo = false;\n            }\n        }\n    }\n\n    Connections {\n        function onLogoChanged(): void {\n            osRelease.reload();\n        }\n\n        target: Config.general\n    }\n\n    Timer {\n        running: true\n        repeat: true\n        interval: 15000\n        onTriggered: fileUptime.reload()\n    }\n\n    FileView {\n        id: fileUptime\n\n        path: \"/proc/uptime\"\n        onLoaded: {\n            const up = parseInt(text().split(\" \")[0] ?? 0);\n\n            const days = Math.floor(up / 86400);\n            const hours = Math.floor((up % 86400) / 3600);\n            const minutes = Math.floor((up % 3600) / 60);\n\n            let str = \"\";\n            if (days > 0)\n                str += `${days} day${days === 1 ? \"\" : \"s\"}`;\n            if (hours > 0)\n                str += `${str ? \", \" : \"\"}${hours} hour${hours === 1 ? \"\" : \"s\"}`;\n            if (minutes > 0 || !str)\n                str += `${str ? \", \" : \"\"}${minutes} minute${minutes === 1 ? \"\" : \"s\"}`;\n            root.uptime = str;\n        }\n    }\n}\n"
  },
  {
    "path": "utils/scripts/fuzzysort.js",
    "content": ".pragma library\n\n/*\nhttps://github.com/farzher/fuzzysort\n\nMIT License\n\nCopyright (c) 2018 Stephen Kamenar\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n*/\n\nvar single =  (search, target) => {\n    if(!search || !target) return NULL\n\n    var preparedSearch = getPreparedSearch(search)\n    if(!isPrepared(target)) target = getPrepared(target)\n\n    var searchBitflags = preparedSearch.bitflags\n    if((searchBitflags & target._bitflags) !== searchBitflags) return NULL\n\n    return algorithm(preparedSearch, target)\n}\n\nvar go = (search, targets, options) => {\n    if(!search) return options?.all ? all(targets, options) : noResults\n\n    var preparedSearch = getPreparedSearch(search)\n    var searchBitflags = preparedSearch.bitflags\n    var containsSpace  = preparedSearch.containsSpace\n\n    var threshold = denormalizeScore( options?.threshold || 0 )\n    var limit     = options?.limit || INFINITY\n\n    var resultsLen = 0; var limitedCount = 0\n    var targetsLen = targets.length\n\n    function push_result(result) {\n    if(resultsLen < limit) { q.add(result); ++resultsLen }\n    else {\n        ++limitedCount\n        if(result._score > q.peek()._score) q.replaceTop(result)\n    }\n    }\n\n    // This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys]\n\n    // options.key\n    if(options?.key) {\n    var key = options.key\n    for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]\n        var target = getValue(obj, key)\n        if(!target) continue\n        if(!isPrepared(target)) target = getPrepared(target)\n\n        if((searchBitflags & target._bitflags) !== searchBitflags) continue\n        var result = algorithm(preparedSearch, target)\n        if(result === NULL) continue\n        if(result._score < threshold) continue\n\n        result.obj = obj\n        push_result(result)\n    }\n\n    // options.keys\n    } else if(options?.keys) {\n    var keys = options.keys\n    var keysLen = keys.length\n\n    outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]\n\n        { // early out based on bitflags\n        var keysBitflags = 0\n        for (var keyI = 0; keyI < keysLen; ++keyI) {\n            var key = keys[keyI]\n            var target = getValue(obj, key)\n            if(!target) { tmpTargets[keyI] = noTarget; continue }\n            if(!isPrepared(target)) target = getPrepared(target)\n            tmpTargets[keyI] = target\n\n            keysBitflags |= target._bitflags\n        }\n\n        if((searchBitflags & keysBitflags) !== searchBitflags) continue\n        }\n\n        if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) keysSpacesBestScores[i] = NEGATIVE_INFINITY\n\n        for (var keyI = 0; keyI < keysLen; ++keyI) {\n        target = tmpTargets[keyI]\n        if(target === noTarget) { tmpResults[keyI] = noTarget; continue }\n\n        tmpResults[keyI] = algorithm(preparedSearch, target, /*allowSpaces=*/false, /*allowPartialMatch=*/containsSpace)\n        if(tmpResults[keyI] === NULL) { tmpResults[keyI] = noTarget; continue }\n\n        // todo: this seems weird and wrong. like what if our first match wasn't good. this should just replace it instead of averaging with it\n        // if our second match isn't good we ignore it instead of averaging with it\n        if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) {\n            if(allowPartialMatchScores[i] > -1000) {\n            if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) {\n                var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/\n                if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp\n            }\n            }\n            if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i]\n        }\n        }\n\n        if(containsSpace) {\n        for(let i=0; i<preparedSearch.spaceSearches.length; i++) { if(keysSpacesBestScores[i] === NEGATIVE_INFINITY) continue outer }\n        } else {\n        var hasAtLeast1Match = false\n        for(let i=0; i < keysLen; i++) { if(tmpResults[i]._score !== NEGATIVE_INFINITY) { hasAtLeast1Match = true; break } }\n        if(!hasAtLeast1Match) continue\n        }\n\n        var objResults = new KeysResult(keysLen)\n        for(let i=0; i < keysLen; i++) { objResults[i] = tmpResults[i] }\n\n        if(containsSpace) {\n        var score = 0\n        for(let i=0; i<preparedSearch.spaceSearches.length; i++) score += keysSpacesBestScores[i]\n        } else {\n        // todo could rewrite this scoring to be more similar to when there's spaces\n        // if we match multiple keys give us bonus points\n        var score = NEGATIVE_INFINITY\n        for(let i=0; i<keysLen; i++) {\n            var result = objResults[i]\n            if(result._score > -1000) {\n            if(score > NEGATIVE_INFINITY) {\n                var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/\n                if(tmp > score) score = tmp\n            }\n            }\n            if(result._score > score) score = result._score\n        }\n        }\n\n        objResults.obj = obj\n        objResults._score = score\n        if(options?.scoreFn) {\n        score = options.scoreFn(objResults)\n        if(!score) continue\n        score = denormalizeScore(score)\n        objResults._score = score\n        }\n\n        if(score < threshold) continue\n        push_result(objResults)\n    }\n\n    // no keys\n    } else {\n    for(var i = 0; i < targetsLen; ++i) { var target = targets[i]\n        if(!target) continue\n        if(!isPrepared(target)) target = getPrepared(target)\n\n        if((searchBitflags & target._bitflags) !== searchBitflags) continue\n        var result = algorithm(preparedSearch, target)\n        if(result === NULL) continue\n        if(result._score < threshold) continue\n\n        push_result(result)\n    }\n    }\n\n    if(resultsLen === 0) return noResults\n    var results = new Array(resultsLen)\n    for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()\n    results.total = resultsLen + limitedCount\n    return results\n}\n\n\n// this is written as 1 function instead of 2 for minification. perf seems fine ...\n// except when minified. the perf is very slow\nvar highlight = (result, open='<b>', close='</b>') => {\n    var callback = typeof open === 'function' ? open : undefined\n\n    var target      = result.target\n    var targetLen   = target.length\n    var indexes     = result.indexes\n    var highlighted = ''\n    var matchI      = 0\n    var indexesI    = 0\n    var opened      = false\n    var parts       = []\n\n    for(var i = 0; i < targetLen; ++i) { var char = target[i]\n    if(indexes[indexesI] === i) {\n        ++indexesI\n        if(!opened) { opened = true\n        if(callback) {\n            parts.push(highlighted); highlighted = ''\n        } else {\n            highlighted += open\n        }\n        }\n\n        if(indexesI === indexes.length) {\n        if(callback) {\n            highlighted += char\n            parts.push(callback(highlighted, matchI++)); highlighted = ''\n            parts.push(target.substr(i+1))\n        } else {\n            highlighted += char + close + target.substr(i+1)\n        }\n        break\n        }\n    } else {\n        if(opened) { opened = false\n        if(callback) {\n            parts.push(callback(highlighted, matchI++)); highlighted = ''\n        } else {\n            highlighted += close\n        }\n        }\n    }\n    highlighted += char\n    }\n\n    return callback ? parts : highlighted\n}\n\n\nvar prepare = (target) => {\n    if(typeof target === 'number') target = ''+target\n    else if(typeof target !== 'string') target = ''\n    var info = prepareLowerInfo(target)\n    return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags})\n}\n\nvar cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() }\n\n\n// Below this point is only internal code\n// Below this point is only internal code\n// Below this point is only internal code\n// Below this point is only internal code\n\n\nclass Result {\n    get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) }\n    set ['indexes'](indexes) { return this._indexes = indexes }\n    ['highlight'](open, close) { return highlight(this, open, close) }\n    get ['score']() { return normalizeScore(this._score) }\n    set ['score'](score) { this._score = denormalizeScore(score) }\n}\n\nclass KeysResult extends Array {\n    get ['score']() { return normalizeScore(this._score) }\n    set ['score'](score) { this._score = denormalizeScore(score) }\n}\n\nvar new_result = (target, options) => {\n    const result = new Result()\n    result['target']             = target\n    result['obj']                = options.obj                   ?? NULL\n    result._score                = options._score                ?? NEGATIVE_INFINITY\n    result._indexes              = options._indexes              ?? []\n    result._targetLower          = options._targetLower          ?? ''\n    result._targetLowerCodes     = options._targetLowerCodes     ?? NULL\n    result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL\n    result._bitflags             = options._bitflags             ?? 0\n    return result\n}\n\n\nvar normalizeScore = score => {\n    if(score === NEGATIVE_INFINITY) return 0\n    if(score > 1) return score\n    return Math.E ** ( ((-score + 1)**.04307 - 1) * -2)\n}\nvar denormalizeScore = normalizedScore => {\n    if(normalizedScore === 0) return NEGATIVE_INFINITY\n    if(normalizedScore > 1) return normalizedScore\n    return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307)\n}\n\n\nvar prepareSearch = (search) => {\n    if(typeof search === 'number') search = ''+search\n    else if(typeof search !== 'string') search = ''\n    search = search.trim()\n    var info = prepareLowerInfo(search)\n\n    var spaceSearches = []\n    if(info.containsSpace) {\n    var searches = search.split(/\\s+/)\n    searches = [...new Set(searches)] // distinct\n    for(var i=0; i<searches.length; i++) {\n        if(searches[i] === '') continue\n        var _info = prepareLowerInfo(searches[i])\n        spaceSearches.push({lowerCodes:_info.lowerCodes, _lower:searches[i].toLowerCase(), containsSpace:false})\n    }\n    }\n\n    return {lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches}\n}\n\n\n\nvar getPrepared = (target) => {\n    if(target.length > 999) return prepare(target) // don't cache huge targets\n    var targetPrepared = preparedCache.get(target)\n    if(targetPrepared !== undefined) return targetPrepared\n    targetPrepared = prepare(target)\n    preparedCache.set(target, targetPrepared)\n    return targetPrepared\n}\nvar getPreparedSearch = (search) => {\n    if(search.length > 999) return prepareSearch(search) // don't cache huge searches\n    var searchPrepared = preparedSearchCache.get(search)\n    if(searchPrepared !== undefined) return searchPrepared\n    searchPrepared = prepareSearch(search)\n    preparedSearchCache.set(search, searchPrepared)\n    return searchPrepared\n}\n\n\nvar all = (targets, options) => {\n    var results = []; results.total = targets.length // this total can be wrong if some targets are skipped\n\n    var limit = options?.limit || INFINITY\n\n    if(options?.key) {\n    for(var i=0;i<targets.length;i++) { var obj = targets[i]\n        var target = getValue(obj, options.key)\n        if(target == NULL) continue\n        if(!isPrepared(target)) target = getPrepared(target)\n        var result = new_result(target.target, {_score: target._score, obj: obj})\n        results.push(result); if(results.length >= limit) return results\n    }\n    } else if(options?.keys) {\n    for(var i=0;i<targets.length;i++) { var obj = targets[i]\n        var objResults = new KeysResult(options.keys.length)\n        for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) {\n        var target = getValue(obj, options.keys[keyI])\n        if(!target) { objResults[keyI] = noTarget; continue }\n        if(!isPrepared(target)) target = getPrepared(target)\n        target._score = NEGATIVE_INFINITY\n        target._indexes.len = 0\n        objResults[keyI] = target\n        }\n        objResults.obj = obj\n        objResults._score = NEGATIVE_INFINITY\n        results.push(objResults); if(results.length >= limit) return results\n    }\n    } else {\n    for(var i=0;i<targets.length;i++) { var target = targets[i]\n        if(target == NULL) continue\n        if(!isPrepared(target)) target = getPrepared(target)\n        target._score = NEGATIVE_INFINITY\n        target._indexes.len = 0\n        results.push(target); if(results.length >= limit) return results\n    }\n    }\n\n    return results\n}\n\n\nvar algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => {\n    if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch)\n\n    var searchLower      = preparedSearch._lower\n    var searchLowerCodes = preparedSearch.lowerCodes\n    var searchLowerCode  = searchLowerCodes[0]\n    var targetLowerCodes = prepared._targetLowerCodes\n    var searchLen        = searchLowerCodes.length\n    var targetLen        = targetLowerCodes.length\n    var searchI          = 0 // where we at\n    var targetI          = 0 // where you at\n    var matchesSimpleLen = 0\n\n    // very basic fuzzy match; to remove non-matching targets ASAP!\n    // walk through target. find sequential matches.\n    // if all chars aren't found then exit\n    for(;;) {\n    var isMatch = searchLowerCode === targetLowerCodes[targetI]\n    if(isMatch) {\n        matchesSimple[matchesSimpleLen++] = targetI\n        ++searchI; if(searchI === searchLen) break\n        searchLowerCode = searchLowerCodes[searchI]\n    }\n    ++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI\n    }\n\n    var searchI = 0\n    var successStrict = false\n    var matchesStrictLen = 0\n\n    var nextBeginningIndexes = prepared._nextBeginningIndexes\n    if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target)\n    targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]\n\n    // Our target string successfully matched all characters in sequence!\n    // Let's try a more advanced and strict test to improve the score\n    // only count it as a match if it's consecutive or a beginning character!\n    var backtrackCount = 0\n    if(targetI !== targetLen) for(;;) {\n    if(targetI >= targetLen) {\n        // We failed to find a good spot for this search char, go back to the previous search char and force it forward\n        if(searchI <= 0) break // We failed to push chars forward for a better match\n\n        ++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match\n\n        --searchI\n        var lastMatch = matchesStrict[--matchesStrictLen]\n        targetI = nextBeginningIndexes[lastMatch]\n\n    } else {\n        var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]\n        if(isMatch) {\n        matchesStrict[matchesStrictLen++] = targetI\n        ++searchI; if(searchI === searchLen) { successStrict = true; break }\n        ++targetI\n        } else {\n        targetI = nextBeginningIndexes[targetI]\n        }\n    }\n    }\n\n    // check if it's a substring match\n    var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow\n    var isSubstring = !!~substringIndex\n    var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex\n\n    // if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score\n    if(isSubstring && !isSubstringBeginning) {\n    for(var i=0; i<nextBeginningIndexes.length; i=nextBeginningIndexes[i]) {\n        if(i <= substringIndex) continue\n\n        for(var s=0; s<searchLen; s++) if(searchLowerCodes[s] !== prepared._targetLowerCodes[i+s]) break\n        if(s === searchLen) { substringIndex = i; isSubstringBeginning = true; break }\n    }\n    }\n\n    // tally up the score & keep track of matches for highlighting later\n    // if it's a simple match, we'll switch to a substring match if a substring exists\n    // if it's a strict match, we'll switch to a substring match only if that's a better score\n\n    var calculateScore = matches => {\n    var score = 0\n\n    var extraMatchGroupCount = 0\n    for(var i = 1; i < searchLen; ++i) {\n        if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount}\n    }\n    var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1)\n\n    score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups\n\n    if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning\n\n    if(!successStrict) {\n        score *= 1000\n    } else {\n        // successStrict on a target with too many beginning indexes loses points for being a bad target\n        var uniqueBeginningIndexes = 1\n        for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes\n\n        if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ...\n    }\n\n    score -= (targetLen - searchLen)/2 // penality for longer targets\n\n    if(isSubstring)          score /= 1+searchLen*searchLen*1 // bonus for being a full substring\n    if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex\n\n    score -= (targetLen - searchLen)/2 // penality for longer targets\n\n    return score\n    }\n\n    if(!successStrict) {\n    if(isSubstring) for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches\n    var matchesBest = matchesSimple\n    var score = calculateScore(matchesBest)\n    } else {\n    if(isSubstringBeginning) {\n        for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches\n        var matchesBest = matchesSimple\n        var score = calculateScore(matchesSimple)\n    } else {\n        var matchesBest = matchesStrict\n        var score = calculateScore(matchesStrict)\n    }\n    }\n\n    prepared._score = score\n\n    for(var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i]\n    prepared._indexes.len = searchLen\n\n    const result    = new Result()\n    result.target   = prepared.target\n    result._score   = prepared._score\n    result._indexes = prepared._indexes\n    return result\n}\nvar algorithmSpaces = (preparedSearch, target, allowPartialMatch) => {\n    var seen_indexes = new Set()\n    var score = 0\n    var result = NULL\n\n    var first_seen_index_last_search = 0\n    var searches = preparedSearch.spaceSearches\n    var searchesLen = searches.length\n    var changeslen = 0\n\n    // Return _nextBeginningIndexes back to its normal state\n    var resetNextBeginningIndexes = () => {\n    for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1]\n    }\n\n    var hasAtLeast1Match = false\n    for(var i=0; i<searchesLen; ++i) {\n    allowPartialMatchScores[i] = NEGATIVE_INFINITY\n    var search = searches[i]\n\n    result = algorithm(search, target)\n    if(allowPartialMatch) {\n        if(result === NULL) continue\n        hasAtLeast1Match = true\n    } else {\n        if(result === NULL) {resetNextBeginningIndexes(); return NULL}\n    }\n\n    // if not the last search, we need to mutate _nextBeginningIndexes for the next search\n    var isTheLastSearch = i === searchesLen - 1\n    if(!isTheLastSearch) {\n        var indexes = result._indexes\n\n        var indexesIsConsecutiveSubstring = true\n        for(let i=0; i<indexes.len-1; i++) {\n        if(indexes[i+1] - indexes[i] !== 1) {\n            indexesIsConsecutiveSubstring = false; break;\n        }\n        }\n\n        if(indexesIsConsecutiveSubstring) {\n        var newBeginningIndex = indexes[indexes.len-1] + 1\n        var toReplace = target._nextBeginningIndexes[newBeginningIndex-1]\n        for(let i=newBeginningIndex-1; i>=0; i--) {\n            if(toReplace !== target._nextBeginningIndexes[i]) break\n            target._nextBeginningIndexes[i] = newBeginningIndex\n            nextBeginningIndexesChanges[changeslen*2 + 0] = i\n            nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace\n            changeslen++\n        }\n        }\n    }\n\n    score += result._score / searchesLen\n    allowPartialMatchScores[i] = result._score / searchesLen\n\n    // dock points based on order otherwise \"c man\" returns Manifest.cpp instead of CheatManager.h\n    if(result._indexes[0] < first_seen_index_last_search) {\n        score -= (first_seen_index_last_search - result._indexes[0]) * 2\n    }\n    first_seen_index_last_search = result._indexes[0]\n\n    for(var j=0; j<result._indexes.len; ++j) seen_indexes.add(result._indexes[j])\n    }\n\n    if(allowPartialMatch && !hasAtLeast1Match) return NULL\n\n    resetNextBeginningIndexes()\n\n    // allows a search with spaces that's an exact substring to score well\n    var allowSpacesResult = algorithm(preparedSearch, target, /*allowSpaces=*/true)\n    if(allowSpacesResult !== NULL && allowSpacesResult._score > score) {\n    if(allowPartialMatch) {\n        for(var i=0; i<searchesLen; ++i) {\n        allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen\n        }\n    }\n    return allowSpacesResult\n    }\n\n    if(allowPartialMatch) result = target\n    result._score = score\n\n    var i = 0\n    for (let index of seen_indexes) result._indexes[i++] = index\n    result._indexes.len = i\n\n    return result\n}\n\n// we use this instead of just .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '') because that screws with japanese characters\nvar remove_accents = (str) => str.replace(/\\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\\u0300-\\u036f]/g, '')\n\nvar prepareLowerInfo = (str) => {\n    str = remove_accents(str)\n    var strLen = str.length\n    var lower = str.toLowerCase()\n    var lowerCodes = [] // new Array(strLen)    sparse array is too slow\n    var bitflags = 0\n    var containsSpace = false // space isn't stored in bitflags because of how searching with a space works\n\n    for(var i = 0; i < strLen; ++i) {\n    var lowerCode = lowerCodes[i] = lower.charCodeAt(i)\n\n    if(lowerCode === 32) {\n        containsSpace = true\n        continue // it's important that we don't set any bitflags for space\n    }\n\n    var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet\n            : lowerCode>=48&&lowerCode<=57  ? 26           // numbers\n                                                            // 3 bits available\n            : lowerCode<=127                ? 30           // other ascii\n            :                                 31           // other utf8\n    bitflags |= 1<<bit\n    }\n\n    return {lowerCodes:lowerCodes, bitflags:bitflags, containsSpace:containsSpace, _lower:lower}\n}\nvar prepareBeginningIndexes = (target) => {\n    var targetLen = target.length\n    var beginningIndexes = []; var beginningIndexesLen = 0\n    var wasUpper = false\n    var wasAlphanum = false\n    for(var i = 0; i < targetLen; ++i) {\n    var targetCode = target.charCodeAt(i)\n    var isUpper = targetCode>=65&&targetCode<=90\n    var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57\n    var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum\n    wasUpper = isUpper\n    wasAlphanum = isAlphanum\n    if(isBeginning) beginningIndexes[beginningIndexesLen++] = i\n    }\n    return beginningIndexes\n}\nvar prepareNextBeginningIndexes = (target) => {\n    target = remove_accents(target)\n    var targetLen = target.length\n    var beginningIndexes = prepareBeginningIndexes(target)\n    var nextBeginningIndexes = [] // new Array(targetLen)     sparse array is too slow\n    var lastIsBeginning = beginningIndexes[0]\n    var lastIsBeginningI = 0\n    for(var i = 0; i < targetLen; ++i) {\n    if(lastIsBeginning > i) {\n        nextBeginningIndexes[i] = lastIsBeginning\n    } else {\n        lastIsBeginning = beginningIndexes[++lastIsBeginningI]\n        nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning\n    }\n    }\n    return nextBeginningIndexes\n}\n\nvar preparedCache       = new Map()\nvar preparedSearchCache = new Map()\n\n// the theory behind these being globals is to reduce garbage collection by not making new arrays\nvar matchesSimple = []; var matchesStrict = []\nvar nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search\nvar keysSpacesBestScores = []; var allowPartialMatchScores = []\nvar tmpTargets = []; var tmpResults = []\n\n// prop = 'key'                  2.5ms optimized for this case, seems to be about as fast as direct obj[prop]\n// prop = 'key1.key2'            10ms\n// prop = ['key1', 'key2']       27ms\n// prop = obj => obj.tags.join() ??ms\nvar getValue = (obj, prop) => {\n    var tmp = obj[prop]; if(tmp !== undefined) return tmp\n    if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower\n    var segs = prop\n    if(!Array.isArray(prop)) segs = prop.split('.')\n    var len = segs.length\n    var i = -1\n    while (obj && (++i < len)) obj = obj[segs[i]]\n    return obj\n}\n\nvar isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' }\nvar INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY\nvar noResults = []; noResults.total = 0\nvar NULL = null\n\nvar noTarget = prepare('')\n\n// Hacked version of https://github.com/lemire/FastPriorityQueue.js\nvar fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c<o;){var s=c+1;a=c,s<o&&e[s]._score<e[c]._score&&(a=s),e[a-1>>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score<e[f]._score;f=(a=f)-1>>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score<e[v]._score;v=(a=v)-1>>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a}\nvar q = fastpriorityqueue() // reuse this\n\n"
  },
  {
    "path": "utils/scripts/fzf.js",
    "content": ".pragma library\n\n/*\nhttps://github.com/ajitid/fzf-for-js\n\nBSD 3-Clause License\n\nCopyright (c) 2021, Ajit\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/\n\nconst normalized = {\n  216: \"O\",\n  223: \"s\",\n  248: \"o\",\n  273: \"d\",\n  295: \"h\",\n  305: \"i\",\n  320: \"l\",\n  322: \"l\",\n  359: \"t\",\n  383: \"s\",\n  384: \"b\",\n  385: \"B\",\n  387: \"b\",\n  390: \"O\",\n  392: \"c\",\n  393: \"D\",\n  394: \"D\",\n  396: \"d\",\n  398: \"E\",\n  400: \"E\",\n  402: \"f\",\n  403: \"G\",\n  407: \"I\",\n  409: \"k\",\n  410: \"l\",\n  412: \"M\",\n  413: \"N\",\n  414: \"n\",\n  415: \"O\",\n  421: \"p\",\n  427: \"t\",\n  429: \"t\",\n  430: \"T\",\n  434: \"V\",\n  436: \"y\",\n  438: \"z\",\n  477: \"e\",\n  485: \"g\",\n  544: \"N\",\n  545: \"d\",\n  549: \"z\",\n  564: \"l\",\n  565: \"n\",\n  566: \"t\",\n  567: \"j\",\n  570: \"A\",\n  571: \"C\",\n  572: \"c\",\n  573: \"L\",\n  574: \"T\",\n  575: \"s\",\n  576: \"z\",\n  579: \"B\",\n  580: \"U\",\n  581: \"V\",\n  582: \"E\",\n  583: \"e\",\n  584: \"J\",\n  585: \"j\",\n  586: \"Q\",\n  587: \"q\",\n  588: \"R\",\n  589: \"r\",\n  590: \"Y\",\n  591: \"y\",\n  592: \"a\",\n  593: \"a\",\n  595: \"b\",\n  596: \"o\",\n  597: \"c\",\n  598: \"d\",\n  599: \"d\",\n  600: \"e\",\n  603: \"e\",\n  604: \"e\",\n  605: \"e\",\n  606: \"e\",\n  607: \"j\",\n  608: \"g\",\n  609: \"g\",\n  610: \"G\",\n  613: \"h\",\n  614: \"h\",\n  616: \"i\",\n  618: \"I\",\n  619: \"l\",\n  620: \"l\",\n  621: \"l\",\n  623: \"m\",\n  624: \"m\",\n  625: \"m\",\n  626: \"n\",\n  627: \"n\",\n  628: \"N\",\n  629: \"o\",\n  633: \"r\",\n  634: \"r\",\n  635: \"r\",\n  636: \"r\",\n  637: \"r\",\n  638: \"r\",\n  639: \"r\",\n  640: \"R\",\n  641: \"R\",\n  642: \"s\",\n  647: \"t\",\n  648: \"t\",\n  649: \"u\",\n  651: \"v\",\n  652: \"v\",\n  653: \"w\",\n  654: \"y\",\n  655: \"Y\",\n  656: \"z\",\n  657: \"z\",\n  663: \"c\",\n  665: \"B\",\n  666: \"e\",\n  667: \"G\",\n  668: \"H\",\n  669: \"j\",\n  670: \"k\",\n  671: \"L\",\n  672: \"q\",\n  686: \"h\",\n  867: \"a\",\n  868: \"e\",\n  869: \"i\",\n  870: \"o\",\n  871: \"u\",\n  872: \"c\",\n  873: \"d\",\n  874: \"h\",\n  875: \"m\",\n  876: \"r\",\n  877: \"t\",\n  878: \"v\",\n  879: \"x\",\n  7424: \"A\",\n  7427: \"B\",\n  7428: \"C\",\n  7429: \"D\",\n  7431: \"E\",\n  7432: \"e\",\n  7433: \"i\",\n  7434: \"J\",\n  7435: \"K\",\n  7436: \"L\",\n  7437: \"M\",\n  7438: \"N\",\n  7439: \"O\",\n  7440: \"O\",\n  7441: \"o\",\n  7442: \"o\",\n  7443: \"o\",\n  7446: \"o\",\n  7447: \"o\",\n  7448: \"P\",\n  7449: \"R\",\n  7450: \"R\",\n  7451: \"T\",\n  7452: \"U\",\n  7453: \"u\",\n  7454: \"u\",\n  7455: \"m\",\n  7456: \"V\",\n  7457: \"W\",\n  7458: \"Z\",\n  7522: \"i\",\n  7523: \"r\",\n  7524: \"u\",\n  7525: \"v\",\n  7834: \"a\",\n  7835: \"s\",\n  8305: \"i\",\n  8341: \"h\",\n  8342: \"k\",\n  8343: \"l\",\n  8344: \"m\",\n  8345: \"n\",\n  8346: \"p\",\n  8347: \"s\",\n  8348: \"t\",\n  8580: \"c\"\n};\nfor (let i = \"\\u0300\".codePointAt(0); i <= \"\\u036F\".codePointAt(0); ++i) {\n  const diacritic = String.fromCodePoint(i);\n  for (const asciiChar of \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\") {\n    const withDiacritic = (asciiChar + diacritic).normalize();\n    const withDiacriticCodePoint = withDiacritic.codePointAt(0);\n    if (withDiacriticCodePoint > 126) {\n      normalized[withDiacriticCodePoint] = asciiChar;\n    }\n  }\n}\nconst ranges = {\n  a: [7844, 7863],\n  e: [7870, 7879],\n  o: [7888, 7907],\n  u: [7912, 7921]\n};\nfor (const lowerChar of Object.keys(ranges)) {\n  const upperChar = lowerChar.toUpperCase();\n  for (let i = ranges[lowerChar][0]; i <= ranges[lowerChar][1]; ++i) {\n    normalized[i] = i % 2 === 0 ? upperChar : lowerChar;\n  }\n}\nfunction normalizeRune(rune) {\n  if (rune < 192 || rune > 8580) {\n    return rune;\n  }\n  const normalizedChar = normalized[rune];\n  if (normalizedChar !== void 0)\n    return normalizedChar.codePointAt(0);\n  return rune;\n}\nfunction toShort(number) {\n  return number;\n}\nfunction toInt(number) {\n  return number;\n}\nfunction maxInt16(num1, num2) {\n  return num1 > num2 ? num1 : num2;\n}\nconst strToRunes = (str) => str.split(\"\").map((s) => s.codePointAt(0));\nconst runesToStr = (runes) => runes.map((r) => String.fromCodePoint(r)).join(\"\");\nconst whitespaceRunes = new Set(\n  \" \\f\\n\\r\t\\v\\xA0\\u1680\\u2028\\u2029\\u202F\\u205F\\u3000\\uFEFF\".split(\"\").map((v) => v.codePointAt(0))\n);\nfor (let codePoint = \"\\u2000\".codePointAt(0); codePoint <= \"\\u200A\".codePointAt(0); codePoint++) {\n  whitespaceRunes.add(codePoint);\n}\nconst isWhitespace = (rune) => whitespaceRunes.has(rune);\nconst whitespacesAtStart = (runes) => {\n  let whitespaces = 0;\n  for (const rune of runes) {\n    if (isWhitespace(rune))\n      whitespaces++;\n    else\n      break;\n  }\n  return whitespaces;\n};\nconst whitespacesAtEnd = (runes) => {\n  let whitespaces = 0;\n  for (let i = runes.length - 1; i >= 0; i--) {\n    if (isWhitespace(runes[i]))\n      whitespaces++;\n    else\n      break;\n  }\n  return whitespaces;\n};\nconst MAX_ASCII = \"\\x7F\".codePointAt(0);\nconst CAPITAL_A_RUNE = \"A\".codePointAt(0);\nconst CAPITAL_Z_RUNE = \"Z\".codePointAt(0);\nconst SMALL_A_RUNE = \"a\".codePointAt(0);\nconst SMALL_Z_RUNE = \"z\".codePointAt(0);\nconst NUMERAL_ZERO_RUNE = \"0\".codePointAt(0);\nconst NUMERAL_NINE_RUNE = \"9\".codePointAt(0);\nfunction indexAt(index, max, forward) {\n  if (forward) {\n    return index;\n  }\n  return max - index - 1;\n}\nconst SCORE_MATCH = 16, SCORE_GAP_START = -3, SCORE_GAP_EXTENTION = -1, BONUS_BOUNDARY = SCORE_MATCH / 2, BONUS_NON_WORD = SCORE_MATCH / 2, BONUS_CAMEL_123 = BONUS_BOUNDARY + SCORE_GAP_EXTENTION, BONUS_CONSECUTIVE = -(SCORE_GAP_START + SCORE_GAP_EXTENTION), BONUS_FIRST_CHAR_MULTIPLIER = 2;\nfunction createPosSet(withPos) {\n  if (withPos) {\n    return /* @__PURE__ */ new Set();\n  }\n  return null;\n}\nfunction alloc16(offset, slab2, size) {\n  if (slab2 !== null && slab2.i16.length > offset + size) {\n    const subarray = slab2.i16.subarray(offset, offset + size);\n    return [offset + size, subarray];\n  }\n  return [offset, new Int16Array(size)];\n}\nfunction alloc32(offset, slab2, size) {\n  if (slab2 !== null && slab2.i32.length > offset + size) {\n    const subarray = slab2.i32.subarray(offset, offset + size);\n    return [offset + size, subarray];\n  }\n  return [offset, new Int32Array(size)];\n}\nfunction charClassOfAscii(rune) {\n  if (rune >= SMALL_A_RUNE && rune <= SMALL_Z_RUNE) {\n    return 1;\n  } else if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) {\n    return 2;\n  } else if (rune >= NUMERAL_ZERO_RUNE && rune <= NUMERAL_NINE_RUNE) {\n    return 4;\n  } else {\n    return 0;\n  }\n}\nfunction charClassOfNonAscii(rune) {\n  const char = String.fromCodePoint(rune);\n  if (char !== char.toUpperCase()) {\n    return 1;\n  } else if (char !== char.toLowerCase()) {\n    return 2;\n  } else if (char.match(/\\p{Number}/gu) !== null) {\n    return 4;\n  } else if (char.match(/\\p{Letter}/gu) !== null) {\n    return 3;\n  }\n  return 0;\n}\nfunction charClassOf(rune) {\n  if (rune <= MAX_ASCII) {\n    return charClassOfAscii(rune);\n  }\n  return charClassOfNonAscii(rune);\n}\nfunction bonusFor(prevClass, currClass) {\n  if (prevClass === 0 && currClass !== 0) {\n    return BONUS_BOUNDARY;\n  } else if (prevClass === 1 && currClass === 2 || prevClass !== 4 && currClass === 4) {\n    return BONUS_CAMEL_123;\n  } else if (currClass === 0) {\n    return BONUS_NON_WORD;\n  }\n  return 0;\n}\nfunction bonusAt(input, idx) {\n  if (idx === 0) {\n    return BONUS_BOUNDARY;\n  }\n  return bonusFor(charClassOf(input[idx - 1]), charClassOf(input[idx]));\n}\nfunction trySkip(input, caseSensitive, char, from) {\n  let rest = input.slice(from);\n  let idx = rest.indexOf(char);\n  if (idx === 0) {\n    return from;\n  }\n  if (!caseSensitive && char >= SMALL_A_RUNE && char <= SMALL_Z_RUNE) {\n    if (idx > 0) {\n      rest = rest.slice(0, idx);\n    }\n    const uidx = rest.indexOf(char - 32);\n    if (uidx >= 0) {\n      idx = uidx;\n    }\n  }\n  if (idx < 0) {\n    return -1;\n  }\n  return from + idx;\n}\nfunction isAscii(runes) {\n  for (const rune of runes) {\n    if (rune >= 128) {\n      return false;\n    }\n  }\n  return true;\n}\nfunction asciiFuzzyIndex(input, pattern, caseSensitive) {\n  if (!isAscii(input)) {\n    return 0;\n  }\n  if (!isAscii(pattern)) {\n    return -1;\n  }\n  let firstIdx = 0, idx = 0;\n  for (let pidx = 0; pidx < pattern.length; pidx++) {\n    idx = trySkip(input, caseSensitive, pattern[pidx], idx);\n    if (idx < 0) {\n      return -1;\n    }\n    if (pidx === 0 && idx > 0) {\n      firstIdx = idx - 1;\n    }\n    idx++;\n  }\n  return firstIdx;\n}\nconst fuzzyMatchV2 = (caseSensitive, normalize, forward, input, pattern, withPos, slab2) => {\n  const M = pattern.length;\n  if (M === 0) {\n    return [{ start: 0, end: 0, score: 0 }, createPosSet(withPos)];\n  }\n  const N = input.length;\n  if (slab2 !== null && N * M > slab2.i16.length) {\n    return fuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos);\n  }\n  const idx = asciiFuzzyIndex(input, pattern, caseSensitive);\n  if (idx < 0) {\n    return [{ start: -1, end: -1, score: 0 }, null];\n  }\n  let offset16 = 0, offset32 = 0, H0 = null, C0 = null, B = null, F = null;\n  [offset16, H0] = alloc16(offset16, slab2, N);\n  [offset16, C0] = alloc16(offset16, slab2, N);\n  [offset16, B] = alloc16(offset16, slab2, N);\n  [offset32, F] = alloc32(offset32, slab2, M);\n  const [, T] = alloc32(offset32, slab2, N);\n  for (let i = 0; i < T.length; i++) {\n    T[i] = input[i];\n  }\n  let maxScore = toShort(0), maxScorePos = 0;\n  let pidx = 0, lastIdx = 0;\n  const pchar0 = pattern[0];\n  let pchar = pattern[0], prevH0 = toShort(0), prevCharClass = 0, inGap = false;\n  let Tsub = T.subarray(idx);\n  let H0sub = H0.subarray(idx).subarray(0, Tsub.length), C0sub = C0.subarray(idx).subarray(0, Tsub.length), Bsub = B.subarray(idx).subarray(0, Tsub.length);\n  for (let [off, char] of Tsub.entries()) {\n    let charClass = null;\n    if (char <= MAX_ASCII) {\n      charClass = charClassOfAscii(char);\n      if (!caseSensitive && charClass === 2) {\n        char += 32;\n      }\n    } else {\n      charClass = charClassOfNonAscii(char);\n      if (!caseSensitive && charClass === 2) {\n        char = String.fromCodePoint(char).toLowerCase().codePointAt(0);\n      }\n      if (normalize) {\n        char = normalizeRune(char);\n      }\n    }\n    Tsub[off] = char;\n    const bonus = bonusFor(prevCharClass, charClass);\n    Bsub[off] = bonus;\n    prevCharClass = charClass;\n    if (char === pchar) {\n      if (pidx < M) {\n        F[pidx] = toInt(idx + off);\n        pidx++;\n        pchar = pattern[Math.min(pidx, M - 1)];\n      }\n      lastIdx = idx + off;\n    }\n    if (char === pchar0) {\n      const score = SCORE_MATCH + bonus * BONUS_FIRST_CHAR_MULTIPLIER;\n      H0sub[off] = score;\n      C0sub[off] = 1;\n      if (M === 1 && (forward && score > maxScore || !forward && score >= maxScore)) {\n        maxScore = score;\n        maxScorePos = idx + off;\n        if (forward && bonus === BONUS_BOUNDARY) {\n          break;\n        }\n      }\n      inGap = false;\n    } else {\n      if (inGap) {\n        H0sub[off] = maxInt16(prevH0 + SCORE_GAP_EXTENTION, 0);\n      } else {\n        H0sub[off] = maxInt16(prevH0 + SCORE_GAP_START, 0);\n      }\n      C0sub[off] = 0;\n      inGap = true;\n    }\n    prevH0 = H0sub[off];\n  }\n  if (pidx !== M) {\n    return [{ start: -1, end: -1, score: 0 }, null];\n  }\n  if (M === 1) {\n    const result = {\n      start: maxScorePos,\n      end: maxScorePos + 1,\n      score: maxScore\n    };\n    if (!withPos) {\n      return [result, null];\n    }\n    const pos2 = /* @__PURE__ */ new Set();\n    pos2.add(maxScorePos);\n    return [result, pos2];\n  }\n  const f0 = F[0];\n  const width = lastIdx - f0 + 1;\n  let H = null;\n  [offset16, H] = alloc16(offset16, slab2, width * M);\n  {\n    const toCopy = H0.subarray(f0, lastIdx + 1);\n    for (const [i, v] of toCopy.entries()) {\n      H[i] = v;\n    }\n  }\n  let [, C] = alloc16(offset16, slab2, width * M);\n  {\n    const toCopy = C0.subarray(f0, lastIdx + 1);\n    for (const [i, v] of toCopy.entries()) {\n      C[i] = v;\n    }\n  }\n  const Fsub = F.subarray(1);\n  const Psub = pattern.slice(1).slice(0, Fsub.length);\n  for (const [off, f] of Fsub.entries()) {\n    let inGap2 = false;\n    const pchar2 = Psub[off], pidx2 = off + 1, row = pidx2 * width, Tsub2 = T.subarray(f, lastIdx + 1), Bsub2 = B.subarray(f).subarray(0, Tsub2.length), Csub = C.subarray(row + f - f0).subarray(0, Tsub2.length), Cdiag = C.subarray(row + f - f0 - 1 - width).subarray(0, Tsub2.length), Hsub = H.subarray(row + f - f0).subarray(0, Tsub2.length), Hdiag = H.subarray(row + f - f0 - 1 - width).subarray(0, Tsub2.length), Hleft = H.subarray(row + f - f0 - 1).subarray(0, Tsub2.length);\n    Hleft[0] = 0;\n    for (const [off2, char] of Tsub2.entries()) {\n      const col = off2 + f;\n      let s1 = 0, s2 = 0, consecutive = 0;\n      if (inGap2) {\n        s2 = Hleft[off2] + SCORE_GAP_EXTENTION;\n      } else {\n        s2 = Hleft[off2] + SCORE_GAP_START;\n      }\n      if (pchar2 === char) {\n        s1 = Hdiag[off2] + SCORE_MATCH;\n        let b = Bsub2[off2];\n        consecutive = Cdiag[off2] + 1;\n        if (b === BONUS_BOUNDARY) {\n          consecutive = 1;\n        } else if (consecutive > 1) {\n          b = maxInt16(b, maxInt16(BONUS_CONSECUTIVE, B[col - consecutive + 1]));\n        }\n        if (s1 + b < s2) {\n          s1 += Bsub2[off2];\n          consecutive = 0;\n        } else {\n          s1 += b;\n        }\n      }\n      Csub[off2] = consecutive;\n      inGap2 = s1 < s2;\n      const score = maxInt16(maxInt16(s1, s2), 0);\n      if (pidx2 === M - 1 && (forward && score > maxScore || !forward && score >= maxScore)) {\n        maxScore = score;\n        maxScorePos = col;\n      }\n      Hsub[off2] = score;\n    }\n  }\n  const pos = createPosSet(withPos);\n  let j = f0;\n  if (withPos && pos !== null) {\n    let i = M - 1;\n    j = maxScorePos;\n    let preferMatch = true;\n    while (true) {\n      const I = i * width, j0 = j - f0, s = H[I + j0];\n      let s1 = 0, s2 = 0;\n      if (i > 0 && j >= F[i]) {\n        s1 = H[I - width + j0 - 1];\n      }\n      if (j > F[i]) {\n        s2 = H[I + j0 - 1];\n      }\n      if (s > s1 && (s > s2 || s === s2 && preferMatch)) {\n        pos.add(j);\n        if (i === 0) {\n          break;\n        }\n        i--;\n      }\n      preferMatch = C[I + j0] > 1 || I + width + j0 + 1 < C.length && C[I + width + j0 + 1] > 0;\n      j--;\n    }\n  }\n  return [{ start: j, end: maxScorePos + 1, score: maxScore }, pos];\n};\nfunction calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos) {\n  let pidx = 0, score = 0, inGap = false, consecutive = 0, firstBonus = toShort(0);\n  const pos = createPosSet(withPos);\n  let prevCharClass = 0;\n  if (sidx > 0) {\n    prevCharClass = charClassOf(text[sidx - 1]);\n  }\n  for (let idx = sidx; idx < eidx; idx++) {\n    let rune = text[idx];\n    const charClass = charClassOf(rune);\n    if (!caseSensitive) {\n      if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) {\n        rune += 32;\n      } else if (rune > MAX_ASCII) {\n        rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0);\n      }\n    }\n    if (normalize) {\n      rune = normalizeRune(rune);\n    }\n    if (rune === pattern[pidx]) {\n      if (withPos && pos !== null) {\n        pos.add(idx);\n      }\n      score += SCORE_MATCH;\n      let bonus = bonusFor(prevCharClass, charClass);\n      if (consecutive === 0) {\n        firstBonus = bonus;\n      } else {\n        if (bonus === BONUS_BOUNDARY) {\n          firstBonus = bonus;\n        }\n        bonus = maxInt16(maxInt16(bonus, firstBonus), BONUS_CONSECUTIVE);\n      }\n      if (pidx === 0) {\n        score += bonus * BONUS_FIRST_CHAR_MULTIPLIER;\n      } else {\n        score += bonus;\n      }\n      inGap = false;\n      consecutive++;\n      pidx++;\n    } else {\n      if (inGap) {\n        score += SCORE_GAP_EXTENTION;\n      } else {\n        score += SCORE_GAP_START;\n      }\n      inGap = true;\n      consecutive = 0;\n      firstBonus = 0;\n    }\n    prevCharClass = charClass;\n  }\n  return [score, pos];\n}\nfunction fuzzyMatchV1(caseSensitive, normalize, forward, text, pattern, withPos, slab2) {\n  if (pattern.length === 0) {\n    return [{ start: 0, end: 0, score: 0 }, null];\n  }\n  if (asciiFuzzyIndex(text, pattern, caseSensitive) < 0) {\n    return [{ start: -1, end: -1, score: 0 }, null];\n  }\n  let pidx = 0, sidx = -1, eidx = -1;\n  const lenRunes = text.length;\n  const lenPattern = pattern.length;\n  for (let index = 0; index < lenRunes; index++) {\n    let rune = text[indexAt(index, lenRunes, forward)];\n    if (!caseSensitive) {\n      if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) {\n        rune += 32;\n      } else if (rune > MAX_ASCII) {\n        rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0);\n      }\n    }\n    if (normalize) {\n      rune = normalizeRune(rune);\n    }\n    const pchar = pattern[indexAt(pidx, lenPattern, forward)];\n    if (rune === pchar) {\n      if (sidx < 0) {\n        sidx = index;\n      }\n      pidx++;\n      if (pidx === lenPattern) {\n        eidx = index + 1;\n        break;\n      }\n    }\n  }\n  if (sidx >= 0 && eidx >= 0) {\n    pidx--;\n    for (let index = eidx - 1; index >= sidx; index--) {\n      const tidx = indexAt(index, lenRunes, forward);\n      let rune = text[tidx];\n      if (!caseSensitive) {\n        if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) {\n          rune += 32;\n        } else if (rune > MAX_ASCII) {\n          rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0);\n        }\n      }\n      const pidx_ = indexAt(pidx, lenPattern, forward);\n      const pchar = pattern[pidx_];\n      if (rune === pchar) {\n        pidx--;\n        if (pidx < 0) {\n          sidx = index;\n          break;\n        }\n      }\n    }\n    if (!forward) {\n      const sidxTemp = sidx;\n      sidx = lenRunes - eidx;\n      eidx = lenRunes - sidxTemp;\n    }\n    const [score, pos] = calculateScore(\n      caseSensitive,\n      normalize,\n      text,\n      pattern,\n      sidx,\n      eidx,\n      withPos\n    );\n    return [{ start: sidx, end: eidx, score }, pos];\n  }\n  return [{ start: -1, end: -1, score: 0 }, null];\n};\nconst exactMatchNaive = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => {\n  if (pattern.length === 0) {\n    return [{ start: 0, end: 0, score: 0 }, null];\n  }\n  const lenRunes = text.length;\n  const lenPattern = pattern.length;\n  if (lenRunes < lenPattern) {\n    return [{ start: -1, end: -1, score: 0 }, null];\n  }\n  if (asciiFuzzyIndex(text, pattern, caseSensitive) < 0) {\n    return [{ start: -1, end: -1, score: 0 }, null];\n  }\n  let pidx = 0;\n  let bestPos = -1, bonus = toShort(0), bestBonus = toShort(-1);\n  for (let index = 0; index < lenRunes; index++) {\n    const index_ = indexAt(index, lenRunes, forward);\n    let rune = text[index_];\n    if (!caseSensitive) {\n      if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) {\n        rune += 32;\n      } else if (rune > MAX_ASCII) {\n        rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0);\n      }\n    }\n    if (normalize) {\n      rune = normalizeRune(rune);\n    }\n    const pidx_ = indexAt(pidx, lenPattern, forward);\n    const pchar = pattern[pidx_];\n    if (pchar === rune) {\n      if (pidx_ === 0) {\n        bonus = bonusAt(text, index_);\n      }\n      pidx++;\n      if (pidx === lenPattern) {\n        if (bonus > bestBonus) {\n          bestPos = index;\n          bestBonus = bonus;\n        }\n        if (bonus === BONUS_BOUNDARY) {\n          break;\n        }\n        index -= pidx - 1;\n        pidx = 0;\n        bonus = 0;\n      }\n    } else {\n      index -= pidx;\n      pidx = 0;\n      bonus = 0;\n    }\n  }\n  if (bestPos >= 0) {\n    let sidx = 0, eidx = 0;\n    if (forward) {\n      sidx = bestPos - lenPattern + 1;\n      eidx = bestPos + 1;\n    } else {\n      sidx = lenRunes - (bestPos + 1);\n      eidx = lenRunes - (bestPos - lenPattern + 1);\n    }\n    const [score] = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false);\n    return [{ start: sidx, end: eidx, score }, null];\n  }\n  return [{ start: -1, end: -1, score: 0 }, null];\n};\nconst prefixMatch = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => {\n  if (pattern.length === 0) {\n    return [{ start: 0, end: 0, score: 0 }, null];\n  }\n  let trimmedLen = 0;\n  if (!isWhitespace(pattern[0])) {\n    trimmedLen = whitespacesAtStart(text);\n  }\n  if (text.length - trimmedLen < pattern.length) {\n    return [{ start: -1, end: -1, score: 0 }, null];\n  }\n  for (const [index, r] of pattern.entries()) {\n    let rune = text[trimmedLen + index];\n    if (!caseSensitive) {\n      rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0);\n    }\n    if (normalize) {\n      rune = normalizeRune(rune);\n    }\n    if (rune !== r) {\n      return [{ start: -1, end: -1, score: 0 }, null];\n    }\n  }\n  const lenPattern = pattern.length;\n  const [score] = calculateScore(\n    caseSensitive,\n    normalize,\n    text,\n    pattern,\n    trimmedLen,\n    trimmedLen + lenPattern,\n    false\n  );\n  return [{ start: trimmedLen, end: trimmedLen + lenPattern, score }, null];\n};\nconst suffixMatch = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => {\n  const lenRunes = text.length;\n  let trimmedLen = lenRunes;\n  if (pattern.length === 0 || !isWhitespace(pattern[pattern.length - 1])) {\n    trimmedLen -= whitespacesAtEnd(text);\n  }\n  if (pattern.length === 0) {\n    return [{ start: trimmedLen, end: trimmedLen, score: 0 }, null];\n  }\n  const diff = trimmedLen - pattern.length;\n  if (diff < 0) {\n    return [{ start: -1, end: -1, score: 0 }, null];\n  }\n  for (const [index, r] of pattern.entries()) {\n    let rune = text[index + diff];\n    if (!caseSensitive) {\n      rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0);\n    }\n    if (normalize) {\n      rune = normalizeRune(rune);\n    }\n    if (rune !== r) {\n      return [{ start: -1, end: -1, score: 0 }, null];\n    }\n  }\n  const lenPattern = pattern.length;\n  const sidx = trimmedLen - lenPattern;\n  const eidx = trimmedLen;\n  const [score] = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false);\n  return [{ start: sidx, end: eidx, score }, null];\n};\nconst equalMatch = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => {\n  const lenPattern = pattern.length;\n  if (lenPattern === 0) {\n    return [{ start: -1, end: -1, score: 0 }, null];\n  }\n  let trimmedLen = 0;\n  if (!isWhitespace(pattern[0])) {\n    trimmedLen = whitespacesAtStart(text);\n  }\n  let trimmedEndLen = 0;\n  if (!isWhitespace(pattern[lenPattern - 1])) {\n    trimmedEndLen = whitespacesAtEnd(text);\n  }\n  if (text.length - trimmedLen - trimmedEndLen != lenPattern) {\n    return [{ start: -1, end: -1, score: 0 }, null];\n  }\n  let match = true;\n  if (normalize) {\n    const runes = text;\n    for (const [idx, pchar] of pattern.entries()) {\n      let rune = runes[trimmedLen + idx];\n      if (!caseSensitive) {\n        rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0);\n      }\n      if (normalizeRune(pchar) !== normalizeRune(rune)) {\n        match = false;\n        break;\n      }\n    }\n  } else {\n    let runesStr = runesToStr(text).substring(trimmedLen, text.length - trimmedEndLen);\n    if (!caseSensitive) {\n      runesStr = runesStr.toLowerCase();\n    }\n    match = runesStr === runesToStr(pattern);\n  }\n  if (match) {\n    return [\n      {\n        start: trimmedLen,\n        end: trimmedLen + lenPattern,\n        score: (SCORE_MATCH + BONUS_BOUNDARY) * lenPattern + (BONUS_FIRST_CHAR_MULTIPLIER - 1) * BONUS_BOUNDARY\n      },\n      null\n    ];\n  }\n  return [{ start: -1, end: -1, score: 0 }, null];\n};\nconst SLAB_16_SIZE = 100 * 1024;\nconst SLAB_32_SIZE = 2048;\nfunction makeSlab(size16, size32) {\n  return {\n    i16: new Int16Array(size16),\n    i32: new Int32Array(size32)\n  };\n}\nconst slab = makeSlab(SLAB_16_SIZE, SLAB_32_SIZE);\nvar TermType = /* @__PURE__ */ ((TermType2) => {\n  TermType2[TermType2[\"Fuzzy\"] = 0] = \"Fuzzy\";\n  TermType2[TermType2[\"Exact\"] = 1] = \"Exact\";\n  TermType2[TermType2[\"Prefix\"] = 2] = \"Prefix\";\n  TermType2[TermType2[\"Suffix\"] = 3] = \"Suffix\";\n  TermType2[TermType2[\"Equal\"] = 4] = \"Equal\";\n  return TermType2;\n})(TermType || {});\nconst termTypeMap = {\n  [0]: fuzzyMatchV2,\n  [1]: exactMatchNaive,\n  [2]: prefixMatch,\n  [3]: suffixMatch,\n  [4]: equalMatch\n};\nfunction buildPatternForExtendedMatch(fuzzy, caseMode, normalize, str) {\n  let cacheable = true;\n  str = str.trimLeft();\n  {\n    const trimmedAtRightStr = str.trimRight();\n    if (trimmedAtRightStr.endsWith(\"\\\\\") && str[trimmedAtRightStr.length] === \" \") {\n      str = trimmedAtRightStr + \" \";\n    } else {\n      str = trimmedAtRightStr;\n    }\n  }\n  let sortable = false;\n  let termSets = [];\n  termSets = parseTerms(fuzzy, caseMode, normalize, str);\n  Loop:\n    for (const termSet of termSets) {\n      for (const [idx, term] of termSet.entries()) {\n        if (!term.inv) {\n          sortable = true;\n        }\n        if (!cacheable || idx > 0 || term.inv || fuzzy && term.typ !== 0 || !fuzzy && term.typ !== 1) {\n          cacheable = false;\n          if (sortable) {\n            break Loop;\n          }\n        }\n      }\n    }\n  return {\n    str,\n    termSets,\n    sortable,\n    cacheable,\n    fuzzy\n  };\n}\nfunction parseTerms(fuzzy, caseMode, normalize, str) {\n  str = str.replace(/\\\\ /g, \"\t\");\n  const tokens = str.split(/ +/);\n  const sets = [];\n  let set = [];\n  let switchSet = false;\n  let afterBar = false;\n  for (const token of tokens) {\n    let typ = 0, inv = false, text = token.replace(/\\t/g, \" \");\n    const lowerText = text.toLowerCase();\n    const caseSensitive = caseMode === \"case-sensitive\" || caseMode === \"smart-case\" && text !== lowerText;\n    const normalizeTerm = normalize && lowerText === runesToStr(strToRunes(lowerText).map(normalizeRune));\n    if (!caseSensitive) {\n      text = lowerText;\n    }\n    if (!fuzzy) {\n      typ = 1;\n    }\n    if (set.length > 0 && !afterBar && text === \"|\") {\n      switchSet = false;\n      afterBar = true;\n      continue;\n    }\n    afterBar = false;\n    if (text.startsWith(\"!\")) {\n      inv = true;\n      typ = 1;\n      text = text.substring(1);\n    }\n    if (text !== \"$\" && text.endsWith(\"$\")) {\n      typ = 3;\n      text = text.substring(0, text.length - 1);\n    }\n    if (text.startsWith(\"'\")) {\n      if (fuzzy && !inv) {\n        typ = 1;\n      } else {\n        typ = 0;\n      }\n      text = text.substring(1);\n    } else if (text.startsWith(\"^\")) {\n      if (typ === 3) {\n        typ = 4;\n      } else {\n        typ = 2;\n      }\n      text = text.substring(1);\n    }\n    if (text.length > 0) {\n      if (switchSet) {\n        sets.push(set);\n        set = [];\n      }\n      let textRunes = strToRunes(text);\n      if (normalizeTerm) {\n        textRunes = textRunes.map(normalizeRune);\n      }\n      set.push({\n        typ,\n        inv,\n        text: textRunes,\n        caseSensitive,\n        normalize: normalizeTerm\n      });\n      switchSet = true;\n    }\n  }\n  if (set.length > 0) {\n    sets.push(set);\n  }\n  return sets;\n}\nconst buildPatternForBasicMatch = (query, casing, normalize) => {\n  let caseSensitive = false;\n  switch (casing) {\n    case \"smart-case\":\n      if (query.toLowerCase() !== query) {\n        caseSensitive = true;\n      }\n      break;\n    case \"case-sensitive\":\n      caseSensitive = true;\n      break;\n    case \"case-insensitive\":\n      query = query.toLowerCase();\n      caseSensitive = false;\n      break;\n  }\n  let queryRunes = strToRunes(query);\n  if (normalize) {\n    queryRunes = queryRunes.map(normalizeRune);\n  }\n  return {\n    queryRunes,\n    caseSensitive\n  };\n};\nfunction iter(algoFn, tokens, caseSensitive, normalize, forward, pattern, slab2) {\n  for (const part of tokens) {\n    const [res, pos] = algoFn(caseSensitive, normalize, forward, part.text, pattern, true, slab2);\n    if (res.start >= 0) {\n      const sidx = res.start + part.prefixLength;\n      const eidx = res.end + part.prefixLength;\n      if (pos !== null) {\n        const newPos = /* @__PURE__ */ new Set();\n        pos.forEach((v) => newPos.add(part.prefixLength + v));\n        return [[sidx, eidx], res.score, newPos];\n      }\n      return [[sidx, eidx], res.score, pos];\n    }\n  }\n  return [[-1, -1], 0, null];\n}\nfunction computeExtendedMatch(text, pattern, fuzzyAlgo, forward) {\n  const input = [\n    {\n      text,\n      prefixLength: 0\n    }\n  ];\n  const offsets = [];\n  let totalScore = 0;\n  const allPos = /* @__PURE__ */ new Set();\n  for (const termSet of pattern.termSets) {\n    let offset = [0, 0];\n    let currentScore = 0;\n    let matched = false;\n    for (const term of termSet) {\n      let algoFn = termTypeMap[term.typ];\n      if (term.typ === TermType.Fuzzy) {\n        algoFn = fuzzyAlgo;\n      }\n      const [off, score, pos] = iter(\n        algoFn,\n        input,\n        term.caseSensitive,\n        term.normalize,\n        forward,\n        term.text,\n        slab\n      );\n      const sidx = off[0];\n      if (sidx >= 0) {\n        if (term.inv) {\n          continue;\n        }\n        offset = off;\n        currentScore = score;\n        matched = true;\n        if (pos !== null) {\n          pos.forEach((v) => allPos.add(v));\n        } else {\n          for (let idx = off[0]; idx < off[1]; ++idx) {\n            allPos.add(idx);\n          }\n        }\n        break;\n      } else if (term.inv) {\n        offset = [0, 0];\n        currentScore = 0;\n        matched = true;\n        continue;\n      }\n    }\n    if (matched) {\n      offsets.push(offset);\n      totalScore += currentScore;\n    }\n  }\n  return { offsets, totalScore, allPos };\n}\nfunction getResultFromScoreMap(scoreMap, limit) {\n  const scoresInDesc = Object.keys(scoreMap).map((v) => parseInt(v, 10)).sort((a, b) => b - a);\n  let result = [];\n  for (const score of scoresInDesc) {\n    result = result.concat(scoreMap[score]);\n    if (result.length >= limit) {\n      break;\n    }\n  }\n  return result;\n}\nfunction getBasicMatchIter(scoreMap, queryRunes, caseSensitive) {\n  return (idx) => {\n    const itemRunes = this.runesList[idx];\n    if (queryRunes.length > itemRunes.length)\n      return;\n    let [match, positions] = this.algoFn(\n      caseSensitive,\n      this.opts.normalize,\n      this.opts.forward,\n      itemRunes,\n      queryRunes,\n      true,\n      slab\n    );\n    if (match.start === -1)\n      return;\n    if (this.opts.fuzzy === false) {\n      positions = /* @__PURE__ */ new Set();\n      for (let position = match.start; position < match.end; ++position) {\n        positions.add(position);\n      }\n    }\n    const scoreKey = this.opts.sort ? match.score : 0;\n    if (scoreMap[scoreKey] === void 0) {\n      scoreMap[scoreKey] = [];\n    }\n    scoreMap[scoreKey].push(Object.assign({\n      item: this.items[idx],\n      positions: positions != null ? positions : /* @__PURE__ */ new Set()\n    }, match));\n  };\n}\nfunction getExtendedMatchIter(scoreMap, pattern) {\n  return (idx) => {\n    const runes = this.runesList[idx];\n    const match = computeExtendedMatch(runes, pattern, this.algoFn, this.opts.forward);\n    if (match.offsets.length !== pattern.termSets.length)\n      return;\n    let sidx = -1, eidx = -1;\n    if (match.allPos.size > 0) {\n      sidx = Math.min(...match.allPos);\n      eidx = Math.max(...match.allPos) + 1;\n    }\n    const scoreKey = this.opts.sort ? match.totalScore : 0;\n    if (scoreMap[scoreKey] === void 0) {\n      scoreMap[scoreKey] = [];\n    }\n    scoreMap[scoreKey].push({\n      score: match.totalScore,\n      item: this.items[idx],\n      positions: match.allPos,\n      start: sidx,\n      end: eidx\n    });\n  };\n}\nfunction basicMatch(query) {\n  const { queryRunes, caseSensitive } = buildPatternForBasicMatch(\n    query,\n    this.opts.casing,\n    this.opts.normalize\n  );\n  const scoreMap = {};\n  const iter2 = getBasicMatchIter.bind(this)(\n    scoreMap,\n    queryRunes,\n    caseSensitive\n  );\n  for (let i = 0, len = this.runesList.length; i < len; ++i) {\n    iter2(i);\n  }\n  return getResultFromScoreMap(scoreMap, this.opts.limit);\n}\nfunction extendedMatch(query) {\n  const pattern = buildPatternForExtendedMatch(\n    Boolean(this.opts.fuzzy),\n    this.opts.casing,\n    this.opts.normalize,\n    query\n  );\n  const scoreMap = {};\n  const iter2 = getExtendedMatchIter.bind(this)(scoreMap, pattern);\n  for (let i = 0, len = this.runesList.length; i < len; ++i) {\n    iter2(i);\n  }\n  return getResultFromScoreMap(scoreMap, this.opts.limit);\n}\nconst defaultOpts = {\n  limit: Infinity,\n  selector: (v) => v,\n  casing: \"smart-case\",\n  normalize: true,\n  fuzzy: \"v2\",\n  tiebreakers: [],\n  sort: true,\n  forward: true,\n  match: basicMatch\n};\nclass Finder {\n  constructor(list, ...optionsTuple) {\n    this.opts = Object.assign(defaultOpts, optionsTuple[0]);\n    this.items = list;\n    this.runesList = list.map((item) => strToRunes(this.opts.selector(item).normalize()));\n    this.algoFn = exactMatchNaive;\n    switch (this.opts.fuzzy) {\n      case \"v2\":\n        this.algoFn = fuzzyMatchV2;\n        break;\n      case \"v1\":\n        this.algoFn = fuzzyMatchV1;\n        break;\n    }\n  }\n  find(query) {\n    if (query.length === 0 || this.items.length === 0)\n      return this.items.slice(0, this.opts.limit).map(createResultItemWithEmptyPos);\n    query = query.normalize();\n    let result = this.opts.match.bind(this)(query);\n    return postProcessResultItems(result, this.opts);\n  }\n}\nfunction createResultItemWithEmptyPos(item) {\n  return ({\n    item,\n    start: -1,\n    end: -1,\n    score: 0,\n    positions: /* @__PURE__ */ new Set()\n  })\n};\nfunction postProcessResultItems(result, opts) {\n  if (opts.sort) {\n    const { selector } = opts;\n    result.sort((a, b) => {\n      if (a.score === b.score) {\n        for (const tiebreaker of opts.tiebreakers) {\n          const diff = tiebreaker(a, b, selector);\n          if (diff !== 0) {\n            return diff;\n          }\n        }\n      }\n      return 0;\n    });\n  }\n  if (Number.isFinite(opts.limit)) {\n    result.splice(opts.limit);\n  }\n  return result;\n}\nfunction byLengthAsc(a, b, selector) {\n  return selector(a.item).length - selector(b.item).length;\n}\nfunction byStartAsc(a, b) {\n  return a.start - b.start;\n}\n"
  },
  {
    "path": "utils/scripts/lrcparser.js",
    "content": "function parseLrc(text) {\n    if (!text) return [];\n    let lines = text.split(\"\\n\");\n    let result = [];\n\n    let timeRegex = /\\[(\\d+):(\\d+\\.\\d+|\\d+)\\]/g;\n\n    // Blacklist for credits/metadata often found in NetEase lyrics\n    const creditKeywords = [\n        \"作词\", \"作曲\", \"编曲\", \"制作\", \"收录\", \"演奏\", \"词：\", \"曲：\", \"Lyricist\", \"Composer\", \"Arranger\", \"Producer\", \"Mixing\", \"Mastering\"\n    ];\n\n    for (let line of lines) {\n\n        timeRegex.lastIndex = 0;\n        let matches = [];\n        let match;\n\n        while ((match = timeRegex.exec(line)) !== null) {\n            matches.push(match);\n        }\n\n        if (matches.length === 0) continue;\n\n        let lyric = line.replace(timeRegex, \"\").trim();\n\n        let min = parseInt(matches[0][1]);\n        let sec = parseFloat(matches[0][2]);\n        let totalTime = min * 60 + sec;\n\n        // Only filter credits if they appear in the first 20 seconds\n        if (totalTime < 20) {\n            let isCreditFormat = creditKeywords.some(k => lyric.includes(k));\n            if (isCreditFormat && (lyric.includes(\":\") || lyric.includes(\"：\") || lyric.length < 25)) {\n                continue;\n            }\n        }\n\n        for (let match of matches) {\n            let min = parseInt(match[1]);\n            let sec = parseFloat(match[2]);\n\n            result.push({\n                time: min * 60 + sec,\n                text: lyric\n            });\n        }\n    }\n\n    result.sort((a, b) => a.time - b.time);\n    return result;\n}\n\nfunction getCurrentLine(lyrics, position) {\n    const epsilon = 0.1; // 100ms tolerance\n    for (let i = lyrics.length - 1; i >= 0; i--) {\n        if ((position + epsilon) >= lyrics[i].time) {\n            return i;\n        }\n    }\n    return -1;\n}\n"
  }
]